diff --git a/.all-contributorsrc b/.all-contributorsrc index 076156741b7e..0c263b6a4675 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1067,6 +1067,15 @@ "contributions": [ "code" ] + }, + { + "login": "bianca-sparxs", + "name": "Bianca Sparxs", + "avatar_url": "https://avatars.githubusercontent.com/u/33003148?v=4", + "profile": "https://github.com/bianca-sparxs", + "contributions": [ + "code" + ] } ], "commitConvention": "none" diff --git a/.yarn/cache/@carbon-icon-helpers-npm-10.38.0-92740e8d36-04277dcb45.zip b/.yarn/cache/@carbon-icon-helpers-npm-10.38.0-92740e8d36-04277dcb45.zip deleted file mode 100644 index 999b30376074..000000000000 Binary files a/.yarn/cache/@carbon-icon-helpers-npm-10.38.0-92740e8d36-04277dcb45.zip and /dev/null differ diff --git a/README.md b/README.md index a825390d5bb9..508c4c7ff635 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,7 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Jesse Hull

💻
Ashvin Warrier

💻
GalvinGao

💻 +
Bianca Sparxs

💻 diff --git a/config/babel-preset-carbon/package.json b/config/babel-preset-carbon/package.json index 46ba489367a2..86f75bdc683f 100644 --- a/config/babel-preset-carbon/package.json +++ b/config/babel-preset-carbon/package.json @@ -1,7 +1,7 @@ { "name": "babel-preset-carbon", "private": true, - "version": "0.4.0-rc.0", + "version": "0.4.0", "license": "Apache-2.0", "main": "index.js", "repository": { @@ -26,6 +26,6 @@ "@babel/preset-env": "^7.18.2", "@babel/preset-react": "^7.17.12", "babel-plugin-dev-expression": "^0.2.3", - "browserslist-config-carbon": "^11.1.0-rc.0" + "browserslist-config-carbon": "^11.1.0" } } diff --git a/config/browserslist-config-carbon/package.json b/config/browserslist-config-carbon/package.json index 0251b9de1883..85495ab82bc0 100644 --- a/config/browserslist-config-carbon/package.json +++ b/config/browserslist-config-carbon/package.json @@ -1,7 +1,7 @@ { "name": "browserslist-config-carbon", "description": "Browserslist config for the Carbon Design System", - "version": "11.1.0-rc.0", + "version": "11.1.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/config/eslint-config-carbon/package.json b/config/eslint-config-carbon/package.json index 521a01008eac..767949c55bde 100644 --- a/config/eslint-config-carbon/package.json +++ b/config/eslint-config-carbon/package.json @@ -1,7 +1,7 @@ { "name": "eslint-config-carbon", "description": "ESLint configuration for Carbon", - "version": "3.1.0-rc.0", + "version": "3.1.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/config/jest-config-carbon/package.json b/config/jest-config-carbon/package.json index ae2bb492ba13..ff82d4b82fab 100644 --- a/config/jest-config-carbon/package.json +++ b/config/jest-config-carbon/package.json @@ -2,7 +2,7 @@ "name": "jest-config-carbon", "private": true, "description": "Jest configuration and preset for Carbon", - "version": "1.7.0-rc.0", + "version": "1.7.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/config/prettier-config-carbon/package.json b/config/prettier-config-carbon/package.json index 630e007b3d9e..f2b168c0803c 100644 --- a/config/prettier-config-carbon/package.json +++ b/config/prettier-config-carbon/package.json @@ -1,7 +1,7 @@ { "name": "prettier-config-carbon", "description": "Prettier config for the Carbon Design System", - "version": "0.9.0-rc.0", + "version": "0.9.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/config/stylelint-config-carbon/package.json b/config/stylelint-config-carbon/package.json index c1098abe38ec..3ff664d1aadd 100644 --- a/config/stylelint-config-carbon/package.json +++ b/config/stylelint-config-carbon/package.json @@ -1,7 +1,7 @@ { "name": "stylelint-config-carbon", "description": "Stylelint configuration for Carbon", - "version": "1.12.0-rc.0", + "version": "1.12.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/docs/style.md b/docs/style.md index 4c9e37910107..bd294fbbd490 100644 --- a/docs/style.md +++ b/docs/style.md @@ -255,6 +255,41 @@ change. When creating a new component, even if you do not anticipate an explicit need to provide a forwarded ref, it's likely still worthwhile to include one to avoid unecessary breaking changes in the future. +#### Authoring dynamic/inline styles + +It's increasingly common for applications to use a Content Security Policy (CSP) +header with a +[`style-src` directive](https://content-security-policy.com/style-src/). When +this is configured, inline styles are blocked. Due to this, `style={{}}` can not +be used on any element within the codebase. The `react/forbid-component-props` +eslint rule is configured to flag invalid usages of the `style` attribute/prop. + +Components that need dynamic or inline styles can author these via the +[CSS Object Model (CSSOM)](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model). +Dynamic styles can be set via individual properties on the +[`CSSStyleDeclaration`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) +interface object provided to +[`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). +This will usually need to be wrapped in a `useIsomorphicEffect` hook to ensure +compatibility between SSR and browser environments and also to ensure the value +is unset if not provided. + +```jsx +function MyComponent({ width }) { + const ref = useRef(); + + useIsomorphicEffect(() => { + if (width) { + ref.current.style.width = `${width}px`; + } else { + ref.current.style.width = null; + } + }, [width]); + + return
; +} +``` + #### Translating a component Certain components will need to expose a way for the caller to pass in diff --git a/e2e/components/Popover/Popover-test.e2e.js b/e2e/components/Popover/Popover-test.e2e.js new file mode 100644 index 000000000000..fcedb5ed4a8a --- /dev/null +++ b/e2e/components/Popover/Popover-test.e2e.js @@ -0,0 +1,45 @@ +/** + * Copyright IBM Corp. 2022 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const { expect, test } = require('@playwright/test'); +const { themes } = require('../../test-utils/env'); +const { snapshotStory, visitStory } = require('../../test-utils/storybook'); + +test.describe('Popover', () => { + themes.forEach((theme) => { + test.describe(theme, () => { + test('Popover - auto align @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'Popover', + id: 'components-popover--auto-align', + theme, + }); + }); + + test('Popover - isTabTip @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'Popover', + id: 'components-popover--tab-tip', + theme, + }); + }); + }); + }); + + test('accessibility-checker @avt', async ({ page }) => { + await visitStory(page, { + component: 'Popover', + id: 'components-popover--auto-align', + globals: { + theme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('Popover'); + }); +}); diff --git a/examples/class-prefix/.gitignore b/examples/class-prefix/.gitignore new file mode 100644 index 000000000000..a547bf36d8d1 --- /dev/null +++ b/examples/class-prefix/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/class-prefix/README.md b/examples/class-prefix/README.md new file mode 100644 index 000000000000..0c804d4aad30 --- /dev/null +++ b/examples/class-prefix/README.md @@ -0,0 +1,52 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with +[`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Class Prefix + +By default, the prefix used by components is cds. However, you can change this +prefix in Sass and coordinate that change to React using the ClassPrefix +component. + +## Getting Started + +First, run `yarn` or `npm install` and then run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the +result. + +## Sass + +First and foremost, if you want to use v11 styles in any capacity, you'll have +to migrate to use `dart-sass`. `node-sass` has been deprecated and we migrated +to `dart-sass` in v11. For more information about migrating, visit our docs +[here](https://github.com/carbon-design-system/carbon/blob/main/docs/migration/v11.md#changing-from-node-sass-to-sass). + +In Sass, you can customize this prefix by writing the following: + +`@use '@carbon/react' with ( $prefix: 'custom' );` + +Similarly, you can configure scss/config directly: + +`@use '@carbon/react/scss/config' with ( $prefix: 'custom' );` + +## V11 + +This example is of how to use ClassPrefix from v11 🎉. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js + features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out +[the Next.js GitHub repository](https://github.com/vercel/next.js/) - your +feedback and contributions are welcome! diff --git a/examples/class-prefix/index.html b/examples/class-prefix/index.html new file mode 100644 index 000000000000..b46ab83364e3 --- /dev/null +++ b/examples/class-prefix/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/examples/class-prefix/package.json b/examples/class-prefix/package.json new file mode 100644 index 000000000000..228febf9bc3a --- /dev/null +++ b/examples/class-prefix/package.json @@ -0,0 +1,22 @@ +{ + "name": "class-prefix", + "private": true, + "version": "0.21.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@carbon/react": "^1.24.0", + "react": "^17.0.0", + "react-dom": "^17.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "1.1.3", + "sass": "^1.51.0", + "vite": "^2.9.5" + } +} diff --git a/examples/class-prefix/src/App.jsx b/examples/class-prefix/src/App.jsx new file mode 100644 index 000000000000..80dd0a6b0cde --- /dev/null +++ b/examples/class-prefix/src/App.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { usePrefix } from '@carbon/react'; +import { ClassPrefix } from '@carbon/react'; + +function ExampleComponent() { + const prefix = usePrefix(); + + return ( +

The current prefix is: {prefix}

+ ) +} + +function App() { + return ( + <> + + + + + + ); +} + +export default App diff --git a/examples/class-prefix/src/index.scss b/examples/class-prefix/src/index.scss new file mode 100644 index 000000000000..f34a79c34733 --- /dev/null +++ b/examples/class-prefix/src/index.scss @@ -0,0 +1,3 @@ +@use '@carbon/react' with ( + $prefix: 'custom' +); diff --git a/examples/class-prefix/src/main.jsx b/examples/class-prefix/src/main.jsx new file mode 100644 index 000000000000..475f242ecfef --- /dev/null +++ b/examples/class-prefix/src/main.jsx @@ -0,0 +1,12 @@ +import './index.scss' + +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/examples/class-prefix/vite.config.js b/examples/class-prefix/vite.config.js new file mode 100644 index 000000000000..a50be4eeb474 --- /dev/null +++ b/examples/class-prefix/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react({ + jsxRuntime: 'classic', + }), + ], +}); diff --git a/examples/codesandbox-styles/package.json b/examples/codesandbox-styles/package.json index d1e645bfd1f5..b24239921216 100644 --- a/examples/codesandbox-styles/package.json +++ b/examples/codesandbox-styles/package.json @@ -1,7 +1,7 @@ { "name": "codesandbox-styles", "private": true, - "version": "0.27.0-rc.0", + "version": "0.27.0", "scripts": { "develop": "vite" }, @@ -9,7 +9,7 @@ "vite": "^2.8.0" }, "dependencies": { - "@carbon/styles": "^1.24.0-rc.0", + "@carbon/styles": "^1.24.0", "sass": "^1.51.0" } } diff --git a/examples/codesandbox-with-sass-compilation/package.json b/examples/codesandbox-with-sass-compilation/package.json index 0fe2bf16dc89..4027d59cdd8e 100644 --- a/examples/codesandbox-with-sass-compilation/package.json +++ b/examples/codesandbox-with-sass-compilation/package.json @@ -1,9 +1,9 @@ { "name": "codesandbox-with-sass-compilation", - "version": "0.25.0-rc.0", + "version": "0.25.0", "private": true, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/examples/codesandbox/package.json b/examples/codesandbox/package.json index f811e7649fd2..a7806e572400 100644 --- a/examples/codesandbox/package.json +++ b/examples/codesandbox/package.json @@ -1,9 +1,9 @@ { "name": "codesandbox", - "version": "0.25.0-rc.0", + "version": "0.25.0", "private": true, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/examples/custom-theme/package.json b/examples/custom-theme/package.json index 3cee7361517b..fd5a71b6703a 100644 --- a/examples/custom-theme/package.json +++ b/examples/custom-theme/package.json @@ -1,14 +1,14 @@ { "name": "custom-theme", "private": true, - "version": "0.22.0-rc.0", + "version": "0.22.0", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/examples/id-prefix/.gitignore b/examples/id-prefix/.gitignore new file mode 100644 index 000000000000..a547bf36d8d1 --- /dev/null +++ b/examples/id-prefix/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/id-prefix/README.md b/examples/id-prefix/README.md new file mode 100644 index 000000000000..4852068bcb5b --- /dev/null +++ b/examples/id-prefix/README.md @@ -0,0 +1,49 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with +[`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Id Prefix + +This component is intended to be used in limited cases, primarily only if you +have id conflicts when using v10 and v11 packages at the same time during +migration. + +In React, you can use IdPrefix anywhere in your component tree and specify the +prefix with the prefix prop. Most often it's used in the project root wrapping +the entire project + +## Getting Started + +First, run `yarn` or `npm install` and then run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the +result. + +## Sass + +First and foremost, if you want to use v11 styles in any capacity, you'll have +to migrate to use `dart-sass`. `node-sass` has been deprecated and we migrated +to `dart-sass` in v11. For more information about migrating, visit our docs +[here](https://github.com/carbon-design-system/carbon/blob/main/docs/migration/v11.md#changing-from-node-sass-to-sass). + +## V10 and V11 + +This example is a v11 feature using the IdPrefix 🎉. As mentioned above, it will +help with any id conflicts as you migrate over to v11. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js + features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out +[the Next.js GitHub repository](https://github.com/vercel/next.js/) - your +feedback and contributions are welcome! diff --git a/examples/id-prefix/index.html b/examples/id-prefix/index.html new file mode 100644 index 000000000000..b46ab83364e3 --- /dev/null +++ b/examples/id-prefix/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/examples/id-prefix/package.json b/examples/id-prefix/package.json new file mode 100644 index 000000000000..761112b93c5d --- /dev/null +++ b/examples/id-prefix/package.json @@ -0,0 +1,22 @@ +{ + "name": "id-prefix", + "private": true, + "version": "0.21.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@carbon/react": "^1.24.0", + "react": "^17.0.0", + "react-dom": "^17.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "1.1.3", + "sass": "^1.51.0", + "vite": "^2.9.5" + } +} diff --git a/examples/id-prefix/src/App.jsx b/examples/id-prefix/src/App.jsx new file mode 100644 index 000000000000..e837b2ed72ef --- /dev/null +++ b/examples/id-prefix/src/App.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useIdPrefix } from '@carbon/react'; +import { IdPrefix } from '@carbon/react'; + +function ExampleComponent() { + const idPrefix = useIdPrefix(); + + return ( +

The current id prefix is: {idPrefix}

+ ) +} + +function App() { + return ( + <> + + + + + + ); +} + +export default App diff --git a/examples/id-prefix/src/index.scss b/examples/id-prefix/src/index.scss new file mode 100644 index 000000000000..f34a79c34733 --- /dev/null +++ b/examples/id-prefix/src/index.scss @@ -0,0 +1,3 @@ +@use '@carbon/react' with ( + $prefix: 'custom' +); diff --git a/examples/id-prefix/src/main.jsx b/examples/id-prefix/src/main.jsx new file mode 100644 index 000000000000..475f242ecfef --- /dev/null +++ b/examples/id-prefix/src/main.jsx @@ -0,0 +1,12 @@ +import './index.scss' + +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/examples/id-prefix/vite.config.js b/examples/id-prefix/vite.config.js new file mode 100644 index 000000000000..a50be4eeb474 --- /dev/null +++ b/examples/id-prefix/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react({ + jsxRuntime: 'classic', + }), + ], +}); diff --git a/examples/incremental-migration/package.json b/examples/incremental-migration/package.json index 54256b367e1f..dde6bbfe2168 100644 --- a/examples/incremental-migration/package.json +++ b/examples/incremental-migration/package.json @@ -1,7 +1,7 @@ { "name": "incremental-migration", "private": true, - "version": "0.24.0-rc.0", + "version": "0.24.0", "scripts": { "build": "next build", "dev": "next dev", @@ -13,7 +13,7 @@ }, "dependencies": { "@carbon/icons-react": "^10.49.0", - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "carbon-components": "^10.57.0", "carbon-components-react": "^7.57.0", "carbon-icons": "^7.0.7", diff --git a/examples/light-dark-mode/package.json b/examples/light-dark-mode/package.json index 77e8a892806b..434e19f6cebf 100644 --- a/examples/light-dark-mode/package.json +++ b/examples/light-dark-mode/package.json @@ -1,7 +1,7 @@ { "name": "examples-light-dark", "private": true, - "version": "0.22.0-rc.0", + "version": "0.22.0", "scripts": { "build": "next build", "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "next": "12.1.4", "react": "18.0.0", "react-dom": "18.0.0" diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 352013bb9ce0..970c84b642ad 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,7 +1,7 @@ { "name": "examples-nextjs", "private": true, - "version": "0.24.0-rc.0", + "version": "0.24.0", "scripts": { "build": "next build", "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "next": "12.1.4", "react": "18.0.0", "react-dom": "18.0.0" diff --git a/examples/vite/package.json b/examples/vite/package.json index e1c85e693837..21fbb7785972 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,14 +1,14 @@ { "name": "vite", "private": true, - "version": "0.22.0-rc.0", + "version": "0.22.0", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/package.json b/package.json index 183939d8972a..df8f687e38db 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "ci-check": "carbon-cli ci-check", "clean": "lerna run clean && lerna clean --yes && rimraf node_modules", "doctoc": "doctoc --title '## Table of Contents'", - "format": "prettier --cache --write '**/*.{js,md,scss,ts}' '!**/{build,es,lib,storybook,ts,umd}/**'", - "format:diff": "prettier --cache --list-different '**/*.{js,md,scss,ts}' '!**/{build,es,lib,storybook,ts,umd}/**' '!packages/components/**'", + "format": "prettier --cache --write '**/*.{js,md,scss,ts,tsx}' '!**/{build,es,lib,storybook,ts,umd}/**'", + "format:diff": "prettier --cache --list-different '**/*.{js,md,scss,ts,tsx}' '!**/{build,es,lib,storybook,ts,umd}/**' '!packages/components/**'", "lint": "eslint actions config packages www", "lint:styles": "stylelint '**/*.{css,scss}' --report-needless-disables --report-invalid-scope-disables", "sync": "carbon-cli sync", diff --git a/packages/carbon-components-react/package.json b/packages/carbon-components-react/package.json index 2d7cbdaaf730..00c8a88c027a 100644 --- a/packages/carbon-components-react/package.json +++ b/packages/carbon-components-react/package.json @@ -1,7 +1,7 @@ { "name": "carbon-components-react", "description": "The Carbon Design System is IBM’s open-source design system for products and experiences.", - "version": "8.24.0-rc.0", + "version": "8.24.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -39,8 +39,8 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/react": "^1.24.0-rc.0", - "@carbon/styles": "^1.24.0-rc.0", + "@carbon/react": "^1.24.0", + "@carbon/styles": "^1.24.0", "@carbon/telemetry": "0.1.0" }, "devDependencies": { @@ -51,13 +51,13 @@ "@babel/plugin-transform-react-constant-elements": "^7.17.12", "@babel/preset-env": "^7.18.2", "@babel/preset-react": "^7.17.12", - "@carbon/test-utils": "^10.27.0-rc.0", + "@carbon/test-utils": "^10.27.0", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-commonjs": "^24.0.0", "@rollup/plugin-node-resolve": "^15.0.0", "babel-plugin-dev-expression": "^0.2.3", - "babel-preset-carbon": "^0.4.0-rc.0", - "browserslist-config-carbon": "^11.1.0-rc.0", + "babel-preset-carbon": "^0.4.0", + "browserslist-config-carbon": "^11.1.0", "fs-extra": "^10.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/carbon-components/package.json b/packages/carbon-components/package.json index 88022325dd1b..e5dcce555870 100644 --- a/packages/carbon-components/package.json +++ b/packages/carbon-components/package.json @@ -1,7 +1,7 @@ { "name": "carbon-components", "description": "The Carbon Design System is IBM’s open-source design system for products and experiences.", - "version": "11.24.0-rc.0", + "version": "11.24.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -40,11 +40,11 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/styles": "^1.24.0-rc.0", + "@carbon/styles": "^1.24.0", "@carbon/telemetry": "0.1.0" }, "devDependencies": { - "@carbon/test-utils": "^10.27.0-rc.0", + "@carbon/test-utils": "^10.27.0", "fs-extra": "^10.0.0", "rimraf": "^4.0.0", "sass": "^1.51.0" diff --git a/packages/cli-reporter/package.json b/packages/cli-reporter/package.json index 16fe6aa0798e..79699bd327a8 100644 --- a/packages/cli-reporter/package.json +++ b/packages/cli-reporter/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/cli-reporter", "description": "Reporter for CLI-based tools in the Carbon Design System", - "version": "10.6.0-rc.0", + "version": "10.6.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 9715a6a0a35f..87594dffb08e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/cli", "description": "Task automation for working with the Carbon Design System", - "version": "11.9.0-rc.0", + "version": "11.9.0", "license": "Apache-2.0", "bin": { "carbon-cli": "./bin/carbon-cli.js" @@ -24,7 +24,7 @@ }, "dependencies": { "@babel/core": "^7.18.2", - "@carbon/cli-reporter": "^10.6.0-rc.0", + "@carbon/cli-reporter": "^10.6.0", "@octokit/plugin-retry": "^3.0.7", "@octokit/plugin-throttling": "^4.0.0", "@octokit/rest": "^19.0.0", @@ -43,7 +43,7 @@ "lodash.template": "^4.5.0", "markdown-toc": "^1.2.0", "prettier": "^2.7.1", - "prettier-config-carbon": "^0.9.0-rc.0", + "prettier-config-carbon": "^0.9.0", "progress-estimator": "^0.3.0", "remark": "^10.0.1", "replace-in-file": "^6.1.0", diff --git a/packages/colors/package.json b/packages/colors/package.json index bffb44132814..c89a532203c9 100644 --- a/packages/colors/package.json +++ b/packages/colors/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/colors", "description": "Colors for digital and software products using the Carbon Design System", - "version": "11.13.0-rc.0", + "version": "11.13.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -36,10 +36,10 @@ "clean": "rimraf css es lib umd scss index.scss" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", - "@carbon/cli-reporter": "^10.6.0-rc.0", - "@carbon/scss-generator": "^10.16.0-rc.0", - "@carbon/test-utils": "^10.27.0-rc.0", + "@carbon/cli": "^11.9.0", + "@carbon/cli-reporter": "^10.6.0", + "@carbon/scss-generator": "^10.16.0", + "@carbon/test-utils": "^10.27.0", "change-case": "^4.1.1", "fs-extra": "^10.0.0", "rimraf": "^4.0.0" diff --git a/packages/elements/package.json b/packages/elements/package.json index 0243785248f5..9cb9717ab462 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/elements", "description": "A collection of design elements in code for the IBM Design Language", - "version": "11.20.0-rc.0", + "version": "11.20.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -35,16 +35,16 @@ "clean": "rimraf es lib umd" }, "dependencies": { - "@carbon/colors": "^11.13.0-rc.0", - "@carbon/grid": "^11.12.0-rc.0", - "@carbon/icons": "^11.17.0-rc.0", - "@carbon/layout": "^11.12.0-rc.0", - "@carbon/motion": "^11.10.0-rc.0", - "@carbon/themes": "^11.17.0-rc.0", - "@carbon/type": "^11.16.0-rc.0" + "@carbon/colors": "^11.13.0", + "@carbon/grid": "^11.12.0", + "@carbon/icons": "^11.17.0", + "@carbon/layout": "^11.12.0", + "@carbon/motion": "^11.10.0", + "@carbon/themes": "^11.17.0", + "@carbon/type": "^11.16.0" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", + "@carbon/cli": "^11.9.0", "fs-extra": "^10.0.0", "klaw-sync": "^6.0.0", "replace-in-file": "^3.4.2", diff --git a/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap index eae2258db781..1a1dd1143eb8 100644 --- a/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -149,6 +149,7 @@ Array [ "helperText02", "highlight", "iconDisabled", + "iconInteractive", "iconInverse", "iconOnColor", "iconOnColorDisabled", diff --git a/packages/feature-flags/package.json b/packages/feature-flags/package.json index 089b86b0475a..c15d621443d4 100644 --- a/packages/feature-flags/package.json +++ b/packages/feature-flags/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/feature-flags", "description": "Build with feature flags in Carbon", - "version": "0.13.0-rc.0", + "version": "0.13.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -38,7 +38,7 @@ "@babel/preset-env": "^7.18.2", "@babel/template": "^7.16.7", "@babel/types": "^7.18.4", - "@carbon/scss-generator": "^10.16.0-rc.0", + "@carbon/scss-generator": "^10.16.0", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.0", "change-case": "^4.1.2", diff --git a/packages/grid/package.json b/packages/grid/package.json index 42ee4c4d789d..f9db4f413ef4 100644 --- a/packages/grid/package.json +++ b/packages/grid/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/grid", "description": "Grid for digital and software products using the Carbon Design System", - "version": "11.12.0-rc.0", + "version": "11.12.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -32,10 +32,10 @@ "clean": "rimraf scss/_inlined scss/vendor" }, "dependencies": { - "@carbon/layout": "^11.12.0-rc.0" + "@carbon/layout": "^11.12.0" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", + "@carbon/cli": "^11.9.0", "rimraf": "^4.0.0" }, "eyeglass": { diff --git a/packages/icon-build-helpers/package.json b/packages/icon-build-helpers/package.json index f0c3f7b38d34..c7593c3ca75e 100644 --- a/packages/icon-build-helpers/package.json +++ b/packages/icon-build-helpers/package.json @@ -2,7 +2,7 @@ "name": "@carbon/icon-build-helpers", "private": true, "description": "Build helpers for the Carbon Design System icon library", - "version": "1.11.0-rc.0", + "version": "1.11.0", "license": "Apache-2.0", "main": "src/index.js", "repository": { @@ -28,11 +28,11 @@ "@babel/preset-react": "^7.17.12", "@babel/template": "^7.16.7", "@babel/types": "^7.18.4", - "@carbon/cli-reporter": "^10.6.0-rc.0", - "@carbon/icon-helpers": "^10.39.0-rc.0", + "@carbon/cli-reporter": "^10.6.0", + "@carbon/icon-helpers": "^10.39.0", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-replace": "^5.0.0", - "browserslist-config-carbon": "^11.1.0-rc.0", + "browserslist-config-carbon": "^11.1.0", "change-case": "^4.1.1", "core-js": "^3.16.0", "fs-extra": "^10.0.0", diff --git a/packages/icon-helpers/package.json b/packages/icon-helpers/package.json index 588d4c90b12f..e210a7925fd4 100644 --- a/packages/icon-helpers/package.json +++ b/packages/icon-helpers/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icon-helpers", "description": "Helpers used alongside icons for digital and software products using the Carbon Design System", - "version": "10.39.0-rc.0", + "version": "10.39.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -33,7 +33,7 @@ "clean": "rimraf es lib umd" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", + "@carbon/cli": "^11.9.0", "rimraf": "^4.0.0" }, "sideEffects": false diff --git a/packages/icons-react/package.json b/packages/icons-react/package.json index f36f9a88d10d..b05cc08a0aa7 100644 --- a/packages/icons-react/package.json +++ b/packages/icons-react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-react", "description": "React components for icons in digital and software products using the Carbon Design System", - "version": "11.17.0-rc.0", + "version": "11.17.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -36,13 +36,13 @@ "react": ">=16" }, "dependencies": { - "@carbon/icon-helpers": "^10.39.0-rc.0", + "@carbon/icon-helpers": "^10.39.0", "@carbon/telemetry": "0.1.0", "prop-types": "^15.7.2" }, "devDependencies": { - "@carbon/icon-build-helpers": "^1.11.0-rc.0", - "@carbon/icons": "^11.17.0-rc.0", + "@carbon/icon-build-helpers": "^1.11.0", + "@carbon/icons": "^11.17.0", "rimraf": "^4.0.0" }, "sideEffects": false diff --git a/packages/icons-vue/package.json b/packages/icons-vue/package.json index 1a7793af081a..6424e753e4f7 100644 --- a/packages/icons-vue/package.json +++ b/packages/icons-vue/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-vue", "description": "Vue components for icons in digital and software products using the Carbon Design System", - "version": "10.66.0-rc.0", + "version": "10.66.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -28,11 +28,11 @@ "clean": "rimraf es lib" }, "dependencies": { - "@carbon/icon-helpers": "^10.39.0-rc.0" + "@carbon/icon-helpers": "^10.39.0" }, "devDependencies": { - "@carbon/cli-reporter": "^10.6.0-rc.0", - "@carbon/icons": "^11.17.0-rc.0", + "@carbon/cli-reporter": "^10.6.0", + "@carbon/icons": "^11.17.0", "fs-extra": "^10.0.0", "prettier": "^2.7.1", "rimraf": "^4.0.0", diff --git a/packages/icons/package.json b/packages/icons/package.json index 5deb4bbc1490..9028a013bebb 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons", "description": "Icons for digital and software products using the Carbon Design System", - "version": "11.17.0-rc.0", + "version": "11.17.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -38,8 +38,8 @@ "prepublishOnly": "yarn build" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", - "@carbon/icon-build-helpers": "^1.11.0-rc.0", + "@carbon/cli": "^11.9.0", + "@carbon/icon-build-helpers": "^1.11.0", "rimraf": "^4.0.0" } } diff --git a/packages/layout/package.json b/packages/layout/package.json index fe5d4d9a3ea6..c6c7811fb13a 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/layout", "description": "Layout helpers for digital and software products using the Carbon Design System", - "version": "11.12.0-rc.0", + "version": "11.12.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -28,10 +28,10 @@ "clean": "rimraf es lib umd scss/generated" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", - "@carbon/cli-reporter": "^10.6.0-rc.0", - "@carbon/scss-generator": "^10.16.0-rc.0", - "@carbon/test-utils": "^10.27.0-rc.0", + "@carbon/cli": "^11.9.0", + "@carbon/cli-reporter": "^10.6.0", + "@carbon/scss-generator": "^10.16.0", + "@carbon/test-utils": "^10.27.0", "core-js": "^3.16.0", "rimraf": "^4.0.0" } diff --git a/packages/motion/package.json b/packages/motion/package.json index 3c5a00ac50f1..474dfbb2aac7 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/motion", "description": "Motion helpers for digital and software products using the Carbon Design System", - "version": "11.10.0-rc.0", + "version": "11.10.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -28,7 +28,7 @@ "clean": "rimraf es lib umd" }, "devDependencies": { - "@carbon/cli": "^11.9.0-rc.0", + "@carbon/cli": "^11.9.0", "rimraf": "^4.0.0" } } diff --git a/packages/pictograms-react/package.json b/packages/pictograms-react/package.json index 34ac4042046e..68a96ec88069 100644 --- a/packages/pictograms-react/package.json +++ b/packages/pictograms-react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/pictograms-react", "description": "React components for pictograms in digital and software products using the Carbon Design System", - "version": "11.40.0-rc.0", + "version": "11.40.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -37,13 +37,13 @@ "react": ">=16" }, "dependencies": { - "@carbon/icon-helpers": "^10.39.0-rc.0", + "@carbon/icon-helpers": "^10.39.0", "@carbon/telemetry": "0.1.0", "prop-types": "^15.7.2" }, "devDependencies": { - "@carbon/icon-build-helpers": "^1.11.0-rc.0", - "@carbon/pictograms": "^12.14.0-rc.0" + "@carbon/icon-build-helpers": "^1.11.0", + "@carbon/pictograms": "^12.14.0" }, "sideEffects": false } diff --git a/packages/pictograms/package.json b/packages/pictograms/package.json index b6bc64250449..2093b774b6d3 100644 --- a/packages/pictograms/package.json +++ b/packages/pictograms/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/pictograms", "description": "Pictograms for digital and software products using the Carbon Design System", - "version": "12.14.0-rc.0", + "version": "12.14.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -31,7 +31,7 @@ "prepublishOnly": "yarn build" }, "devDependencies": { - "@carbon/icon-build-helpers": "^1.11.0-rc.0", + "@carbon/icon-build-helpers": "^1.11.0", "rimraf": "^4.0.0" } } diff --git a/packages/react/.storybook/preview.js b/packages/react/.storybook/preview.js index cce9912be450..f60d4b5787ab 100644 --- a/packages/react/.storybook/preview.js +++ b/packages/react/.storybook/preview.js @@ -8,7 +8,6 @@ import './styles.scss'; import '../src/feature-flags'; -import { configureActions } from '@storybook/addon-actions'; import { white, g10, g90, g100 } from '@carbon/themes'; import React from 'react'; import { breakpoints } from '@carbon/layout'; @@ -203,11 +202,6 @@ export const parameters = { }, }; -configureActions({ - depth: 3, - limit: 10, -}); - export const decorators = [ (Story, context) => { const { locale, theme } = context.globals; diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index f56ee3089ede..827e196a0f4a 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -970,7 +970,7 @@ Map { "ComboBox" => Object { "$$typeof": Symbol(react.forward_ref), "defaultProps": Object { - "ariaLabel": "Choose an item", + "aria-label": "Choose an item", "direction": "bottom", "disabled": false, "itemToElement": null, @@ -979,9 +979,10 @@ Map { "type": "default", }, "propTypes": Object { - "ariaLabel": Object { + "aria-label": Object { "type": "string", }, + "ariaLabel": [Function], "className": Object { "type": "string", }, @@ -3295,7 +3296,7 @@ Map { "FilterableMultiSelect" => Object { "$$typeof": Symbol(react.forward_ref), "defaultProps": Object { - "ariaLabel": "Choose an item", + "aria-label": "Choose an item", "compareItems": [Function], "direction": "bottom", "disabled": false, @@ -3308,9 +3309,10 @@ Map { "sortItems": [Function], }, "propTypes": Object { - "ariaLabel": Object { + "aria-label": Object { "type": "string", }, + "ariaLabel": [Function], "compareItems": Object { "isRequired": true, "type": "func", @@ -4535,7 +4537,7 @@ Map { "Filterable": Object { "$$typeof": Symbol(react.forward_ref), "defaultProps": Object { - "ariaLabel": "Choose an item", + "aria-label": "Choose an item", "compareItems": [Function], "direction": "bottom", "disabled": false, @@ -4548,9 +4550,10 @@ Map { "sortItems": [Function], }, "propTypes": Object { - "ariaLabel": Object { + "aria-label": Object { "type": "string", }, + "ariaLabel": [Function], "compareItems": Object { "isRequired": true, "type": "func", @@ -5666,6 +5669,9 @@ Map { "highContrast": Object { "type": "bool", }, + "isTabTip": Object { + "type": "bool", + }, "open": Object { "isRequired": true, "type": "bool", @@ -5961,6 +5967,7 @@ Map { }, }, "RadioTile" => Object { + "$$typeof": Symbol(react.forward_ref), "defaultProps": Object { "onChange": [Function], "tabIndex": 0, @@ -6006,6 +6013,7 @@ Map { "type": "oneOfType", }, }, + "render": [Function], }, "Row" => Object { "propTypes": Object { @@ -6885,15 +6893,16 @@ Map { }, "StructuredListWrapper" => Object { "defaultProps": Object { - "ariaLabel": "Structured list section", + "aria-label": "Structured list section", "isCondensed": false, "isFlush": false, "selection": false, }, "propTypes": Object { - "ariaLabel": Object { + "aria-label": Object { "type": "string", }, + "ariaLabel": [Function], "children": Object { "type": "node", }, @@ -7088,9 +7097,7 @@ Map { "leftOverflowButtonProps": Object { "type": "object", }, - "light": Object { - "type": "bool", - }, + "light": [Function], "rightOverflowButtonProps": Object { "type": "object", }, @@ -8528,7 +8535,9 @@ Map { "labelB": Object { "type": "node", }, - "labelText": [Function], + "labelText": Object { + "type": "string", + }, "onClick": Object { "type": "func", }, diff --git a/packages/react/examples/react-router/.yarn/install-state.gz b/packages/react/examples/react-router/.yarn/install-state.gz index 2ed6c3b99937..f68ed8954225 100644 Binary files a/packages/react/examples/react-router/.yarn/install-state.gz and b/packages/react/examples/react-router/.yarn/install-state.gz differ diff --git a/packages/react/examples/react-router/yarn.lock b/packages/react/examples/react-router/yarn.lock index aba468044bf4..41140a647d8e 100644 --- a/packages/react/examples/react-router/yarn.lock +++ b/packages/react/examples/react-router/yarn.lock @@ -10664,9 +10664,9 @@ fsevents@^1.2.7: linkType: hard "minimist@npm:^1.1.1, minimist@npm:^1.1.3, minimist@npm:^1.2.0, minimist@npm:^1.2.5": - version: 1.2.5 - resolution: "minimist@npm:1.2.5" - checksum: 86706ce5b36c16bfc35c5fe3dbb01d5acdc9a22f2b6cc810b6680656a1d2c0e44a0159c9a3ba51fb072bb5c203e49e10b51dcd0eec39c481f4c42086719bae52 + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 languageName: node linkType: hard diff --git a/packages/react/package.json b/packages/react/package.json index 13bfa11235a4..2133f31ca309 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/react", "description": "React components for the Carbon Design System", - "version": "1.24.0-rc.0", + "version": "1.24.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -43,10 +43,10 @@ }, "dependencies": { "@babel/runtime": "^7.18.3", - "@carbon/feature-flags": "^0.13.0-rc.0", - "@carbon/icons-react": "^11.17.0-rc.0", - "@carbon/layout": "^11.12.0-rc.0", - "@carbon/styles": "^1.24.0-rc.0", + "@carbon/feature-flags": "^0.13.0", + "@carbon/icons-react": "^11.17.0", + "@carbon/layout": "^11.12.0", + "@carbon/styles": "^1.24.0", "@carbon/telemetry": "0.1.0", "classnames": "2.3.2", "copy-to-clipboard": "^3.3.1", @@ -72,8 +72,8 @@ "@babel/plugin-transform-react-constant-elements": "^7.17.12", "@babel/preset-env": "^7.18.2", "@babel/preset-react": "^7.17.12", - "@carbon/test-utils": "^10.27.0-rc.0", - "@carbon/themes": "^11.17.0-rc.0", + "@carbon/test-utils": "^10.27.0", + "@carbon/themes": "^11.17.0", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-commonjs": "^24.0.0", "@rollup/plugin-node-resolve": "^15.0.0", @@ -92,9 +92,9 @@ "autoprefixer": "^10.4.0", "babel-loader": "^9.0.0", "babel-plugin-dev-expression": "^0.2.3", - "babel-preset-carbon": "^0.4.0-rc.0", + "babel-preset-carbon": "^0.4.0", "browserify-zlib": "^0.2.0", - "browserslist-config-carbon": "^11.1.0-rc.0", + "browserslist-config-carbon": "^11.1.0", "clipboardy": "^2.1.0", "css-loader": "^6.5.1", "enquirer": "^2.3.6", diff --git a/packages/react/src/components/ComboBox/ComboBox.stories.js b/packages/react/src/components/ComboBox/ComboBox.stories.js index 40119b05da01..2aad23664b04 100644 --- a/packages/react/src/components/ComboBox/ComboBox.stories.js +++ b/packages/react/src/components/ComboBox/ComboBox.stories.js @@ -117,7 +117,6 @@ export const WithLayer = () => ( export const Playground = (args) => (
{}} id="carbon-combobox" items={items} downshiftProps={{ @@ -135,6 +134,11 @@ export const Playground = (args) => ( ); Playground.argTypes = { + ['aria-label']: { + table: { + disable: true, + }, + }, ariaLabel: { table: { disable: true, @@ -179,7 +183,7 @@ Playground.argTypes = { }, }, onChange: { - action: 'clicked', + action: 'changed', }, onClick: { action: 'clicked', diff --git a/packages/react/src/components/ComboBox/ComboBox.tsx b/packages/react/src/components/ComboBox/ComboBox.tsx index 0a6c3321ead9..15e7af471cae 100644 --- a/packages/react/src/components/ComboBox/ComboBox.tsx +++ b/packages/react/src/components/ComboBox/ComboBox.tsx @@ -86,6 +86,13 @@ export interface ComboBoxProps ExcludedAttributes > { /** + * Specify a label to be read by screen readers on the container node + * 'aria-label' of the ListBox component. + */ + ['aria-label']?: string; + + /** + * @deprecated please use `aria-label` instead. * 'aria-label' of the ListBox component. */ ariaLabel?: string; @@ -250,7 +257,8 @@ export interface ComboBoxProps const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const { - ariaLabel, + ['aria-label']: ariaLabel, + ariaLabel: deprecatedAriaLabel, className: containerClassName, direction, disabled, @@ -480,6 +488,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { if (match(event, keys.Enter) && !inputValue) { toggleMenu(); } + + if (match(event, keys.Escape) && inputValue) { + if (event.target === textInput.current && isOpen) { + toggleMenu(); + event.preventDownshiftDefault = true; + event.persist(); + } + } }, }); @@ -560,7 +576,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { translateWithId={translateWithId} />
- + {isOpen ? filterItems(items, itemToString, inputValue).map( (item, index) => { @@ -627,8 +646,19 @@ ComboBox.displayName = 'ComboBox'; ComboBox.propTypes = { /** * 'aria-label' of the ListBox component. + * Specify a label to be read by screen readers on the container node */ - ariaLabel: PropTypes.string, + ['aria-label']: PropTypes.string, + + /** + * Deprecated, please use `aria-label` instead. + * Specify a label to be read by screen readers on the container note. + * 'aria-label' of the ListBox component. + */ + ariaLabel: deprecate( + PropTypes.string, + 'This prop syntax has been deprecated. Please use the new `aria-label`.' + ), /** * An optional className to add to the container node @@ -805,7 +835,7 @@ ComboBox.defaultProps = { itemToElement: null, shouldFilterItem: defaultShouldFilterItem, type: 'default', - ariaLabel: 'Choose an item', + ['aria-label']: 'Choose an item', direction: 'bottom', }; diff --git a/packages/react/src/components/ComposedModal/ComposedModal.stories.js b/packages/react/src/components/ComposedModal/ComposedModal.stories.js index d920e29b23c7..1c441f94ddea 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.stories.js +++ b/packages/react/src/components/ComposedModal/ComposedModal.stories.js @@ -252,10 +252,10 @@ Playground.argTypes = { }, }, onClose: { - action: 'clicked', + action: 'onClose', }, onKeyDown: { - action: 'clicked', + action: 'onKeyDown', }, selectorPrimaryFocus: { table: { diff --git a/packages/react/src/components/DataTable/TableToolbar.js b/packages/react/src/components/DataTable/TableToolbar.tsx similarity index 74% rename from packages/react/src/components/DataTable/TableToolbar.js rename to packages/react/src/components/DataTable/TableToolbar.tsx index 405646742e17..e55322ca01c6 100644 --- a/packages/react/src/components/DataTable/TableToolbar.js +++ b/packages/react/src/components/DataTable/TableToolbar.tsx @@ -8,10 +8,27 @@ import cx from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import { AriaLabelPropType } from '../../prop-types/AriaPropTypes'; import { usePrefix } from '../../internal/usePrefix'; +import { AriaLabelPropType } from '../../prop-types/AriaPropTypes'; + +export interface TableToolbarProps + extends React.HTMLAttributes { + /** + * Pass in the children that will be rendered inside the TableToolbar + */ + children: React.ReactNode; + + /** + * `lg` Change the row height of table + */ + size?: 'sm' | 'lg'; +} -const TableToolbar = ({ children, size, ...rest }) => { +const TableToolbar: React.FC = ({ + children, + size, + ...rest +}) => { const prefix = usePrefix(); const className = cx({ [`${prefix}--table-toolbar`]: true, diff --git a/packages/react/src/components/DataTable/TableToolbarAction.js b/packages/react/src/components/DataTable/TableToolbarAction.js deleted file mode 100644 index 7ac9ec8d9b69..000000000000 --- a/packages/react/src/components/DataTable/TableToolbarAction.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2023 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; -import OverflowMenuItem from '../OverflowMenuItem'; - -const TableToolbarAction = React.forwardRef(({ children, ...rest }, ref) => { - return ; -}); - -TableToolbarAction.displayName = 'TableToolbarAction'; -TableToolbarAction.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - onClick: PropTypes.func.isRequired, -}; - -export default TableToolbarAction; diff --git a/packages/react/src/components/DataTable/TableToolbarAction.tsx b/packages/react/src/components/DataTable/TableToolbarAction.tsx new file mode 100644 index 000000000000..e747201b1a5c --- /dev/null +++ b/packages/react/src/components/DataTable/TableToolbarAction.tsx @@ -0,0 +1,44 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import OverflowMenuItem from '../OverflowMenuItem'; +import { ForwardRefReturn } from '../../types/common'; + +export interface TableToolbarActionProps + extends Omit, 'onClick'> { + /** + * Pass in the children that will be rendered inside the TableToolbarAction + */ + children?: React.ReactNode; + + /** + * onClick handler for the TableToolbarAction + */ + onClick: (event: React.MouseEvent) => void; +} + +export type TableToolbarActionComponent = ForwardRefReturn< + HTMLDivElement, + TableToolbarActionProps +>; + +const TableToolbarAction: TableToolbarActionComponent = React.forwardRef( + ({ children, ...rest }, ref) => { + return ; + } +); + +TableToolbarAction.displayName = 'TableToolbarAction'; +TableToolbarAction.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + onClick: PropTypes.func.isRequired, +}; + +export default TableToolbarAction; diff --git a/packages/react/src/components/DataTable/TableToolbarContent.js b/packages/react/src/components/DataTable/TableToolbarContent.tsx similarity index 100% rename from packages/react/src/components/DataTable/TableToolbarContent.js rename to packages/react/src/components/DataTable/TableToolbarContent.tsx diff --git a/packages/react/src/components/DataTable/TableToolbarMenu.js b/packages/react/src/components/DataTable/TableToolbarMenu.tsx similarity index 76% rename from packages/react/src/components/DataTable/TableToolbarMenu.js rename to packages/react/src/components/DataTable/TableToolbarMenu.tsx index 532bede28638..2fd3baca6612 100644 --- a/packages/react/src/components/DataTable/TableToolbarMenu.js +++ b/packages/react/src/components/DataTable/TableToolbarMenu.tsx @@ -5,14 +5,34 @@ * LICENSE file in the root directory of this source tree. */ +import { Settings } from '@carbon/icons-react'; import cx from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import OverflowMenu from '../OverflowMenu'; -import { Settings } from '@carbon/icons-react'; import { usePrefix } from '../../internal/usePrefix'; +import OverflowMenu from '../OverflowMenu'; + +export interface TableToolbarMenuProps + extends React.HTMLAttributes { + children: React.ReactNode; + + /** + * Provide an optional class name for the toolbar menu + */ + className?: string; + + /** + * The description of the menu icon. + */ + iconDescription: string; + + /** + * Optional prop to allow overriding the default menu icon + */ + renderIcon?: React.ReactNode; +} -const TableToolbarMenu = ({ +const TableToolbarMenu: React.FC = ({ className, renderIcon, iconDescription, diff --git a/packages/react/src/components/DataTable/__tests__/DataTable-test.js b/packages/react/src/components/DataTable/__tests__/DataTable-test.js index c3d829b4a7f6..f04c2a4ebab3 100644 --- a/packages/react/src/components/DataTable/__tests__/DataTable-test.js +++ b/packages/react/src/components/DataTable/__tests__/DataTable-test.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2023 + * Copyright IBM Corp. 2022 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -28,21 +28,12 @@ import DataTable, { TableToolbarSearch, TableToolbarMenu, } from '../'; -import { sortStates } from '../state/sorting'; -import { mount } from 'enzyme'; +import userEvent from '@testing-library/user-event'; +import { render, screen, within } from '@testing-library/react'; // Test helpers -const getHeaderAt = (wrapper, index) => - wrapper.find('TableHeader button').at(index); -const getRowAt = (wrapper, index) => wrapper.find('tbody tr').at(index); -const getFilterInput = (wrapper) => - wrapper.find('TableToolbarSearch Search input'); -const getSelectAll = (wrapper) => - wrapper.find('TableSelectAll input[type="checkbox"]'); const getLastCallFor = (mocker) => mocker.mock.calls[mocker.mock.calls.length - 1]; -const getInputAtIndex = ({ wrapper, index, inputType }) => - getRowAt(wrapper, index).find(`input[type="${inputType}"]`); describe('DataTable', () => { let mockProps; @@ -89,6 +80,7 @@ describe('DataTable', () => { }) => ( @@ -144,738 +136,825 @@ describe('DataTable', () => { }; }); - it('should render', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); - }); - - describe('sorting', () => { - it('should sort a row by a header when a header is clicked', () => { - const wrapper = mount(); - const header = getHeaderAt(wrapper, 0); - header.simulate('click'); - expect(wrapper.state('rowIds')).toEqual(['a', 'b', 'c']); + describe('renders as expected - Component API', () => { + it('should spread extra props onto outermost element', () => { + const { container } = render(); - header.simulate('click'); - expect(wrapper.state('rowIds')).toEqual(['c', 'b', 'a']); + expect(container.firstChild).toHaveAttribute('data-testid', 'test-id'); + }); - header.simulate('click'); - expect(wrapper.state('rowIds')).toEqual(['b', 'a', 'c']); + it('should render and match snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); }); + }); - it('should re-sort new row props by the current sort state', () => { - const wrapper = mount(); - const header = getHeaderAt(wrapper, 0); + describe('behaves as expected', () => { + describe('sorting', () => { + it('should sort a row by a header when a header is clicked', () => { + render(); + const header = within(screen.getAllByRole('columnheader')[0]).getByRole( + 'button' + ); + const cells = () => { + return screen.getAllByRole('cell').map((cell) => { + return cell.textContent; + }); + }; - header.simulate('click'); - expect(wrapper.state('rowIds')).toEqual(['a', 'b', 'c']); + expect(cells()).toEqual([ + 'Field 2:A', + 'Field 2:B', + 'Field 1:A', + 'Field 1:B', + 'Field 3:A', + 'Field 3:B', + ]); + + // Click to sort rows by Field A in ascending order + userEvent.click(header); + expect(cells()).toEqual([ + 'Field 1:A', + 'Field 1:B', + 'Field 2:A', + 'Field 2:B', + 'Field 3:A', + 'Field 3:B', + ]); + + // Click to sort rows by Field A in descending order + userEvent.click(header); + expect(cells()).toEqual([ + 'Field 3:A', + 'Field 3:B', + 'Field 2:A', + 'Field 2:B', + 'Field 1:A', + 'Field 1:B', + ]); + + // Click to unsort rows by Field A in descending order + userEvent.click(header); + expect(cells()).toEqual([ + 'Field 2:A', + 'Field 2:B', + 'Field 1:A', + 'Field 1:B', + 'Field 3:A', + 'Field 3:B', + ]); + }); - wrapper.setProps({ rows: mockProps.rows }); - expect(wrapper.state('rowIds')).toEqual(['a', 'b', 'c']); - }); + it('should re-sort new row props by the current sort state', () => { + const { rerender } = render( + + ); + const header = within(screen.getAllByRole('columnheader')[0]).getByRole( + 'button' + ); + + const cells = () => { + return screen.getAllByRole('cell').map((cell) => { + return cell.textContent; + }); + }; - it('should reset to ASC ordering when another header is clicked', () => { - const wrapper = mount(); + // Click to sort rows by Field A in ascending order + userEvent.click(header); + expect(cells()).toEqual([ + 'Field 1:A', + 'Field 1:B', + 'Field 2:A', + 'Field 2:B', + 'Field 3:A', + 'Field 3:B', + ]); + + rerender(); + expect(cells()).toEqual([ + 'Field 1:A', + 'Field 1:B', + 'Field 2:A', + 'Field 2:B', + 'Field 3:A', + 'Field 3:B', + ]); + }); - const firstHeader = getHeaderAt(wrapper, 0); - const secondHeader = getHeaderAt(wrapper, 1); + it('should reset to ASC ordering when another header is clicked', () => { + render(); - firstHeader.simulate('click'); - expect(wrapper.state('rowIds')).toEqual(['a', 'b', 'c']); + const firstHeader = () => screen.getAllByRole('columnheader')[0]; + const secondHeader = () => screen.getAllByRole('columnheader')[1]; + const firstHeaderButton = within(firstHeader()).getByRole('button'); + const secondHeaderButton = within(secondHeader()).getByRole('button'); - firstHeader.simulate('click'); - expect(wrapper.state('rowIds')).toEqual(['c', 'b', 'a']); - expect(wrapper.state('sortDirection')).toBe(sortStates.DESC); + expect(firstHeader()).toHaveTextContent('ascending'); - secondHeader.simulate('click'); - expect(wrapper.state('sortDirection')).toBe(sortStates.ASC); - }); - }); + userEvent.click(firstHeaderButton); + expect(firstHeader()).toHaveTextContent('descending'); - describe('filtering', () => { - it('should filter rows by the given input', () => { - const wrapper = mount(); - const filterInput = getFilterInput(wrapper); + userEvent.click(firstHeaderButton); + expect(firstHeader()).toHaveTextContent('unsort'); + + userEvent.click(secondHeaderButton); + // After clicking the second header once, the table will now be + // sorted ascending based on that header, which means the button + // should now be in a state where clicking _again_ will sort it + // "descending" via that header: + expect(secondHeader()).toHaveTextContent('descending'); + }); + }); - expect(wrapper.state('rowIds').length).toBe(mockProps.rows.length); + describe('filtering', () => { + it('should filter rows by the given input', () => { + render(); + const filterInput = screen.getByRole('searchbox'); + + // +1 for the header row + expect(screen.getAllByRole('row').length).toBe( + mockProps.rows.length + 1 + ); + + userEvent.type(filterInput, 'Field 1'); + + expect(mockProps.render).toHaveBeenCalledWith( + expect.objectContaining({ + rows: [ + expect.objectContaining({ + id: 'a', + }), + ], + }) + ); + }); + }); - filterInput.getDOMNode().value = 'Field 1'; - filterInput.simulate('change'); + describe('selection', () => { + let mockProps; - expect(mockProps.render).toHaveBeenCalledWith( - expect.objectContaining({ + beforeEach(() => { + mockProps = { rows: [ - expect.objectContaining({ + { + id: 'b', + fieldA: 'Field 2:A', + fieldB: 'Field 2:B', + }, + { id: 'a', - }), + fieldA: 'Field 1:A', + fieldB: 'Field 1:B', + }, + { + id: 'c', + fieldA: 'Field 3:A', + fieldB: 'Field 3:B', + }, ], - }) - ); - }); - }); - - describe('selection', () => { - let mockProps; - - beforeEach(() => { - mockProps = { - rows: [ - { - id: 'b', - fieldA: 'Field 2:A', - fieldB: 'Field 2:B', - }, - { - id: 'a', - fieldA: 'Field 1:A', - fieldB: 'Field 1:B', - }, - { - id: 'c', - fieldA: 'Field 3:A', - fieldB: 'Field 3:B', - }, - ], - headers: [ - { - key: 'fieldA', - header: 'Field A', - }, - { - key: 'fieldB', - header: 'Field B', - }, - ], - locale: 'en', - render: jest.fn( - ({ rows, headers, getHeaderProps, getSelectionProps }) => ( - - - - - - {headers.map((header, i) => ( - - {header.header} - - ))} - - - - {rows.map((row) => ( - - - {row.cells.map((cell) => ( - {cell.value} + headers: [ + { + key: 'fieldA', + header: 'Field A', + }, + { + key: 'fieldB', + header: 'Field B', + }, + ], + locale: 'en', + render: jest.fn( + ({ + rows, + headers, + getHeaderProps, + getSelectionProps, + getBatchActionProps, + onInputChange, + }) => ( + + + + + Ghost + + + Ghost + + + Ghost + + + + + + + Action 1 + + + Action 2 + + + Action 3 + + + + + +
+ + + + {headers.map((header, i) => ( + + {header.header} + ))} - ))} - -
-
- ) - ), - }; - }); + + + {rows.map((row) => ( + + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + + +
+ ) + ), + }; + }); - it('should render', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); - }); + it('should render and match snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); - it('should have select-all default to un-checked if no rows are present', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); - }); + it('should have select-all default to un-checked if no rows are present', () => { + render(); + expect(screen.getAllByRole('checkbox')[0]).not.toHaveAttribute( + 'checked' + ); + }); - it('should select all rows if a user interacts with select all', () => { - const wrapper = mount(); - expect(getSelectAll(wrapper).prop('checked')).toBe(false); + it('should select all rows if a user interacts with select all', () => { + render(); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; + expect(selectAllCheckbox).not.toBeChecked(); - getSelectAll(wrapper).simulate('click'); + userEvent.click(selectAllCheckbox); - expect(getSelectAll(wrapper).prop('checked')).toBe(true); + expect(selectAllCheckbox).toBeChecked(); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(mockProps.rows.length); - }); + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(mockProps.rows.length); + }); - it('should select a specific row when a user interacts with select row', () => { - const wrapper = mount(); - expect(getSelectAll(wrapper).prop('checked')).toBe(false); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'checkbox' }).prop( - 'checked' - ) - ).toBe(false); + it('should select a specific row when a user interacts with select row', () => { + render(); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; + const firstRowCheckbox = screen.getAllByRole('checkbox')[1]; - getInputAtIndex({ wrapper, index: 0, inputType: 'checkbox' }).simulate( - 'click' - ); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'checkbox' }).prop( - 'checked' - ) - ).toBe(true); + expect(selectAllCheckbox).not.toBeChecked(); + expect(firstRowCheckbox).not.toBeChecked(); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(1); - }); + userEvent.click(firstRowCheckbox); + + expect(firstRowCheckbox).toBeChecked(); + expect(selectAllCheckbox).toBePartiallyChecked(); + + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(1); + }); - it('should deselect all rows when onCancel invoked', () => { - const wrapper = mount(); - getSelectAll(wrapper).simulate('click'); - expect(getSelectAll(wrapper).prop('checked')).toBe(true); + it('should deselect all rows when batch action cancel is invoked', async () => { + render(); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; - const { getBatchActionProps } = getLastCallFor(mockProps.render)[0]; - expect(getBatchActionProps().shouldShowBatchActions).toBe(true); + userEvent.click(selectAllCheckbox); + expect(selectAllCheckbox).toBeChecked(); - getBatchActionProps().onCancel(); + const { getBatchActionProps } = getLastCallFor(mockProps.render)[0]; + expect(getBatchActionProps().shouldShowBatchActions).toBe(true); - wrapper.update(); + const cancelButton = await screen.findByText('Cancel'); + userEvent.click(cancelButton); - expect(getSelectAll(wrapper).prop('checked')).toBe(false); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(0); + expect(selectAllCheckbox).not.toBeChecked(); + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(0); + }); }); - }); - describe('selection with filtering', () => { - let mockProps; + describe('selection with filtering', () => { + let mockProps; - beforeEach(() => { - mockProps = { - rows: [ - { - id: 'b', - fieldA: 'Field 2:A', - fieldB: 'Field 2:B', - }, - { - id: 'a', - fieldA: 'Field 1:A', - fieldB: 'Field 1:B', - }, - { - id: 'c', - fieldA: 'Field 3:A', - fieldB: 'Field 3:B', - }, - ], - headers: [ - { - key: 'fieldA', - header: 'Field A', - }, - { - key: 'fieldB', - header: 'Field B', - }, - ], - locale: 'en', - render: jest.fn( - ({ - rows, - headers, - getHeaderProps, - getSelectionProps, - onInputChange, - }) => ( - - - - - - - Action 1 - - - Action 2 - - - Action 3 - - - - - - - - - - {headers.map((header, i) => ( - - {header.header} - - ))} - - - - {rows.map((row) => ( - - - {row.cells.map((cell) => ( - {cell.value} + beforeEach(() => { + mockProps = { + rows: [ + { + id: 'b', + fieldA: 'Field 2:A', + fieldB: 'Field 2:B', + }, + { + id: 'a', + fieldA: 'Field 1:A', + fieldB: 'Field 1:B', + }, + { + id: 'c', + fieldA: 'Field 3:A', + fieldB: 'Field 3:B', + }, + ], + headers: [ + { + key: 'fieldA', + header: 'Field A', + }, + { + key: 'fieldB', + header: 'Field B', + }, + ], + locale: 'en', + render: jest.fn( + ({ + rows, + headers, + getHeaderProps, + getSelectionProps, + onInputChange, + }) => ( + + + + + + + Action 1 + + + Action 2 + + + Action 3 + + + + + +
+ + + + {headers.map((header, i) => ( + + {header.header} + ))} - ))} - -
-
- ) - ), - }; - }); - - it('should only select all from filtered items', () => { - const wrapper = mount(); + + + {rows.map((row) => ( + + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + + + + ) + ), + }; + }); - expect(getSelectAll(wrapper).prop('checked')).toBe(false); + it('should only select all from filtered items', () => { + render(); - const filterInput = getFilterInput(wrapper); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; + const firstRowCheckbox = () => screen.getAllByRole('checkbox')[1]; + const filterInput = screen.getByRole('searchbox'); - filterInput.getDOMNode().value = 'Field 1'; - filterInput.simulate('change'); + expect(selectAllCheckbox).not.toBeChecked(); - getInputAtIndex({ wrapper, index: 0, inputType: 'checkbox' }).simulate( - 'click' - ); + userEvent.type(filterInput, 'Field 1'); + userEvent.click(firstRowCheckbox()); + userEvent.clear(filterInput); - filterInput.getDOMNode().value = ''; - filterInput.simulate('change'); + expect(selectAllCheckbox).toBePartiallyChecked(); - expect(wrapper.find('TableSelectAll').prop('indeterminate')).toBe(true); + let { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(1); - let { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(1); + userEvent.click(selectAllCheckbox); - getSelectAll(wrapper).simulate('click'); + selectedRows = getLastCallFor(mockProps.render)[0].selectedRows; + expect(selectedRows.length).toBe(0); + }); - selectedRows = getLastCallFor(mockProps.render)[0].selectedRows; - expect(selectedRows.length).toBe(0); - }); + it('should only select rows that are not disabled even when filtered', () => { + const nextRows = [ + ...mockProps.rows.map((row) => ({ ...row })), + { + id: 'd', + fieldA: 'Field 3:A', + fieldB: 'Field 3:B', + disabled: true, + }, + ]; + render(); - it('should only select rows that are not disabled even when filtered', () => { - const wrapper = mount(); + const filterInput = screen.getByRole('searchbox'); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; - const nextRows = [ - ...mockProps.rows.map((row) => ({ ...row })), - { - id: 'd', - fieldA: 'Field 3:A', - fieldB: 'Field 3:B', - disabled: true, - }, - ]; + userEvent.type(filterInput, 'Field 3'); + userEvent.click(selectAllCheckbox); - wrapper.setProps({ rows: nextRows }); + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(1); - const filterInput = getFilterInput(wrapper); + expect(selectAllCheckbox).toBePartiallyChecked(); + }); - filterInput.getDOMNode().value = 'Field 3'; - filterInput.simulate('change'); + it('does not select a row if they are all disabled', () => { + const nextRows = [ + ...mockProps.rows.map((row) => ({ ...row, disabled: true })), + ]; + render(); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; - getSelectAll(wrapper).simulate('click'); + userEvent.click(selectAllCheckbox); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(1); + expect(selectAllCheckbox).not.toBePartiallyChecked(); + expect(selectAllCheckbox).not.toBeChecked(); - expect(wrapper.find('TableSelectAll').prop('indeterminate')).toBe(true); - }); + const filterInput = screen.getByRole('searchbox'); - it('does not select a row if they are all disabled', () => { - const wrapper = mount(); + userEvent.type(filterInput, 'Field 3'); + userEvent.click(selectAllCheckbox); - const nextRows = [ - ...mockProps.rows.map((row) => ({ ...row, disabled: true })), - ]; + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(0); + }); + }); - wrapper.setProps({ rows: nextRows }); + describe('selection -- radio buttons', () => { + let mockProps; - getSelectAll(wrapper).simulate('click'); + beforeEach(() => { + mockProps = { + rows: [ + { + id: 'b', + fieldA: 'Field 2:A', + fieldB: 'Field 2:B', + }, + { + id: 'a', + fieldA: 'Field 1:A', + fieldB: 'Field 1:B', + }, + { + id: 'c', + fieldA: 'Field 3:A', + fieldB: 'Field 3:B', + }, + ], + headers: [ + { + key: 'fieldA', + header: 'Field A', + }, + { + key: 'fieldB', + header: 'Field B', + }, + ], + locale: 'en', + radio: true, + render: jest.fn( + ({ rows, headers, getHeaderProps, getSelectionProps }) => ( + + + + + {headers.map((header, i) => ( + + {header.header} + + ))} + + + + {rows.map((row) => ( + + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+
+ ) + ), + }; + }); - expect(wrapper.find('TableSelectAll').prop('indeterminate')).toBe(false); - expect(wrapper.find('TableSelectAll').prop('checked')).toBe(false); + it('should render', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); - const filterInput = getFilterInput(wrapper); + it('should not have select-all checkbox', () => { + const { container } = render(); + expect(screen.queryAllByRole('checkbox').length).toBe(0); + expect(container).toMatchSnapshot(); + }); - filterInput.getDOMNode().value = 'Field 3'; - filterInput.simulate('change'); + it('should select a specific row when a user interacts with select row', () => { + render(); + const radioButton = screen.getAllByRole('radio')[0]; - getSelectAll(wrapper).simulate('click'); + expect(radioButton).not.toBeChecked(); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(0); - }); - }); + userEvent.click(radioButton); + expect(radioButton).toBeChecked(); - describe('selection -- radio buttons', () => { - let mockProps; + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(1); + }); - beforeEach(() => { - mockProps = { - rows: [ - { - id: 'b', - fieldA: 'Field 2:A', - fieldB: 'Field 2:B', - }, - { - id: 'a', - fieldA: 'Field 1:A', - fieldB: 'Field 1:B', - }, - { - id: 'c', - fieldA: 'Field 3:A', - fieldB: 'Field 3:B', - }, - ], - headers: [ - { - key: 'fieldA', - header: 'Field A', - }, - { - key: 'fieldB', - header: 'Field B', - }, - ], - locale: 'en', - radio: true, - render: jest.fn( - ({ rows, headers, getHeaderProps, getSelectionProps }) => ( - - - - - {headers.map((header, i) => ( - - {header.header} - - ))} - - - - {rows.map((row) => ( - - - {row.cells.map((cell) => ( - {cell.value} - ))} - - ))} - -
-
- ) - ), - }; - }); + it('should deselect all other rows when a row is selected', () => { + render(); + const radioButtonOne = screen.getAllByRole('radio')[0]; + const radioButtonTwo = screen.getAllByRole('radio')[1]; - it('should render', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); - }); + expect(radioButtonOne).not.toBeChecked(); - it('should not have select-all checkbox', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); - }); + userEvent.click(radioButtonOne); - it('should select a specific row when a user interacts with select row', () => { - const wrapper = mount(); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(false); + expect(radioButtonOne).toBeChecked(); + expect(radioButtonTwo).not.toBeChecked(); - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).simulate( - 'click' - ); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(true); + userEvent.click(radioButtonTwo); + expect(radioButtonOne).not.toBeChecked(); + expect(radioButtonTwo).toBeChecked(); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(1); + const { selectedRows } = getLastCallFor(mockProps.render)[0]; + expect(selectedRows.length).toBe(1); + }); }); - it('should deselect all other rows when a row is selected', () => { - const wrapper = mount(); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(false); + describe('updates properly when passed new props', () => { + let mockProps; - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).simulate( - 'click' - ); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(true); - expect( - getInputAtIndex({ wrapper, index: 1, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(false); - - getInputAtIndex({ wrapper, index: 1, inputType: 'radio' }).simulate( - 'click' - ); - expect( - getInputAtIndex({ wrapper, index: 0, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(false); - expect( - getInputAtIndex({ wrapper, index: 1, inputType: 'radio' }).prop( - 'checked' - ) - ).toBe(true); + beforeEach(() => { + mockProps = { + rows: [ + { + id: 'b', + fieldA: 'Field 2:A', + fieldB: 'Field 2:B', + }, + { + id: 'a', + fieldA: 'Field 1:A', + fieldB: 'Field 1:B', + }, + { + id: 'c', + fieldA: 'Field 3:A', + fieldB: 'Field 3:B', + }, + ], + headers: [ + { + key: 'fieldA', + header: 'Field A', + }, + { + key: 'fieldB', + header: 'Field B', + }, + ], + locale: 'en', + render: jest.fn( + ({ + rows, + headers, + getHeaderProps, + getExpandHeaderProps, + getSelectionProps, + getBatchActionProps, + getRowProps, + onInputChange, + }) => ( + + + + + Ghost + + + + + + + + + + {headers.map((header, i) => ( + + {header.header} + + ))} + + + + {rows.map((row) => ( + + + + {row.cells.map((cell) => ( + {cell.value} + ))} + + {row.isExpanded && ( + +

Expandable row content

+

Description here

+
+ )} +
+ ))} +
+
+
+ ) + ), + }; + }); - const { selectedRows } = getLastCallFor(mockProps.render)[0]; - expect(selectedRows.length).toBe(1); - }); - }); + it('should add additional rows when receiving new props', () => { + const { rerender } = render(); + const args = mockProps.render.mock.calls[0][0]; - describe('componentDidUpdate', () => { - let mockProps; + expect(args.rows.length).toEqual(mockProps.rows.length); - beforeEach(() => { - mockProps = { - rows: [ - { - id: 'b', - fieldA: 'Field 2:A', - fieldB: 'Field 2:B', - }, + const nextRows = [ + ...mockProps.rows, { - id: 'a', - fieldA: 'Field 1:A', - fieldB: 'Field 1:B', + id: 'd', + fieldA: 'Field 4:A', + fieldB: 'Field 4:B', }, - { - id: 'c', - fieldA: 'Field 3:A', - fieldB: 'Field 3:B', - }, - ], - headers: [ - { - key: 'fieldA', - header: 'Field A', - }, - { - key: 'fieldB', - header: 'Field B', - }, - ], - locale: 'en', - render: jest.fn( - ({ - rows, - headers, - getHeaderProps, - getExpandHeaderProps, - getSelectionProps, - getBatchActionProps, - getRowProps, - onInputChange, - }) => ( - - - - Ghost - - - - - - - - - {headers.map((header, i) => ( - - {header.header} - - ))} - - - - {rows.map((row) => ( - - - - {row.cells.map((cell) => ( - {cell.value} - ))} - - {row.isExpanded && ( - -

Expandable row content

-

Description here

-
- )} -
- ))} -
-
-
- ) - ), - }; - }); - - it('should add additional rows when receiving new props', () => { - const wrapper = mount(); - const args = mockProps.render.mock.calls[0][0]; - - expect(args.rows.length).toEqual(mockProps.rows.length); - - const nextRows = [ - ...mockProps.rows, - { - id: 'd', - fieldA: 'Field 4:A', - fieldB: 'Field 4:B', - }, - ]; + ]; + + rerender(); + + const nextArgs = getLastCallFor(mockProps.render)[0]; + expect(nextArgs.rows.length).toBe(nextRows.length); + expect(nextArgs.rows.map((row) => row.id)).toEqual([ + 'b', + 'a', + 'c', + 'd', + ]); + }); - wrapper.setProps({ rows: nextRows }); + it('should add additional headers when receiving new props', () => { + const { rerender } = render(); + const args = mockProps.render.mock.calls[0][0]; + + expect(args.headers).toEqual(mockProps.headers); + + const nextProps = { + rows: mockProps.rows.map((row) => ({ + ...row, + fieldC: 'Field X:C', + })), + headers: [ + ...mockProps.headers, + { + key: 'fieldC', + header: 'Field C', + }, + ], + }; - const nextArgs = getLastCallFor(mockProps.render)[0]; - expect(nextArgs.rows.length).toBe(nextRows.length); - expect(nextArgs.rows.map((row) => row.id)).toEqual(['b', 'a', 'c', 'd']); - }); + rerender(); - it('should add additional headers when receiving new props', () => { - const wrapper = mount(); - const args = mockProps.render.mock.calls[0][0]; + const nextArgs = getLastCallFor(mockProps.render)[0]; + expect(nextArgs.headers).toEqual(nextProps.headers); + }); - expect(args.headers).toEqual(mockProps.headers); + it('should keep batch action after adding rows, as long as some existing rows are selected', () => { + const { rerender } = render(); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; + userEvent.click(selectAllCheckbox); - const nextProps = { - rows: mockProps.rows.map((row) => ({ - ...row, - fieldC: 'Field X:C', - })), - headers: [ - ...mockProps.headers, + const nextRows = [ + ...mockProps.rows.map((row) => ({ ...row, isSelected: true })), { - key: 'fieldC', - header: 'Field C', + id: 'd', + fieldA: 'Field 4:A', + fieldB: 'Field 4:B', + isSelected: false, }, - ], - }; - - wrapper.setProps(nextProps); + ]; - const nextArgs = getLastCallFor(mockProps.render)[0]; - expect(nextArgs.headers).toEqual(nextProps.headers); - }); - - it('should keep batch action after adding rows, as long as some existing rows are selected', () => { - const wrapper = mount(); - getSelectAll(wrapper).simulate('click'); + rerender(); - const nextRows = [ - ...mockProps.rows.map((row) => ({ ...row, isSelected: true })), - { - id: 'd', - fieldA: 'Field 4:A', - fieldB: 'Field 4:B', - isSelected: false, - }, - ]; + expect(selectAllCheckbox).not.toBeChecked(); + const { getBatchActionProps, selectedRows } = getLastCallFor( + mockProps.render + )[0]; + expect(getBatchActionProps().shouldShowBatchActions).toBe(true); + expect(selectedRows.length).toBe(3); + }); - wrapper.setProps({ rows: nextRows }); - wrapper.update(); + it('should keep selected all state after adding rows, as long as all existing rows and new row are selected', () => { + const { rerender } = render(); + const selectAllCheckbox = screen.getAllByRole('checkbox')[0]; + userEvent.click(selectAllCheckbox); - expect(getSelectAll(wrapper).prop('checked')).toBe(false); - const { getBatchActionProps, selectedRows } = getLastCallFor( - mockProps.render - )[0]; - expect(getBatchActionProps().shouldShowBatchActions).toBe(true); - expect(selectedRows.length).toBe(3); - }); + const nextRows = [ + ...mockProps.rows, + { + id: 'd', + fieldA: 'Field 4:A', + fieldB: 'Field 4:B', + }, + ]; - it('should keep selected all state after adding rows, as long as all existing rows and new row are selected', () => { - const wrapper = mount(); - getSelectAll(wrapper).simulate('click'); + rerender(); - const nextRows = [ - ...mockProps.rows, - { - id: 'd', - fieldA: 'Field 4:A', - fieldB: 'Field 4:B', - }, - ]; + const { getBatchActionProps, selectedRows } = getLastCallFor( + mockProps.render + )[0]; + expect(getBatchActionProps().shouldShowBatchActions).toBe(true); + expect(selectedRows.length).toBe(3); + }); - wrapper.setProps({ rows: nextRows }); + it('should update rows when receiving new props', () => { + const { rerender } = render(); + const args = mockProps.render.mock.calls[0][0]; - const { getBatchActionProps, selectedRows } = getLastCallFor( - mockProps.render - )[0]; - expect(getBatchActionProps().shouldShowBatchActions).toBe(true); - expect(selectedRows.length).toBe(3); - }); + expect(args.rows.length).toEqual(mockProps.rows.length); - it('should update rows when receiving new props', () => { - const wrapper = mount(); - const args = mockProps.render.mock.calls[0][0]; + const nextRows = mockProps.rows.slice().reverse(); - expect(args.rows.length).toEqual(mockProps.rows.length); + rerender(); - const nextRows = mockProps.rows.slice().reverse(); + const nextArgs = getLastCallFor(mockProps.render)[0]; + expect(nextArgs.rows.map((row) => row.id)).toEqual(['c', 'a', 'b']); + }); - wrapper.setProps({ rows: nextRows }); + it('should update cells when receiving new props', () => { + const { rerender } = render(); + const args = mockProps.render.mock.calls[0][0]; - const nextArgs = getLastCallFor(mockProps.render)[0]; - expect(nextArgs.rows.map((row) => row.id)).toEqual(['c', 'a', 'b']); - }); + expect(args.rows.length).toEqual(mockProps.rows.length); - it('should update cells when receiving new props', () => { - const wrapper = mount(); - const args = mockProps.render.mock.calls[0][0]; + const nextRows = mockProps.rows.map((row) => { + return { + ...row, + fieldA: row.fieldA + '!', + }; + }); - expect(args.rows.length).toEqual(mockProps.rows.length); + rerender(); - const nextRows = mockProps.rows.map((row) => { - return { - ...row, - fieldA: row.fieldA + '!', - }; + const nextArgs = getLastCallFor(mockProps.render)[0]; + expect(nextArgs.rows.map((row) => row.cells[0].value)).toEqual([ + 'Field 2:A!', + 'Field 1:A!', + 'Field 3:A!', + ]); }); - - wrapper.setProps({ rows: nextRows }); - - const nextArgs = getLastCallFor(mockProps.render)[0]; - expect(nextArgs.rows.map((row) => row.cells[0].value)).toEqual([ - 'Field 2:A!', - 'Field 1:A!', - 'Field 3:A!', - ]); - }); - }); - - describe('sticky header', () => { - it('should render', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); }); }); }); diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap index d6a506c787c8..6ec2b0a52cd5 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap @@ -1,3863 +1,1065 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataTable selection -- radio buttons should not have select-all checkbox 1`] = ` - - - - - - Field A - - - Field B - - - - -
- , - }, - ], - } - } - rows={Array []} - size="lg" - sortRow={[Function]} - translateWithId={[Function]} -> - +
-
-

- DataTable with selection -

-

-

- +

+ +

+
-
-
- - - - - - - - - - - - - - - + + - -
-
- Field A -
-
-
- Field B -
-
-
-
- -
-
-
-`; - -exports[`DataTable selection -- radio buttons should render 1`] = ` - - + Field A + + + + + + + + + + + + + + + + + +
- - - - Field A - - - Field B - - - - - - +
+
+ + +
+
+ Field 2:A + + Field 2:B +
+
+ + +
+
+ Field 1:A + + Field 1:B +
+
+ +
- , - }, - ], - } - } - rows={ - Array [ - Object { - "fieldA": "Field 2:A", - "fieldB": "Field 2:B", - "id": "b", - }, - Object { - "fieldA": "Field 1:A", - "fieldB": "Field 1:B", - "id": "a", - }, - Object { - "fieldA": "Field 3:A", - "fieldB": "Field 3:B", - "id": "c", - }, - ] - } - size="lg" - sortRow={[Function]} - translateWithId={[Function]} -> - + Select row + + +
+ + + Field 3:A + + + Field 3:B + + + + + + + +`; + +exports[`DataTable behaves as expected selection -- radio buttons should render 1`] = ` +
+
-
-

- DataTable with selection -

-

-

- +

+ +

+
-
+
+ + + + + -
+
+ Field A +
+
+
+ Field B +
+
- - - - - - - - - - - - - - - + - + - - - - - - - - - - - - + + Select row + + + + + + + + + - - - - - - - - - - - - + + Select row + + + + + + + + + - - - - - - - - - - - - - -
-
- Field A -
-
-
- Field B -
-
-
- -
- - -
-
-
- Field 2:A - - Field 2:B -
+ Field 2:A + + Field 2:B +
+
+ +
- -
- - -
-
-
- Field 1:A - - Field 1:B -
+ Field 1:A + + Field 1:B +
+
+ +
- -
- - -
-
-
- Field 3:A - - Field 3:B -
-
- + + + Select row + + +
+ + + Field 3:A + + + Field 3:B + + + +
- - + + `; -exports[`DataTable selection should have select-all default to un-checked if no rows are present 1`] = ` - - - - - - - Field A - - - Field B - - - - -
- , - }, - ], - } - } - rows={Array []} - size="lg" - sortRow={[Function]} - translateWithId={[Function]} -> - +
+

+ DataTable with selection +

+

+

+
- +

+ + 0 items selected + +

+
-
- - - - - - - - - - - - - - - - - - - -
- -
- -
-
-
-
- Field A -
-
-
- Field B -
-
-
- - -
-
-`; - -exports[`DataTable selection should render 1`] = ` - + + + + + + -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
-
-
- Field A -
-
-
- Field B -
-
- -
- -
-
-
- Field 2:A - - Field 2:B -
- -
- -
-
-
- Field 1:A - - Field 1:B -
- -
- -
-
-
- Field 3:A - - Field 3:B -
+ + + + + + - - - -
-`; - -exports[`DataTable should render 1`] = ` - + - - + + +
- , - }, - ], - } - } - rows={ - Array [ - Object { - "fieldA": "Field 2:A", - "fieldB": "Field 2:B", - "id": "b", - }, - Object { - "fieldA": "Field 1:A", - "fieldB": "Field 1:B", - "id": "a", - }, - Object { - "fieldA": "Field 3:A", - "fieldB": "Field 3:B", - "id": "c", - }, - ] - } - size="lg" - sortRow={[Function]} - translateWithId={[Function]} -> - -
-
-

+ + + + +

-

+ Add new +

- +
+ -
- -
+
+ +
-

- - - 0 items selected - - -

+ +
- -
- - - - - - - - - - - - - - -
-
- - - -
+
- - -
-
- - - - - - - - - -
- - - -
-
-
- - - - - - - - - - - - - - - - - - - - - -
+
- - -
- - - - -
+ + + +
-
- - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- Field A -
-
-
- Field B -
-
-
- Field 2:A - - Field 2:B -
- Field 1:A - - Field 1:B -
- Field 3:A - - Field 3:B -
- -
-
- - -`; - -exports[`DataTable sticky header should render 1`] = ` - - +
+ + + Field 2:A + + + Field 2:B + + + + - - - Ghost - - - Ghost - - - Ghost - - - - - - - Action 1 - - - Action 2 - - - Action 3 - - - - - - + + + + + + +
+ Field 1:A + + Field 1:B +
- - - - Field A - - - Field B - - - - - - - Field 2:A - - - Field 2:B - - - - - Field 1:A - - - Field 1:B - - - - - Field 3:A - - - Field 3:B - - - -
-
, - }, - ], - } - } - rows={ - Array [ - Object { - "fieldA": "Field 2:A", - "fieldB": "Field 2:B", - "id": "b", - }, - Object { - "fieldA": "Field 1:A", - "fieldB": "Field 1:B", - "id": "a", - }, - Object { - "fieldA": "Field 3:A", - "fieldB": "Field 3:B", - "id": "c", - }, - ] - } - size="lg" - sortRow={[Function]} - translateWithId={[Function]} -> - + -
+ + `; diff --git a/packages/react/src/components/DataTable/stories/DataTable-batch-actions.stories.js b/packages/react/src/components/DataTable/stories/DataTable-batch-actions.stories.js index 77dc8d633b38..9102ad44ac83 100644 --- a/packages/react/src/components/DataTable/stories/DataTable-batch-actions.stories.js +++ b/packages/react/src/components/DataTable/stories/DataTable-batch-actions.stories.js @@ -203,7 +203,10 @@ export const Playground = (args) => ( aria-hidden={batchActionProps.shouldShowBatchActions}> { + action('TableToolbarSearch - onChange')(evt); + onInputChange(evt); + }} /> @@ -240,7 +243,10 @@ export const Playground = (args) => ( {rows.map((row, i) => ( - + {row.cells.map((cell) => ( {cell.value} ))} diff --git a/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js b/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js index 60c06235b18b..86b47e201403 100644 --- a/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js +++ b/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js @@ -130,7 +130,12 @@ export const Playground = (args) => ( {/* pass in `onInputChange` change here to make filtering work */} - + { + action('TableToolbarSearch - onChange')(evt); + onInputChange(evt); + }} + /> Action 1 diff --git a/packages/react/src/components/DataTable/stories/DataTable-selection.stories.js b/packages/react/src/components/DataTable/stories/DataTable-selection.stories.js index b0ca3f2d68d2..d3fab40e3a29 100644 --- a/packages/react/src/components/DataTable/stories/DataTable-selection.stories.js +++ b/packages/react/src/components/DataTable/stories/DataTable-selection.stories.js @@ -70,19 +70,8 @@ export const Default = () => ( {rows.map((row, i) => ( - { - action('TableRow onClick')(evt); - }}> - { - action('TableSelectRow onSelect')(evt); - getSelectionProps({ row }).onSelect(evt); - }} - /> + + {row.cells.map((cell) => ( {cell.value} ))} @@ -214,10 +203,7 @@ export const Playground = (args) => ( }}> { - action('TableSelectRow onSelect')(evt); - getSelectionProps({ row }).onSelect(evt); - }} + onChange={action('TableSelectRow - onChange')} /> {row.cells.map((cell) => ( {cell.value} diff --git a/packages/react/src/components/DataTable/stories/DataTable-sorting.stories.js b/packages/react/src/components/DataTable/stories/DataTable-sorting.stories.js index 2fa8c80ca529..f3670adf7129 100644 --- a/packages/react/src/components/DataTable/stories/DataTable-sorting.stories.js +++ b/packages/react/src/components/DataTable/stories/DataTable-sorting.stories.js @@ -67,7 +67,7 @@ export const Default = () => ( ); export const Playground = (args) => ( - + {({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => ( diff --git a/packages/react/src/components/DataTable/stories/DataTable-toolbar.stories.js b/packages/react/src/components/DataTable/stories/DataTable-toolbar.stories.js index 31333f670b2d..6a0a6a270996 100644 --- a/packages/react/src/components/DataTable/stories/DataTable-toolbar.stories.js +++ b/packages/react/src/components/DataTable/stories/DataTable-toolbar.stories.js @@ -300,7 +300,12 @@ export const Playground = (args) => ( - + { + action('TableToolbarSearch - onChange')(evt); + onInputChange(evt); + }} + /> Action 1 diff --git a/packages/react/src/components/DataTable/stories/dynamic-content/DataTable-dynamic-content.stories.js b/packages/react/src/components/DataTable/stories/dynamic-content/DataTable-dynamic-content.stories.js index c937b5b00423..684442c11849 100644 --- a/packages/react/src/components/DataTable/stories/dynamic-content/DataTable-dynamic-content.stories.js +++ b/packages/react/src/components/DataTable/stories/dynamic-content/DataTable-dynamic-content.stories.js @@ -9,6 +9,7 @@ import './story.scss'; import React from 'react'; import { TrashCan, Save, Download } from '@carbon/icons-react'; +import { action } from '@storybook/addon-actions'; import DataTable, { Table, TableBatchAction, @@ -299,12 +300,25 @@ export const Playground = (args) => { - + { + action('TableToolbarSearch - onChange')(evt); + onInputChange(evt); + }} + /> - + { + action('handleOnRowAdd')(evt); + this.handleOnRowAdd(); + }}> Add row - + { + action('handleOnHeaderAdd')(evt); + this.handleOnHeaderAdd(); + }}> Add header @@ -325,8 +339,13 @@ export const Playground = (args) => { {rows.map((row) => ( - - + + {row.cells.map((cell) => ( {cell.value} ))} diff --git a/packages/react/src/components/DataTable/stories/expansion/DataTable-expansion.stories.js b/packages/react/src/components/DataTable/stories/expansion/DataTable-expansion.stories.js index f4955d470d42..f8e70ecff332 100644 --- a/packages/react/src/components/DataTable/stories/expansion/DataTable-expansion.stories.js +++ b/packages/react/src/components/DataTable/stories/expansion/DataTable-expansion.stories.js @@ -22,6 +22,7 @@ import DataTable, { import { Pagination } from '../../../../'; import { rows, headers } from '../shared'; import mdx from '../../DataTable.mdx'; +import { action } from '@storybook/addon-actions'; export default { title: 'Components/DataTable/Expansion', @@ -288,7 +289,9 @@ export const Playground = (args) => ( {rows.map((row) => ( - + {row.cells.map((cell) => ( {cell.value} ))} diff --git a/packages/react/src/components/DataTable/stories/shared.js b/packages/react/src/components/DataTable/stories/shared.js index 085405ea0f58..6273d3b453da 100644 --- a/packages/react/src/components/DataTable/stories/shared.js +++ b/packages/react/src/components/DataTable/stories/shared.js @@ -93,4 +93,4 @@ export const headers = [ ]; export const batchActionClick = (selectedRows) => () => - action('batch action click')(selectedRows); + action('Batch action click')(selectedRows); diff --git a/packages/react/src/components/DataTable/tools/sorting.js b/packages/react/src/components/DataTable/tools/sorting.js index 92427d419a8d..506875df34ce 100644 --- a/packages/react/src/components/DataTable/tools/sorting.js +++ b/packages/react/src/components/DataTable/tools/sorting.js @@ -100,6 +100,7 @@ export const sortRows = ({ locale, sortStates, compare, + rowIds: [a, b], }); }); diff --git a/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.stories.js b/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.stories.js index 93020e8ba6bc..239ded4f1fb9 100644 --- a/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.stories.js +++ b/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.stories.js @@ -20,7 +20,7 @@ const props = () => ({ }); export default { - title: 'Components/DataTable', + title: 'Components/DataTable/Skeleton', decorators: [withKnobs], component: DataTableSkeleton, }; @@ -34,3 +34,25 @@ export const Skeleton = () => { ); }; + +export const Playground = (args) => { + return ( +
+ +
+
+ ); +}; + +Playground.argTypes = { + headers: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, +}; diff --git a/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.js b/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.tsx similarity index 76% rename from packages/react/src/components/DataTableSkeleton/DataTableSkeleton.js rename to packages/react/src/components/DataTableSkeleton/DataTableSkeleton.tsx index d0e1668fb566..f7c8e71aca36 100644 --- a/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.js +++ b/packages/react/src/components/DataTableSkeleton/DataTableSkeleton.tsx @@ -10,6 +10,49 @@ import React from 'react'; import cx from 'classnames'; import { usePrefix } from '../../internal/usePrefix'; +export interface DataTableSkeletonProps { + /** + * Specify an optional className to add. + */ + className?: string; + + /** + * Specify the number of columns that you want to render in the skeleton state + */ + columnCount: number; + + /** + * Optionally specify whether you want the Skeleton to be rendered as a + * compact DataTable + */ + compact: boolean; + + /** + * Optionally specify the displayed headers + */ + headers?: [{ header: string; key: string }] | { header: string; key: string }; + + /** + * Specify the number of rows that you want to render in the skeleton state + */ + rowCount: number; + + /** + * Specify if the table header should be rendered as part of the skeleton. + */ + showHeader: boolean; + + /** + * Specify if the table toolbar should be rendered as part of the skeleton. + */ + showToolbar: boolean; + + /** + * Optionally specify whether you want the DataTable to be zebra striped + */ + zebra: boolean; +} + const DataTableSkeleton = ({ headers, rowCount, @@ -48,8 +91,8 @@ const DataTableSkeleton = ({
{showHeader ? (
-
-
+
+
) : null} {showToolbar ? ( @@ -58,7 +101,8 @@ const DataTableSkeleton = ({ className={`${prefix}--table-toolbar`}>
+ className={`${prefix}--skeleton ${prefix}--btn ${prefix}--btn--sm`} + />
) : null} @@ -72,7 +116,7 @@ const DataTableSkeleton = ({ {headers[i]?.header}
) : ( - + )} ))} diff --git a/packages/react/src/components/DatePicker/DatePicker.stories.js b/packages/react/src/components/DatePicker/DatePicker.stories.js index 08eaab0d08b0..e2cda293ca02 100644 --- a/packages/react/src/components/DatePicker/DatePicker.stories.js +++ b/packages/react/src/components/DatePicker/DatePicker.stories.js @@ -257,13 +257,13 @@ Playground.argTypes = { }, }, onChange: { - action: 'clicked', + action: 'onChange', }, onClose: { - action: 'clicked', + action: 'onClose', }, onOpen: { - action: 'clicked', + action: 'onOpen', }, readOnly: { control: { diff --git a/packages/react/src/components/Dropdown/Dropdown.Skeleton.tsx b/packages/react/src/components/Dropdown/Dropdown.Skeleton.tsx index 4f58dc3744d5..3f2cb75dfd6a 100644 --- a/packages/react/src/components/Dropdown/Dropdown.Skeleton.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.Skeleton.tsx @@ -13,9 +13,7 @@ import { usePrefix } from '../../internal/usePrefix'; import { ReactAttr } from '../../types/common'; export interface DropdownSkeletonProps extends ReactAttr { - size?: ListBoxSize; - } const DropdownSkeleton: React.FC = ({ diff --git a/packages/react/src/components/Dropdown/Dropdown.stories.js b/packages/react/src/components/Dropdown/Dropdown.stories.js index 2ee11f1862ed..69c5f0e15650 100644 --- a/packages/react/src/components/Dropdown/Dropdown.stories.js +++ b/packages/react/src/components/Dropdown/Dropdown.stories.js @@ -121,7 +121,7 @@ Playground.argTypes = { control: { type: 'text', }, - defaultValue: 'this is an example label', + defaultValue: 'This is an example label', }, warn: { control: { @@ -139,7 +139,7 @@ Playground.argTypes = { control: { type: 'text', }, - defaultValue: 'this is an example title', + defaultValue: 'This is an example title', }, size: { options: ['sm', 'md', 'lg'], @@ -158,7 +158,6 @@ export const Default = () => ( id="default" titleText="Dropdown label" helperText="This is some helper text" - initialSelectedItem={items[0]} label="Dropdown menu options" items={items} itemToString={(item) => (item ? item.text : '')} @@ -171,7 +170,6 @@ export const Inline = () => ( ( id="default" titleText="First Layer" helperText="This is some helper text" - initialSelectedItem={items[0]} label="Dropdown menu options" items={items} itemToString={(item) => (item ? item.text : '')} @@ -196,7 +193,6 @@ export const WithLayer = () => ( id="default" titleText="Second Layer" helperText="This is some helper text" - initialSelectedItem={items[0]} label="Dropdown menu options" items={items} itemToString={(item) => (item ? item.text : '')} @@ -206,7 +202,6 @@ export const WithLayer = () => ( id="default" titleText="Third Layer" helperText="This is some helper text" - initialSelectedItem={items[0]} label="Dropdown menu options" items={items} itemToString={(item) => (item ? item.text : '')} @@ -221,7 +216,6 @@ export const InlineWithLayer = () => ( ( ( (item?: ItemType): string => { return item; } if (typeof item === 'number') { - return `${item}` + return `${item}`; } - if (item !== null && typeof item === 'object' - && 'label' in item && typeof item['label'] === 'string') { - return item['label'] + if ( + item !== null && + typeof item === 'object' && + 'label' in item && + typeof item['label'] === 'string' + ) { + return item['label']; } return ''; }; @@ -44,7 +60,6 @@ export interface OnChangeData { export interface DropdownProps extends Omit, ExcludedAttributes> { - /** * 'aria-label' of the ListBox component. */ @@ -183,249 +198,268 @@ export interface DropdownProps warnText?: React.ReactNode; } -const Dropdown = React.forwardRef(( - { - className: containerClassName, - disabled, - direction, - items, - label, - ariaLabel, - itemToString = defaultItemToString, - itemToElement, - renderSelectedItem, - type, - size, - onChange, - id, - titleText, - hideLabel, - helperText, - translateWithId, - light, - invalid, - invalidText, - warn, - warnText, - initialSelectedItem, - selectedItem: controlledSelectedItem, - downshiftProps, - readOnly, - ...other - }: DropdownProps, - ref: ForwardedRef -) => { - const prefix = usePrefix(); - const { isFluid } = useContext(FormContext); - const selectProps: UseSelectProps = { - ...downshiftProps, - items, - itemToString, - initialSelectedItem, - onSelectedItemChange, - }; - - // only set selectedItem if the prop is defined. Setting if it is undefined - // will overwrite default selected items from useSelect - if (controlledSelectedItem !== undefined) { - selectProps.selectedItem = controlledSelectedItem; - } - - const { - isOpen, - getToggleButtonProps, - getLabelProps, - getMenuProps, - getItemProps, - highlightedIndex, - selectedItem, - } = useSelect(selectProps); - const inline = type === 'inline'; - const showWarning = !invalid && warn; - - const enabled = useFeatureFlag('enable-v11-release'); - - const [isFocused, setIsFocused] = useState(false); - - const className = cx( - `${prefix}--dropdown`, - [enabled ? null : containerClassName], +const Dropdown = React.forwardRef( + ( { - [`${prefix}--dropdown--invalid`]: invalid, - [`${prefix}--dropdown--warning`]: showWarning, - [`${prefix}--dropdown--open`]: isOpen, - [`${prefix}--dropdown--inline`]: inline, - [`${prefix}--dropdown--disabled`]: disabled, - [`${prefix}--dropdown--light`]: light, - [`${prefix}--dropdown--readonly`]: readOnly, - [`${prefix}--dropdown--${size}`]: size, - [`${prefix}--list-box--up`]: direction === 'top', + className: containerClassName, + disabled, + direction, + items, + label, + ariaLabel, + itemToString = defaultItemToString, + itemToElement, + renderSelectedItem, + type, + size, + onChange, + id, + titleText, + hideLabel, + helperText, + translateWithId, + light, + invalid, + invalidText, + warn, + warnText, + initialSelectedItem, + selectedItem: controlledSelectedItem, + downshiftProps, + readOnly, + ...other + }: DropdownProps, + ref: ForwardedRef + ) => { + const prefix = usePrefix(); + const { isFluid } = useContext(FormContext); + const selectProps: UseSelectProps = { + ...downshiftProps, + items, + itemToString, + initialSelectedItem, + onSelectedItemChange, + }; + + // only set selectedItem if the prop is defined. Setting if it is undefined + // will overwrite default selected items from useSelect + if (controlledSelectedItem !== undefined) { + selectProps.selectedItem = controlledSelectedItem; } - ); - const titleClasses = cx(`${prefix}--label`, { - [`${prefix}--label--disabled`]: disabled, - [`${prefix}--visually-hidden`]: hideLabel, - }); + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + getItemProps, + highlightedIndex, + selectedItem, + } = useSelect(selectProps); + const inline = type === 'inline'; + const showWarning = !invalid && warn; + + const enabled = useFeatureFlag('enable-v11-release'); + + const [isFocused, setIsFocused] = useState(false); + + const className = cx( + `${prefix}--dropdown`, + [enabled ? null : containerClassName], + { + [`${prefix}--dropdown--invalid`]: invalid, + [`${prefix}--dropdown--warning`]: showWarning, + [`${prefix}--dropdown--open`]: isOpen, + [`${prefix}--dropdown--inline`]: inline, + [`${prefix}--dropdown--disabled`]: disabled, + [`${prefix}--dropdown--light`]: light, + [`${prefix}--dropdown--readonly`]: readOnly, + [`${prefix}--dropdown--${size}`]: size, + [`${prefix}--list-box--up`]: direction === 'top', + } + ); + + const titleClasses = cx(`${prefix}--label`, { + [`${prefix}--label--disabled`]: disabled, + [`${prefix}--visually-hidden`]: hideLabel, + }); + + const helperClasses = cx(`${prefix}--form__helper-text`, { + [`${prefix}--form__helper-text--disabled`]: disabled, + }); + + const wrapperClasses = cx( + `${prefix}--dropdown__wrapper`, + `${prefix}--list-box__wrapper`, + [enabled ? containerClassName : null], + { + [`${prefix}--dropdown__wrapper--inline`]: inline, + [`${prefix}--list-box__wrapper--inline`]: inline, + [`${prefix}--dropdown__wrapper--inline--invalid`]: inline && invalid, + [`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid, + [`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid, + [`${prefix}--list-box__wrapper--fluid--focus`]: + isFluid && isFocused && !isOpen, + } + ); + + // needs to be Capitalized for react to render it correctly + const ItemToElement = itemToElement; + const toggleButtonProps = getToggleButtonProps(); + const helper = + helperText && !isFluid ? ( +
{helperText}
+ ) : null; + + function onSelectedItemChange({ + selectedItem, + }: Partial>) { + setIsFocused(false); + if (onChange) { + onChange({ selectedItem: selectedItem ?? null }); + } + } - const helperClasses = cx(`${prefix}--form__helper-text`, { - [`${prefix}--form__helper-text--disabled`]: disabled, - }); + const menuItemOptionRefs = useRef(items.map((_) => React.createRef())); - const wrapperClasses = cx( - `${prefix}--dropdown__wrapper`, - `${prefix}--list-box__wrapper`, - [enabled ? containerClassName : null], - { - [`${prefix}--dropdown__wrapper--inline`]: inline, - [`${prefix}--list-box__wrapper--inline`]: inline, - [`${prefix}--dropdown__wrapper--inline--invalid`]: inline && invalid, - [`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid, - [`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid, - [`${prefix}--list-box__wrapper--fluid--focus`]: - isFluid && isFocused && !isOpen, - } - ); - - // needs to be Capitalized for react to render it correctly - const ItemToElement = itemToElement; - const toggleButtonProps = getToggleButtonProps(); - const helper = - helperText && !isFluid ? ( -
{helperText}
- ) : null; - - function onSelectedItemChange({ selectedItem }: Partial>) { - setIsFocused(false); - if (onChange) { - onChange({ selectedItem: selectedItem ?? null }); - } - } + const handleFocus = (evt: FocusEvent) => { + setIsFocused(evt.type === 'focus' ? true : false); + }; + + const mergedRef = mergeRefs(toggleButtonProps.ref, ref); - const menuItemOptionRefs = useRef(items.map((_) => React.createRef())); - - const handleFocus = (evt: FocusEvent) => { - setIsFocused(evt.type === 'focus' ? true : false); - }; - - const mergedRef = mergeRefs(toggleButtonProps.ref, ref); - - const readOnlyEventHandlers = readOnly - ? { - onClick: (evt: MouseEvent) => { - // NOTE: does not prevent click - evt.preventDefault(); - // focus on the element as per readonly input behavior - if (mergedRef.current !== undefined) { - mergedRef.current.focus(); - } - }, - onKeyDown: (evt: React.KeyboardEvent) => { - const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' ', 'Enter']; - // This prevents the select from opening for the above keys - if (selectAccessKeys.includes(evt.key)) { + const readOnlyEventHandlers = readOnly + ? { + onClick: (evt: MouseEvent) => { + // NOTE: does not prevent click evt.preventDefault(); - } - }, - } - : {}; - - return ( -
- {titleText && ( - - )} - - {invalid && ( - - )} - {showWarning && ( - + // focus on the element as per readonly input behavior + if (mergedRef.current !== undefined) { + mergedRef.current.focus(); + } + }, + onKeyDown: (evt: React.KeyboardEvent) => { + const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' ', 'Enter']; + // This prevents the select from opening for the above keys + if (selectAccessKeys.includes(evt.key)) { + evt.preventDefault(); + } + }, + } + : {}; + + return ( +
+ {titleText && ( + )} - - - {isOpen && - items.map((item, index) => { - const isObject = item !== null && typeof item === 'object'; - const disabled = isObject && 'disabled' in item && item.disabled === true; - const itemProps = getItemProps({ - item, - index, - disabled, - }); - const title = isObject && 'text' in item && itemToElement ? item.text : itemToString(item); - return ( - - {typeof item === 'object' && ItemToElement !== undefined - && ItemToElement !== null ? ( - - ) : ( - itemToString(item) - )} - {selectedItem === item && ( - - )} - - ); - })} - - - {!inline && !invalid && !warn && helper} -
- ); -}); - -type DropdownComponentProps = - React.PropsWithoutRef> - & React.RefAttributes> + + {invalid && ( + + )} + {showWarning && ( + + )} + + + {isOpen && + items.map((item, index) => { + const isObject = item !== null && typeof item === 'object'; + const disabled = + isObject && 'disabled' in item && item.disabled === true; + const itemProps = getItemProps({ + item, + index, + disabled, + }); + const title = + isObject && 'text' in item && itemToElement + ? item.text + : itemToString(item); + return ( + + {typeof item === 'object' && + ItemToElement !== undefined && + ItemToElement !== null ? ( + + ) : ( + itemToString(item) + )} + {selectedItem === item && ( + + )} + + ); + })} + + + {!inline && !invalid && !warn && helper} +
+ ); + } +); + +type DropdownComponentProps = React.PropsWithoutRef< + React.PropsWithChildren> & + React.RefAttributes +>; interface DropdownComponent { - (props: DropdownComponentProps): React.ReactElement | null + ( + props: DropdownComponentProps + ): React.ReactElement | null; } Dropdown.displayName = 'Dropdown'; diff --git a/packages/react/src/components/FluidForm/FluidForm.tsx b/packages/react/src/components/FluidForm/FluidForm.tsx index e2179c276280..f5878c9047ba 100644 --- a/packages/react/src/components/FluidForm/FluidForm.tsx +++ b/packages/react/src/components/FluidForm/FluidForm.tsx @@ -13,7 +13,7 @@ import { FormContext } from './FormContext'; import { usePrefix } from '../../internal/usePrefix'; import { ReactAttr } from '../../types/common'; -export type FluidFormProps = ReactAttr +export type FluidFormProps = ReactAttr; const FluidForm: React.FC = ({ className, @@ -30,7 +30,7 @@ const FluidForm: React.FC = ({ ); -} +}; FluidForm.propTypes = { /** diff --git a/packages/react/src/components/Grid/CSSGrid.tsx b/packages/react/src/components/Grid/CSSGrid.tsx index 5d249e1ea5f0..c1181ad16df0 100644 --- a/packages/react/src/components/Grid/CSSGrid.tsx +++ b/packages/react/src/components/Grid/CSSGrid.tsx @@ -38,8 +38,7 @@ function CSSGrid({ as={BaseComponent} className={customClassName} mode={mode} - {...rest} - > + {...rest}> {children} @@ -54,7 +53,7 @@ function CSSGrid({ }); // cast as any to let TypeScript allow passing in attributes to base component - const BaseComponentAsAny: any = BaseComponent + const BaseComponentAsAny: any = BaseComponent; return ( @@ -98,10 +97,9 @@ CSSGrid.propTypes = { narrow: PropTypes.bool, }; -type SubgridMode = 'wide' | 'narrow' | 'condensed' +type SubgridMode = 'wide' | 'narrow' | 'condensed'; interface SubgridBaseProps { - /** * Pass in content that will be rendered within the `Subgrid` */ @@ -116,10 +114,9 @@ interface SubgridBaseProps { * Specify the grid mode for the subgrid */ mode?: SubgridMode; - } -type SubgridProps = PolymorphicProps +type SubgridProps = PolymorphicProps; const Subgrid = ({ as: BaseComponent = 'div', @@ -140,7 +137,7 @@ const Subgrid = ({ {children} ); -} +}; Subgrid.propTypes = { /** diff --git a/packages/react/src/components/Grid/Column.tsx b/packages/react/src/components/Grid/Column.tsx index aed36085232b..3bafe090a26d 100644 --- a/packages/react/src/components/Grid/Column.tsx +++ b/packages/react/src/components/Grid/Column.tsx @@ -15,10 +15,9 @@ import { useGridSettings } from './GridContext'; type ColumnSpanPercent = '25%' | '50%' | '75%' | '100%'; -type ColumnSpanSimple = boolean | number | ColumnSpanPercent +type ColumnSpanSimple = boolean | number | ColumnSpanPercent; interface ColumnSpanObject { - span?: ColumnSpanSimple; offset?: number; @@ -26,13 +25,11 @@ interface ColumnSpanObject { start?: number; end?: number; - } -export type ColumnSpan = ColumnSpanSimple | ColumnSpanObject +export type ColumnSpan = ColumnSpanSimple | ColumnSpanObject; interface ColumnBaseProps { - /** * Pass in content that will be rendered within the `Column` */ @@ -41,7 +38,7 @@ interface ColumnBaseProps { /** * Specify a custom className to be applied to the `Column` */ - className?: string, + className?: string; /** * Specify column span for the `lg` breakpoint (Default breakpoint up to 1312px) @@ -88,13 +85,18 @@ interface ColumnBaseProps { * based on breakpoint */ span?: ColumnSpan; - } -export type ColumnProps = PolymorphicProps; +export type ColumnProps = PolymorphicProps< + T, + ColumnBaseProps +>; export interface ColumnComponent { - (props: ColumnProps, context?: any): React.ReactElement | null; + ( + props: ColumnProps, + context?: any + ): React.ReactElement | null; } function Column({ @@ -136,7 +138,7 @@ function Column({ }); // cast as any to let TypeScript allow passing in attributes to base component - const BaseComponentAsAny: any = BaseComponent + const BaseComponentAsAny: any = BaseComponent; return ( {children} @@ -337,7 +339,10 @@ const breakpointNames = ['sm', 'md', 'lg', 'xlg', 'max']; * @param {Array} breakpoints * @returns {string} */ -function getClassNameForBreakpoints(breakpoints: (ColumnSpan | undefined)[], prefix: string): string { +function getClassNameForBreakpoints( + breakpoints: (ColumnSpan | undefined)[], + prefix: string +): string { const classNames: string[] = []; for (let i = 0; i < breakpoints.length; i++) { @@ -375,15 +380,15 @@ function getClassNameForBreakpoints(breakpoints: (ColumnSpan | undefined)[], pre if (typeof offset === 'number' && offset > 0) { classNames.push(`${prefix}--${name}:col-start-${offset + 1}`); } - + if (typeof start === 'number') { classNames.push(`${prefix}--${name}:col-start-${start}`); } - + if (typeof end === 'number') { classNames.push(`${prefix}--${name}:col-end-${end}`); } - + if (typeof span === 'number') { classNames.push(`${prefix}--${name}:col-span-${span}`); } else if (typeof span === 'string') { @@ -401,7 +406,10 @@ function getClassNameForBreakpoints(breakpoints: (ColumnSpan | undefined)[], pre * @param {Array} breakpoints * @returns {string} */ -function getClassNameForFlexGridBreakpoints(breakpoints: (ColumnSpan | undefined)[], prefix: string): string { +function getClassNameForFlexGridBreakpoints( + breakpoints: (ColumnSpan | undefined)[], + prefix: string +): string { const classNames: string[] = []; for (let i = 0; i < breakpoints.length; i++) { @@ -448,7 +456,10 @@ function getClassNameForFlexGridBreakpoints(breakpoints: (ColumnSpan | undefined /** * Build the appropriate className for a span value */ -function getClassNameForSpan(value: ColumnSpan | undefined, prefix: string): string { +function getClassNameForSpan( + value: ColumnSpan | undefined, + prefix: string +): string { const classNames: string[] = []; if (typeof value === 'number' || typeof value === 'string') { diff --git a/packages/react/src/components/Grid/ColumnHang.tsx b/packages/react/src/components/Grid/ColumnHang.tsx index f22e78895da6..9dc9473d5e71 100644 --- a/packages/react/src/components/Grid/ColumnHang.tsx +++ b/packages/react/src/components/Grid/ColumnHang.tsx @@ -12,7 +12,6 @@ import { usePrefix } from '../../internal/usePrefix'; import { PolymorphicProps } from '../../types/common'; interface ColumnHangBaseProps { - /** * Pass in content that will be rendered within the `ColumnHang` */ @@ -22,13 +21,18 @@ interface ColumnHangBaseProps { * Specify a custom className to be applied to the `ColumnHang` */ className?: string; - } -export type ColumnHangProps = PolymorphicProps +export type ColumnHangProps = PolymorphicProps< + T, + ColumnHangBaseProps +>; export interface ColumnHangComponent { - (props: ColumnHangProps, context?: any): React.ReactElement | null; + ( + props: ColumnHangProps, + context?: any + ): React.ReactElement | null; } /** @@ -44,7 +48,7 @@ function ColumnHang({ const prefix = usePrefix(); const className = cx(customClassName, `${prefix}--grid-column-hang`); // cast as any to let TypeScript allow passing in attributes to base component - const BaseComponentAsAny: any = BaseComponent + const BaseComponentAsAny: any = BaseComponent; return ( {children} @@ -69,6 +73,6 @@ ColumnHang.propTypes = { className: PropTypes.string, }; -const ColumnHangComponent = ColumnHang as ColumnHangComponent +const ColumnHangComponent = ColumnHang as ColumnHangComponent; export { ColumnHangComponent as ColumnHang }; diff --git a/packages/react/src/components/Grid/FlexGrid.tsx b/packages/react/src/components/Grid/FlexGrid.tsx index 83f5f70a85ea..aa9fc6a23efa 100644 --- a/packages/react/src/components/Grid/FlexGrid.tsx +++ b/packages/react/src/components/Grid/FlexGrid.tsx @@ -29,7 +29,7 @@ function FlexGrid({ [`${prefix}--grid--full-width`]: fullWidth, }); // cast as any to let TypeScript allow passing in attributes to base component - const BaseComponentAsAny: any = BaseComponent + const BaseComponentAsAny: any = BaseComponent; return ( diff --git a/packages/react/src/components/Grid/GridContext.tsx b/packages/react/src/components/Grid/GridContext.tsx index eb9d8a0500da..9b0085a9c409 100644 --- a/packages/react/src/components/Grid/GridContext.tsx +++ b/packages/react/src/components/Grid/GridContext.tsx @@ -11,7 +11,6 @@ import * as React from 'react'; export type GridMode = 'flexbox' | 'css-grid'; export interface GridSettingContext { - /** * The grid mode for the GridContext */ @@ -21,7 +20,6 @@ export interface GridSettingContext { * Specifies whether subgrid should be enabled */ subgrid?: boolean; - } /** @@ -34,7 +32,6 @@ const GridSettingsContext = React.createContext({ }); export interface GridSettingsProps { - /** * Pass in components which will be rendered within the `GridSettings` * component @@ -50,13 +47,12 @@ export interface GridSettingsProps { * Specify whether subgrid should be enabled */ subgrid?: boolean; - } export const GridSettings: React.FC = ({ children, mode, - subgrid = false + subgrid = false, }) => { const value = React.useMemo(() => { return { @@ -69,7 +65,7 @@ export const GridSettings: React.FC = ({ {children} ); -} +}; const gridModes: GridMode[] = ['flexbox', 'css-grid']; @@ -96,4 +92,4 @@ GridSettings.propTypes = { */ export const useGridSettings = () => { return React.useContext(GridSettingsContext); -} +}; diff --git a/packages/react/src/components/Grid/Row.tsx b/packages/react/src/components/Grid/Row.tsx index a6418c4d841d..99525f7405e4 100644 --- a/packages/react/src/components/Grid/Row.tsx +++ b/packages/react/src/components/Grid/Row.tsx @@ -12,7 +12,6 @@ import { usePrefix } from '../../internal/usePrefix'; import { PolymorphicProps } from '../../types/common'; export interface RowBaseProps { - /** * Pass in content that will be rendered within the `Row` */ @@ -34,13 +33,18 @@ export interface RowBaseProps { * 16px into the gutter. */ narrow?: boolean; - } -export type RowProps = PolymorphicProps +export type RowProps = PolymorphicProps< + T, + RowBaseProps +>; export interface RowComponent { - (props: RowProps, context?: any): React.ReactElement | null; + ( + props: RowProps, + context?: any + ): React.ReactElement | null; } function Row({ @@ -58,7 +62,7 @@ function Row({ [`${prefix}--row--narrow`]: narrow, }); // TypeScript type validation reports conflicts on different instances of keyof JSX.IntrinsicElements - const BaseComponentAsAny: any = BaseComponent + const BaseComponentAsAny: any = BaseComponent; return ( {children} diff --git a/packages/react/src/components/IdPrefix/IdPrefix.mdx b/packages/react/src/components/IdPrefix/IdPrefix.mdx index 0c46810f3ab3..4b408b476736 100644 --- a/packages/react/src/components/IdPrefix/IdPrefix.mdx +++ b/packages/react/src/components/IdPrefix/IdPrefix.mdx @@ -25,8 +25,8 @@ automatically generated `id` attributes placed on certain DOM elements. -This component is used intended to be used in limited cases, primarily only if -you have id conflics when using v10 and v11 packages at the same time during +This component is intended to be used in limited cases, primarily only if +you have id conflicts when using v10 and v11 packages at the same time during migration. In React, you can use `IdPrefix` anywhere in your component tree and specify the diff --git a/packages/react/src/components/ListBox/ListBox.tsx b/packages/react/src/components/ListBox/ListBox.tsx index d857421502d2..80e4a979049f 100644 --- a/packages/react/src/components/ListBox/ListBox.tsx +++ b/packages/react/src/components/ListBox/ListBox.tsx @@ -6,7 +6,7 @@ */ import cx from 'classnames'; -import React, { KeyboardEvent, MouseEvent, useContext} from 'react'; +import React, { KeyboardEvent, MouseEvent, useContext } from 'react'; import PropTypes from 'prop-types'; import deprecate from '../../prop-types/deprecate'; import { ListBoxType, ListBoxSize } from './ListBoxPropTypes'; @@ -25,11 +25,10 @@ const handleClick = (event: MouseEvent) => { event.stopPropagation(); }; -type ExcludedAttributes = 'onKeyDown' | 'onKeyPress' | 'ref' +type ExcludedAttributes = 'onKeyDown' | 'onKeyPress' | 'ref'; export interface ListBoxProps extends Omit, ExcludedAttributes> { - /** * Specify whether the ListBox is currently disabled */ @@ -53,7 +52,7 @@ export interface ListBoxProps /** * `true` to use the light version. For use on $ui-01 backgrounds only. * Don't use this to make tile background color same as container background color. - * + * * @deprecated The `light` prop for `ListBox` has been deprecated in favor of * the new `Layer` component. It will be removed in the next major release. */ @@ -81,7 +80,7 @@ export interface ListBoxProps warnText?: React.ReactNode; } -export type ListBoxComponent = ForwardRefReturn +export type ListBoxComponent = ForwardRefReturn; /** * `ListBox` is a generic container component that handles creating the @@ -109,7 +108,7 @@ const ListBox: ListBoxComponent = React.forwardRef(function ListBox( const showWarning = !invalid && warn; const className = cx({ - ...(containerClassName && {[containerClassName]: true}), + ...(containerClassName && { [containerClassName]: true }), [`${prefix}--list-box`]: true, [`${prefix}--list-box--${size}`]: size, [`${prefix}--list-box--inline`]: type === 'inline', diff --git a/packages/react/src/components/ListBox/ListBoxField.tsx b/packages/react/src/components/ListBox/ListBoxField.tsx index 47912db6a641..9e6e58133281 100644 --- a/packages/react/src/components/ListBox/ListBoxField.tsx +++ b/packages/react/src/components/ListBox/ListBoxField.tsx @@ -14,12 +14,10 @@ import { ReactAttr } from '../../types/common'; export const translationIds = {}; export interface ListBoxFieldProps extends ReactAttr { - /** * Specify if the parent is disabled */ disabled?: boolean; - } /** @@ -27,7 +25,12 @@ export interface ListBoxFieldProps extends ReactAttr { * elements inside of a field. It also provides a11y-related attributes like * `role` to make sure a user can focus the given field. */ -function ListBoxField({ children, disabled, tabIndex, ...rest }: ListBoxFieldProps) { +function ListBoxField({ + children, + disabled, + tabIndex, + ...rest +}: ListBoxFieldProps) { const prefix = usePrefix(); return ( @@ -69,6 +72,6 @@ ListBoxField.propTypes = { tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; -export type ListBoxFieldComponent = React.FC +export type ListBoxFieldComponent = React.FC; export default ListBoxField as ListBoxFieldComponent; diff --git a/packages/react/src/components/ListBox/ListBoxMenu.tsx b/packages/react/src/components/ListBox/ListBoxMenu.tsx index 957f58c142b4..8f793e2abe63 100644 --- a/packages/react/src/components/ListBox/ListBoxMenu.tsx +++ b/packages/react/src/components/ListBox/ListBoxMenu.tsx @@ -15,14 +15,16 @@ type ExcludedAttributes = 'id'; export interface ListBoxMenuProps extends Omit, ExcludedAttributes> { - /** * Specify a custom `id` */ id: string; } -export type ListBoxMenuComponent = ForwardRefReturn +export type ListBoxMenuComponent = ForwardRefReturn< + HTMLDivElement, + ListBoxMenuProps +>; /** * `ListBoxMenu` is a simple container node that isolates the `list-box__menu` @@ -31,7 +33,7 @@ export type ListBoxMenuComponent = ForwardRefReturn, + ref: ForwardedRef ) { const prefix = usePrefix(); return ( diff --git a/packages/react/src/components/ListBox/ListBoxMenuIcon.tsx b/packages/react/src/components/ListBox/ListBoxMenuIcon.tsx index 206d5bc4104a..aa55a958b746 100644 --- a/packages/react/src/components/ListBox/ListBoxMenuIcon.tsx +++ b/packages/react/src/components/ListBox/ListBoxMenuIcon.tsx @@ -21,7 +21,7 @@ const defaultTranslations = { [translationIds['open.menu']]: 'Open menu', }; -const defaultTranslateWithId = (id: string) => defaultTranslations[id] +const defaultTranslateWithId = (id: string) => defaultTranslations[id]; export interface ListBoxMenuIconProps { /** diff --git a/packages/react/src/components/ListBox/ListBoxMenuItem.tsx b/packages/react/src/components/ListBox/ListBoxMenuItem.tsx index 0ef179668def..a36b2d8f969c 100644 --- a/packages/react/src/components/ListBox/ListBoxMenuItem.tsx +++ b/packages/react/src/components/ListBox/ListBoxMenuItem.tsx @@ -23,7 +23,6 @@ function useIsTruncated(ref) { } export interface ListBoxMenuItemProps extends ReactAttr { - /** * Specify whether the current menu item is "active". */ @@ -33,46 +32,52 @@ export interface ListBoxMenuItemProps extends ReactAttr { * Specify whether the current menu item is "highlighted". */ isHighlighted?: boolean; - } -export type ListBoxMenuItemForwardedRef = ForwardedRef & { - menuItemOptionRef?: React.Ref; - } | null; +export type ListBoxMenuItemForwardedRef = + | (ForwardedRef & { + menuItemOptionRef?: React.Ref; + }) + | null; -export type ListBoxMenuItemComponent = ForwardRefReturn; +export type ListBoxMenuItemComponent = ForwardRefReturn< + ListBoxMenuItemForwardedRef, + ListBoxMenuItemProps +>; /** * `ListBoxMenuItem` is a helper component for managing the container class * name, alongside any classes for any corresponding states, for a generic list * box menu item. */ -const ListBoxMenuItem = React.forwardRef(function ListBoxMenuItem( - { children, isActive, isHighlighted, title, ...rest }: ListBoxMenuItemProps, - forwardedRef: ListBoxMenuItemForwardedRef -) { - const prefix = usePrefix(); - const ref = useRef(null); - const isTruncated = useIsTruncated(forwardedRef?.menuItemOptionRef || ref); - const className = cx(`${prefix}--list-box__menu-item`, { - [`${prefix}--list-box__menu-item--active`]: isActive, - [`${prefix}--list-box__menu-item--highlighted`]: isHighlighted, - }); - - return ( -
+const ListBoxMenuItem = React.forwardRef( + function ListBoxMenuItem( + { children, isActive, isHighlighted, title, ...rest }: ListBoxMenuItemProps, + forwardedRef: ListBoxMenuItemForwardedRef + ) { + const prefix = usePrefix(); + const ref = useRef(null); + const isTruncated = useIsTruncated(forwardedRef?.menuItemOptionRef || ref); + const className = cx(`${prefix}--list-box__menu-item`, { + [`${prefix}--list-box__menu-item--active`]: isActive, + [`${prefix}--list-box__menu-item--highlighted`]: isHighlighted, + }); + + return (
- {children} + {...rest} + className={className} + title={isTruncated ? title : undefined} + tabIndex={-1}> +
+ {children} +
-
- ); -}); + ); + } +); ListBoxMenuItem.displayName = 'ListBoxMenuItem'; ListBoxMenuItem.propTypes = { diff --git a/packages/react/src/components/ListBox/ListBoxSelection.tsx b/packages/react/src/components/ListBox/ListBoxSelection.tsx index f3c32675d41a..e88a88cbdc5a 100644 --- a/packages/react/src/components/ListBox/ListBoxSelection.tsx +++ b/packages/react/src/components/ListBox/ListBoxSelection.tsx @@ -9,7 +9,6 @@ import cx from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import { Close } from '@carbon/icons-react'; -import { match, keys } from '../../internal/keyboard'; import { usePrefix } from '../../internal/usePrefix'; import { KeyboardEvent, MouseEvent } from 'react'; @@ -18,7 +17,9 @@ export interface ListBoxSelectionProps { * Specify a function to be invoked when a user interacts with the clear * selection element. */ - clearSelection(event: MouseEvent | KeyboardEvent): void; + clearSelection( + event: MouseEvent | KeyboardEvent + ): void; /** * Specify whether or not the clear selection element should be disabled @@ -29,7 +30,9 @@ export interface ListBoxSelectionProps { * Specify an optional `onClearSelection` handler that is called when the underlying * element is cleared */ - onClearSelection?(event: MouseEvent | KeyboardEvent): void; + onClearSelection?( + event: MouseEvent | KeyboardEvent + ): void; /** * Whether or not the Dropdown is readonly @@ -50,7 +53,7 @@ export interface ListBoxSelectionProps { translateWithId(messageId: string, args?: Record): string; } -export type ListBoxSelectionComponent = React.FC +export type ListBoxSelectionComponent = React.FC; /** * `ListBoxSelection` is used to provide controls for clearing a selection, in @@ -80,20 +83,6 @@ const ListBoxSelection: ListBoxSelectionComponent = ({ onClearSelection(event); } }; - const handleOnKeyDown = (event: KeyboardEvent) => { - event.stopPropagation(); - if (disabled || readOnly) { - return; - } - - // When a user hits ENTER, we'll clear the selection - if (match(event.code, keys.Enter)) { - clearSelection(event); - if (onClearSelection) { - onClearSelection(event); - } - } - }; const description = selectionCount ? t('clear.all') : t('clear.selection'); const tagClasses = cx( `${prefix}--tag`, @@ -103,6 +92,8 @@ const ListBoxSelection: ListBoxSelectionComponent = ({ [`${prefix}--tag--disabled`]: disabled, } ); + + /* eslint-disable jsx-a11y/click-events-have-key-events */ return selectionCount ? (
@@ -110,10 +101,9 @@ const ListBoxSelection: ListBoxSelectionComponent = ({
@@ -124,16 +114,15 @@ const ListBoxSelection: ListBoxSelectionComponent = ({
{selectionCount}
); -} +}; export const translationIds = { 'clear.all': 'clear.all', diff --git a/packages/react/src/components/ListBox/next/ListBoxSelection.js b/packages/react/src/components/ListBox/next/ListBoxSelection.js index 6beba16636db..d7fbe6c67bd6 100644 --- a/packages/react/src/components/ListBox/next/ListBoxSelection.js +++ b/packages/react/src/components/ListBox/next/ListBoxSelection.js @@ -9,7 +9,6 @@ import cx from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import { Close } from '@carbon/icons-react'; -import { match, keys } from '../../../internal/keyboard'; import { usePrefix } from '../../../internal/usePrefix'; /** @@ -51,21 +50,6 @@ function ListBoxSelection({ } } - function onKeyDown(event) { - event.stopPropagation(); - if (disabled) { - return; - } - - // When a user hits ENTER, we'll clear the selection - if (match(event, keys.Enter)) { - clearSelection(event); - if (onClearSelection) { - onClearSelection(event); - } - } - } - if (selectionCount) { return (
@@ -77,8 +61,7 @@ function ListBoxSelection({ className={`${prefix}--tag__close-icon`} disabled={disabled} onClick={onClick} - onKeyDown={onKeyDown} - tabIndex={disabled ? -1 : 0} + tabIndex={-1} title={description} type="button"> @@ -94,8 +77,7 @@ function ListBoxSelection({ className={className} disabled={disabled} onClick={onClick} - onKeyDown={onKeyDown} - tabIndex={disabled ? -1 : 0} + tabIndex={-1} title={description} type="button"> diff --git a/packages/react/src/components/Modal/Modal.js b/packages/react/src/components/Modal/Modal.js index f0af83fe2d73..ea26b22ca861 100644 --- a/packages/react/src/components/Modal/Modal.js +++ b/packages/react/src/components/Modal/Modal.js @@ -23,6 +23,7 @@ const getInstanceId = setupGetInstanceId(); const Modal = React.forwardRef(function Modal( { + 'aria-label': ariaLabelProp, children, className, modalHeading, @@ -158,7 +159,7 @@ const Modal = React.forwardRef(function Modal( ); const ariaLabel = - modalLabel || rest['aria-label'] || modalAriaLabel || modalHeading; + modalLabel || ariaLabelProp || modalAriaLabel || modalHeading; const getAriaLabelledBy = modalLabel ? modalLabelId : modalHeadingId; const hasScrollingContentProps = hasScrollingContent diff --git a/packages/react/src/components/Modal/Modal.stories.js b/packages/react/src/components/Modal/Modal.stories.js index d1aa1a84abfb..d75d48aaa391 100644 --- a/packages/react/src/components/Modal/Modal.stories.js +++ b/packages/react/src/components/Modal/Modal.stories.js @@ -315,16 +315,16 @@ Playground.argTypes = { }, }, onKeyDown: { - action: 'clicked', + action: 'onKeyDown', }, onRequestClose: { - action: 'clicked', + action: 'onRequestClose', }, onRequestSubmit: { - action: 'clicked', + action: 'onRequestSubmit', }, onSecondarySubmit: { - action: 'clicked', + action: 'onSecondarySubmit', table: { disable: true, }, diff --git a/packages/react/src/components/MultiSelect/FilterableMultiSelect.js b/packages/react/src/components/MultiSelect/FilterableMultiSelect.js index cbb0e39d2a23..78d43a284978 100644 --- a/packages/react/src/components/MultiSelect/FilterableMultiSelect.js +++ b/packages/react/src/components/MultiSelect/FilterableMultiSelect.js @@ -28,7 +28,8 @@ import { FormContext } from '../FluidForm'; const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect( { - ariaLabel, + ['aria-label']: ariaLabel, + ariaLabel: deprecatedAriaLabel, className: containerClassName, compareItems, direction, @@ -87,6 +88,7 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect( const wrapperClasses = cx( `${prefix}--multi-select__wrapper`, + `${prefix}--multi-select--filterable__wrapper`, `${prefix}--list-box__wrapper`, [enabled ? containerClassName : null], { @@ -243,6 +245,8 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect( [enabled ? null : containerClassName], { [`${prefix}--multi-select--invalid`]: invalid, + [`${prefix}--multi-select--invalid--focused`]: + invalid && inputFocused, [`${prefix}--multi-select--open`]: isOpen, [`${prefix}--multi-select--inline`]: inline, [`${prefix}--multi-select--selected`]: selectedItem.length > 0, @@ -299,6 +303,24 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect( event.stopPropagation(); } + if (match(event, keys.Enter)) { + handleOnMenuChange(true); + } + + if (!disabled) { + if (match(event, keys.Delete) || match(event, keys.Escape)) { + if (isOpen) { + handleOnMenuChange(true); + clearInputValue(); + event.stopPropagation(); + } else if (!isOpen) { + clearInputValue(); + clearSelection(); + event.stopPropagation(); + } + } + } + if (match(event, keys.Tab)) { handleOnMenuChange(false); } @@ -308,6 +330,7 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect( }, onBlur: () => { setInputFocused(false); + setInputValue(''); }, }); @@ -339,6 +362,7 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect( ) : null} /** * `onMenuChange` is a utility for this controlled component to communicate to a - * consuming component that the menu was opend(`true`)/closed(`false`). + * consuming component that the menu was open(`true`)/closed(`false`). */ onMenuChange?(open: boolean): void; @@ -315,7 +315,8 @@ const MultiSelect = React.forwardRef(function MultiSelect( const { current: multiSelectInstanceId } = useRef(getInstanceId()); const [highlightedIndex, setHighlightedIndex] = useState(); const [isFocused, setIsFocused] = useState(false); - const [isOpen, setIsOpen] = useState(open); + const [inputFocused, setInputFocused] = useState(false); + const [isOpen, setIsOpen] = useState(open || false); const [prevOpenProp, setPrevOpenProp] = useState(open); const [topItems, setTopItems] = useState([]); const { @@ -347,7 +348,14 @@ const MultiSelect = React.forwardRef(function MultiSelect( items, }); - const toggleButtonProps = getToggleButtonProps(); + const toggleButtonProps = getToggleButtonProps({ + onFocus: () => { + setInputFocused(true); + }, + onBlur: () => { + setInputFocused(false); + }, + }); const mergedRef = mergeRefs(toggleButtonProps.ref, ref); const selectedItems = selectedItem as ItemType[]; @@ -406,6 +414,7 @@ const MultiSelect = React.forwardRef(function MultiSelect( [enabled ? null : containerClassName], { [`${prefix}--multi-select--invalid`]: invalid, + [`${prefix}--multi-select--invalid--focused`]: invalid && inputFocused, [`${prefix}--multi-select--warning`]: showWarning, [`${prefix}--multi-select--inline`]: inline, [`${prefix}--multi-select--selected`]: @@ -464,12 +473,25 @@ const MultiSelect = React.forwardRef(function MultiSelect( } const onKeyDown = (e) => { - if (match(e, keys.Delete) && !disabled) { - clearSelection(); - e.stopPropagation(); + if (!disabled) { + if (match(e, keys.Delete) || match(e, keys.Escape)) { + clearSelection(); + e.stopPropagation(); + } + + if (match(e, keys.Space) || match(e, keys.ArrowDown)) { + setIsOpenWrapper(true); + } } }; + const multiSelectFieldWrapperClasses = cx( + `${prefix}--list-box__field--wrapper`, + { + [`${prefix}--list-box__field--wrapper--input-focused`]: inputFocused, + } + ); + const handleFocus = (evt: React.FocusEvent) => { evt.target.classList.contains(`${prefix}--tag__close-icon`) ? setIsFocused(false) @@ -529,15 +551,7 @@ const MultiSelect = React.forwardRef(function MultiSelect( className={`${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`} /> )} - + +
{isOpen && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -737,7 +761,7 @@ MultiSelect.propTypes = { /** * `onMenuChange` is a utility for this controlled component to communicate to a - * consuming component that the menu was opend(`true`)/closed(`false`). + * consuming component that the menu was open(`true`)/closed(`false`). */ onMenuChange: PropTypes.func, diff --git a/packages/react/src/components/Notification/stories/ActionableNotification.stories.js b/packages/react/src/components/Notification/stories/ActionableNotification.stories.js index 5416b8182ba6..a4372c1fc921 100644 --- a/packages/react/src/components/Notification/stories/ActionableNotification.stories.js +++ b/packages/react/src/components/Notification/stories/ActionableNotification.stories.js @@ -49,13 +49,13 @@ Playground.argTypes = { }, }, onActionButtonClick: { - action: 'clicked', + action: 'onActionButtonClick', }, onClose: { - action: 'clicked', + action: 'onClose', }, onCloseButtonClick: { - action: 'clicked', + action: 'onCloseButtonClick', }, children: { table: { diff --git a/packages/react/src/components/Notification/stories/InlineNotification.stories.js b/packages/react/src/components/Notification/stories/InlineNotification.stories.js index b8ec96c41817..b9ec104df91c 100644 --- a/packages/react/src/components/Notification/stories/InlineNotification.stories.js +++ b/packages/react/src/components/Notification/stories/InlineNotification.stories.js @@ -56,10 +56,10 @@ Playground.argTypes = { }, }, onClose: { - action: 'clicked', + action: 'onClose', }, onCloseButtonClick: { - action: 'clicked', + action: 'onCloseButtonClick', }, children: { table: { diff --git a/packages/react/src/components/Notification/stories/ToastNotification.stories.js b/packages/react/src/components/Notification/stories/ToastNotification.stories.js index 0411cb537379..4af5bacffe31 100644 --- a/packages/react/src/components/Notification/stories/ToastNotification.stories.js +++ b/packages/react/src/components/Notification/stories/ToastNotification.stories.js @@ -59,10 +59,10 @@ Playground.argTypes = { }, }, onClose: { - action: 'clicked', + action: 'onClose', }, onCloseButtonClick: { - action: 'clicked', + action: 'onCloseButtonClick', }, children: { table: { diff --git a/packages/react/src/components/NumberInput/NumberInput.stories.js b/packages/react/src/components/NumberInput/NumberInput.stories.js index f934740dd43c..c0fc4924cc28 100644 --- a/packages/react/src/components/NumberInput/NumberInput.stories.js +++ b/packages/react/src/components/NumberInput/NumberInput.stories.js @@ -80,13 +80,13 @@ Playground.argTypes = { }, }, onChange: { - action: 'clicked', + action: 'onChange', }, onClick: { - action: 'clicked', + action: 'onClick', }, onKeyUp: { - action: 'clicked', + action: 'onKeyUp', }, translateWithId: { table: { diff --git a/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js b/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js deleted file mode 100644 index 128adc13a7de..000000000000 --- a/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2023 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import cx from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { match, keys } from '../../internal/keyboard'; -import { warning } from '../../internal/warning'; -import { usePrefix } from '../../internal/usePrefix'; - -const OverflowMenuItem = React.forwardRef(function OverflowMenuItem( - { - className, - closeMenu, - disabled = false, - handleOverflowMenuItemFocus, - hasDivider = false, - href, - isDelete = false, - index, - itemText = 'Provide itemText', - onClick = () => {}, - onKeyDown = () => {}, - requireTitle, - title, - wrapperClassName, - ...rest - }, - ref -) { - const prefix = usePrefix(); - - function setTabFocus(evt) { - if (match(evt, keys.ArrowDown)) { - handleOverflowMenuItemFocus({ - currentIndex: index, - direction: 1, - }); - } - if (match(evt, keys.ArrowUp)) { - handleOverflowMenuItemFocus({ - currentIndex: index, - direction: -1, - }); - } - } - - function handleClick(evt) { - onClick(evt); - if (closeMenu) { - closeMenu(); - } - } - - if (__DEV__) { - warning( - closeMenu, - '`` detected missing `closeMenu` prop. ' + - '`closeMenu` is required to let `` close the menu upon actions on ``. ' + - 'Please make sure `` is a direct child of `.' - ); - } - - const overflowMenuBtnClasses = cx( - `${prefix}--overflow-menu-options__btn`, - className - ); - const overflowMenuItemClasses = cx( - `${prefix}--overflow-menu-options__option`, - { - [`${prefix}--overflow-menu--divider`]: hasDivider, - [`${prefix}--overflow-menu-options__option--danger`]: isDelete, - [`${prefix}--overflow-menu-options__option--disabled`]: disabled, - }, - wrapperClassName - ); - - const TagToUse = href ? 'a' : 'button'; - - const OverflowMenuItemContent = (() => { - if (typeof itemText !== 'string') { - return itemText; - } - return ( -
- {itemText} -
- ); - })(); - - return ( -
  • - { - setTabFocus(evt); - onKeyDown(evt); - }} - role="menuitem" - ref={ref} - tabIndex="-1" - title={requireTitle ? title || itemText : null} - {...rest}> - {OverflowMenuItemContent} - -
  • - ); -}); - -OverflowMenuItem.propTypes = { - /** - * The CSS class name to be placed on the button element - */ - className: PropTypes.string, - - /** - * A callback to tell the parent menu component that the menu should be closed. - */ - closeMenu: PropTypes.func, - - /** - * `true` to make this menu item disabled. - */ - disabled: PropTypes.bool, - - handleOverflowMenuItemFocus: PropTypes.func, - - /** - * `true` to make this menu item a divider. - */ - hasDivider: PropTypes.bool, - - /** - * If given, overflow item will render as a link with the given href - */ - href: PropTypes.string, - - index: PropTypes.number, - - /** - * `true` to make this menu item a "danger button". - */ - isDelete: PropTypes.bool, - - /** - * The text in the menu item. - */ - itemText: PropTypes.node.isRequired, - - /** - * event handlers - */ - onBlur: PropTypes.func, - onClick: PropTypes.func, - onFocus: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyUp: PropTypes.func, - onMouseDown: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - onMouseUp: PropTypes.func, - - /** - * `true` if this menu item has long text and requires a browser tooltip - */ - requireTitle: PropTypes.bool, - - /** - * Specify a title for the OverflowMenuItem - */ - title: PropTypes.string, - - /** - * The CSS class name to be placed on the wrapper list item element - */ - wrapperClassName: PropTypes.string, -}; - -export default OverflowMenuItem; diff --git a/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.tsx b/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.tsx new file mode 100644 index 000000000000..374f69af13b0 --- /dev/null +++ b/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.tsx @@ -0,0 +1,260 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { keys, match } from '../../internal/keyboard'; +import { usePrefix } from '../../internal/usePrefix'; +import { warning } from '../../internal/warning'; +import { ForwardRefReturn } from '../../types/common'; + +export interface OverflowMenuItemProps + extends React.HTMLAttributes { + /** + * The CSS class name to be placed on the button element + */ + className?: string; + + /** + * A callback to tell the parent menu component that the menu should be closed. + */ + closeMenu?: () => void; + + /** + * `true` to make this menu item disabled. + */ + disabled?: boolean; + + handleOverflowMenuItemFocus?: (options: { + currentIndex?: number; + direction: number; + }) => void; + + /** + * `true` to make this menu item a divider. + */ + hasDivider?: boolean; + + /** + * If given, overflow item will render as a link with the given href + */ + href?: string; + + index?: number; + + /** + * The text to show for the menu item + */ + itemText?: React.ReactNode; + + /** + * `true` to make this menu item a danger button. + */ + isDelete?: boolean; + + /** + * `true` to require the title attribute. + */ + requireTitle?: boolean; + + /** + * The title attribute. + */ + title?: string; + + /** + * The CSS class name to be placed on the wrapper element + */ + wrapperClassName?: string; +} + +export type OverflowMenuItemComponent = ForwardRefReturn< + HTMLElement, + OverflowMenuItemProps +>; + +const OverflowMenuItem: OverflowMenuItemComponent = React.forwardRef( + function OverflowMenuItem( + { + className, + closeMenu, + disabled = false, + handleOverflowMenuItemFocus, + hasDivider = false, + href, + isDelete = false, + index, + itemText = 'Provide itemText', + onClick = () => {}, + onKeyDown = () => {}, + requireTitle, + title, + wrapperClassName, + ...rest + }, + ref + ) { + const prefix = usePrefix(); + + function setTabFocus(evt) { + if (match(evt, keys.ArrowDown)) { + handleOverflowMenuItemFocus?.({ + currentIndex: index, + direction: 1, + }); + } + if (match(evt, keys.ArrowUp)) { + handleOverflowMenuItemFocus?.({ + currentIndex: index, + direction: -1, + }); + } + } + + function handleClick(evt) { + onClick(evt); + if (closeMenu) { + closeMenu(); + } + } + + if (__DEV__) { + warning( + closeMenu, + '`` detected missing `closeMenu` prop. ' + + '`closeMenu` is required to let `` close the menu upon actions on ``. ' + + 'Please make sure `` is a direct child of `.' + ); + } + + const overflowMenuBtnClasses = cx( + `${prefix}--overflow-menu-options__btn`, + className + ); + const overflowMenuItemClasses = cx( + `${prefix}--overflow-menu-options__option`, + { + [`${prefix}--overflow-menu--divider`]: hasDivider, + [`${prefix}--overflow-menu-options__option--danger`]: isDelete, + [`${prefix}--overflow-menu-options__option--disabled`]: disabled, + }, + wrapperClassName + ); + + const TagToUse = href ? 'a' : 'button'; + + const OverflowMenuItemContent = (() => { + if (typeof itemText !== 'string') { + return itemText; + } + return ( +
    + {itemText} +
    + ); + })(); + + return ( +
  • + { + setTabFocus(evt); + onKeyDown(evt); + }} + role="menuitem" + // ref as any: the type of `ref` is `ForwardedRef` in `Button` component + // but `OverflowMenuItem` can be rendered as `a` tag as well, which is `HTMLAnchorElement` + // so we have to use `any` here + ref={ref as any} + tabIndex={-1} + // itemText as any: itemText may be a ReactNode, but `title` only accepts string + // to avoid compatibility issue, we use `any` here. Consider to enforce `itemText` to be `string?` + // in the next major release + title={requireTitle ? title || (itemText as any) : undefined} + {...rest}> + {OverflowMenuItemContent} + +
  • + ); + } +); + +OverflowMenuItem.propTypes = { + /** + * The CSS class name to be placed on the button element + */ + className: PropTypes.string, + + /** + * A callback to tell the parent menu component that the menu should be closed. + */ + closeMenu: PropTypes.func, + + /** + * `true` to make this menu item disabled. + */ + disabled: PropTypes.bool, + + handleOverflowMenuItemFocus: PropTypes.func, + + /** + * `true` to make this menu item a divider. + */ + hasDivider: PropTypes.bool, + + /** + * If given, overflow item will render as a link with the given href + */ + href: PropTypes.string, + + index: PropTypes.number, + + /** + * `true` to make this menu item a "danger button". + */ + isDelete: PropTypes.bool, + + /** + * The text in the menu item. + */ + itemText: PropTypes.node.isRequired, + + /** + * event handlers + */ + onBlur: PropTypes.func, + onClick: PropTypes.func, + onFocus: PropTypes.func, + onKeyDown: PropTypes.func, + onKeyUp: PropTypes.func, + onMouseDown: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onMouseUp: PropTypes.func, + + /** + * `true` if this menu item has long text and requires a browser tooltip + */ + requireTitle: PropTypes.bool, + + /** + * Specify a title for the OverflowMenuItem + */ + title: PropTypes.string, + + /** + * The CSS class name to be placed on the wrapper list item element + */ + wrapperClassName: PropTypes.string, +}; + +export default OverflowMenuItem; diff --git a/packages/react/src/components/Popover/Popover.stories.js b/packages/react/src/components/Popover/Popover.stories.js index 46ba2e9b402a..735fa35c9678 100644 --- a/packages/react/src/components/Popover/Popover.stories.js +++ b/packages/react/src/components/Popover/Popover.stories.js @@ -6,10 +6,17 @@ */ import './story.scss'; -import { Checkbox } from '@carbon/icons-react'; +import { Checkbox as CheckboxIcon } from '@carbon/icons-react'; import React, { useState } from 'react'; import { Popover, PopoverContent } from '../Popover'; +import RadioButton from '../RadioButton'; +import RadioButtonGroup from '../RadioButtonGroup'; +import { default as Checkbox } from '../Checkbox'; import mdx from './Popover.mdx'; +import { Settings } from '@carbon/icons-react'; +import { keys, match } from '../../internal/keyboard'; + +const prefix = 'cds'; export default { title: 'Components/Popover', @@ -59,7 +66,7 @@ const PlaygroundStory = (props) => { highContrast={highContrast} open={open}>
    - +

    Available storage

    @@ -71,6 +78,85 @@ const PlaygroundStory = (props) => { ); }; +export const TabTip = () => { + const [open, setOpen] = useState(true); + const [openTwo, setOpenTwo] = useState(false); + return ( +
    + { + if (match(evt, keys.Escape)) { + setOpen(false); + } + }} + isTabTip> + + + + + + +
    +
    + Edit columns + + + +
    +
    +
    + + + + + + + + +
    +
    + Edit columns + + + +
    +
    +
    +
    + ); +}; + export const Playground = PlaygroundStory.bind({}); Playground.argTypes = { @@ -141,7 +227,7 @@ export const AutoAlign = () => { }}>
    - { setOpen(!open); }} @@ -157,7 +243,7 @@ export const AutoAlign = () => {
    - +

    Available storage

    @@ -169,7 +255,7 @@ export const AutoAlign = () => {
    - +

    Available storage

    @@ -183,7 +269,7 @@ export const AutoAlign = () => { style={{ position: 'absolute', bottom: 0, right: 0, margin: '3rem' }}>
    - +

    Available storage

    @@ -196,7 +282,7 @@ export const AutoAlign = () => {
    - +

    Available storage

    diff --git a/packages/react/src/components/Popover/__tests__/Popover-test.js b/packages/react/src/components/Popover/__tests__/Popover-test.js index e93fa35adda7..1c616d0e3323 100644 --- a/packages/react/src/components/Popover/__tests__/Popover-test.js +++ b/packages/react/src/components/Popover/__tests__/Popover-test.js @@ -9,6 +9,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Popover, PopoverContent } from '../../Popover'; +const prefix = 'cds'; + describe('Popover', () => { it('should support a ref on the outermost element', () => { const ref = jest.fn(); @@ -75,5 +77,31 @@ describe('Popover', () => { ); expect(container.firstChild).toHaveAttribute('id', 'test'); }); + + // Tab Tip tests + it('should respect isTabTip prop', () => { + const { container } = render( + + + test + + ); + expect(container.firstChild).toHaveClass(`${prefix}--popover--tab-tip`); + }); + + it('should not allow other alignments than bottom-left or bottom-right when isTabTip is present', () => { + const { container } = render( + + + test + + ); + expect(container.firstChild).not.toHaveClass( + `${prefix}--popover--top-left` + ); + expect(container.firstChild).toHaveClass( + `${prefix}--popover--bottom-left` + ); + }); }); }); diff --git a/packages/react/src/components/Popover/index.tsx b/packages/react/src/components/Popover/index.tsx index ee0f99324646..3bccc34eead7 100644 --- a/packages/react/src/components/Popover/index.tsx +++ b/packages/react/src/components/Popover/index.tsx @@ -14,9 +14,7 @@ import { usePrefix } from '../../internal/usePrefix'; import { PolymorphicProps } from '../../types/common'; interface PopoverContext { - - floating: React.Ref - + floating: React.Ref; } const PopoverContext = React.createContext({ @@ -25,223 +23,282 @@ const PopoverContext = React.createContext({ }, }); -export type PopoverAlignment = 'top' | 'top-left' | 'top-right' | - 'bottom' | 'bottom-left' | 'bottom-right' | - 'left' | 'left-bottom' | 'left-top' | - 'right' | 'right-bottom' | 'right-top' +export type PopoverAlignment = + | 'top' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-bottom' + | 'left-top' + | 'right' + | 'right-bottom' + | 'right-top'; interface PopoverBaseProps { - /** * Specify how the popover should align with the trigger element */ - align?: PopoverAlignment + align?: PopoverAlignment; /** - * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes. + * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes. */ - autoAlign?: boolean + autoAlign?: boolean; /** * Specify whether a caret should be rendered */ - caret?: boolean + caret?: boolean; /** * Provide elements to be rendered inside of the component */ - children?: React.ReactNode + children?: React.ReactNode; /** * Provide a custom class name to be added to the outermost node in the * component */ - className?: string + className?: string; /** * Specify whether a drop shadow should be rendered on the popover */ - dropShadow?: boolean + dropShadow?: boolean; /** * Render the component using the high-contrast variant */ - highContrast?: boolean + highContrast?: boolean; /** - * Specify whether the component is currently open or closed + * Render the component using the tab tip variant */ - open: boolean + isTabTip?: boolean; + /** + * Specify whether the component is currently open or closed + */ + open: boolean; } -export type PopoverProps = PolymorphicProps - -const Popover = React.forwardRef(({ - align = 'bottom', - as, - autoAlign = false, - caret = true, - className: customClassName, - children, - dropShadow = true, - highContrast = false, - open, - ...rest -}: PopoverProps, forwardRef: React.ForwardedRef) => { - const prefix = usePrefix(); - const floating = useRef(null); - const popover = useRef(null); - - const value = useMemo(() => { - return { - floating, - }; - }, []); - - const ref = useMergedRefs([forwardRef, popover]); - const [autoAligned, setAutoAligned] = useState(false); - const [autoAlignment, setAutoAlignment] = useState(align); - const className = cx({ - [`${prefix}--popover-container`]: true, - [`${prefix}--popover--caret`]: caret, - [`${prefix}--popover--drop-shadow`]: dropShadow, - [`${prefix}--popover--high-contrast`]: highContrast, - [`${prefix}--popover--open`]: open, - [`${prefix}--popover--${autoAlignment}`]: autoAligned, - [`${prefix}--popover--${align}`]: !autoAligned, - }, customClassName); - - useIsomorphicEffect(() => { - if (!open) { - return; - } - - if (!autoAlign) { - setAutoAligned(false); - return; - } - - if (!floating.current) { - return; - } - - if (autoAligned === true) { - return; +export type PopoverProps = PolymorphicProps< + T, + PopoverBaseProps +>; + +const Popover = React.forwardRef( + ( + { + isTabTip, + align = isTabTip ? 'bottom-left' : 'bottom', + as, + autoAlign = false, + caret = isTabTip ? false : true, + className: customClassName, + children, + dropShadow = true, + highContrast = false, + open, + ...rest + }: PopoverProps, + forwardRef: React.ForwardedRef + ) => { + const prefix = usePrefix(); + const floating = useRef(null); + const popover = useRef(null); + + const value = useMemo(() => { + return { + floating, + }; + }, []); + + if (isTabTip) { + const tabTipAlignments: PopoverAlignment[] = [ + 'bottom-left', + 'bottom-right', + ]; + + if (!tabTipAlignments.includes(align)) { + align = 'bottom-left'; + } } - const rect = floating.current.getBoundingClientRect(); - - // The conditions, per side, of when the popover is not visible, excluding the popover's internal padding(16) - const conditions = { - left: rect.x < -16, - top: rect.y < -16, - right: rect.x + (rect.width - 16) > document.documentElement.clientWidth, - bottom: - rect.y + (rect.height - 16) > document.documentElement.clientHeight, - }; - - if ( - !conditions.left && - !conditions.top && - !conditions.right && - !conditions.bottom - ) { - setAutoAligned(false); - return; - } + const ref = useMergedRefs([forwardRef, popover]); + const [autoAligned, setAutoAligned] = useState(false); + const [autoAlignment, setAutoAlignment] = useState(align); + const className = cx( + { + [`${prefix}--popover-container`]: true, + [`${prefix}--popover--caret`]: caret, + [`${prefix}--popover--drop-shadow`]: dropShadow, + [`${prefix}--popover--high-contrast`]: highContrast, + [`${prefix}--popover--open`]: open, + [`${prefix}--popover--${autoAlignment}`]: autoAligned && !isTabTip, + [`${prefix}--popover--${align}`]: !autoAligned, + [`${prefix}--popover--tab-tip`]: isTabTip, + }, + customClassName + ); + + useIsomorphicEffect(() => { + if (!open) { + return; + } - const alignments: PopoverAlignment[] = [ - 'top', - 'top-left', - 'right-bottom', - 'right', - 'right-top', - 'bottom-left', - 'bottom', - 'bottom-right', - 'left-top', - 'left', - 'left-bottom', - 'top-right', - ]; - - // Creates the prioritized list of options depending on ideal alignment coming from `align` - const options = [align]; - let option = - alignments[(alignments.indexOf(align) + 1) % alignments.length]; - - while (option) { - if (options.includes(option)) { - break; + if (!autoAlign || isTabTip) { + setAutoAligned(false); + return; } - options.push(option); - option = alignments[(alignments.indexOf(option) + 1) % alignments.length]; - } - function isVisible(alignment: PopoverAlignment) { - if (!popover.current || !floating.current) { - return false; + if (!floating.current) { + return; } - popover.current.classList.add(`${prefix}--popover--${alignment}`); + if (autoAligned === true) { + return; + } const rect = floating.current.getBoundingClientRect(); - // Check if popover is not visible to the left of the screen - if (rect.x < -16) { - popover.current.classList.remove(`${prefix}--popover--${alignment}`); - return false; + // The conditions, per side, of when the popover is not visible, excluding the popover's internal padding(16) + const conditions = { + left: rect.x < -16, + top: rect.y < -16, + right: + rect.x + (rect.width - 16) > document.documentElement.clientWidth, + bottom: + rect.y + (rect.height - 16) > document.documentElement.clientHeight, + }; + + if ( + !conditions.left && + !conditions.top && + !conditions.right && + !conditions.bottom + ) { + setAutoAligned(false); + return; } - // Check if popover is not visible at the top of the screen - if (rect.y < -16) { - popover.current.classList.remove(`${prefix}--popover--${alignment}`); - return false; + const alignments: PopoverAlignment[] = [ + 'top', + 'top-left', + 'right-bottom', + 'right', + 'right-top', + 'bottom-left', + 'bottom', + 'bottom-right', + 'left-top', + 'left', + 'left-bottom', + 'top-right', + ]; + + // Creates the prioritized list of options depending on ideal alignment coming from `align` + const options = [align]; + let option = + alignments[(alignments.indexOf(align) + 1) % alignments.length]; + + while (option) { + if (options.includes(option)) { + break; + } + options.push(option); + option = + alignments[(alignments.indexOf(option) + 1) % alignments.length]; } - // Check if popover is not visible to right of screen - if (rect.x + (rect.width - 16) > document.documentElement.clientWidth) { - popover.current.classList.remove(`${prefix}--popover--${alignment}`); - return false; - } + function isVisible(alignment: PopoverAlignment) { + if (!popover.current || !floating.current) { + return false; + } + + popover.current.classList.add(`${prefix}--popover--${alignment}`); + + const rect = floating.current.getBoundingClientRect(); + + // Check if popover is not visible to the left of the screen + if (rect.x < -16) { + popover.current.classList.remove(`${prefix}--popover--${alignment}`); + return false; + } + + // Check if popover is not visible at the top of the screen + if (rect.y < -16) { + popover.current.classList.remove(`${prefix}--popover--${alignment}`); + return false; + } + + // Check if popover is not visible to right of screen + if (rect.x + (rect.width - 16) > document.documentElement.clientWidth) { + popover.current.classList.remove(`${prefix}--popover--${alignment}`); + return false; + } + + // Check if popover is not visible to bottom of screen + if ( + rect.y + (rect.height - 16) > + document.documentElement.clientHeight + ) { + popover.current.classList.remove(`${prefix}--popover--${alignment}`); + return false; + } - // Check if popover is not visible to bottom of screen - if (rect.y + (rect.height - 16) > document.documentElement.clientHeight) { popover.current.classList.remove(`${prefix}--popover--${alignment}`); - return false; + return true; } - popover.current.classList.remove(`${prefix}--popover--${alignment}`); - return true; - } + let alignment: PopoverAlignment | null = null; - let alignment: PopoverAlignment | null = null; + for (let i = 0; i < options.length; i++) { + const option = options[i]; - for (let i = 0; i < options.length; i++) { - const option = options[i]; - - if (isVisible(option)) { - alignment = option; - break; + if (isVisible(option)) { + alignment = option; + break; + } } - } - - if (alignment) { - setAutoAligned(true); - setAutoAlignment(alignment); - } - }, [autoAligned, align, autoAlign, prefix, open]); - const BaseComponent: React.ElementType = as ?? 'span' - return ( - - - {children} - - - ); -}); + if (alignment) { + setAutoAligned(true); + setAutoAlignment(alignment); + } + }, [autoAligned, align, autoAlign, prefix, open, isTabTip]); + + const BaseComponent: React.ElementType = as ?? 'span'; + + const mappedChildren = React.Children.map(children, (child) => { + const item = child as any; + + if (item?.type === 'button') { + const { className } = item.props; + const tabTipClasses = cx( + `${prefix}--popover--tab-tip__button`, + className + ); + return React.cloneElement(item, { + className: tabTipClasses, + }); + } else { + return item; + } + }); + + return ( + + + {isTabTip ? mappedChildren : children} + + + ); + } +); // Note: this displayName is temporarily set so that Storybook ArgTable // correctly displays the name of this component @@ -278,7 +335,7 @@ Popover.propTypes = { as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), /** - * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes. + * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes. */ autoAlign: PropTypes.bool, @@ -308,13 +365,18 @@ Popover.propTypes = { */ highContrast: PropTypes.bool, + /** + * Render the component using the tab tip variant + */ + isTabTip: PropTypes.bool, + /** * Specify whether the component is currently open or closed */ open: PropTypes.bool.isRequired, }; -export type PopoverContentProps = React.HTMLAttributes +export type PopoverContentProps = React.HTMLAttributes; const PopoverContent = React.forwardRef(function PopoverContent( { className, children, ...rest }: PopoverContentProps, diff --git a/packages/react/src/components/Popover/story.scss b/packages/react/src/components/Popover/story.scss index 08aea270b5f4..83432e1f0076 100644 --- a/packages/react/src/components/Popover/story.scss +++ b/packages/react/src/components/Popover/story.scss @@ -98,3 +98,22 @@ display: flex; flex-direction: column; } + +.popover-tabtip-story .cds--popover-content { + width: 16rem; +} + +.popover-tabtip-story .cds--radio-button-wrapper { + margin-bottom: 0.5rem; +} + +.popover-tabtip-story hr { + border: none; + background: theme.$border-subtle; + height: 1px; + margin: 8px 0 16px; +} + +.popover-tabtip-story .cds--popover-container:last-of-type { + margin-left: 15rem; +} diff --git a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js index b65419125200..0c0172461ba9 100644 --- a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js +++ b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js @@ -26,6 +26,7 @@ const RadioButtonGroup = React.forwardRef(function RadioButtonGroup( orientation = 'horizontal', readOnly, valueSelected, + ...rest }, ref ) { @@ -88,7 +89,8 @@ const RadioButtonGroup = React.forwardRef(function RadioButtonGroup(
    + aria-readonly={readOnly} + {...rest}> {legendText && ( {legendText} )} diff --git a/packages/react/src/components/RadioTile/RadioTile-test.js b/packages/react/src/components/RadioTile/RadioTile-test.js index ae9be5381f49..13e3aad80ab6 100644 --- a/packages/react/src/components/RadioTile/RadioTile-test.js +++ b/packages/react/src/components/RadioTile/RadioTile-test.js @@ -86,5 +86,13 @@ describe('RadioTile', () => { expect(screen.getByRole('radio')).toHaveAttribute('value', 'standard'); }); + + it('should pass a given ref to the input element', () => { + const ref = React.createRef(); + render(); + + expect(ref.current.type).toEqual('radio'); + expect(ref.current.value).toEqual('some test value'); + }); }); }); diff --git a/packages/react/src/components/RadioTile/RadioTile.js b/packages/react/src/components/RadioTile/RadioTile.js index 80bf810d92b3..a26ab772ad1e 100644 --- a/packages/react/src/components/RadioTile/RadioTile.js +++ b/packages/react/src/components/RadioTile/RadioTile.js @@ -14,20 +14,23 @@ import { useFallbackId } from '../../internal/useId'; import { usePrefix } from '../../internal/usePrefix'; import deprecate from '../../prop-types/deprecate'; -function RadioTile({ - children, - className: customClassName, - disabled, - // eslint-disable-next-line no-unused-vars - light, - checked, - name, - value, - id, - onChange, - tabIndex, - ...rest -}) { +const RadioTile = React.forwardRef(function RadioTile( + { + children, + className: customClassName, + disabled, + // eslint-disable-next-line no-unused-vars + light, + checked, + name, + value, + id, + onChange, + tabIndex, + ...rest + }, + ref +) { const prefix = usePrefix(); const inputId = useFallbackId(id); const className = cx( @@ -65,6 +68,7 @@ function RadioTile({ tabIndex={!disabled ? tabIndex : null} type="radio" value={value} + ref={ref} /> ); -} +}); RadioTile.propTypes = { /** diff --git a/packages/react/src/components/Select/Select.js b/packages/react/src/components/Select/Select.tsx similarity index 75% rename from packages/react/src/components/Select/Select.js rename to packages/react/src/components/Select/Select.tsx index 9a7e261ad49c..2368506f653a 100644 --- a/packages/react/src/components/Select/Select.js +++ b/packages/react/src/components/Select/Select.tsx @@ -6,7 +6,14 @@ */ import PropTypes from 'prop-types'; -import React, { useContext, useState } from 'react'; +import React, { + ChangeEventHandler, + ComponentPropsWithRef, + ForwardedRef, + ReactNode, + useContext, + useState, +} from 'react'; import classNames from 'classnames'; import { ChevronDown, @@ -18,6 +25,106 @@ import { useFeatureFlag } from '../FeatureFlags'; import { usePrefix } from '../../internal/usePrefix'; import { FormContext } from '../FluidForm'; +type ExcludedAttributes = 'size'; + +interface SelectProps + extends Omit, ExcludedAttributes> { + /** + * Provide the contents of your Select + */ + children?: ReactNode; + + /** + * Specify an optional className to be applied to the node containing the label and the select box + */ + className?: string; + + /** + * Optionally provide the default value of the `` + */ + id: string; + + /** + * Specify whether you want the inline version of this control + */ + inline?: boolean; + + /** + * Specify if the currently value is invalid. + */ + invalid?: boolean; + + /** + * Message which is displayed if the value is invalid. + */ + invalidText?: ReactNode; + + /** + * Provide label text to be read by screen readers when interacting with the control. + */ + labelText?: ReactNode; + + /** + * `true` to use the light version. For use on $ui-01 backgrounds only. + * Don't use this to make tile background color same as container background color. + * + * @deprecated The `light` prop for `Select` is no longer needed and has been deprecated in v11 in favor of the new `Layer` component. + * It will be moved in the next major release. + */ + light?: boolean; + + /** + * Reserved for use with component. Will not render a label for the + * select since Pagination renders one for us. + */ + noLabel?: boolean; + + /** + * Provide an optional `onChange` hook that is called each time the value of the underlying `` changes. + */ + onChange?: ChangeEventHandler; + + /** + * Whether the select should be read-only + */ + readOnly?: boolean; + + /** + * Specify the size of the Select Input. + */ + size?: 'sm' | 'md' | 'lg'; + + /** + * Specify whether the control is currently in warning state + */ + warn?: boolean; + + /** + * Provide the text that is displayed when the control is in warning state + */ + warnText?: ReactNode; +} + const Select = React.forwardRef(function Select( { className, @@ -39,8 +146,8 @@ const Select = React.forwardRef(function Select( warn = false, warnText, ...other - }, - ref + }: SelectProps, + ref: ForwardedRef ) { const prefix = usePrefix(); const enabled = useFeatureFlag('enable-v11-release'); diff --git a/packages/react/src/components/Select/__tests__/Select-test.js b/packages/react/src/components/Select/__tests__/Select-test.js index acbb99a356a3..b47506fb3018 100644 --- a/packages/react/src/components/Select/__tests__/Select-test.js +++ b/packages/react/src/components/Select/__tests__/Select-test.js @@ -174,7 +174,7 @@ describe('Select', () => { render( ` node + */ + className?: string; + + /** + * Optionally provide the default value of the `` + */ + defaultValue?: string | number; + + /** + * Specify whether the `` should be disabled + */ + disabled?: boolean; + + /** + * Specify whether to display the character counter + */ + enableCounter?: boolean; + + /** + * Provide text that is used alongside the control label for additional help + */ + helperText?: ReactNode; + + /** + * Specify whether you want the underlying label to be visually hidden + */ + hideLabel?: boolean; + + /** + * Specify a custom `id` for the `` + */ + id: string; + + /** + * `true` to use the inline version. + */ + inline?: boolean; + + /** + * Specify whether the control is currently invalid + */ + invalid?: boolean; + + /** + * Provide the text that is displayed when the control is in an invalid state + */ + invalidText?: ReactNode; + + /** + * Provide the text that will be read by a screen reader when visiting this + * control + */ + labelText: ReactNode; + + /** + * `true` to use the light version. For use on $ui-01 backgrounds only. + * Don't use this to make tile background color same as container background color. + * 'The `light` prop for `TextInput` has ' + + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.' + */ + light?: boolean; + + /** + * Max character count allowed for the input. This is needed in order for enableCounter to display + */ + maxCount?: number; + + /** + * Optionally provide an `onChange` handler that is called whenever `` + * is updated + */ + onChange?: ( + event: React.ChangeEvent + ) => void; + + /** + * Optionally provide an `onClick` handler that is called whenever the + * `` is clicked + */ + onClick?: (event: React.MouseEvent) => void; + + /** + * Specify the placeholder attribute for the `` + */ + placeholder?: string; + + /** + * Whether the input should be read-only + */ + readOnly?: boolean; + + /** + * Specify the size of the Text Input. Currently supports the following: + */ + size?: 'sm' | 'md' | 'lg' | 'xl'; + + /** + * Specify the type of the `` + */ + type?: string; + + /** + * Specify the value of the `` + */ + value?: string | number | undefined; + + /** + * Specify whether the control is currently in warning state + */ + warn?: boolean; + + /** + * Provide the text that is displayed when the control is in warning state + */ + warnText?: ReactNode; +} + const TextInput = React.forwardRef(function TextInput( { className, @@ -42,7 +168,7 @@ const TextInput = React.forwardRef(function TextInput( enableCounter = false, maxCount, ...rest - }, + }: TextInputProps, ref ) { const prefix = usePrefix(); @@ -51,7 +177,7 @@ const TextInput = React.forwardRef(function TextInput( const { defaultValue, value } = rest; const [textCount, setTextCount] = useState( - defaultValue?.length || value?.length || 0 + defaultValue?.toString().length || value?.toString().length || 0 ); const normalizedProps = useNormalizedInputProps({ @@ -187,7 +313,8 @@ const TextInput = React.forwardRef(function TextInput( ); const { isFluid } = useContext(FormContext); - let ariaAnnouncement = useAnnouncer(textCount, maxCount); + const ariaAnnouncement = useAnnouncer(textCount, maxCount); + const Icon = normalizedProps.icon as any; return (
    @@ -203,9 +330,7 @@ const TextInput = React.forwardRef(function TextInput(
    - {normalizedProps.icon && ( - - )} + {Icon && } {input} {ariaAnnouncement} @@ -220,8 +345,8 @@ const TextInput = React.forwardRef(function TextInput( }); TextInput.displayName = 'TextInput'; -TextInput.PasswordInput = PasswordInput; -TextInput.ControlledPasswordInput = ControlledPasswordInput; +(TextInput as any).PasswordInput = PasswordInput; +(TextInput as any).ControlledPasswordInput = ControlledPasswordInput; TextInput.propTypes = { /** * Specify an optional className to be applied to the `` node diff --git a/packages/react/src/components/Toggle/Toggle-test.js b/packages/react/src/components/Toggle/Toggle-test.js index 8a7ec415028f..8d5eb958bc5d 100644 --- a/packages/react/src/components/Toggle/Toggle-test.js +++ b/packages/react/src/components/Toggle/Toggle-test.js @@ -92,10 +92,15 @@ describe('Toggle', () => { ).toBe(props.labelText); }); - it("doesn't render sideLabel if props.hideLabel and props['aria-labelledby'] are provided", () => { + it("doesn't render sideLabel if props.hideLabel and no props.labelText is provided", () => { const externalElementId = 'external-element-id'; wrapper.rerender( - + ); expect( diff --git a/packages/react/src/components/Toggle/Toggle.js b/packages/react/src/components/Toggle/Toggle.js index 5d950ba4a32a..e2bc83c5b684 100644 --- a/packages/react/src/components/Toggle/Toggle.js +++ b/packages/react/src/components/Toggle/Toggle.js @@ -47,8 +47,8 @@ export function Toggle({ const isSm = size === 'sm'; const sideLabel = hideLabel ? labelText : checked ? labelB : labelA; - const renderSideLabel = !(hideLabel && ariaLabelledby); - const LabelComponent = ariaLabelledby ? 'div' : 'label'; + const renderSideLabel = !(hideLabel && !labelText); + const LabelComponent = labelText ? 'label' : 'div'; const wrapperClasses = classNames( `${prefix}--toggle`, @@ -76,7 +76,7 @@ export function Toggle({
    { // the underlying