From 95d434e5114a6033a8e8cf7df93cef8e311e37ee Mon Sep 17 00:00:00 2001 From: Nicholas Boll Date: Tue, 5 Nov 2024 10:18:46 -0700 Subject: [PATCH 01/32] fix: SystemIcon supports rem by default (#3031) Add a `rem` value for the default size to `SystemIcon` Fixes #3028 [category:Components] --- modules/react/icon/lib/SystemIcon.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/react/icon/lib/SystemIcon.tsx b/modules/react/icon/lib/SystemIcon.tsx index e274fed753..32984d04a1 100644 --- a/modules/react/icon/lib/SystemIcon.tsx +++ b/modules/react/icon/lib/SystemIcon.tsx @@ -4,7 +4,7 @@ import {CanvasSystemIcon, CanvasIconTypes} from '@workday/design-assets-types'; import {CSSObject} from '@emotion/styled'; import {createComponent, getColor} from '@workday/canvas-kit-react/common'; import {cssVar, createStencil, handleCsProp, px2rem, createVars} from '@workday/canvas-kit-styling'; -import {base} from '@workday/canvas-tokens-web'; +import {base, system} from '@workday/canvas-tokens-web'; import {Svg, SvgProps, svgStencil, transformColorNameToToken} from './Svg'; /** @@ -153,7 +153,11 @@ export const systemIconStencil = createStencil({ accentColor: '', backgroundColor: '', }, - base: ({accentColor, backgroundColor, color}) => ({ + base: ({size, width, height, accentColor, backgroundColor, color}) => ({ + '& svg': { + width: cssVar(width, cssVar(size, system.space.x6)), + height: cssVar(height, cssVar(size, system.space.x6)), + }, [backgroundColor]: cssVar(backgroundColor, 'transparent'), '& .wd-icon-fill': { fill: cssVar(color, base.licorice200), From 9fd33169b55ace27ca9dcf051dd86e7f50522d80 Mon Sep 17 00:00:00 2001 From: Manuel Carrera Date: Tue, 5 Nov 2024 10:19:29 -0700 Subject: [PATCH 02/32] ci: Update node version in our github actions [skip release] (#3009) Update Github Actions node version to 18 [category:Infrastructure] Co-authored-by: manuel.carrera --- .github/workflows/canary.yml | 2 +- .github/workflows/dist-tag.yaml | 2 +- .github/workflows/forward-merge.yml | 2 +- .github/workflows/pull-request.yml | 10 +++++----- .github/workflows/release-major.yml | 2 +- .github/workflows/release-minor.yml | 2 +- .github/workflows/release.yml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index bc14e43372..18f0f8465a 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -21,7 +21,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x ## Build Storybook and extract component stories for Storybook aggregation. This will be used ## for Chromatic rebaselining and publishing to GH Pages. Should be before `yarn build` since diff --git a/.github/workflows/dist-tag.yaml b/.github/workflows/dist-tag.yaml index 689d707897..1493c4cf76 100644 --- a/.github/workflows/dist-tag.yaml +++ b/.github/workflows/dist-tag.yaml @@ -22,7 +22,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x - name: Check packages run: node utils/dist-tag.mjs diff --git a/.github/workflows/forward-merge.yml b/.github/workflows/forward-merge.yml index 9cb03d6f54..b967243290 100644 --- a/.github/workflows/forward-merge.yml +++ b/.github/workflows/forward-merge.yml @@ -94,7 +94,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x ## A `yarn bump` will create a commit and a tag. We need to set up the git user to do this. ## We'll make that user be the github-actions user. diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index acac0f1e11..5289963d65 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -13,7 +13,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x check: runs-on: ubuntu-latest @@ -24,7 +24,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x # Keep steps separate for Github Actions annotation matching: https://github.com/actions/setup-node/blob/83c9f7a7df54d6b57455f7c57ac414f2ae5fb8de/src/setup-node.ts#L26-L33 - name: Lint @@ -48,7 +48,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x - name: Build Storybook run: yarn build-storybook --quiet @@ -71,7 +71,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x - name: Restore Build uses: actions/cache@v3 @@ -102,7 +102,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x - name: Restore Build uses: actions/cache@v3 diff --git a/.github/workflows/release-major.yml b/.github/workflows/release-major.yml index b6f8b85212..4ff11c8e95 100644 --- a/.github/workflows/release-major.yml +++ b/.github/workflows/release-major.yml @@ -34,7 +34,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x # Run the release job to publish the next major version to npm - uses: Workday/canvas-kit-actions/release@v1 diff --git a/.github/workflows/release-minor.yml b/.github/workflows/release-minor.yml index 514077cd93..a0e0d3d820 100644 --- a/.github/workflows/release-minor.yml +++ b/.github/workflows/release-minor.yml @@ -19,7 +19,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x - uses: Workday/canvas-kit-actions/release@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71bf3433fd..3a0e33d080 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: - uses: Workday/canvas-kit-actions/install@v1 with: - node_version: 16.x + node_version: 18.x - uses: Workday/canvas-kit-actions/release@v1 with: From 6ed8303b2c295f908351852b1f7faf0d3d97726c Mon Sep 17 00:00:00 2001 From: alanbsmith Date: Tue, 5 Nov 2024 17:19:36 +0000 Subject: [PATCH 03/32] chore: Release v11.1.22 [skip release] --- CHANGELOG.md | 7 +++++++ lerna.json | 2 +- modules/codemod/package.json | 2 +- modules/css/package.json | 2 +- modules/docs/package.json | 10 +++++----- modules/labs-css/package.json | 2 +- modules/labs-react/package.json | 4 ++-- modules/popup-stack/package.json | 2 +- modules/preview-css/package.json | 2 +- modules/preview-react/package.json | 6 +++--- modules/react-fonts/package.json | 2 +- modules/react/package.json | 6 +++--- modules/styling-transform/package.json | 4 ++-- modules/styling/package.json | 4 ++-- 14 files changed, 31 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39b9c799d..47c7b6fd3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [v11.1.22](https://github.com/Workday/canvas-kit/releases/tag/v11.1.22) (2024-11-05) + +### Components + +- fix: SystemIcon supports rem by default ([#3031](https://github.com/Workday/canvas-kit/pull/3031)) ([@NicholasBoll](https://github.com/NicholasBoll)) + + ## [v11.1.21](https://github.com/Workday/canvas-kit/releases/tag/v11.1.21) (2024-11-04) ### Components diff --git a/lerna.json b/lerna.json index 8b8fce188e..55f9d164d7 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "modules/**" ], - "version": "11.1.21", + "version": "11.1.22", "npmClient": "yarn", "useWorkspaces": true, "command": { diff --git a/modules/codemod/package.json b/modules/codemod/package.json index 02d4157d7e..52a3467286 100644 --- a/modules/codemod/package.json +++ b/modules/codemod/package.json @@ -2,7 +2,7 @@ "name": "@workday/canvas-kit-codemod", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", - "version": "11.1.21", + "version": "11.1.22", "description": "A collection of codemods for use on Workday Canvas Kit packages.", "main": "dist/es6/index.js", "sideEffects": false, diff --git a/modules/css/package.json b/modules/css/package.json index 1cf5838581..994527d6c1 100644 --- a/modules/css/package.json +++ b/modules/css/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-css", - "version": "11.1.21", + "version": "11.1.22", "description": "The parent module that contains all Workday Canvas Kit CSS components", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", diff --git a/modules/docs/package.json b/modules/docs/package.json index 1512e507a5..2948ff7fde 100644 --- a/modules/docs/package.json +++ b/modules/docs/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-docs", - "version": "11.1.21", + "version": "11.1.22", "description": "Documentation components of Canvas Kit components", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", @@ -44,10 +44,10 @@ "dependencies": { "@emotion/styled": "^11.6.0", "@storybook/csf": "0.0.1", - "@workday/canvas-kit-labs-react": "^11.1.21", - "@workday/canvas-kit-preview-react": "^11.1.21", - "@workday/canvas-kit-react": "^11.1.21", - "@workday/canvas-kit-styling": "^11.1.21", + "@workday/canvas-kit-labs-react": "^11.1.22", + "@workday/canvas-kit-preview-react": "^11.1.22", + "@workday/canvas-kit-react": "^11.1.22", + "@workday/canvas-kit-styling": "^11.1.22", "@workday/canvas-system-icons-web": "^3.0.0", "@workday/canvas-tokens-web": "^2.0.0", "markdown-to-jsx": "^7.2.0", diff --git a/modules/labs-css/package.json b/modules/labs-css/package.json index a1b7386040..87141e0900 100644 --- a/modules/labs-css/package.json +++ b/modules/labs-css/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-labs-css", - "version": "11.1.21", + "version": "11.1.22", "description": "The parent module that contains all Workday Canvas Kit Labs CSS components", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", diff --git a/modules/labs-react/package.json b/modules/labs-react/package.json index a4a696ebb2..67f703d60d 100644 --- a/modules/labs-react/package.json +++ b/modules/labs-react/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-labs-react", - "version": "11.1.21", + "version": "11.1.22", "description": "Canvas Kit Labs is an incubator for new and experimental components. Since we have a rather rigorous process for getting components in at a production level, it can be valuable to make them available earlier while we continuously iterate on the API/functionality. The Labs modules allow us to do that as needed.", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", @@ -46,7 +46,7 @@ "dependencies": { "@emotion/react": "^11.7.1", "@emotion/styled": "^11.6.0", - "@workday/canvas-kit-react": "^11.1.21", + "@workday/canvas-kit-react": "^11.1.22", "@workday/canvas-system-icons-web": "^3.0.0", "@workday/design-assets-types": "^0.2.8", "chroma-js": "^2.1.0", diff --git a/modules/popup-stack/package.json b/modules/popup-stack/package.json index 2f04c7817e..702ea00c86 100644 --- a/modules/popup-stack/package.json +++ b/modules/popup-stack/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-popup-stack", - "version": "11.1.21", + "version": "11.1.22", "description": "Stack for managing popup UIs to coordinate global concerns like escape key handling and rendering order", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", diff --git a/modules/preview-css/package.json b/modules/preview-css/package.json index 36f70fd511..c89b9d618b 100644 --- a/modules/preview-css/package.json +++ b/modules/preview-css/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-preview-css", - "version": "11.1.21", + "version": "11.1.22", "description": "The parent module that contains all Workday Canvas Kit Preview CSS components", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", diff --git a/modules/preview-react/package.json b/modules/preview-react/package.json index 1e8e175be4..f4eaf05923 100644 --- a/modules/preview-react/package.json +++ b/modules/preview-react/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-preview-react", - "version": "11.1.21", + "version": "11.1.22", "description": "Canvas Kit Preview is made up of components that have the full design and a11y review, are part of the DS ecosystem and are approved for use in product. The API's could be subject to change, but not without strong communication and migration strategies.", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", @@ -46,8 +46,8 @@ "dependencies": { "@emotion/react": "^11.7.1", "@emotion/styled": "^11.6.0", - "@workday/canvas-kit-react": "^11.1.21", - "@workday/canvas-kit-styling": "^11.1.21", + "@workday/canvas-kit-react": "^11.1.22", + "@workday/canvas-kit-styling": "^11.1.22", "@workday/canvas-system-icons-web": "^3.0.0", "@workday/canvas-tokens-web": "^2.0.0", "@workday/design-assets-types": "^0.2.8" diff --git a/modules/react-fonts/package.json b/modules/react-fonts/package.json index 0bbb6afa1e..953fbc23aa 100644 --- a/modules/react-fonts/package.json +++ b/modules/react-fonts/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-react-fonts", - "version": "11.1.21", + "version": "11.1.22", "description": "Fonts for canvas-kit-react", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", diff --git a/modules/react/package.json b/modules/react/package.json index e4e375e4ec..f61497ae72 100644 --- a/modules/react/package.json +++ b/modules/react/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-react", - "version": "11.1.21", + "version": "11.1.22", "description": "The parent module that contains all Workday Canvas Kit React components", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", @@ -49,8 +49,8 @@ "@emotion/styled": "^11.6.0", "@popperjs/core": "^2.5.4", "@workday/canvas-colors-web": "^2.0.0", - "@workday/canvas-kit-popup-stack": "^11.1.21", - "@workday/canvas-kit-styling": "^11.1.21", + "@workday/canvas-kit-popup-stack": "^11.1.22", + "@workday/canvas-kit-styling": "^11.1.22", "@workday/canvas-system-icons-web": "^3.0.0", "@workday/canvas-tokens-web": "^2.0.0", "@workday/design-assets-types": "^0.2.8", diff --git a/modules/styling-transform/package.json b/modules/styling-transform/package.json index 8371a59782..df0bbf2719 100644 --- a/modules/styling-transform/package.json +++ b/modules/styling-transform/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-styling-transform", - "version": "11.1.21", + "version": "11.1.22", "description": "The custom CSS in JS solution that takes JS styles and turns them into static CSS", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", @@ -34,7 +34,7 @@ ], "dependencies": { "@emotion/serialize": "^1.0.2", - "@workday/canvas-kit-styling": "^11.1.21", + "@workday/canvas-kit-styling": "^11.1.22", "@workday/canvas-tokens-web": "^2.0.0", "stylis": "4.0.13", "typescript": "4.2" diff --git a/modules/styling/package.json b/modules/styling/package.json index 589c8944bd..8d4aa96281 100644 --- a/modules/styling/package.json +++ b/modules/styling/package.json @@ -1,6 +1,6 @@ { "name": "@workday/canvas-kit-styling", - "version": "11.1.21", + "version": "11.1.22", "description": "The custom CSS in JS solution that takes JS styles and turns them into static CSS", "author": "Workday, Inc. (https://www.workday.com)", "license": "Apache-2.0", @@ -53,7 +53,7 @@ "@emotion/react": "^11.7.1", "@emotion/serialize": "^1.0.2", "@emotion/styled": "^11.6.0", - "@workday/canvas-kit-react": "^11.1.21", + "@workday/canvas-kit-react": "^11.1.22", "@workday/canvas-system-icons-web": "^3.0.0", "@workday/canvas-tokens-web": "^2.0.0", "typescript": "4.2" From c0410e2e4d7d322d7e91a5f1fca2af6b87ea2ec9 Mon Sep 17 00:00:00 2001 From: Manuel Carrera Date: Tue, 5 Nov 2024 11:23:38 -0700 Subject: [PATCH 04/32] chore: SNYK Security upgrade jscodeshift from 0.14.0 to 17.1.0 (#3016) Upgrade jscodeshift from `0.14.0` to `17.1.0` [SNYK-JS-INFLIGHT-6095116](https://snyk.io/vuln/SNYK-JS-INFLIGHT-6095116). There should be no breaking changes, for more information view their [changeset](https://github.com/facebook/jscodeshift/blob/main/CHANGELOG.md#1700-2024-08-06). [category:Dependencies] Co-authored-by: @snyk-bot Co-authored-by: manuel.carrera --- modules/codemod/package.json | 2 +- yarn.lock | 33 ++++----------------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/modules/codemod/package.json b/modules/codemod/package.json index 6891e112c4..28ef5e45a1 100644 --- a/modules/codemod/package.json +++ b/modules/codemod/package.json @@ -31,7 +31,7 @@ "homepage": "https://github.com/Workday/canvas-kit#readme", "dependencies": { "chalk": "4.1.2", - "jscodeshift": "^0.14.0", + "jscodeshift": "^17.1.0", "yargs": "^16.2.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 028945b291..6107951906 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11558,7 +11558,7 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jscodeshift@0.14.0, jscodeshift@^0.14.0, jscodeshift@^0.15.1: +jscodeshift@0.14.0, jscodeshift@^0.15.1, jscodeshift@^17.1.0: version "0.14.0" resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.14.0.tgz#7542e6715d6d2e8bde0b4e883f0ccea358b46881" integrity sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA== @@ -15660,16 +15660,7 @@ string-replace-loader@^3.1.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -15756,7 +15747,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -15777,13 +15768,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -16972,7 +16956,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -16990,15 +16974,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From b0122ce4912dbd26971c9fdeaa71ef307bed60c3 Mon Sep 17 00:00:00 2001 From: Manuel Carrera Date: Wed, 6 Nov 2024 11:04:04 -0700 Subject: [PATCH 05/32] feat(collection): Add vertical overflow support (#3035) Fixes: https://github.com/Workday/canvas-kit/issues/3024 [category:Components] Release Note: - Add vertical overflow support to `useOverflowListModel`. - We've deprecated `addItemWidth`, use `additemSize` instead. Add either the height or the width based on the orientation. - We've deprecated `setContainerWidth`, use `setContainerSize` to either set the height or the width of the element. - We've deprecated `setOverflowTargetWidth`, use `setOverflowTargetSize` instead. - We've deprecated `removeItemWidth`, use `removeItemSize` instead. Co-authored-by: manuel.carrera Co-authored-by: @NicholasBoll --- .../lib/useOverflowListItemMeasure.tsx | 8 +- .../collection/lib/useOverflowListMeasure.ts | 2 +- .../collection/lib/useOverflowListModel.tsx | 141 +++++++++++------- .../collection/lib/useOverflowListTarget.tsx | 6 +- .../collection/spec/useOverflowModel.spec.tsx | 48 +++--- .../collection/stories/mdx/Collection.mdx | 11 ++ .../mdx/examples/OverflowVerticalList.tsx | 73 +++++++++ 7 files changed, 210 insertions(+), 79 deletions(-) create mode 100644 modules/react/collection/stories/mdx/examples/OverflowVerticalList.tsx diff --git a/modules/react/collection/lib/useOverflowListItemMeasure.tsx b/modules/react/collection/lib/useOverflowListItemMeasure.tsx index 7da8fb8258..a1772ad17c 100644 --- a/modules/react/collection/lib/useOverflowListItemMeasure.tsx +++ b/modules/react/collection/lib/useOverflowListItemMeasure.tsx @@ -26,17 +26,21 @@ export const useOverflowListItemMeasure = createElemPropsHook(useOverflowListMod useMountLayout(() => { if (localRef.current) { const styles = getComputedStyle(localRef.current); - model.events.addItemWidth({ + model.events.addItemSize({ id: name, width: localRef.current.offsetWidth + parseFloat(styles.marginLeft) + parseFloat(styles.marginRight), + height: + localRef.current.offsetHeight + + parseFloat(styles.marginTop) + + parseFloat(styles.marginBottom), }); } return () => { - model.events.removeItemWidth({id: name}); + model.events.removeItemSize({id: name}); }; }); diff --git a/modules/react/collection/lib/useOverflowListMeasure.ts b/modules/react/collection/lib/useOverflowListMeasure.ts index 4dbf953efc..f72948ab50 100644 --- a/modules/react/collection/lib/useOverflowListMeasure.ts +++ b/modules/react/collection/lib/useOverflowListMeasure.ts @@ -19,7 +19,7 @@ export const useOverflowListMeasure = createElemPropsHook(useOverflowListModel)( useResizeObserver({ ref: localRef, - onResize: model.events.setContainerWidth, + onResize: model.events.setContainerSize, }); useMountLayout(() => { if (localRef.current) { diff --git a/modules/react/collection/lib/useOverflowListModel.tsx b/modules/react/collection/lib/useOverflowListModel.tsx index 97e09cc304..b78637ecf0 100644 --- a/modules/react/collection/lib/useOverflowListModel.tsx +++ b/modules/react/collection/lib/useOverflowListModel.tsx @@ -5,17 +5,17 @@ import {useSelectionListModel} from './useSelectionListModel'; import {Item} from './useBaseListModel'; export function getHiddenIds( - containerWidth: number, + containerSize: number, containerGap: number, - overflowTargetWidth: number, - itemWidthCache: Record, + overflowTargetSize: number, + itemSizeCache: Record, selectedIds: string[] | 'all', items: Item[] ): string[] { /** Allows us to prioritize showing the selected item */ let selectedKey: undefined | string; /** Tally of combined item widths. We'll add items that fit until the container is full */ - let itemWidth = 0; + let itemSize = 0; /** Tally ids that won't fit inside the container. These will be used by components to hide * elements that won't fit in the container */ const hiddenIds: string[] = []; @@ -31,31 +31,31 @@ export function getHiddenIds( } if ( - Object.keys(itemWidthCache).reduce( - (sum, key, index) => sum + itemWidthCache[key] + (index > 0 ? containerGap : 0), + Object.keys(itemSizeCache).reduce( + (sum, key, index) => sum + itemSizeCache[key] + (index > 0 ? containerGap : 0), 0 - ) <= containerWidth + ) <= containerSize ) { // All items fit, return empty array return []; } else if (selectedKey) { - if (itemWidthCache[selectedKey] + overflowTargetWidth > containerWidth) { + if (itemSizeCache[selectedKey] + overflowTargetSize > containerSize) { // If the selected item doesn't fit, only show overflow (all items hidden) - return Object.keys(itemWidthCache); + return Object.keys(itemSizeCache); } else { // at least the selected item and overflow target fit. Update our itemWidth with the sum - itemWidth += itemWidthCache[selectedKey] + overflowTargetWidth; + itemSize += itemSizeCache[selectedKey] + overflowTargetSize; shouldAddGap = true; } } else { - itemWidth += overflowTargetWidth; + itemSize += overflowTargetSize; } - for (const key in itemWidthCache) { + for (const key in itemSizeCache) { if (key !== selectedKey) { - itemWidth += itemWidthCache[key] + (shouldAddGap ? containerGap : 0); + itemSize += itemSizeCache[key] + (shouldAddGap ? containerGap : 0); shouldAddGap = true; - if (itemWidth > containerWidth) { + if (itemSize > containerSize) { hiddenIds.push(key); } } @@ -81,13 +81,13 @@ export const useOverflowListModel = createModelHook({ const shouldCalculateOverflow = config.shouldCalculateOverflow === undefined ? true : config.shouldCalculateOverflow; const [hiddenIds, setHiddenIds] = React.useState(config.initialHiddenIds); - const [itemWidthCache, setItemWidthCache] = React.useState>({}); - const [containerWidth, setContainerWidth] = React.useState(0); + const [itemSizeCache, setItemSizeCache] = React.useState>({}); + const [containerSize, setContainerSize] = React.useState(0); const [containerGap, setContainerGap] = React.useState(0); - const containerWidthRef = React.useRef(0); - const itemWidthCacheRef = React.useRef(itemWidthCache); + const containerSizeRef = React.useRef(0); + const itemSizeCacheRef = React.useRef(itemSizeCache); const [overflowTargetWidth, setOverflowTargetWidth] = React.useState(0); - const overflowTargetWidthRef = React.useRef(0); + const overflowTargetSizeRef = React.useRef(0); const internalHiddenIds = shouldCalculateOverflow ? hiddenIds : []; @@ -103,8 +103,16 @@ export const useOverflowListModel = createModelHook({ const state = { ...model.state, hiddenIds: internalHiddenIds, - itemWidthCache, - containerWidth, + itemSizeCache, + /** + * @deprecated Use `itemSizeCache` instead + */ + itemWidthCache: itemSizeCache, + containerSize, + /** + * @deprecated Use `containerSize` instead + */ + containerWidth: containerSize, containerGap, overflowTargetWidth, }; @@ -114,10 +122,10 @@ export const useOverflowListModel = createModelHook({ select(data: Parameters[0]) { const {selectedIds} = model.selection.select(data.id, state); const ids = getHiddenIds( - containerWidthRef.current, + containerSizeRef.current, containerGap, - overflowTargetWidthRef.current, - itemWidthCacheRef.current, + overflowTargetSizeRef.current, + itemSizeCacheRef.current, selectedIds, config.items ); @@ -125,69 +133,93 @@ export const useOverflowListModel = createModelHook({ setHiddenIds(ids); }, - setContainerWidth(data: {width?: number}) { - containerWidthRef.current = data.width || 0; - setContainerWidth(data.width || 0); - + setContainerSize(data: {width?: number; height?: number}) { + containerSizeRef.current = + model.state.orientation === 'horizontal' ? data.width || 0 : data.height || 0; + setContainerSize(containerSizeRef.current); const ids = getHiddenIds( - containerWidthRef.current, + containerSizeRef.current, containerGap, - overflowTargetWidthRef.current, - itemWidthCacheRef.current, + overflowTargetSizeRef.current, + itemSizeCacheRef.current, state.selectedIds, config.items ); - setHiddenIds(ids); }, + /** + * @deprecated Use `setContainerSize` instead and pass both `width` and `height` + */ + setContainerWidth(data: {width?: number}) { + events.setContainerSize({width: data.width, height: 0}); + }, setContainerGap(data: {size: number}) { setContainerGap(data.size); const ids = getHiddenIds( - containerWidthRef.current, + containerSizeRef.current, data.size, - overflowTargetWidthRef.current, - itemWidthCacheRef.current, + overflowTargetSizeRef.current, + itemSizeCacheRef.current, state.selectedIds, config.items ); setHiddenIds(ids); }, + setOverflowTargetSize(data: {width: number; height: number}) { + overflowTargetSizeRef.current = + model.state.orientation === 'horizontal' ? data.width || 0 : data.height || 0; + setOverflowTargetWidth(overflowTargetSizeRef.current); + }, + + /** + * + * @deprecated `setOverflowTargetWidth` is deprecated. Please use `setOverflowTargetSize` and pass in the `width` and set `height` to `0`. + */ setOverflowTargetWidth(data: {width: number}) { - overflowTargetWidthRef.current = data.width; - setOverflowTargetWidth(data.width); + overflowTargetSizeRef.current = data.width; + events.setOverflowTargetSize({width: overflowTargetSizeRef.current, height: 0}); }, + + /** + * + * @deprecated `addItemWidth` is deprecated. Please use `addItemSize` and set the `width` + */ addItemWidth(data: {id: string; width: number}) { - itemWidthCacheRef.current = { - ...itemWidthCacheRef.current, - [data.id]: data.width, + events.addItemSize({id: data.id, width: data.width, height: 0}); + }, + addItemSize(data: {id: string; width: number; height: number}) { + itemSizeCacheRef.current = { + ...itemSizeCacheRef.current, + [data.id]: model.state.orientation === 'horizontal' ? data.width : data.height, }; - setItemWidthCache(itemWidthCacheRef.current); + + setItemSizeCache(itemSizeCacheRef.current); const ids = getHiddenIds( - containerWidthRef.current, + containerSizeRef.current, containerGap, - overflowTargetWidthRef.current, - itemWidthCacheRef.current, + overflowTargetSizeRef.current, + itemSizeCacheRef.current, state.selectedIds, config.items ); setHiddenIds(ids); }, - removeItemWidth(data: {id: string}) { - const newCache = {...itemWidthCacheRef.current}; + removeItemSize(data: {id: string}) { + const newCache = {...itemSizeCacheRef.current}; delete newCache[data.id]; - itemWidthCacheRef.current = newCache; - setItemWidthCache(itemWidthCacheRef.current); + itemSizeCacheRef.current = newCache; + setItemSizeCache(itemSizeCacheRef.current); const ids = getHiddenIds( - containerWidthRef.current, + containerSizeRef.current, containerGap, - overflowTargetWidthRef.current, - itemWidthCacheRef.current, + overflowTargetSizeRef.current, + itemSizeCacheRef.current, state.selectedIds !== 'all' ? state.selectedIds.filter(sId => data.id !== sId) : state.selectedIds, @@ -196,6 +228,13 @@ export const useOverflowListModel = createModelHook({ setHiddenIds(ids); }, + /** + * + * @deprecated `removeItemWidth` is deprecated. Please use `removeItemSize`. + */ + removeItemWidth(data: {id: string}) { + events.removeItemSize({id: data.id}); + }, addHiddenKey(data: {id: string}) { setHiddenIds(ids => ids.concat(data.id)); }, diff --git a/modules/react/collection/lib/useOverflowListTarget.tsx b/modules/react/collection/lib/useOverflowListTarget.tsx index 86dfacb81a..c3ccf969eb 100644 --- a/modules/react/collection/lib/useOverflowListTarget.tsx +++ b/modules/react/collection/lib/useOverflowListTarget.tsx @@ -24,11 +24,15 @@ export const useOverflowListTarget = createElemPropsHook(useOverflowListModel)(( if (localRef.current) { const styles = getComputedStyle(localRef.current); - model.events.setOverflowTargetWidth({ + model.events.setOverflowTargetSize({ width: localRef.current.offsetWidth + parseFloat(styles.marginLeft) + parseFloat(styles.marginRight), + height: + localRef.current.offsetWidth + + parseFloat(styles.marginTop) + + parseFloat(styles.marginBottom), }); } }); diff --git a/modules/react/collection/spec/useOverflowModel.spec.tsx b/modules/react/collection/spec/useOverflowModel.spec.tsx index 0bc2e41b0d..e90071b250 100644 --- a/modules/react/collection/spec/useOverflowModel.spec.tsx +++ b/modules/react/collection/spec/useOverflowModel.spec.tsx @@ -2,83 +2,83 @@ import {getHiddenIds} from '../lib/useOverflowListModel'; describe('useOverflowModel', () => { describe('getHiddenIds', () => { - const itemWidthCache = { + const itemSizeCache = { first: 100, second: 150, third: 200, fourth: 250, }; - const overflowTargetWidth = 100; + const overflowTargeSize = 100; [ { - containerWidth: 100, + containerSize: 100, gap: 0, selected: 'first', hiddenIds: ['first', 'second', 'third', 'fourth'], }, { - containerWidth: 199, + containerSize: 199, gap: 0, selected: 'first', hiddenIds: ['first', 'second', 'third', 'fourth'], }, { - containerWidth: 200, + containerSize: 200, gap: 0, selected: 'first', hiddenIds: ['second', 'third', 'fourth'], }, - {containerWidth: 700, gap: 0, selected: 'first', hiddenIds: []}, - {containerWidth: 350, gap: 0, selected: 'first', hiddenIds: ['third', 'fourth']}, - {containerWidth: 549, gap: 0, selected: 'first', hiddenIds: ['third', 'fourth']}, - {containerWidth: 550, gap: 0, selected: 'first', hiddenIds: ['fourth']}, + {containerSize: 700, gap: 0, selected: 'first', hiddenIds: []}, + {containerSize: 350, gap: 0, selected: 'first', hiddenIds: ['third', 'fourth']}, + {containerSize: 549, gap: 0, selected: 'first', hiddenIds: ['third', 'fourth']}, + {containerSize: 550, gap: 0, selected: 'first', hiddenIds: ['fourth']}, { - containerWidth: 250, + containerSize: 250, gap: 0, selected: 'second', hiddenIds: ['first', 'third', 'fourth'], }, // gap { - containerWidth: 100, + containerSize: 100, gap: 10, selected: 'first', hiddenIds: ['first', 'second', 'third', 'fourth'], }, { - containerWidth: 199, + containerSize: 199, gap: 10, selected: 'first', hiddenIds: ['first', 'second', 'third', 'fourth'], }, { - containerWidth: 200, + containerSize: 200, gap: 10, selected: 'first', hiddenIds: ['second', 'third', 'fourth'], }, - {containerWidth: 729, gap: 10, selected: 'first', hiddenIds: ['fourth']}, - {containerWidth: 730, gap: 10, selected: 'first', hiddenIds: []}, - {containerWidth: 360, gap: 10, selected: 'first', hiddenIds: ['third', 'fourth']}, - {containerWidth: 559, gap: 10, selected: 'first', hiddenIds: ['third', 'fourth']}, - {containerWidth: 570, gap: 10, selected: 'first', hiddenIds: ['fourth']}, + {containerSize: 729, gap: 10, selected: 'first', hiddenIds: ['fourth']}, + {containerSize: 730, gap: 10, selected: 'first', hiddenIds: []}, + {containerSize: 360, gap: 10, selected: 'first', hiddenIds: ['third', 'fourth']}, + {containerSize: 559, gap: 10, selected: 'first', hiddenIds: ['third', 'fourth']}, + {containerSize: 570, gap: 10, selected: 'first', hiddenIds: ['fourth']}, { - containerWidth: 250, + containerSize: 250, gap: 10, selected: 'second', hiddenIds: ['first', 'third', 'fourth'], }, - ].forEach(({containerWidth, hiddenIds, gap, selected}) => { - it(`when containerWidth is ${containerWidth} and selected is '${selected}' should contain hiddenIds [${hiddenIds.join( + ].forEach(({containerSize, hiddenIds, gap, selected}) => { + it(`when containerSize is ${containerSize} and selected is '${selected}' should contain hiddenIds [${hiddenIds.join( ', ' )}] `, () => { expect( getHiddenIds( - containerWidth, + containerSize, gap, - overflowTargetWidth, - itemWidthCache, + overflowTargeSize, + itemSizeCache, [selected], [ {id: 'first', value: 'first', index: 0, textValue: 'first'}, diff --git a/modules/react/collection/stories/mdx/Collection.mdx b/modules/react/collection/stories/mdx/Collection.mdx index 03fa4d83c9..5d5bee0e93 100644 --- a/modules/react/collection/stories/mdx/Collection.mdx +++ b/modules/react/collection/stories/mdx/Collection.mdx @@ -13,6 +13,7 @@ import {Selection} from './examples/Selection'; import {MultiSelection} from './examples/MultiSelection'; import {BasicGrid} from './examples/BasicGrid'; import {WrappingGrid} from './examples/WrappingGrid'; +import {OverflowVerticalList} from './examples/OverflowVerticalList'; @@ -168,6 +169,16 @@ cursor wraps around columns and rows when an edge of a column or row is encounte +### Overflow Vertical List + +A List can overflow vertically or horizontally to account for responsive resizing or an overflow of +items. Using multiple hooks from the Collection system like `useOverflowListModel` and ensuring that +`orientation`is set to`vertical`, you can achieve vertical overflow lists. In the example below, +when the window is resized vertically, items in the Sidebar will overflow into the "More Actions" +button. + + + ## Component API ### ListBox diff --git a/modules/react/collection/stories/mdx/examples/OverflowVerticalList.tsx b/modules/react/collection/stories/mdx/examples/OverflowVerticalList.tsx new file mode 100644 index 0000000000..f95fa08a7a --- /dev/null +++ b/modules/react/collection/stories/mdx/examples/OverflowVerticalList.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import {ActionBar, useActionBarModel} from '@workday/canvas-kit-react/action-bar'; +import {PrimaryButton} from '@workday/canvas-kit-react/button'; +import {Box} from '@workday/canvas-kit-react/layout'; +import styled from '@emotion/styled'; +import {StyledType} from '@workday/canvas-kit-react/common'; + +type MyActionItem = { + id: string; + text: React.ReactNode; +}; + +const StyledActionbarList = styled(ActionBar.List)({ + '> *': { + flex: '0 0 auto', + }, +}); + +export const OverflowVerticalList = () => { + const [items] = React.useState([ + {id: 'first', text: 'First Action'}, + {id: 'second', text: 'Second Action'}, + {id: 'third', text: 'Third Action'}, + {id: 'fourth', text: 'Fourth Action'}, + {id: 'fifth', text: 'Fifth Action'}, + {id: 'sixth', text: 'Sixth Action'}, + {id: 'seventh', text: 'Seventh Action'}, + ]); + + const model = useActionBarModel({items, orientation: 'vertical', maximumVisible: 4}); + + return ( + <> + + + + } + > + {(item: MyActionItem, index) => ( + console.log(item.id)} + > + {item.text} + + )} + + + + + {(item: MyActionItem) => ( + console.log(item.id)}> + {item.text} + + )} + + + + + + + ); +}; From cbfbb06dd0be8cd5feca6ee2114cd2b039da66db Mon Sep 17 00:00:00 2001 From: Nicholas Boll Date: Wed, 6 Nov 2024 12:00:02 -0700 Subject: [PATCH 06/32] feat(collection): Add removable support (#3036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #3025 Adds `remove` event to the `ListModel`. An `onRemove` config should be added to dynamic lists to remove the item from the collection. The `MultiSelect.Input` uses this new remove event to handle removing items from the Selected pill list when the user uses the “Delete” key. Focus is managed by the collection system when an item is removed. [category:Components] --- cypress/component/Tabs.spec.tsx | 19 +-- .../multi-select/lib/MultiSelectInput.tsx | 113 +++++------------- .../multi-select/lib/MultiSelectedItem.tsx | 46 +++++++ .../multi-select/lib/MultiSelectedList.tsx | 30 +++++ .../lib/useMultiSelectItemRemove.ts | 48 ++++++++ .../multi-select/lib/useMultiSelectModel.ts | 25 +++- .../multi-select/stories/examples/Basic.tsx | 8 +- .../multi-select/stories/examples/Complex.tsx | 1 + .../stories/examples/Controlled.tsx | 1 + .../multi-select/stories/examples/Icons.tsx | 6 +- .../stories/examples/Searching.tsx | 1 + modules/react/collection/index.ts | 3 + .../collection/lib/focusOnCurrentCursor.ts | 55 +++++++++ .../react/collection/lib/listItemRemove.ts | 21 ++++ .../lib/useListItemRemoveOnDeleteKey.tsx | 40 +++++++ .../collection/lib/useListItemRovingFocus.tsx | 42 ++----- modules/react/collection/lib/useListLoader.ts | 2 + .../lib/useListResetCursorOnBlur.tsx | 18 ++- .../collection/lib/useSelectionListModel.tsx | 10 ++ .../combobox/lib/hooks/useComboboxInput.ts | 17 ++- .../select/stories/examples/Controlled.tsx | 7 +- 21 files changed, 372 insertions(+), 141 deletions(-) create mode 100644 modules/preview-react/multi-select/lib/MultiSelectedItem.tsx create mode 100644 modules/preview-react/multi-select/lib/MultiSelectedList.tsx create mode 100644 modules/preview-react/multi-select/lib/useMultiSelectItemRemove.ts create mode 100644 modules/react/collection/lib/focusOnCurrentCursor.ts create mode 100644 modules/react/collection/lib/listItemRemove.ts create mode 100644 modules/react/collection/lib/useListItemRemoveOnDeleteKey.tsx diff --git a/cypress/component/Tabs.spec.tsx b/cypress/component/Tabs.spec.tsx index 027a23761d..1a83048366 100644 --- a/cypress/component/Tabs.spec.tsx +++ b/cypress/component/Tabs.spec.tsx @@ -75,7 +75,7 @@ describe('Tabs', () => { context('when the tab key is pressed', () => { beforeEach(() => { - cy.tab(); + cy.realPress('Tab'); }); it('should move focus to the tabpanel', () => { @@ -148,7 +148,7 @@ describe('Tabs', () => { context('when the tab key is pressed', () => { beforeEach(() => { - cy.tab(); + cy.realPress('Tab'); }); it('should focus on the tab panel of the first tab', () => { @@ -158,7 +158,9 @@ describe('Tabs', () => { // verify the original intent is no longer a tab stop context('when shift + tab keys are pressed', () => { beforeEach(() => { - cy.tab({shift: true}); + // wait for tabindex to reset + cy.findByRole('tab', {name: 'First Tab'}).should('not.have.attr', 'tabindex', '-1'); + cy.realPress(['Shift', 'Tab']); }); it('should not have tabindex=-1 on the first tab', () => { @@ -248,7 +250,7 @@ describe('Tabs', () => { context('when the first tab is active and focused', () => { beforeEach(() => { - cy.findByRole('tab', {name: 'First Tab'}).click().focus(); + cy.findByRole('tab', {name: 'First Tab'}).click(); }); context('when the right arrow key is pressed', () => { @@ -416,7 +418,7 @@ describe('Tabs', () => { context('when the tab key is pressed', () => { beforeEach(() => { - cy.tab(); + cy.realPress('Tab'); }); it('should move focus to the tabpanel', () => { @@ -547,12 +549,15 @@ describe('Tabs', () => { context('when the "First Tab" is focused', () => { beforeEach(() => { - cy.findByRole('tab', {name: 'First Tab'}).focus().tab(); + cy.findByRole('tab', {name: 'First Tab'}).focus(); }); context('when the Tab key is pressed', () => { + beforeEach(() => { + cy.realPress('Tab'); + }); + it('should focus on the "More" button', () => { - cy.findByRole('button', {name: 'More'}).focus(); cy.findByRole('button', {name: 'More'}).should('have.focus'); }); }); diff --git a/modules/preview-react/multi-select/lib/MultiSelectInput.tsx b/modules/preview-react/multi-select/lib/MultiSelectInput.tsx index 03eca34017..f3ca49d0e4 100644 --- a/modules/preview-react/multi-select/lib/MultiSelectInput.tsx +++ b/modules/preview-react/multi-select/lib/MultiSelectInput.tsx @@ -11,16 +11,11 @@ import { import {createStencil, CSProps, handleCsProp} from '@workday/canvas-kit-styling'; import {InputGroup, TextInput} from '@workday/canvas-kit-react/text-input'; import {SystemIcon} from '@workday/canvas-kit-react/icon'; -import { - ListBox, - useListItemRegister, - useListItemRovingFocus, - useListModel, -} from '@workday/canvas-kit-react/collection'; import {useComboboxInput, useComboboxInputConstrained} from '@workday/canvas-kit-react/combobox'; -import {Pill} from '@workday/canvas-kit-preview-react/pill'; import {useMultiSelectModel} from './useMultiSelectModel'; +import {MultiSelectedItemProps} from './MultiSelectedItem'; +import {MultiSelectedList} from './MultiSelectedList'; export const multiSelectStencil = createStencil({ base: { @@ -121,61 +116,28 @@ export const useMultiSelectInput = composeHooks( useComboboxInput ); -const removeItem = (id: string, model: ReturnType) => { - const index = model.state.items.findIndex(item => item.id === model.state.cursorId); - const nextIndex = index === model.state.items.length - 1 ? index - 1 : index + 1; - const nextId = model.state.items[nextIndex].id; - if (model.state.cursorId === id) { - // We're removing the currently focused item. Focus next item - model.events.goTo({id: nextId}); - } -}; - -const useMultiSelectedItem = composeHooks( - createElemPropsHook(useListModel)((model, ref, elemProps) => { - return { - onKeyDown(event: React.KeyboardEvent) { - const id = event.currentTarget.dataset.id || ''; - if (event.key === 'Backspace' || event.key === 'Delete') { - model.events.select({id}); - removeItem(id, model); - } - }, - onClick(event: React.MouseEvent) { - const id = event.currentTarget.dataset.id || ''; - model.events.select({id}); - }, - }; - }), - useListItemRovingFocus, - useListItemRegister -); - -const MultiSelectedItem = createSubcomponent('span')({ - modelHook: useListModel, - elemPropsHook: useMultiSelectedItem, -})(({children, ref, ...elemProps}, Element) => { - return ( - - {children} - - - ); -}); - export interface MultiSelectInputProps extends CSProps, Pick< React.InputHTMLAttributes, 'disabled' | 'className' | 'style' | 'aria-labelledby' - > {} + >, + Pick {} export const MultiSelectInput = createSubcomponent(TextInput)({ modelHook: useMultiSelectModel, elemPropsHook: useMultiSelectInput, })( ( - {className, cs, style, 'aria-labelledby': ariaLabelledBy, formInputProps, ...elemProps}, + { + className, + cs, + style, + 'aria-labelledby': ariaLabelledBy, + removeLabel, + formInputProps, + ...elemProps + }, Element, model ) => { @@ -194,20 +156,7 @@ export const MultiSelectInput = createSubcomponent(TextInput)({ - {model.selected.state.items.length ? ( - <> -
- - {item => {item.textValue}} - - - ) : null} +
); } @@ -218,7 +167,16 @@ export const MultiSelectSearchInput = createSubcomponent(TextInput)({ elemPropsHook: useMultiSelectInput, })( ( - {className, cs, style, 'aria-labelledby': ariaLabelledBy, formInputProps, ref, ...elemProps}, + { + className, + cs, + style, + 'aria-labelledby': ariaLabelledBy, + removeLabel, + formInputProps, + ref, + ...elemProps + }, Element, model ) => { @@ -228,34 +186,25 @@ export const MultiSelectSearchInput = createSubcomponent(TextInput)({ - + - + - {model.selected.state.items.length ? ( - <> -
- - {item => {item.textValue}} - - - ) : null} +
); } diff --git a/modules/preview-react/multi-select/lib/MultiSelectedItem.tsx b/modules/preview-react/multi-select/lib/MultiSelectedItem.tsx new file mode 100644 index 0000000000..9e23587392 --- /dev/null +++ b/modules/preview-react/multi-select/lib/MultiSelectedItem.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { + composeHooks, + createElemPropsHook, + createSubModelElemPropsHook, + createSubcomponent, +} from '@workday/canvas-kit-react/common'; +import {useListItemRegister, useListItemRovingFocus} from '@workday/canvas-kit-react/collection'; +import {Pill} from '@workday/canvas-kit-preview-react/pill'; + +import {useMultiSelectItemRemove} from './useMultiSelectItemRemove'; +import {useMultiSelectModel} from './useMultiSelectModel'; + +export interface MultiSelectedItemProps { + /** + * Remove label on a MultiSelectedItem. In English, the label may be "Remove" and the screen + * reader will read out "Remove {option}". + * + * @default "remove" + */ + removeLabel?: string; +} + +export const useMultiSelectedItem = composeHooks( + createElemPropsHook(useMultiSelectModel)(model => { + return { + 'aria-selected': true, + }; + }), + useMultiSelectItemRemove, + createSubModelElemPropsHook(useMultiSelectModel)(m => m.selected, useListItemRovingFocus), + createSubModelElemPropsHook(useMultiSelectModel)(m => m.selected, useListItemRegister) +); + +export const MultiSelectedItem = createSubcomponent('span')({ + modelHook: useMultiSelectModel, + elemPropsHook: useMultiSelectedItem, +})(({children, removeLabel, ref, ...elemProps}, Element) => { + return ( + + {children} + + + ); +}); diff --git a/modules/preview-react/multi-select/lib/MultiSelectedList.tsx b/modules/preview-react/multi-select/lib/MultiSelectedList.tsx new file mode 100644 index 0000000000..671a9092d8 --- /dev/null +++ b/modules/preview-react/multi-select/lib/MultiSelectedList.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import {createSubcomponent} from '@workday/canvas-kit-react/common'; +import {ListBox} from '@workday/canvas-kit-react/collection'; + +import {useMultiSelectModel} from './useMultiSelectModel'; +import {MultiSelectedItem, MultiSelectedItemProps} from './MultiSelectedItem'; + +export interface MultiSelectedListProps + extends MultiSelectedItemProps, + React.HTMLAttributes {} + +export const MultiSelectedList = createSubcomponent()({ + modelHook: useMultiSelectModel, +})(({'aria-labelledby': ariaLabelledBy, removeLabel}, Element, model) => { + return model.selected.state.items.length ? ( + <> +
+ + {item => {item.textValue}} + + + ) : null; +}); diff --git a/modules/preview-react/multi-select/lib/useMultiSelectItemRemove.ts b/modules/preview-react/multi-select/lib/useMultiSelectItemRemove.ts new file mode 100644 index 0000000000..1c183d6224 --- /dev/null +++ b/modules/preview-react/multi-select/lib/useMultiSelectItemRemove.ts @@ -0,0 +1,48 @@ +import React from 'react'; +import {createElemPropsHook} from '@workday/canvas-kit-react/common'; + +import {useMultiSelectModel} from './useMultiSelectModel'; +import {focusOnCurrentCursor, listItemRemove} from '@workday/canvas-kit-react/collection'; + +/** + * This elemProps hook is used when a menu item is expected to be removed. It will advance the cursor to + * another item. + * This elemProps hook is used for cursor navigation by using [Roving + * Tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex). Only a single item in the + * collection has a tab stop. Pressing an arrow key moves the tab stop to a different item in the + * corresponding direction. See the [Roving Tabindex](#roving-tabindex) example. This elemProps hook + * should be applied to an `*.Item` component. + * + * ```ts + * const useMyItem = composeHooks( + * useListItemRovingFocus, // adds the roving tabindex support + * useListItemRegister + * ); + * ``` + */ +export const useMultiSelectItemRemove = createElemPropsHook(useMultiSelectModel)((model, _ref) => { + return { + onKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Backspace' || event.key === 'Delete') { + const id = event.currentTarget.dataset.id || ''; + const nextId = listItemRemove(id, model.selected); + model.selected.events.remove({id, event}); + if (nextId) { + focusOnCurrentCursor(model.selected, nextId, event.currentTarget); + } else { + model.state.inputRef.current?.focus(); + } + } + }, + onClick(event: React.MouseEvent) { + const id = event.currentTarget.dataset.id || ''; + const nextId = listItemRemove(id, model.selected); + model.selected.events.remove({id, nextId, event}); + if (nextId) { + focusOnCurrentCursor(model.selected, nextId, event.currentTarget); + } else { + model.state.inputRef.current?.focus(); + } + }, + }; +}); diff --git a/modules/preview-react/multi-select/lib/useMultiSelectModel.ts b/modules/preview-react/multi-select/lib/useMultiSelectModel.ts index f96377619f..dc4a369028 100644 --- a/modules/preview-react/multi-select/lib/useMultiSelectModel.ts +++ b/modules/preview-react/multi-select/lib/useMultiSelectModel.ts @@ -34,6 +34,10 @@ export const useMultiSelectModel = createModelHook({ useComboboxModel.mergeConfig(config, { onHide() { setSelectedItems(cachedSelected); + model.events.goTo({id: ''}); + }, + onFilterChange() { + model.events.goTo({id: ''}); }, }) ); @@ -60,7 +64,7 @@ export const useMultiSelectModel = createModelHook({ // The `listbox` of pills under the MultiSelect combobox input. const selected = useListModel({ orientation: 'horizontal', - onSelect({id}) { + onRemove({id}) { model.events.select({id}); }, shouldVirtualize: false, @@ -75,5 +79,22 @@ export const useMultiSelectModel = createModelHook({ ...model.events, }; - return {selected, ...model, state, events}; + return { + selected: { + ...selected, + state: { + ...selected.state, + cursorId: React.useMemo( + () => + selected.state.items.find(item => item.id === selected.state.cursorId) + ? selected.state.cursorId + : selected.state.items[0]?.id || '', + [selected.state.items, selected.state.cursorId] + ), + }, + }, + ...model, + state, + events, + }; }); diff --git a/modules/preview-react/multi-select/stories/examples/Basic.tsx b/modules/preview-react/multi-select/stories/examples/Basic.tsx index 01267e597b..1fe783607f 100644 --- a/modules/preview-react/multi-select/stories/examples/Basic.tsx +++ b/modules/preview-react/multi-select/stories/examples/Basic.tsx @@ -8,10 +8,14 @@ const items = ['Cheese', 'Olives', 'Onions', 'Pepperoni', 'Peppers']; export const Basic = () => { return ( <> - + Toppings - + diff --git a/modules/preview-react/multi-select/stories/examples/Complex.tsx b/modules/preview-react/multi-select/stories/examples/Complex.tsx index a247532c8b..04d34f3844 100644 --- a/modules/preview-react/multi-select/stories/examples/Complex.tsx +++ b/modules/preview-react/multi-select/stories/examples/Complex.tsx @@ -39,6 +39,7 @@ export const Complex = () => { { const value = e.currentTarget.value; diff --git a/modules/preview-react/multi-select/stories/examples/Controlled.tsx b/modules/preview-react/multi-select/stories/examples/Controlled.tsx index ae03e4886f..5aa4bfdbe6 100644 --- a/modules/preview-react/multi-select/stories/examples/Controlled.tsx +++ b/modules/preview-react/multi-select/stories/examples/Controlled.tsx @@ -46,6 +46,7 @@ export const Controlled = () => { { Controls - + diff --git a/modules/preview-react/multi-select/stories/examples/Searching.tsx b/modules/preview-react/multi-select/stories/examples/Searching.tsx index 7d08f00652..8034ad1362 100644 --- a/modules/preview-react/multi-select/stories/examples/Searching.tsx +++ b/modules/preview-react/multi-select/stories/examples/Searching.tsx @@ -88,6 +88,7 @@ export const Searching = () => { { setValue(e.currentTarget.value); diff --git a/modules/react/collection/index.ts b/modules/react/collection/index.ts index 77681d1026..f390fcb936 100644 --- a/modules/react/collection/index.ts +++ b/modules/react/collection/index.ts @@ -15,6 +15,9 @@ export * from './lib/useGridModel'; export * from './lib/useListActiveDescendant'; export * from './lib/useListItemActiveDescendant'; export * from './lib/useListItemAllowChildStrings'; +export * from './lib/useListItemRemoveOnDeleteKey'; +export * from './lib/focusOnCurrentCursor'; +export * from './lib/listItemRemove'; export {ListBox, ListBoxProps} from './lib/ListBox'; export {keyboardEventToCursorEvents} from './lib/keyUtils'; export { diff --git a/modules/react/collection/lib/focusOnCurrentCursor.ts b/modules/react/collection/lib/focusOnCurrentCursor.ts new file mode 100644 index 0000000000..f6b51abae4 --- /dev/null +++ b/modules/react/collection/lib/focusOnCurrentCursor.ts @@ -0,0 +1,55 @@ +import {useCursorListModel} from './useCursorListModel'; + +// retry a function each frame so we don't rely on the timing mechanism of React's render cycle. +const retryEachFrame = (cb: () => boolean, iterations: number, reject?: (reason?: any) => void) => { + if (cb() === false && iterations > 1) { + requestAnimationFrame(() => retryEachFrame(cb, iterations - 1)); + } + reject?.('Retry timeout'); +}; + +export const focusOnCurrentCursor = ( + model: ReturnType, + nextId: string, + /** + * This can be any element in the list. It is used only to get the client-id from the element in + * case it is different than the server ID when DOM is hydrated. + */ + element?: HTMLElement +) => { + return new Promise((resolve, reject) => { + // Attempt to extract the ID from the DOM element. This fixes issues where the server and client + // do not agree on a generated ID + const clientId = (element?.dataset?.focusId || '').split('-')[0] || model.state.id; + + const item = model.navigation.getItem(nextId, model); + + if (item) { + // If the list is virtualized, we need to manually call out to the virtual list's + // `scrollToIndex` + if (model.state.isVirtualized) { + model.state.UNSTABLE_virtual.scrollToIndex(item.index); + } + + const getElement = (id?: string) => { + return document.querySelector(`[data-focus-id="${`${id}-${item.id}`}"]`); + }; + + // In React concurrent mode, there could be several render attempts before the element we're + // looking for could be available in the DOM + retryEachFrame( + () => { + const element = getElement(clientId) || getElement(model.state.id); + + if (element) { + element.focus(); + resolve(element); + } + return !!element; + }, + 5, + reject + ); // 5 should be enough, right?! + } + }); +}; diff --git a/modules/react/collection/lib/listItemRemove.ts b/modules/react/collection/lib/listItemRemove.ts new file mode 100644 index 0000000000..dac25170cf --- /dev/null +++ b/modules/react/collection/lib/listItemRemove.ts @@ -0,0 +1,21 @@ +import {useSelectionListModel} from './useSelectionListModel'; + +export const listItemRemove = ( + id: string, + model: ReturnType +): string | undefined => { + // bail early if an ID isn't available + if (!id) { + return; + } + + const index = model.state.items.findIndex(item => item.id === model.state.cursorId); + const nextIndex = index === model.state.items.length - 1 ? index - 1 : index + 1; + const nextId = model.state.items[nextIndex]?.id; + if (nextId && model.state.cursorId === id) { + // We're removing the currently focused item. Focus next item + model.events.goTo({id: nextId}); + } + + return nextId; +}; diff --git a/modules/react/collection/lib/useListItemRemoveOnDeleteKey.tsx b/modules/react/collection/lib/useListItemRemoveOnDeleteKey.tsx new file mode 100644 index 0000000000..1671ac23ed --- /dev/null +++ b/modules/react/collection/lib/useListItemRemoveOnDeleteKey.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {createElemPropsHook} from '@workday/canvas-kit-react/common'; + +import {useSelectionListModel} from './useSelectionListModel'; +import {focusOnCurrentCursor} from './focusOnCurrentCursor'; +import {listItemRemove} from './listItemRemove'; + +/** + * This elemProps hook is used when a menu item is expected to be removed. It will advance the cursor to + * another item. + * This elemProps hook is used for cursor navigation by using [Roving + * Tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex). Only a single item in the + * collection has a tab stop. Pressing an arrow key moves the tab stop to a different item in the + * corresponding direction. See the [Roving Tabindex](#roving-tabindex) example. This elemProps hook + * should be applied to an `*.Item` component. + * + * ```ts + * const useMyItem = composeHooks( + * useListItemRovingFocus, // adds the roving tabindex support + * useListItemRegister + * ); + * ``` + */ +export const useListItemRemoveOnDeleteKey = createElemPropsHook(useSelectionListModel)(model => { + return { + onKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Backspace' || event.key === 'Delete') { + const id = event.currentTarget.dataset.id || ''; + const nextId = listItemRemove(id, model); + model.events.remove({id, nextId, event}); + if (nextId) { + // use an animation frame to wait for any other model changes that may happen + requestAnimationFrame(() => { + focusOnCurrentCursor(model, nextId, event.currentTarget); + }); + } + } + }, + }; +}); diff --git a/modules/react/collection/lib/useListItemRovingFocus.tsx b/modules/react/collection/lib/useListItemRovingFocus.tsx index c7eebd0695..36894a477e 100644 --- a/modules/react/collection/lib/useListItemRovingFocus.tsx +++ b/modules/react/collection/lib/useListItemRovingFocus.tsx @@ -3,13 +3,7 @@ import {useIsRTL, createElemPropsHook} from '@workday/canvas-kit-react/common'; import {useCursorListModel} from './useCursorListModel'; import {keyboardEventToCursorEvents} from './keyUtils'; - -// retry a function each frame so we don't rely on the timing mechanism of React's render cycle. -const retryEachFrame = (cb: () => boolean, iterations: number) => { - if (cb() === false && iterations > 1) { - requestAnimationFrame(() => retryEachFrame(cb, iterations - 1)); - } -}; +import {focusOnCurrentCursor} from './focusOnCurrentCursor'; /** * This elemProps hook is used for cursor navigation by using [Roving @@ -33,36 +27,16 @@ export const useListItemRovingFocus = createElemPropsHook(useCursorListModel)( const stateRef = React.useRef(model.state); stateRef.current = model.state; - const keyElementRef = React.useRef(null); + const keyElementRef = React.useRef(null); const isRTL = useIsRTL(); React.useEffect(() => { + // If the cursor change was triggered by this hook, we should change focus if (keyElementRef.current) { - const item = model.navigation.getItem(model.state.cursorId, model); - if (item) { - if (model.state.isVirtualized) { - model.state.UNSTABLE_virtual.scrollToIndex(item.index); - } - - const selector = (id?: string) => { - return document.querySelector(`[data-focus-id="${`${id}-${item.id}`}"]`); - }; - - // In React concurrent mode, there could be several render attempts before the element we're - // looking for could be available in the DOM - retryEachFrame(() => { - // Attempt to extract the ID from the DOM element. This fixes issues where the server and client - // do not agree on a generated ID - const clientId = keyElementRef.current?.getAttribute('data-focus-id')?.split('-')[0]; - const element = selector(clientId) || selector(model.state.id); - - element?.focus(); - if (element) { - keyElementRef.current = null; - } - return !!element; - }, 5); // 5 should be enough, right?! - } + focusOnCurrentCursor(model, model.state.cursorId, keyElementRef.current).then(() => { + // Reset key element since focus was successful + keyElementRef.current = null; + }); } // we only want to run this effect if the cursor changes and not any other time // eslint-disable-next-line react-hooks/exhaustive-deps @@ -76,7 +50,7 @@ export const useListItemRovingFocus = createElemPropsHook(useCursorListModel)( }, [model.state.cursorId, model.state.items, model.events]); return { - onKeyDown(event: React.KeyboardEvent) { + onKeyDown(event: React.KeyboardEvent) { const handled = keyboardEventToCursorEvents(event, model, isRTL); if (handled) { event.preventDefault(); diff --git a/modules/react/collection/lib/useListLoader.ts b/modules/react/collection/lib/useListLoader.ts index a64589f16f..b85d77b52f 100644 --- a/modules/react/collection/lib/useListLoader.ts +++ b/modules/react/collection/lib/useListLoader.ts @@ -269,6 +269,8 @@ export function useListLoader< const model = modelHook( modelHook.mergeConfig(config, { + // Loaders should virtualize by default. If they do not, it is an infinite scroll list + shouldVirtualize: true, items, shouldGoToNext: shouldLoadIndex('getNext', 'goToNext'), shouldGoToPrevious: shouldLoadIndex('getPrevious', 'goToPrevious'), diff --git a/modules/react/collection/lib/useListResetCursorOnBlur.tsx b/modules/react/collection/lib/useListResetCursorOnBlur.tsx index 4de675189d..7b32650e89 100644 --- a/modules/react/collection/lib/useListResetCursorOnBlur.tsx +++ b/modules/react/collection/lib/useListResetCursorOnBlur.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {createElemPropsHook} from '@workday/canvas-kit-react/common'; +import {createElemPropsHook, useMountLayout} from '@workday/canvas-kit-react/common'; import {orientationKeyMap} from './keyUtils'; import {useListModel} from './useListModel'; @@ -20,6 +20,15 @@ import {useListModel} from './useListModel'; */ export const useListResetCursorOnBlur = createElemPropsHook(useListModel)(({state, events}) => { const programmaticFocusRef = React.useRef(false); + const requestAnimationFrameRef = React.useRef(0); + + useMountLayout(() => { + return () => { + // Cancelling the animation frame prevents React unmount errors + cancelAnimationFrame(requestAnimationFrameRef.current); + }; + }); + return { onKeyDown(event: React.KeyboardEvent) { // Programmatic focus only on any focus change via keyboard @@ -32,7 +41,12 @@ export const useListResetCursorOnBlur = createElemPropsHook(useListModel)(({stat }, onBlur() { if (!programmaticFocusRef.current) { - events.goTo({id: state.selectedIds[0]}); + // use an animation frame to wait for any other model changes that may happen on a blur + requestAnimationFrameRef.current = requestAnimationFrame(() => { + if (state.selectedIds[0] !== state.cursorId) { + events.goTo({id: state.selectedIds[0]}); + } + }); } }, }; diff --git a/modules/react/collection/lib/useSelectionListModel.tsx b/modules/react/collection/lib/useSelectionListModel.tsx index 6363860bb9..259e35832a 100644 --- a/modules/react/collection/lib/useSelectionListModel.tsx +++ b/modules/react/collection/lib/useSelectionListModel.tsx @@ -109,6 +109,16 @@ export const useSelectionListModel = createModelHook({ setSelectedIds(ids: 'all' | string[]) { setSelectedIds(ids); }, + /** + * The `remove` event can be called by Behavior Hooks based on user interaction. The `onRemove` + * can be added to the model config to signal the user wishes to remove the item in the list. + * The `remove` event requires the dynamic API where `items` are passed to the model. It is up + * to you to remove the item from the list. Focus redirection should be automatically managed, + * if necessary. + */ + remove(data: {id: string; nextId?: string; event?: Event | React.SyntheticEvent}) { + // nothing to do here. It is a signal event + }, }; return {...cursor, state, events, selection}; diff --git a/modules/react/combobox/lib/hooks/useComboboxInput.ts b/modules/react/combobox/lib/hooks/useComboboxInput.ts index 8fe6b4303d..9702bf21a2 100644 --- a/modules/react/combobox/lib/hooks/useComboboxInput.ts +++ b/modules/react/combobox/lib/hooks/useComboboxInput.ts @@ -26,16 +26,13 @@ export const useComboboxInput = composeHooks( if (model.state.isVirtualized && item) { model.state.UNSTABLE_virtual.scrollToIndex(item.index); } else { - const listboxId = model.state.inputRef.current?.getAttribute('aria-controls'); - if (listboxId) { - const menuItem = document.querySelector( - `[id="${listboxId}"] [data-id="${model.state.cursorId}"]` - ); - if (menuItem) { - requestAnimationFrame(() => { - menuItem.scrollIntoView({block: 'nearest'}); - }); - } + const menuItem = document.querySelector( + `[id="${model.state.id}-list"] [data-id="${model.state.cursorId}"]` + ); + if (menuItem) { + requestAnimationFrame(() => { + menuItem.scrollIntoView({block: 'nearest'}); + }); } } } diff --git a/modules/react/select/stories/examples/Controlled.tsx b/modules/react/select/stories/examples/Controlled.tsx index f170db78d4..c6984ad29f 100644 --- a/modules/react/select/stories/examples/Controlled.tsx +++ b/modules/react/select/stories/examples/Controlled.tsx @@ -36,7 +36,12 @@ export const Controlled = () => { Contact