diff --git a/.github/workflows/bundle-size/package-lock.json b/.github/workflows/bundle-size/package-lock.json index 540fd9f8da..25e258f7d4 100644 --- a/.github/workflows/bundle-size/package-lock.json +++ b/.github/workflows/bundle-size/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "bundle-size-test-project", "version": "0.0.0", + "hasInstallScript": true, "dependencies": { "@cloudscape-design/components": "*", "@vitejs/plugin-react": "^4.0.0", @@ -344,72 +345,6 @@ "node": ">=6.9.0" } }, - "node_modules/@cloudscape-design/collection-hooks": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.21.tgz", - "integrity": "sha512-H2svaXsqHK/XX2CAgU0hxIqiiAh3rvlG490DpNHGbQ8ouXCURc8Hz0Hws+DbZ/etEczpyW44uBBO6xfIXE41iQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@cloudscape-design/component-toolkit": { - "version": "1.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@cloudscape-design/component-toolkit/-/component-toolkit-1.0.0-beta.14.tgz", - "integrity": "sha512-FUncISDyZJMOuuFDFzzXUdrew+deTpF7oJeox34b9Xm69L7fJYQV5mOkKckFW5/lnE52n3pENfU5hLb+ic+baw==", - "dependencies": { - "@juggle/resize-observer": "^3.3.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@cloudscape-design/components": { - "version": "3.0.306", - "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.306.tgz", - "integrity": "sha512-zNOaIohtn2ZrH6O8h1CtNjUJ4VrTd4h7d5psOUtGuLV7RKojl1UsJ8eXAGzbCYi3dPte/UdDjmnoB3WwIEEZBw==", - "dependencies": { - "@cloudscape-design/collection-hooks": "^1.0.0", - "@cloudscape-design/component-toolkit": "^1.0.0-beta", - "@cloudscape-design/test-utils-core": "^1.0.0", - "@cloudscape-design/theming-runtime": "^1.0.0", - "@dnd-kit/core": "^6.0.8", - "@dnd-kit/sortable": "^7.0.2", - "@dnd-kit/utilities": "^3.2.1", - "@juggle/resize-observer": "^3.3.1", - "ace-builds": "^1.4.13", - "balanced-match": "^1.0.2", - "clsx": "^1.1.0", - "d3-shape": "^1.3.7", - "date-fns": "^2.25.0", - "intl-messageformat": "^10.3.1", - "mnth": "^2.0.0", - "react-focus-lock": "~2.8.1", - "react-keyed-flatten-children": "^1.3.0", - "react-transition-group": "^4.4.2", - "react-virtual": "^2.8.2", - "tslib": "^2.4.0", - "weekstart": "^1.1.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17 || ^18", - "react-dom": "^16.8 || ^17 || ^18" - } - }, - "node_modules/@cloudscape-design/test-utils-core": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.10.tgz", - "integrity": "sha512-JScy8oujpinTwyPcF0JA1fhxbinvaJANuHjWeVeE63WW3/oum+30Q7f96yJu3sGOlEbCJ3EQ/rpWgaBoE9pq7Q==", - "dependencies": { - "css-selector-tokenizer": "^0.8.0", - "css.escape": "^1.5.1" - } - }, - "node_modules/@cloudscape-design/theming-runtime": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-runtime/-/theming-runtime-1.0.17.tgz", - "integrity": "sha512-vgZ3D3lp3m/48/rfifZNtjNozZ43SO4zWkhHN6jNua/TattCfJo8WqURbNUwh7KDSsJOdls7Ni0HQvl9GzJXJQ==", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@dnd-kit/accessibility": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", @@ -1936,66 +1871,6 @@ "to-fast-properties": "^2.0.0" } }, - "@cloudscape-design/collection-hooks": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.21.tgz", - "integrity": "sha512-H2svaXsqHK/XX2CAgU0hxIqiiAh3rvlG490DpNHGbQ8ouXCURc8Hz0Hws+DbZ/etEczpyW44uBBO6xfIXE41iQ==", - "requires": {} - }, - "@cloudscape-design/component-toolkit": { - "version": "1.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@cloudscape-design/component-toolkit/-/component-toolkit-1.0.0-beta.14.tgz", - "integrity": "sha512-FUncISDyZJMOuuFDFzzXUdrew+deTpF7oJeox34b9Xm69L7fJYQV5mOkKckFW5/lnE52n3pENfU5hLb+ic+baw==", - "requires": { - "@juggle/resize-observer": "^3.3.1", - "tslib": "^2.3.1" - } - }, - "@cloudscape-design/components": { - "version": "3.0.306", - "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.306.tgz", - "integrity": "sha512-zNOaIohtn2ZrH6O8h1CtNjUJ4VrTd4h7d5psOUtGuLV7RKojl1UsJ8eXAGzbCYi3dPte/UdDjmnoB3WwIEEZBw==", - "requires": { - "@cloudscape-design/collection-hooks": "^1.0.0", - "@cloudscape-design/component-toolkit": "^1.0.0-beta", - "@cloudscape-design/test-utils-core": "^1.0.0", - "@cloudscape-design/theming-runtime": "^1.0.0", - "@dnd-kit/core": "^6.0.8", - "@dnd-kit/sortable": "^7.0.2", - "@dnd-kit/utilities": "^3.2.1", - "@juggle/resize-observer": "^3.3.1", - "ace-builds": "^1.4.13", - "balanced-match": "^1.0.2", - "clsx": "^1.1.0", - "d3-shape": "^1.3.7", - "date-fns": "^2.25.0", - "intl-messageformat": "^10.3.1", - "mnth": "^2.0.0", - "react-focus-lock": "~2.8.1", - "react-keyed-flatten-children": "^1.3.0", - "react-transition-group": "^4.4.2", - "react-virtual": "^2.8.2", - "tslib": "^2.4.0", - "weekstart": "^1.1.0" - } - }, - "@cloudscape-design/test-utils-core": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.10.tgz", - "integrity": "sha512-JScy8oujpinTwyPcF0JA1fhxbinvaJANuHjWeVeE63WW3/oum+30Q7f96yJu3sGOlEbCJ3EQ/rpWgaBoE9pq7Q==", - "requires": { - "css-selector-tokenizer": "^0.8.0", - "css.escape": "^1.5.1" - } - }, - "@cloudscape-design/theming-runtime": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-runtime/-/theming-runtime-1.0.17.tgz", - "integrity": "sha512-vgZ3D3lp3m/48/rfifZNtjNozZ43SO4zWkhHN6jNua/TattCfJo8WqURbNUwh7KDSsJOdls7Ni0HQvl9GzJXJQ==", - "requires": { - "tslib": "^2.4.0" - } - }, "@dnd-kit/accessibility": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", diff --git a/.github/workflows/bundle-size/package.json b/.github/workflows/bundle-size/package.json index 6adaf36f7e..cb19638d4f 100644 --- a/.github/workflows/bundle-size/package.json +++ b/.github/workflows/bundle-size/package.json @@ -4,7 +4,8 @@ "version": "0.0.0", "type": "module", "scripts": { - "build": "node build.js" + "build": "node build.js", + "preinstall": "node ./unlock-package-lock.cjs" }, "dependencies": { "@cloudscape-design/components": "*", diff --git a/.github/workflows/bundle-size/unlock-package-lock.cjs b/.github/workflows/bundle-size/unlock-package-lock.cjs new file mode 100644 index 0000000000..f599682120 --- /dev/null +++ b/.github/workflows/bundle-size/unlock-package-lock.cjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +const fs = require('fs'); +const path = require('path'); + +/** + * Remove specific @cloudscape-design/* packages where we should always use the latest minor release. + */ +const filename = path.resolve(__dirname, 'package-lock.json'); +const packageLock = JSON.parse(fs.readFileSync(filename)); + +Object.keys(packageLock.packages).forEach(dependencyName => { + removeDependencies(dependencyName, packageLock.packages); +}); + +Object.keys(packageLock.dependencies).forEach(dependencyName => { + removeDependencies(dependencyName, packageLock.dependencies); +}); + +fs.writeFileSync(filename, JSON.stringify(packageLock, null, 2) + '\n'); +console.log('Removed @cloudscape-design/ dependencies from package-lock file'); + +function removeDependencies(dependencyName, packages) { + if (dependencyName.includes('@cloudscape-design/')) { + delete packages[dependencyName]; + } +} diff --git a/README.md b/README.md index 2423184f65..6811d2b364 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Here is a basic example that renders a primary button: import Button from '@cloudscape-design/components/button'; import '@cloudscape-design/global-styles/index.css'; -function App { +function App() { return ; } ``` diff --git a/package-lock.json b/package-lock.json index 8731d611ab..dbf73401f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -749,122 +749,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@cloudscape-design/browser-test-tools": { - "version": "3.0.30", - "resolved": "https://registry.npmjs.org/@cloudscape-design/browser-test-tools/-/browser-test-tools-3.0.30.tgz", - "integrity": "sha512-JoW7rlwJCVBIVUJ5IVR/rCqV2DLE4wrxn8vAF5DUN6U7KC0k8WsmtS28oo0lqftfdUYPSz3U97Bb+3NmPQIC9Q==", - "dev": true, - "dependencies": { - "@types/pngjs": "^6.0.1", - "aws-sdk": "^2.1354.0", - "get-stream": "^6.0.1", - "lodash": "^4.17.21", - "p-retry": "^4.6.2", - "pixelmatch": "^5.3.0", - "pngjs": "^6.0.0", - "webdriverio": "^7.25.2" - } - }, - "node_modules/@cloudscape-design/collection-hooks": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.22.tgz", - "integrity": "sha512-U5n438CmBuh+FdpREHYlViX/qCU7WcChBJ2gDAeWllNYEtb4/jwtQ2SITuipAZ9QONahkBCQX9JQNKzDfPnPEA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@cloudscape-design/component-toolkit": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/@cloudscape-design/component-toolkit/-/component-toolkit-1.0.0-beta.19.tgz", - "integrity": "sha512-uk53JrX70DBP1xytDENeu6oxZhpBYJqz8dUGG9ku+MCRhOJFuVC2Uadn2BQvFh3TBTXE4Xj98I669pZOa6C5sA==", - "dependencies": { - "@juggle/resize-observer": "^3.3.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@cloudscape-design/documenter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cloudscape-design/documenter/-/documenter-1.0.11.tgz", - "integrity": "sha512-DtezRxmwq5vxd74pPHmKvZ6dTHnQRl6ZFRcCAkzzhHZKnvQlU0mEO5As1ZymggJ69OMcoLB+iczvUpnSYsa7tQ==", - "dev": true, - "dependencies": { - "change-case": "^4.1.1", - "micromatch": "^4.0.2", - "typedoc": "~0.19.2" - } - }, - "node_modules/@cloudscape-design/eslint-plugin": { - "resolved": "build-tools/eslint", - "link": true - }, - "node_modules/@cloudscape-design/global-styles": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cloudscape-design/global-styles/-/global-styles-1.0.11.tgz", - "integrity": "sha512-pHGImwi5K+Dk/FUQHTvoVeEowWZAoRfTMzIEz2eri5fbK4U/Xp7UuhEqoWhNoLDtcw87/o2qHTP0We95mhVOqg==", - "dev": true - }, - "node_modules/@cloudscape-design/jest-preset": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@cloudscape-design/jest-preset/-/jest-preset-2.0.16.tgz", - "integrity": "sha512-Caf52TPSL/flvS8q/o0BQnzRoWEUb6iaKvzU1SKdVn1OD9gVnJD5mC6HK/RPhFIjkxzh7V+Ce/pAZavvYmux5w==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-modules-commonjs": "^7.9.0" - }, - "peerDependencies": { - "babel-jest": ">=24", - "jest": ">=24" - } - }, - "node_modules/@cloudscape-design/test-utils-converter": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-converter/-/test-utils-converter-1.0.12.tgz", - "integrity": "sha512-nh9iHPP3THidg4lIw0yZTA2PGxNKzjvBbmDr2oHXa6nXZ5VokdSgkIUi1HZRwNWuSyyjuZyMg5YgNVYqEWPgQQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-syntax-decorators": "^7.16.0", - "@babel/plugin-syntax-typescript": "^7.16.0" - } - }, - "node_modules/@cloudscape-design/test-utils-core": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.12.tgz", - "integrity": "sha512-dSyFtq73nZM8AQeanBJhZazZSufRsGzYe144qAr8yGWpDZzxSkj8YgyjJccHVE0avGwj+aunL6SMlUkILaZw0Q==", - "dependencies": { - "css-selector-tokenizer": "^0.8.0", - "css.escape": "^1.5.1" - } - }, - "node_modules/@cloudscape-design/theming-build": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-build/-/theming-build-1.0.36.tgz", - "integrity": "sha512-PwaiX9xqpnrTrFEBpVfwgRg2ydiOhzvBGB8XkZFXhqaDEqKr+hdT7r7iCfMET7UZoXjme/zntK9cMyAydPWVFA==", - "dev": true, - "dependencies": { - "autoprefixer": "^10.4.8", - "glob": "^7.2.3", - "jsonschema": "^1.4.1", - "loader-utils": "^3.2.1", - "lodash": "^4.17.21", - "postcss": "^8.4.16", - "postcss-discard-empty": "^5.1.1", - "postcss-initial": "^3.0.4", - "postcss-inline-svg": "^5.0.0", - "postcss-modules": "^4.3.1", - "sass": "^1.49.0", - "string-hash": "^1.1.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@cloudscape-design/theming-runtime": { - "version": "1.0.29", - "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-runtime/-/theming-runtime-1.0.29.tgz", - "integrity": "sha512-UC49ObLxLmmsADpuAZN8UkelVyiIQgCLY+1rpMkLoWIXBhhz+vwxBzfKLLFv2pC4zuEI16/1VwO6IYWtOzMK3A==", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz", @@ -19565,118 +19449,6 @@ "version": "0.2.3", "dev": true }, - "@cloudscape-design/browser-test-tools": { - "version": "3.0.30", - "resolved": "https://registry.npmjs.org/@cloudscape-design/browser-test-tools/-/browser-test-tools-3.0.30.tgz", - "integrity": "sha512-JoW7rlwJCVBIVUJ5IVR/rCqV2DLE4wrxn8vAF5DUN6U7KC0k8WsmtS28oo0lqftfdUYPSz3U97Bb+3NmPQIC9Q==", - "dev": true, - "requires": { - "@types/pngjs": "^6.0.1", - "aws-sdk": "^2.1354.0", - "get-stream": "^6.0.1", - "lodash": "^4.17.21", - "p-retry": "^4.6.2", - "pixelmatch": "^5.3.0", - "pngjs": "^6.0.0", - "webdriverio": "^7.25.2" - } - }, - "@cloudscape-design/collection-hooks": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.22.tgz", - "integrity": "sha512-U5n438CmBuh+FdpREHYlViX/qCU7WcChBJ2gDAeWllNYEtb4/jwtQ2SITuipAZ9QONahkBCQX9JQNKzDfPnPEA==", - "requires": {} - }, - "@cloudscape-design/component-toolkit": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/@cloudscape-design/component-toolkit/-/component-toolkit-1.0.0-beta.19.tgz", - "integrity": "sha512-uk53JrX70DBP1xytDENeu6oxZhpBYJqz8dUGG9ku+MCRhOJFuVC2Uadn2BQvFh3TBTXE4Xj98I669pZOa6C5sA==", - "requires": { - "@juggle/resize-observer": "^3.3.1", - "tslib": "^2.3.1" - } - }, - "@cloudscape-design/documenter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cloudscape-design/documenter/-/documenter-1.0.11.tgz", - "integrity": "sha512-DtezRxmwq5vxd74pPHmKvZ6dTHnQRl6ZFRcCAkzzhHZKnvQlU0mEO5As1ZymggJ69OMcoLB+iczvUpnSYsa7tQ==", - "dev": true, - "requires": { - "change-case": "^4.1.1", - "micromatch": "^4.0.2", - "typedoc": "~0.19.2" - } - }, - "@cloudscape-design/eslint-plugin": { - "version": "file:build-tools/eslint", - "requires": { - "micromatch": "^4.0.4" - } - }, - "@cloudscape-design/global-styles": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cloudscape-design/global-styles/-/global-styles-1.0.11.tgz", - "integrity": "sha512-pHGImwi5K+Dk/FUQHTvoVeEowWZAoRfTMzIEz2eri5fbK4U/Xp7UuhEqoWhNoLDtcw87/o2qHTP0We95mhVOqg==", - "dev": true - }, - "@cloudscape-design/jest-preset": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@cloudscape-design/jest-preset/-/jest-preset-2.0.16.tgz", - "integrity": "sha512-Caf52TPSL/flvS8q/o0BQnzRoWEUb6iaKvzU1SKdVn1OD9gVnJD5mC6HK/RPhFIjkxzh7V+Ce/pAZavvYmux5w==", - "dev": true, - "requires": { - "@babel/plugin-transform-modules-commonjs": "^7.9.0" - } - }, - "@cloudscape-design/test-utils-converter": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-converter/-/test-utils-converter-1.0.12.tgz", - "integrity": "sha512-nh9iHPP3THidg4lIw0yZTA2PGxNKzjvBbmDr2oHXa6nXZ5VokdSgkIUi1HZRwNWuSyyjuZyMg5YgNVYqEWPgQQ==", - "dev": true, - "requires": { - "@babel/core": "^7.16.0", - "@babel/plugin-syntax-decorators": "^7.16.0", - "@babel/plugin-syntax-typescript": "^7.16.0" - } - }, - "@cloudscape-design/test-utils-core": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.12.tgz", - "integrity": "sha512-dSyFtq73nZM8AQeanBJhZazZSufRsGzYe144qAr8yGWpDZzxSkj8YgyjJccHVE0avGwj+aunL6SMlUkILaZw0Q==", - "requires": { - "css-selector-tokenizer": "^0.8.0", - "css.escape": "^1.5.1" - } - }, - "@cloudscape-design/theming-build": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-build/-/theming-build-1.0.36.tgz", - "integrity": "sha512-PwaiX9xqpnrTrFEBpVfwgRg2ydiOhzvBGB8XkZFXhqaDEqKr+hdT7r7iCfMET7UZoXjme/zntK9cMyAydPWVFA==", - "dev": true, - "requires": { - "autoprefixer": "^10.4.8", - "glob": "^7.2.3", - "jsonschema": "^1.4.1", - "loader-utils": "^3.2.1", - "lodash": "^4.17.21", - "postcss": "^8.4.16", - "postcss-discard-empty": "^5.1.1", - "postcss-initial": "^3.0.4", - "postcss-inline-svg": "^5.0.0", - "postcss-modules": "^4.3.1", - "sass": "^1.49.0", - "string-hash": "^1.1.3", - "tslib": "^2.4.0" - } - }, - "@cloudscape-design/theming-runtime": { - "version": "1.0.29", - "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-runtime/-/theming-runtime-1.0.29.tgz", - "integrity": "sha512-UC49ObLxLmmsADpuAZN8UkelVyiIQgCLY+1rpMkLoWIXBhhz+vwxBzfKLLFv2pC4zuEI16/1VwO6IYWtOzMK3A==", - "requires": { - "tslib": "^2.4.0" - } - }, "@csstools/css-parser-algorithms": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz", diff --git a/scripts/unlock-package-lock.js b/scripts/unlock-package-lock.js index a7f29732dd..acf17c3902 100644 --- a/scripts/unlock-package-lock.js +++ b/scripts/unlock-package-lock.js @@ -11,12 +11,6 @@ const path = require('path'); const filename = path.resolve(__dirname, '..', 'package-lock.json'); const packageLock = JSON.parse(fs.readFileSync(filename)); -function removeDependencies(dependencyName, packages) { - if (dependencyName.includes('@cloudscape-design/')) { - delete packages[dependencyName]; - } -} - Object.keys(packageLock.packages).forEach(dependencyName => { removeDependencies(dependencyName, packageLock.packages); }); @@ -27,3 +21,9 @@ Object.keys(packageLock.dependencies).forEach(dependencyName => { fs.writeFileSync(filename, JSON.stringify(packageLock, null, 2) + '\n'); console.log('Removed @cloudscape-design/ dependencies from package-lock file'); + +function removeDependencies(dependencyName, packages) { + if (dependencyName.includes('@cloudscape-design/')) { + delete packages[dependencyName]; + } +} diff --git a/src/app-layout/__tests__/main.test.tsx b/src/app-layout/__tests__/main.test.tsx index 4c30b2dbb8..b91cf4210a 100644 --- a/src/app-layout/__tests__/main.test.tsx +++ b/src/app-layout/__tests__/main.test.tsx @@ -10,7 +10,8 @@ import mobileStyles from '../../../lib/components/app-layout/mobile-toolbar/styl import sharedStyles from '../../../lib/components/app-layout/styles.css.js'; import '../../__a11y__/to-validate-a11y'; -jest.mock('../../../lib/components/internal/motion', () => ({ +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), isMotionDisabled: jest.fn().mockReturnValue(true), })); diff --git a/src/app-layout/__tests__/mobile.test.tsx b/src/app-layout/__tests__/mobile.test.tsx index 839e7cfc74..f98dde0193 100644 --- a/src/app-layout/__tests__/mobile.test.tsx +++ b/src/app-layout/__tests__/mobile.test.tsx @@ -20,7 +20,8 @@ import testUtilsStyles from '../../../lib/components/app-layout/test-classes/sty import visualRefreshRefactoredStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js'; import { findUpUntil } from '../../../lib/components/internal/utils/dom'; -jest.mock('../../../lib/components/internal/motion', () => ({ +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), isMotionDisabled: jest.fn().mockReturnValue(true), })); diff --git a/src/app-layout/__tests__/split-panel.test.tsx b/src/app-layout/__tests__/split-panel.test.tsx index fa8ce0e909..6cef8d3287 100644 --- a/src/app-layout/__tests__/split-panel.test.tsx +++ b/src/app-layout/__tests__/split-panel.test.tsx @@ -28,11 +28,13 @@ const fakeComputedStyle: Window['getComputedStyle'] = (...args) => { jest.mock('../../../lib/components/internal/hooks/use-visual-mode', () => ({ useVisualRefresh: jest.fn().mockReturnValue(false), - useDensityMode: jest.fn().mockReturnValue('comfortable'), - useReducedMotion: jest.fn().mockReturnValue(true), })); -jest.mock('../../../lib/components/internal/motion', () => ({ + +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), isMotionDisabled: jest.fn().mockReturnValue(true), + useDensityMode: jest.fn().mockReturnValue('comfortable'), + useReducedMotion: jest.fn().mockReturnValue(true), })); let isMocked = false; diff --git a/src/app-layout/__tests__/utils.tsx b/src/app-layout/__tests__/utils.tsx index 97814f15ca..b9484982f2 100644 --- a/src/app-layout/__tests__/utils.tsx +++ b/src/app-layout/__tests__/utils.tsx @@ -19,12 +19,13 @@ jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({ jest.mock('../../../lib/components/internal/hooks/use-visual-mode', () => ({ useVisualRefresh: jest.fn().mockReturnValue(false), - useDensityMode: jest.fn().mockReturnValue('comfortable'), - useReducedMotion: jest.fn().mockReturnValue(true), })); -jest.mock('../../../lib/components/internal/motion', () => ({ +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), isMotionDisabled: jest.fn().mockReturnValue(true), + useDensityMode: jest.fn().mockReturnValue('comfortable'), + useReducedMotion: jest.fn().mockReturnValue(true), })); export function renderComponent(jsx: React.ReactElement) { diff --git a/src/area-chart/model/use-chart-model.ts b/src/area-chart/model/use-chart-model.ts index aed775f809..b9602c7c85 100644 --- a/src/area-chart/model/use-chart-model.ts +++ b/src/area-chart/model/use-chart-model.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { AreaChartProps } from '../interfaces'; -import React, { useEffect, useMemo, useRef, RefObject, MouseEvent, useState } from 'react'; +import React, { useEffect, useMemo, useRef, RefObject, MouseEvent } from 'react'; import { findClosest, circleIndex } from './utils'; import { nodeContains } from '../../internal/utils/dom'; @@ -15,7 +15,7 @@ import { ChartModel } from './index'; import { ChartPlotRef } from '../../internal/components/chart-plot'; import { throttle } from '../../internal/utils/throttle'; import { useReaction } from '../async-store'; -import { useResizeObserver } from '../../internal/hooks/container-queries'; +import { useHeightMeasure } from '../../internal/hooks/container-queries/use-height-measure'; const MAX_HOVER_MARGIN = 6; const SVG_HOVER_THROTTLE = 25; @@ -59,12 +59,8 @@ export default function useChartModel({ const verticalMarkerRef = useRef(null); const plotMeasureRef = useRef(null); - const [measuredHeight, setHeight] = useState(0); - useResizeObserver( - () => plotMeasureRef.current, - entry => fitHeight && setHeight(entry.borderBoxHeight) - ); - const height = fitHeight ? measuredHeight : explicitHeight; + const hasVisibleSeries = series.length > 0; + const height = useHeightMeasure(() => plotMeasureRef.current, !fitHeight, [hasVisibleSeries]) ?? explicitHeight; const stableSetVisibleSeries = useStableEventHandler(setVisibleSeries); diff --git a/src/button-dropdown/tooltip.tsx b/src/button-dropdown/tooltip.tsx index 1db4486f15..010ef8402f 100644 --- a/src/button-dropdown/tooltip.tsx +++ b/src/button-dropdown/tooltip.tsx @@ -7,7 +7,7 @@ import PopoverContainer from '../popover/container'; import PopoverBody from '../popover/body'; import Portal from '../internal/components/portal'; import { usePortalModeClasses } from '../internal/hooks/use-portal-mode-classes'; -import { useReducedMotion } from '../internal/hooks/use-visual-mode'; +import { useReducedMotion } from '@cloudscape-design/component-toolkit/internal'; export interface TooltipProps { children?: React.ReactNode; diff --git a/src/code-editor/index.tsx b/src/code-editor/index.tsx index 1f150a9139..11fc2301c7 100644 --- a/src/code-editor/index.tsx +++ b/src/code-editor/index.tsx @@ -30,7 +30,7 @@ import ErrorScreen from './error-screen'; import useBaseComponent from '../internal/hooks/use-base-component'; import useForwardFocus from '../internal/hooks/forward-focus'; import { applyDisplayName } from '../internal/utils/apply-display-name'; -import { useCurrentMode } from '../internal/hooks/use-visual-mode'; +import { useCurrentMode } from '@cloudscape-design/component-toolkit/internal'; import { useInternalI18n } from '../internal/i18n/context'; import { StatusBar } from './status-bar'; import { useFormFieldContext } from '../internal/context/form-field-context'; diff --git a/src/flashbar/__tests__/flashbar.test.tsx b/src/flashbar/__tests__/flashbar.test.tsx index 8b86ea14aa..88fcff7dd6 100644 --- a/src/flashbar/__tests__/flashbar.test.tsx +++ b/src/flashbar/__tests__/flashbar.test.tsx @@ -18,6 +18,13 @@ jest.mock('../../../lib/components/internal/hooks/use-visual-mode', () => { ...originalVisualModeModule, useVisualRefresh: (...args: any) => mockUseAnimations ? useAnimations : originalVisualModeModule.useVisualRefresh(...args), + }; +}); +jest.mock('@cloudscape-design/component-toolkit/internal', () => { + const originalVisualModeModule = jest.requireActual('@cloudscape-design/component-toolkit/internal'); + return { + __esModule: true, + ...originalVisualModeModule, useReducedMotion: (...args: any) => mockUseAnimations ? !useAnimations : originalVisualModeModule.useReducedMotion(...args), }; diff --git a/src/flashbar/common.tsx b/src/flashbar/common.tsx index b01a002bd0..b4f42e75e2 100644 --- a/src/flashbar/common.tsx +++ b/src/flashbar/common.tsx @@ -4,12 +4,12 @@ import useBaseComponent from '../internal/hooks/use-base-component'; import { useMergeRefs } from '../internal/hooks/use-merge-refs'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useContainerBreakpoints } from '../internal/hooks/container-queries'; -import { useReducedMotion, useVisualRefresh } from '../internal/hooks/use-visual-mode'; +import { useVisualRefresh } from '../internal/hooks/use-visual-mode'; import { getBaseProps } from '../internal/base-component'; import { FlashbarProps } from './interfaces'; import { focusFlashById } from './flash'; import { isDevelopment } from '../internal/is-development'; -import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; +import { useReducedMotion, warnOnce } from '@cloudscape-design/component-toolkit/internal'; export const componentName = 'Flashbar'; diff --git a/src/internal/__tests__/motion.test.tsx b/src/internal/__tests__/motion.test.tsx deleted file mode 100644 index 398ab1d8f0..0000000000 --- a/src/internal/__tests__/motion.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React from 'react'; -import { render } from '@testing-library/react'; -import createWrapper from '../../../lib/components/test-utils/dom'; -import { isMotionDisabled } from '../motion'; - -const matchMedia = jest.fn(); -window.matchMedia = matchMedia; - -beforeEach(() => { - matchMedia.mockReturnValue({ matches: false }); -}); - -describe('isMotionDisabled', () => { - test('returns false when there is no awsui-motion-disabled class', () => { - const renderResult = render( -
-
Content
-
- ); - const element = createWrapper(renderResult.container).find('#test-element')!.getElement(); - expect(isMotionDisabled(element)).toEqual(false); - }); - test('returns true when a parent element has awsui-motion-disabled class', () => { - const renderResult = render( -
-
-
-
-
-
Content
-
-
-
-
-
- ); - const element = createWrapper(renderResult.container).find('#test-element')!.getElement(); - expect(isMotionDisabled(element)).toEqual(true); - }); - test('returns false when there is an element with class awsui-motion-disabled, but it is not in the hierarchy', () => { - const renderResult = render( -
-
Content
-
Content
-
- ); - const element = createWrapper(renderResult.container).find('#test-element')!.getElement(); - expect(isMotionDisabled(element)).toEqual(false); - }); - test('returns true with prefers-reduced-motion: reduce ', () => { - matchMedia.mockReturnValue({ matches: true }); - const renderResult = render( -
-
Content
-
- ); - const element = createWrapper(renderResult.container).find('#test-element')!.getElement(); - expect(isMotionDisabled(element)).toEqual(true); - }); - test('returns true with prefers-reduced-motion: reduce and class awsui-motion-disabled', () => { - matchMedia.mockReturnValue({ matches: true }); - const renderResult = render( -
-
Content
-
- ); - const element = createWrapper(renderResult.container).find('#test-element')!.getElement(); - expect(isMotionDisabled(element)).toEqual(true); - }); -}); diff --git a/src/internal/components/abstract-switch/__tests__/abstract-switch.test.tsx b/src/internal/components/abstract-switch/__tests__/abstract-switch.test.tsx index 3cea1cf1f0..63a74d6166 100644 --- a/src/internal/components/abstract-switch/__tests__/abstract-switch.test.tsx +++ b/src/internal/components/abstract-switch/__tests__/abstract-switch.test.tsx @@ -33,12 +33,13 @@ describe('Abstract switch', () => { expect(container).toValidateA11y(); }); - describe('aria-labelledby', () => { - test('should not have a labelId if a label is not provided', () => { + describe('labels and descriptions', () => { + test('should be default use `label` and `description`', () => { const wrapper = renderAbstractSwitch({ controlClassName: '', outlineClassName: '', styledControl:
, + label: 'Label goes here', controlId: 'custom-id', description: 'Description goes here', nativeControl: nativeControlProps => , @@ -46,51 +47,50 @@ describe('Abstract switch', () => { }); const nativeControl = wrapper.find('.switch-element')!.getElement(); - expect(nativeControl).not.toHaveAttribute('aria-labelledby'); - expect(wrapper.find('#custom-id-label')).toBeNull(); + + expect(nativeControl).toHaveAccessibleName('Label goes here'); + expect(nativeControl).toHaveAccessibleDescription('Description goes here'); }); - test('should be set to labelId if a label is provided', () => { + test('label can be overwritten by `ariaLabel`', () => { const wrapper = renderAbstractSwitch({ controlClassName: '', outlineClassName: '', styledControl:
, label: 'Label goes here', controlId: 'custom-id', - description: 'Description goes here', + ariaLabel: 'Custom aria label', nativeControl: nativeControlProps => , onClick: noop, }); const nativeControl = wrapper.find('.switch-element')!.getElement(); - expect(nativeControl).toHaveAttribute('aria-labelledby', 'custom-id-label'); - expect(nativeControl).toHaveAttribute('aria-describedby', 'custom-id-description'); - - expect(wrapper.find('#custom-id-label')?.getElement()).toHaveTextContent('Label goes here'); - expect(wrapper.find('#custom-id-description')?.getElement()).toHaveTextContent('Description goes here'); + expect(nativeControl).toHaveAccessibleName('Custom aria label'); }); - test('should include labelId if an ariaLabelledBy id is provided', () => { + test('can add additional labelledby and describedby references', () => { const wrapper = renderAbstractSwitch({ controlClassName: '', outlineClassName: '', - styledControl:
, + styledControl: ( +
+ Custom label + Custom description +
+ ), controlId: 'custom-id', ariaLabelledby: 'some-custom-label', ariaDescribedby: 'some-custom-description', - label: 'label', - description: 'description', + label: 'Default label', + description: 'Default description', nativeControl: nativeControlProps => , onClick: noop, }); const nativeControl = wrapper.find('.switch-element')!.getElement(); - expect(nativeControl).toHaveAttribute('aria-labelledby', 'custom-id-label some-custom-label'); - expect(nativeControl).toHaveAttribute('aria-describedby', 'some-custom-description custom-id-description'); - - expect(wrapper.find('#custom-id-label')).not.toBe(null); - expect(wrapper.find('#custom-id-description')).not.toBe(null); + expect(nativeControl).toHaveAccessibleName('Default label Custom label'); + expect(nativeControl).toHaveAccessibleDescription('Custom description Default description'); }); }); }); diff --git a/src/internal/components/abstract-switch/index.tsx b/src/internal/components/abstract-switch/index.tsx index 1c2be80489..72ad2bbba9 100644 --- a/src/internal/components/abstract-switch/index.tsx +++ b/src/internal/components/abstract-switch/index.tsx @@ -54,7 +54,7 @@ export default function AbstractSwitch({ const descriptionId = `${id}-description`; const ariaLabelledByIds = []; - if (label) { + if (label && !ariaLabel) { ariaLabelledByIds.push(labelId); } if (ariaLabelledby) { diff --git a/src/internal/components/dark-ribbon/__tests__/dark-ribbon.test.tsx b/src/internal/components/dark-ribbon/__tests__/dark-ribbon.test.tsx index 83bc155118..ceec44965c 100644 --- a/src/internal/components/dark-ribbon/__tests__/dark-ribbon.test.tsx +++ b/src/internal/components/dark-ribbon/__tests__/dark-ribbon.test.tsx @@ -1,11 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import createWrapper, { ElementWrapper } from '../../../../../lib/components/test-utils/dom'; import DarkRibbon from '../../../../../lib/components/internal/components/dark-ribbon'; import styles from '../../../../../lib/components/internal/components/dark-ribbon/styles.css.js'; -import { mutate } from '../../../hooks/use-visual-mode/__tests__/utils'; + +export function mutate(mutation: () => void) { + return act(() => { + mutation(); + // mutation observers are triggered asynchronously + // https://github.com/jsdom/jsdom/blob/04f6c13f4a4d387c7fc979b8f62c6f68d8a0c639/lib/jsdom/living/helpers/mutation-observers.js#L125 + return Promise.resolve(); + }); +} function renderComponent(jsx: React.ReactElement) { const { container } = render(jsx); diff --git a/src/internal/components/dropdown/index.tsx b/src/internal/components/dropdown/index.tsx index 60bb85009b..d4037f122c 100644 --- a/src/internal/components/dropdown/index.tsx +++ b/src/internal/components/dropdown/index.tsx @@ -60,6 +60,8 @@ interface TransitionContentProps { onMouseDown?: React.MouseEventHandler; id?: string; role?: string; + ariaLabelledby?: string; + ariaDescribedby?: string; } const TransitionContent = ({ @@ -80,6 +82,8 @@ const TransitionContent = ({ onMouseDown, id, role, + ariaLabelledby, + ariaDescribedby, }: TransitionContentProps) => { const contentRef = useMergeRefs(dropdownRef, transitionRef); return ( @@ -96,6 +100,8 @@ const TransitionContent = ({ ref={contentRef} id={id} role={role} + aria-labelledby={ariaLabelledby} + aria-describedby={ariaDescribedby} data-open={open} data-animating={state !== 'exited'} aria-hidden={!open} @@ -140,6 +146,8 @@ const Dropdown = ({ contentKey, dropdownContentId, dropdownContentRole, + ariaLabelledby, + ariaDescribedby, }: DropdownProps) => { const wrapperRef = useRef(null); const triggerRef = useRef(null); @@ -404,6 +412,8 @@ const Dropdown = ({ position={position} id={dropdownContentId} role={dropdownContentRole} + ariaLabelledby={ariaLabelledby} + ariaDescribedby={ariaDescribedby} > {children} diff --git a/src/internal/components/dropdown/interfaces.ts b/src/internal/components/dropdown/interfaces.ts index de74406cbb..abb1e3abdf 100644 --- a/src/internal/components/dropdown/interfaces.ts +++ b/src/internal/components/dropdown/interfaces.ts @@ -141,6 +141,14 @@ export interface DropdownProps extends ExpandToViewport { * HTML role for the dropdown content wrapper */ dropdownContentRole?: string; + /** + * Labelledby for the dropdown (required when role="dialog") + */ + ariaLabelledby?: string; + /** + * Describedby for the dropdown (recommended when role="dialog") + */ + ariaDescribedby?: string; } export interface ExpandToViewport { diff --git a/src/internal/components/transition/index.tsx b/src/internal/components/transition/index.tsx index 50b3f286c4..a9599b09d4 100644 --- a/src/internal/components/transition/index.tsx +++ b/src/internal/components/transition/index.tsx @@ -6,7 +6,7 @@ import { Transition as ReactTransitionGroupTransition, TransitionStatus as ReactTransitionGroupTransitionStatus, } from 'react-transition-group'; -import { useReducedMotion } from '../../hooks/use-visual-mode'; +import { useReducedMotion } from '@cloudscape-design/component-toolkit/internal'; export type TransitionStatus = ReactTransitionGroupTransitionStatus | 'enter' | 'exit'; diff --git a/src/internal/hooks/container-queries/use-height-measure.ts b/src/internal/hooks/container-queries/use-height-measure.ts new file mode 100644 index 0000000000..0d28e3276d --- /dev/null +++ b/src/internal/hooks/container-queries/use-height-measure.ts @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useState } from 'react'; +import { useResizeObserver } from './use-resize-observer'; + +/** + * Conditional resize observer for border box height used in charts. + */ +export function useHeightMeasure( + getMeasure: () => null | HTMLElement | SVGElement, + skip = false, + deps: React.DependencyList = [] +) { + const [measuredHeight, setHeight] = useState(0); + // eslint-disable-next-line react-hooks/exhaustive-deps + const stableGetMeasure = useCallback(getMeasure, [...deps, skip]); + useResizeObserver(stableGetMeasure, entry => !skip && setHeight(entry.borderBoxHeight)); + return !skip ? measuredHeight : undefined; +} diff --git a/src/internal/hooks/use-portal-mode-classes/index.ts b/src/internal/hooks/use-portal-mode-classes/index.ts index 0535c0427b..f7c7353520 100644 --- a/src/internal/hooks/use-portal-mode-classes/index.ts +++ b/src/internal/hooks/use-portal-mode-classes/index.ts @@ -3,7 +3,8 @@ import clsx from 'clsx'; import React from 'react'; import { useVisualContext } from '../../components/visual-context'; -import { useCurrentMode, useDensityMode, useVisualRefresh } from '../use-visual-mode'; +import { useCurrentMode, useDensityMode } from '@cloudscape-design/component-toolkit/internal'; +import { useVisualRefresh } from '../use-visual-mode'; export function usePortalModeClasses(ref: React.RefObject) { const colorMode = useCurrentMode(ref); diff --git a/src/internal/hooks/use-visual-mode/__tests__/use-visual-mode.test.tsx b/src/internal/hooks/use-visual-mode/__tests__/use-visual-mode.test.tsx index ebfef99876..02b5a4a3c2 100644 --- a/src/internal/hooks/use-visual-mode/__tests__/use-visual-mode.test.tsx +++ b/src/internal/hooks/use-visual-mode/__tests__/use-visual-mode.test.tsx @@ -1,73 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useRef } from 'react'; -import { useCurrentMode, useDensityMode, useVisualRefresh, clearVisualRefreshState } from '../index'; +import React from 'react'; +import { useVisualRefresh, clearVisualRefreshState } from '../index'; import { render, screen } from '@testing-library/react'; -import { mutate } from './utils'; jest.mock('../../../environment', () => ({ ALWAYS_VISUAL_REFRESH: false }), { virtual: true }); -describe('useCurrentMode', () => { - function ModeRender() { - const ref = useRef(null); - const mode = useCurrentMode(ref); - return ( -
- {mode} -
- ); - } - - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => { - /*do not print anything to browser logs*/ - }); - }); - - afterEach(() => { - // ensure there are no react warnings - expect(console.error).not.toHaveBeenCalled(); - }); - - test('should detect light mode by default', () => { - render(); - expect(screen.getByTestId('current-mode')).toHaveTextContent('light'); - }); - - test('should detect dark mode', () => { - render( -
- -
- ); - expect(screen.getByTestId('current-mode')).toHaveTextContent('dark'); - }); - - test('should detect alternative dark mode class name', () => { - render( -
- -
- ); - expect(screen.getByTestId('current-mode')).toHaveTextContent('dark'); - }); - - test('should detect mode switch both ways', async () => { - const { container } = render(); - expect(screen.getByTestId('current-mode')).toHaveTextContent('light'); - await mutate(() => container.classList.add('awsui-dark-mode')); - expect(screen.getByTestId('current-mode')).toHaveTextContent('dark'); - await mutate(() => container.classList.remove('awsui-dark-mode')); - expect(screen.getByTestId('current-mode')).toHaveTextContent('light'); - }); - - test('should unmount properly', async () => { - const { rerender, container } = render(); - rerender(<>empty content); - await mutate(() => container.classList.add('awsui-dark-mode')); - }); -}); - describe('useVisualRefresh', () => { function App() { const isRefresh = useVisualRefresh(); @@ -101,35 +39,3 @@ describe('useVisualRefresh', () => { expect(screen.getByTestId('current-mode')).toHaveTextContent('false'); }); }); - -// The above suites cover majority of cases -describe('useDensityMode', () => { - function ModeRender() { - const ref = useRef(null); - const mode = useDensityMode(ref); - return ( -
- {mode} -
- ); - } - - test('should detect density modes', async () => { - const { container } = render(); - - // Default - expect(screen.getByTestId('current-mode')).toHaveTextContent('comfortable'); - - // External class - await mutate(() => container.classList.add('awsui-compact-mode')); - expect(screen.getByTestId('current-mode')).toHaveTextContent('compact'); - await mutate(() => container.classList.remove('awsui-compact-mode')); - expect(screen.getByTestId('current-mode')).toHaveTextContent('comfortable'); - - // Internal class - await mutate(() => container.classList.add('awsui-polaris-compact-mode')); - expect(screen.getByTestId('current-mode')).toHaveTextContent('compact'); - await mutate(() => container.classList.remove('awsui-polaris-compact-mode')); - expect(screen.getByTestId('current-mode')).toHaveTextContent('comfortable'); - }); -}); diff --git a/src/internal/hooks/use-visual-mode/__tests__/utils.ts b/src/internal/hooks/use-visual-mode/__tests__/utils.ts deleted file mode 100644 index 036d8cda0f..0000000000 --- a/src/internal/hooks/use-visual-mode/__tests__/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { act } from '@testing-library/react'; - -export function mutate(mutation: () => void) { - return act(() => { - mutation(); - // mutation observers are triggered asynchronously - // https://github.com/jsdom/jsdom/blob/04f6c13f4a4d387c7fc979b8f62c6f68d8a0c639/lib/jsdom/living/helpers/mutation-observers.js#L125 - return Promise.resolve(); - }); -} diff --git a/src/internal/hooks/use-visual-mode/index.ts b/src/internal/hooks/use-visual-mode/index.ts index 4bac236fe7..6edc724230 100644 --- a/src/internal/hooks/use-visual-mode/index.ts +++ b/src/internal/hooks/use-visual-mode/index.ts @@ -1,40 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; + import { ALWAYS_VISUAL_REFRESH } from '../../environment'; -import { isMotionDisabled } from '../../motion'; -import { findUpUntil } from '../../utils/dom'; -import { useMutationObserver } from '../use-mutation-observer'; import { isDevelopment } from '../../is-development'; import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; -// Note that this hook doesn't take into consideration @media print (unlike the dark mode CSS), -// due to challenges with cross-browser implementations of media/print state change listeners. -// This means that components using this hook will render in dark mode even when printing. -export function useCurrentMode(elementRef: React.RefObject) { - const [value, setValue] = useState<'light' | 'dark'>('light'); - useMutationObserver(elementRef, node => { - const darkModeParent = findUpUntil( - node, - node => node.classList.contains('awsui-polaris-dark-mode') || node.classList.contains('awsui-dark-mode') - ); - setValue(darkModeParent ? 'dark' : 'light'); - }); - return value; -} - -export function useDensityMode(elementRef: React.RefObject) { - const [value, setValue] = useState<'comfortable' | 'compact'>('comfortable'); - useMutationObserver(elementRef, node => { - const compactModeParent = findUpUntil( - node, - node => node.classList.contains('awsui-polaris-compact-mode') || node.classList.contains('awsui-compact-mode') - ); - setValue(compactModeParent ? 'compact' : 'comfortable'); - }); - return value; -} - export const useVisualRefresh = ALWAYS_VISUAL_REFRESH ? () => true : useVisualRefreshDynamic; // We expect VR is to be set only once and before the application is rendered. @@ -65,11 +35,3 @@ export function useVisualRefreshDynamic() { } return visualRefreshState; } - -export function useReducedMotion(elementRef: React.RefObject) { - const [value, setValue] = useState(false); - useMutationObserver(elementRef, node => { - setValue(isMotionDisabled(node)); - }); - return value; -} diff --git a/src/internal/motion.ts b/src/internal/motion.ts deleted file mode 100644 index 6bb732a439..0000000000 --- a/src/internal/motion.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { findUpUntil } from './utils/dom'; - -export const isMotionDisabled = (element: HTMLElement): boolean => - !!findUpUntil(element, node => node.classList.contains('awsui-motion-disabled')) || - (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false); diff --git a/src/mixed-line-bar-chart/chart-container.tsx b/src/mixed-line-bar-chart/chart-container.tsx index 5f6ddfb760..9f09421fa3 100644 --- a/src/mixed-line-bar-chart/chart-container.tsx +++ b/src/mixed-line-bar-chart/chart-container.tsx @@ -33,7 +33,7 @@ import useContainerWidth from '../internal/utils/use-container-width'; import { useMergeRefs } from '../internal/hooks/use-merge-refs'; import { nodeBelongs } from '../internal/utils/node-belongs'; import { CartesianChartContainer } from '../internal/components/cartesian-chart/chart-container'; -import { useResizeObserver } from '../internal/hooks/container-queries'; +import { useHeightMeasure } from '../internal/hooks/container-queries/use-height-measure'; const LEFT_LABELS_MARGIN = 16; const BOTTOM_LABELS_OFFSET = 12; @@ -122,12 +122,8 @@ export default function ChartContainer({ const popoverRef = useRef(null); const plotMeasureRef = useRef(null); - const [measuredHeight, setHeight] = useState(0); - useResizeObserver( - () => plotMeasureRef.current, - entry => fitHeight && setHeight(entry.borderBoxHeight) - ); - const plotHeight = fitHeight ? measuredHeight : explicitPlotHeight; + const measuredHeight = useHeightMeasure(() => plotMeasureRef.current, !fitHeight || !bottomLabelsHeight); + const plotHeight = fitHeight ? (bottomLabelsHeight ? measuredHeight ?? 0 : 0) : explicitPlotHeight; const isRefresh = useVisualRefresh(); diff --git a/src/multiselect/__tests__/multiselect.test.tsx b/src/multiselect/__tests__/multiselect.test.tsx index 1c82529e85..25f2805295 100644 --- a/src/multiselect/__tests__/multiselect.test.tsx +++ b/src/multiselect/__tests__/multiselect.test.tsx @@ -482,6 +482,16 @@ describe('a11y properties', () => { wrapper.findDropdown().getElement().parentNode!.querySelector(`#${controlledId}`)!.getAttribute('role') ).toBe('dialog'); }); + test('dropdown (role="dialog") should receive a label when filtering enabled', () => { + const { wrapper } = renderMultiselect( + + ); + wrapper.openDropdown(); + const controlledId = wrapper.findTrigger().getElement().getAttribute('aria-controls'); + expect(wrapper.findDropdown().getElement().parentNode!.querySelector(`#${controlledId}`)!).toHaveAccessibleName( + 'multiselect-label' + ); + }); }); test('Trigger receives focus when autofocus is true', () => { diff --git a/src/multiselect/internal.tsx b/src/multiselect/internal.tsx index 5c2f298fab..f852a373b5 100644 --- a/src/multiselect/internal.tsx +++ b/src/multiselect/internal.tsx @@ -289,6 +289,8 @@ const InternalMultiselect = React.forwardRef( const mergedRef = useMergeRefs(rootRef, __internalRootRef); + const dropdownProps = getDropdownProps(); + return (
{ index: number; @@ -90,12 +90,7 @@ export default ({ const hasLabels = !(hideTitles && hideDescriptions); const isRefresh = useVisualRefresh(); - const [measuredHeight, setHeight] = useState(0); - useResizeObserver( - () => plotRef.current?.svg ?? null, - entry => fitHeight && setHeight(entry.borderBoxHeight) - ); - const height = fitHeight ? measuredHeight : explicitHeight; + const height = useHeightMeasure(() => plotRef.current?.svg ?? null, !fitHeight) ?? explicitHeight; const dimensions = useMemo( () => diff --git a/src/select/__tests__/select-a11y.test.tsx b/src/select/__tests__/select-a11y.test.tsx index d64b8fca8e..c1f3dca290 100644 --- a/src/select/__tests__/select-a11y.test.tsx +++ b/src/select/__tests__/select-a11y.test.tsx @@ -46,6 +46,13 @@ describe.each([false, true])('expandToViewport=%s', expandToViewport => { await expect(container).toValidateA11y(); }); + test('with filtering', async () => { + const { container, wrapper } = renderSelect({ filteringType: 'auto', filteringAriaLabel: 'Filter' }); + wrapper.openDropdown(); + expect(wrapper.findDropdown({ expandToViewport })!.findOptionByValue('1')).toBeTruthy(); + await expect(container).toValidateA11y(); + }); + test('Option should have aria-selected', () => { const { wrapper } = renderSelect({ selectedOption: { label: 'First', value: '1' } }); wrapper.openDropdown(); diff --git a/src/select/__tests__/select.test.tsx b/src/select/__tests__/select.test.tsx index 0b16b07b18..87c843b239 100644 --- a/src/select/__tests__/select.test.tsx +++ b/src/select/__tests__/select.test.tsx @@ -332,6 +332,18 @@ describe.each([false, true])('expandToViewport=%s', expandToViewport => { .getAttribute('role') ).toBe('dialog'); }); + + test('dropdown (role="dialog") should receive a label when filtering enabled', () => { + const { wrapper } = renderSelect({ + filteringType: 'auto', + ariaLabel: 'select-label', + }); + wrapper.openDropdown(); + const controlledId = wrapper.findTrigger().getElement().getAttribute('aria-controls'); + expect( + wrapper.findDropdown({ expandToViewport }).getElement().parentNode!.querySelector(`#${controlledId}`)! + ).toHaveAccessibleName('select-label'); + }); }); describe('Filtering results', () => { diff --git a/src/select/internal.tsx b/src/select/internal.tsx index 148bb82d92..f734ec0b90 100644 --- a/src/select/internal.tsx +++ b/src/select/internal.tsx @@ -222,6 +222,8 @@ const InternalSelect = React.forwardRef( const mergedRef = useMergeRefs(rootRef, __internalRootRef); + const dropdownProps = getDropdownProps(); + return (
Math.round(num); declare global { interface Window { - __tableResizes__: ResizeObserverEntry[]; + __tableResizes__: number; } } @@ -50,21 +50,17 @@ class ResizableColumnsPage extends BasePageObject { async installObserver(selector: string) { await this.browser.execute(selector => { - window.__tableResizes__ = []; + window.__tableResizes__ = 0; const target = document.querySelector(selector)!; const observer = new ResizeObserver(entries => { - window.__tableResizes__.push(...entries); + window.__tableResizes__ += entries.length; }); observer.observe(target); }, selector); } - flushObservations() { - return this.browser.execute(() => { - const resizes = window.__tableResizes__; - window.__tableResizes__ = []; - return resizes; - }); + getObservations() { + return this.browser.execute(() => window.__tableResizes__); } } @@ -92,11 +88,11 @@ test( await page.installObserver(wrapper.find('table').toSelector()); await page.click('#shrink-container'); await page.waitForJsTimers(); - // flush observations from the container resize - await expect(page.flushObservations()).resolves.not.toEqual([]); + // expected 1 observation after creating observer and 1 caused by container shrink + await expect(page.getObservations()).resolves.toBe(2); await page.waitForJsTimers(); - // ensure there are no ongoing resizes after we flushed the expected ones above - await expect(page.flushObservations()).resolves.toEqual([]); + // ensure there are no more observations added after the expected 2 + await expect(page.getObservations()).resolves.toBe(2); }) ); diff --git a/src/tabs/__tests__/smooth-scroll.test.tsx b/src/tabs/__tests__/smooth-scroll.test.tsx index 5bf3aa4650..a5c8b188bd 100644 --- a/src/tabs/__tests__/smooth-scroll.test.tsx +++ b/src/tabs/__tests__/smooth-scroll.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { waitFor } from '@testing-library/react'; -import { isMotionDisabled } from '../../../lib/components/internal/motion'; +import { isMotionDisabled } from '@cloudscape-design/component-toolkit/internal'; import nativeSupport from '../../../lib/components/tabs/native-smooth-scroll-supported'; import smoothScroll from '../../../lib/components/tabs/smooth-scroll'; import createWrapper from '../../../lib/components/test-utils/dom'; @@ -11,10 +11,10 @@ import createWrapper from '../../../lib/components/test-utils/dom'; jest.mock('../../../lib/components/tabs/native-smooth-scroll-supported', () => { return jest.fn(); }); -jest.mock('../../../lib/components/internal/motion', () => { - const mock = jest.fn(); - return { isMotionDisabled: mock }; -}); +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), + isMotionDisabled: jest.fn(), +})); const nativeScrollMock = jest.fn(); Element.prototype.scrollTo = nativeScrollMock; diff --git a/src/tabs/smooth-scroll.ts b/src/tabs/smooth-scroll.ts index 14338cc77e..0af84a2027 100644 --- a/src/tabs/smooth-scroll.ts +++ b/src/tabs/smooth-scroll.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { isMotionDisabled } from '../internal/motion'; +import { isMotionDisabled } from '@cloudscape-design/component-toolkit/internal'; import isNativeSmoothScrollingSupported from './native-smooth-scroll-supported'; interface ScrollContext { diff --git a/src/tiles/tile.tsx b/src/tiles/tile.tsx index 2d1621f010..e96e8cf8a0 100644 --- a/src/tiles/tile.tsx +++ b/src/tiles/tile.tsx @@ -23,7 +23,6 @@ interface TileProps { export const Tile = React.forwardRef( ({ item, selected, name, breakpoint, onChange }: TileProps, forwardedRef: React.Ref) => { const internalRef = useRef(null); - const controlId = item.controlId || `${name}-value-${item.value}`; const isVisualRefresh = useVisualRefresh(); const mergedRef = useMergeRefs(internalRef, forwardedRef); @@ -58,7 +57,7 @@ export const Tile = React.forwardRef( label={item.label} description={item.description} disabled={item.disabled} - controlId={controlId} + controlId={item.controlId} />
{item.image &&
{item.image}
}