diff --git a/.all-contributorsrc b/.all-contributorsrc index 7932b62b9379..90f19bcbb834 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -904,6 +904,42 @@ "contributions": [ "code" ] + }, + { + "login": "torresga", + "name": "G. Torres", + "avatar_url": "https://avatars.githubusercontent.com/u/6892410?v=4", + "profile": "http://torresga.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "FionaDL", + "name": "Fiona", + "avatar_url": "https://avatars.githubusercontent.com/u/28625558?v=4", + "profile": "https://github.com/FionaDL", + "contributions": [ + "code" + ] + }, + { + "login": "kindoflew", + "name": "kindoflew", + "avatar_url": "https://avatars.githubusercontent.com/u/70274722?v=4", + "profile": "https://lewisdavanzo.com/", + "contributions": [ + "code" + ] + }, + { + "login": "mgueyraud", + "name": "Mario Gueyraud", + "avatar_url": "https://avatars.githubusercontent.com/u/9916318?v=4", + "profile": "https://github.com/mgueyraud", + "contributions": [ + "code" + ] } ], "commitConvention": "none" diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yaml b/.github/ISSUE_TEMPLATE/QUESTION.yaml index edd8d6a35bcf..0ec1683555b8 100644 --- a/.github/ISSUE_TEMPLATE/QUESTION.yaml +++ b/.github/ISSUE_TEMPLATE/QUESTION.yaml @@ -30,7 +30,7 @@ body: - type: markdown attributes: value: - "For non-IBMer's, you can ask a question on our [Github Discussions + "For non-IBMer's, you can ask a question on our [GitHub Discussions board](https://github.com/carbon-design-system/carbon/discussions/new) or join [our Discord server](https://discord.gg/KAECRaWdzE) to chat with Carbon maintainers and fellow community members." diff --git a/README.md b/README.md index 4e76583b43b3..6829d187d9f1 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,10 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Hannele Valtanen

💻 +
G. Torres

💻 +
Fiona

💻 +
kindoflew

💻 +
Mario Gueyraud

💻 diff --git a/docs/experimental-code.md b/docs/experimental-code.md index e980a72f28e0..3f969cfb2034 100644 --- a/docs/experimental-code.md +++ b/docs/experimental-code.md @@ -68,7 +68,7 @@ to stable: - [ ] All files have a copyright banner - [ ] All components exported in `src/index.js` and should not be `unstable_` prefixed -- [ ] Component has a label in the github repository +- [ ] Component has a label in the GitHub repository - [ ] Component should be documented on the website - [ ] Component should have a usage, style, and code tab - [ ] Component may have a component demo diff --git a/docs/guides/setup/contributing-with-github-desktop.md b/docs/guides/setup/contributing-with-github-desktop.md index 5350072030a1..511f8f7a11e2 100644 --- a/docs/guides/setup/contributing-with-github-desktop.md +++ b/docs/guides/setup/contributing-with-github-desktop.md @@ -1,9 +1,9 @@ -# Contributing to Carbon using Github Desktop +# Contributing to Carbon using GitHub Desktop ## Initial Setup -1. Download and install [Github Desktop](https://desktop.github.com/) -2. Run the Github Desktop application +1. Download and install [GitHub Desktop](https://desktop.github.com/) +2. Run the GitHub Desktop application 3. Click the button labeled 'Sign in to Github.com'. This will open your browser and take you to an authorization page. 4. Click the 'Authorize desktop' button, which may prompt you for your @@ -18,7 +18,7 @@ create a copy of the repository and add it to your account. This is where you'll make changes and later request the repository you forked pull those changes from (more on that later). -9. Back in the Github Desktop application click the button labeled 'Clone a +9. Back in the GitHub Desktop application click the button labeled 'Clone a repository from the internet...' 10. In the modal that pops up you can search your account for the fork you made. Once you've selected the right repository click the blue button labeled @@ -26,14 +26,14 @@ 11. The _next_ modal is a confirmation screen where you can double check the URL of your fork and select where on your computer you'd like to clone the repository. -12. Once the Github Application is done cloning your repository select that you +12. Once the GitHub Application is done cloning your repository select that you intend 'To contribute to the parent project' and then click the button labeled 'Continue'. You are now ready to work. ## Basic workflow 1. First things first we need to create a "working" branch. At the top of the - Github Desktop application click the drop down labeled 'Current branch' + GitHub Desktop application click the drop down labeled 'Current branch' 2. Click the button labeled 'New branch' and then name your branch. Typically this will be a general description of the work you're doing. Maybe 'updating-accessibility-docs' or 'adding-security-pictograms'. Then click diff --git a/docs/guides/support.md b/docs/guides/support.md index bf4a05f0066e..912e98158d4c 100644 --- a/docs/guides/support.md +++ b/docs/guides/support.md @@ -156,7 +156,7 @@ If they are, remove the question label, label as a `type: enhancement 💡` or
Is there a duplicate question? -We tend to recieve more questions through Slack than through Github so with any +We tend to recieve more questions through Slack than through GitHub so with any incoming question issues, search in both places to see if it has already been answered. If you can find an answer, point the author to where they can find it and close the issue. @@ -389,7 +389,7 @@ that can be particularly helpful: ### Alerting a team or subject matter expert Sometimes an issue comes in that is highly technical or about subject matter -you're unfamiliar with. Nothing wrong with that! Using Github ping system you +you're unfamiliar with. Nothing wrong with that! Using GitHub ping system you can alert specific sub-teams on Carbon: - @carbon-design-system/design diff --git a/docs/migration/v11.md b/docs/migration/v11.md index c9af5e26a32f..0ab024d95e07 100644 --- a/docs/migration/v11.md +++ b/docs/migration/v11.md @@ -1882,7 +1882,7 @@ const { white, g10, g90, g100 } = v10; | inverse-01 | icon-inverse | — | Split | | inverse-01 | focus-inset | — | Split | | inverse-01 | text-inverse | — | Split | -| inverse-02 | "background-inverse " | — | Updated | +| inverse-02 | background-inverse | — | Updated | | inverse-focus-ui | focus-inverse | — | Updated | | inverse-hover-ui | background-inverse-hover | — | Updated | | inverse-link | link-inverse | — | Updated | diff --git a/examples/codesandbox-styles/package.json b/examples/codesandbox-styles/package.json index 352db4a0de17..5248802762ca 100644 --- a/examples/codesandbox-styles/package.json +++ b/examples/codesandbox-styles/package.json @@ -1,7 +1,7 @@ { "name": "codesandbox-styles", "private": true, - "version": "0.19.0-rc.0", + "version": "0.19.0", "scripts": { "develop": "vite" }, @@ -9,7 +9,7 @@ "vite": "^2.8.0" }, "dependencies": { - "@carbon/styles": "^1.16.0-rc.0", + "@carbon/styles": "^1.16.0", "sass": "^1.51.0" } } diff --git a/examples/codesandbox-with-sass-compilation/package.json b/examples/codesandbox-with-sass-compilation/package.json index 91a4d6ee6dd9..7803dd37c0ef 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.17.0-rc.0", + "version": "0.17.0", "private": true, "dependencies": { - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/examples/codesandbox/package.json b/examples/codesandbox/package.json index 95efb4159e62..991301817dbf 100644 --- a/examples/codesandbox/package.json +++ b/examples/codesandbox/package.json @@ -1,9 +1,9 @@ { "name": "codesandbox", - "version": "0.17.0-rc.0", + "version": "0.17.0", "private": true, "dependencies": { - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.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 584a2b571120..0a68c433dae7 100644 --- a/examples/custom-theme/package.json +++ b/examples/custom-theme/package.json @@ -1,14 +1,14 @@ { "name": "custom-theme", "private": true, - "version": "0.14.0-rc.0", + "version": "0.14.0", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/examples/incremental-migration/package.json b/examples/incremental-migration/package.json index 19839b644015..44388725d850 100644 --- a/examples/incremental-migration/package.json +++ b/examples/incremental-migration/package.json @@ -1,7 +1,7 @@ { "name": "incremental-migration", "private": true, - "version": "0.16.0-rc.0", + "version": "0.16.0", "scripts": { "build": "next build", "dev": "next dev", @@ -13,7 +13,7 @@ }, "dependencies": { "@carbon/icons-react": "^10.49.0", - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.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 68198cf66437..44b49dabe398 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.14.0-rc.0", + "version": "0.14.0", "scripts": { "build": "next build", "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.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 38e4667af6b6..e426d20afc92 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,7 +1,7 @@ { "name": "examples-nextjs", "private": true, - "version": "0.16.0-rc.0", + "version": "0.16.0", "scripts": { "build": "next build", "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.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 2e381bee4949..067401ac093b 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,14 +1,14 @@ { "name": "vite", "private": true, - "version": "0.14.0-rc.0", + "version": "0.14.0", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.16.0-rc.0", + "@carbon/react": "^1.16.0", "react": "^17.0.0", "react-dom": "^17.0.0" }, diff --git a/packages/carbon-components-react/package.json b/packages/carbon-components-react/package.json index 941930911626..057adabd9fe9 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.16.0-rc.0", + "version": "8.16.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.16.0-rc.0", - "@carbon/styles": "^1.16.0-rc.0", + "@carbon/react": "^1.16.0", + "@carbon/styles": "^1.16.0", "@carbon/telemetry": "0.1.0" }, "devDependencies": { diff --git a/packages/carbon-components/package.json b/packages/carbon-components/package.json index fef1d250821a..b620d1b06b00 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.16.0-rc.0", + "version": "11.16.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -40,7 +40,7 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/styles": "^1.16.0-rc.0", + "@carbon/styles": "^1.16.0", "@carbon/telemetry": "0.1.0" }, "devDependencies": { diff --git a/packages/colors/package.json b/packages/colors/package.json index 58b13fe649f7..74437da2cf76 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.7.0-rc.0", + "version": "11.7.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", diff --git a/packages/elements/package.json b/packages/elements/package.json index 1b9b053f7fbb..57e18a97f561 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.13.0-rc.0", + "version": "11.13.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -35,13 +35,13 @@ "clean": "rimraf es lib umd" }, "dependencies": { - "@carbon/colors": "^11.7.0-rc.0", + "@carbon/colors": "^11.7.0", "@carbon/grid": "^11.7.0", "@carbon/icons": "^11.10.0", "@carbon/layout": "^11.7.0", "@carbon/motion": "^11.5.0", - "@carbon/themes": "^11.11.0-rc.0", - "@carbon/type": "^11.11.0-rc.0" + "@carbon/themes": "^11.11.0", + "@carbon/type": "^11.11.0" }, "devDependencies": { "@carbon/cli": "^11.5.0", diff --git a/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap index a4f6233c395b..dcb23eb2696d 100644 --- a/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -206,6 +206,7 @@ Array [ "moderate01", "moderate02", "motion", + "notificationTokens", "orange", "orange40", "orange60", diff --git a/packages/react/.storybook/Welcome/Welcome.js b/packages/react/.storybook/Welcome/Welcome.js index 244038eec329..b5ff23ebf841 100644 --- a/packages/react/.storybook/Welcome/Welcome.js +++ b/packages/react/.storybook/Welcome/Welcome.js @@ -40,7 +40,7 @@ export const Welcome = () => { href="https://github.com/carbon-design-system/carbon/tree/main/packages/react" className="welcome__link" renderIcon={ArrowRight}> - Github repo + GitHub repo diff --git a/packages/react/.storybook/preview.js b/packages/react/.storybook/preview.js index 9e0429ebafd0..2f840b711430 100644 --- a/packages/react/.storybook/preview.js +++ b/packages/react/.storybook/preview.js @@ -14,6 +14,8 @@ import React from 'react'; import { breakpoints } from '@carbon/layout'; import { GlobalTheme } from '../src/components/Theme'; +import theme from './theme'; + export const globalTypes = { locale: { name: 'Locale', @@ -87,6 +89,9 @@ export const parameters = { darkMode: { current: 'light', }, + docs: { + theme, + }, // Small (<672) // Medium (672 - 1056px) // Large (1056 - 1312px) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 3702d2afa033..1858710f5454 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -3529,9 +3529,6 @@ Map { "className": Object { "type": "string", }, - "columns": Object { - "type": "number", - }, "condensed": Object { "type": "bool", }, @@ -4969,6 +4966,9 @@ Map { ], "type": "oneOfType", }, + "disableWheel": Object { + "type": "bool", + }, "disabled": Object { "type": "bool", }, @@ -5594,6 +5594,9 @@ Map { "className": Object { "type": "string", }, + "vertical": Object { + "type": "bool", + }, }, }, "ProgressStep" => Object { @@ -7438,15 +7441,8 @@ Map { "className": Object { "type": "string", }, - "type": Object { - "args": Array [ - Array [ - "", - "default", - "container", - ], - ], - "type": "oneOf", + "contained": Object { + "type": "bool", }, }, }, diff --git a/packages/react/package.json b/packages/react/package.json index 74de07900aad..d95bf3f71cc2 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.16.0-rc.0", + "version": "1.16.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -46,7 +46,7 @@ "@carbon/feature-flags": "^0.9.0", "@carbon/icons-react": "^11.10.0", "@carbon/layout": "^11.7.0", - "@carbon/styles": "^1.16.0-rc.0", + "@carbon/styles": "^1.16.0", "@carbon/telemetry": "0.1.0", "classnames": "2.3.2", "copy-to-clipboard": "^3.3.1", @@ -73,7 +73,7 @@ "@babel/preset-env": "^7.18.2", "@babel/preset-react": "^7.17.12", "@carbon/test-utils": "^10.26.0", - "@carbon/themes": "^11.11.0-rc.0", + "@carbon/themes": "^11.11.0", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-commonjs": "^21.0.0", "@rollup/plugin-node-resolve": "^13.0.0", diff --git a/packages/react/src/components/AspectRatio/AspectRatio-story.js b/packages/react/src/components/AspectRatio/AspectRatio-story.js deleted file mode 100644 index cb09ce28f48f..000000000000 --- a/packages/react/src/components/AspectRatio/AspectRatio-story.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * 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 './AspectRatio-story.scss'; - -import { withKnobs, select } from '@storybook/addon-knobs'; -import React from 'react'; -import { Grid, Row, Column } from '../Grid'; -import { AspectRatio } from './'; -import mdx from './AspectRatio.mdx'; - -export default { - title: 'Components/AspectRatio', - component: AspectRatio, - decorators: [ - withKnobs, - (story) =>
{story()}
, - ], - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const Default = () => { - return ( - - - - Content - - - Content - - - Content - - - Content - - - - ); -}; - -export const Playground = () => { - const ratio = select( - 'ratio', - ['16x9', '9x16', '2x1', '1x2', '4x3', '3x4', '1x1'], - '1x1' - ); - return ( - - - - Content - - - Content - - - Content - - - Content - - - - ); -}; diff --git a/packages/react/src/components/AspectRatio/AspectRatio.mdx b/packages/react/src/components/AspectRatio/AspectRatio.mdx index 2ccfa33bdf2a..ccf4fa0f5aa4 100644 --- a/packages/react/src/components/AspectRatio/AspectRatio.mdx +++ b/packages/react/src/components/AspectRatio/AspectRatio.mdx @@ -1,6 +1,6 @@ import { Story, Props, Source, Preview } from '@storybook/addon-docs'; import { Grid, Row, Column } from '../Grid'; -import { AspectRatio } from '../AspectRatio'; +import { AspectRatio } from '.'; # AspectRatio @@ -28,7 +28,7 @@ spanning 100% of the space available in your layout, and the height will be determined by the ratio that you specified. - + To see the full list of ratios supported by the `ratio` prop, check out the prop diff --git a/packages/react/src/components/AspectRatio/next/AspectRatio.stories.js b/packages/react/src/components/AspectRatio/AspectRatio.stories.js similarity index 73% rename from packages/react/src/components/AspectRatio/next/AspectRatio.stories.js rename to packages/react/src/components/AspectRatio/AspectRatio.stories.js index 5ab553707a76..c1cc232e6aa1 100644 --- a/packages/react/src/components/AspectRatio/next/AspectRatio.stories.js +++ b/packages/react/src/components/AspectRatio/AspectRatio.stories.js @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import '../AspectRatio-story.scss'; +import './AspectRatio-story.scss'; import React from 'react'; -import { Grid, Column } from '../../Grid'; -import { AspectRatio } from '../'; -import mdx from '../AspectRatio.mdx'; +import { Grid, Column } from '../Grid'; +import { AspectRatio } from './'; +import mdx from './AspectRatio.mdx'; export default { title: 'Components/AspectRatio', @@ -49,33 +49,9 @@ export const Default = () => { }; export const Playground = { - argTypes: { - as: { - control: { - type: null, - }, - }, - children: { - control: { - type: null, - }, - }, - className: { - control: { - type: null, - }, - }, - ratio: { - control: { - type: 'select', - }, - defaultValue: '1x1', - options: ['16x9', '9x16', '2x1', '1x2', '4x3', '3x4', '1x1'], - }, - }, - render: ({ ratio }) => { + render: ({ ratio }, ...args) => { return ( - + Content @@ -92,3 +68,28 @@ export const Playground = { ); }, }; + +Playground.argTypes = { + as: { + table: { + disable: true, + }, + }, + children: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, + ratio: { + control: { + type: 'select', + }, + defaultValue: '1x1', + options: ['16x9', '9x16', '2x1', '1x2', '4x3', '3x4', '1x1'], + }, +}; diff --git a/packages/react/src/components/CodeSnippet/CodeSnippet.js b/packages/react/src/components/CodeSnippet/CodeSnippet.js index fd1a9b16708d..38af4a6a31e9 100644 --- a/packages/react/src/components/CodeSnippet/CodeSnippet.js +++ b/packages/react/src/components/CodeSnippet/CodeSnippet.js @@ -266,7 +266,6 @@ function CodeSnippet({ {expandCodeBtnText}

DataTable with selection

@@ -467,13 +467,13 @@ exports[`DataTable selection -- radio buttons should render 1`] = ` >

DataTable with selection

@@ -900,13 +900,13 @@ exports[`DataTable selection should have select-all default to un-checked if no >

DataTable with selection

@@ -1304,13 +1304,13 @@ exports[`DataTable selection should render 1`] = ` >

DataTable with selection

@@ -1965,13 +1965,13 @@ exports[`DataTable should render 1`] = ` >

DataTable with toolbar

- - + - - - - - + + + + + +
@@ -2987,13 +2989,13 @@ exports[`DataTable sticky header should render 1`] = ` >

DataTable with toolbar

- - + - - - - - + + + + + +
diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap index aff35f5d1d4c..fe8124b2b101 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap @@ -12,7 +12,6 @@ exports[`DataTable.TableToolbarSearch should render 1`] = ` >
- - + - - - - - + + + + + +
}> -

Content for fourth tab goes here.

- - -); - -export const Skeleton = () => { - const isLoading = boolean('isLoading', true); - - return ( -
- {isLoading ? ( - - ) : ( - - -

Content for first tab goes here.

-
- -

Content for second tab goes here.

-
- -

Content for third tab goes here.

-
- -

Content for fourth tab goes here.

-
- Custom Label
}> -

Content for fifth tab goes here.

- - - )} - - ); -}; diff --git a/packages/react/src/components/Tabs/Tabs-story.scss b/packages/react/src/components/Tabs/Tabs-story.scss deleted file mode 100644 index 6a3d79b15e79..000000000000 --- a/packages/react/src/components/Tabs/Tabs-story.scss +++ /dev/null @@ -1,28 +0,0 @@ -// IMPORTANT: This import path should _not_ be used outside our source tree -// as `src` directory is _not_ meant to be shipped in our NPM package. -// Use e.g. `@import '~carbon-components/scss/globals/scss/styles.scss'` instead. - -$css--font-face: false; -$css--body: false; -$css--reset: false; - -// SEE THE NOTE ABOVE -@import '~carbon-components/src/globals/scss/css--helpers'; - -.bx--tabs--container ~ div { - min-height: 320px; - background-color: $ui-01; -} - -.bx--tabs--container.bx--tabs--light ~ div { - background-color: $ui-background; -} - -.tabs-story-wrapper--light { - background-color: $ui-01; -} - -.container-tabs-story-wrapper--light { - padding: 2rem 1rem; - background-color: $ui-01; -} diff --git a/packages/react/src/components/Tabs/next/Tabs-test.js b/packages/react/src/components/Tabs/Tabs-test.js similarity index 100% rename from packages/react/src/components/Tabs/next/Tabs-test.js rename to packages/react/src/components/Tabs/Tabs-test.js diff --git a/packages/react/src/components/Tabs/Tabs.Skeleton.js b/packages/react/src/components/Tabs/Tabs.Skeleton.js index 5d7764a0eb7e..839dd467482b 100644 --- a/packages/react/src/components/Tabs/Tabs.Skeleton.js +++ b/packages/react/src/components/Tabs/Tabs.Skeleton.js @@ -13,7 +13,7 @@ import { usePrefix } from '../../internal/usePrefix'; function Tab() { const prefix = usePrefix(); return ( -
  • +
  • @@ -21,20 +21,14 @@ function Tab() { ); } -function TabsSkeleton({ className, type, ...rest }) { +function TabsSkeleton({ className, contained, ...rest }) { const prefix = usePrefix(); - const tabClasses = cx( - className, - `${prefix}--tabs`, - `${prefix}--skeleton`, - `${prefix}--tabs--scrollable`, - { - [`${prefix}--tabs--scrollable--container`]: type === 'container', - } - ); + const tabClasses = cx(className, `${prefix}--tabs`, `${prefix}--skeleton`, { + [`${prefix}--tabs--contained`]: contained, + }); return (
    -
      +
        {Tab()} {Tab()} {Tab()} @@ -54,7 +48,7 @@ TabsSkeleton.propTypes = { /** * Provide the type of Tab */ - type: PropTypes.oneOf(['', 'default', 'container']), + contained: PropTypes.bool, }; export default TabsSkeleton; diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index bb9642cf2238..2ce4c6632b13 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -5,568 +5,730 @@ * LICENSE file in the root directory of this source tree. */ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; import { ChevronLeft, ChevronRight } from '@carbon/icons-react'; +import cx from 'classnames'; import debounce from 'lodash.debounce'; +import PropTypes from 'prop-types'; +import React, { useCallback, useState, useRef, useEffect } from 'react'; +import { Tooltip } from '../Tooltip/next'; +import { useControllableState } from '../../internal/useControllableState'; +import { useEffectOnce } from '../../internal/useEffectOnce'; +import { useId } from '../../internal/useId'; +import useIsomorphicEffect from '../../internal/useIsomorphicEffect'; +import { useMergedRefs } from '../../internal/useMergedRefs'; +import { getInteractiveContent } from '../../internal/useNoInteractiveChildren'; +import { usePrefix } from '../../internal/usePrefix'; import { keys, match, matches } from '../../internal/keyboard'; -import TabContent from '../TabContent'; -import { PrefixContext } from '../../internal/usePrefix'; - -export default class Tabs extends React.Component { - static propTypes = { - /** - * Pass in a collection of children to be rendered depending on the - * currently selected tab - */ - children: PropTypes.node, - - /** - * Provide a className that is applied to the root
        component for the - * - */ - className: PropTypes.string, - - /** - * Specify whether the Tab content is hidden - */ - hidden: PropTypes.bool, - - /** - * Provide the props that describe the left overflow button - */ - leftOverflowButtonProps: PropTypes.object, - - /** - * Specify whether or not to use the light component variant - */ - light: PropTypes.bool, - - /** - * Optionally provide an `onClick` handler that is invoked when a is - * clicked - */ - onClick: PropTypes.func, - - /** - * Optionally provide an `onKeyDown` handler that is invoked when keyed - * navigation is triggered - */ - onKeyDown: PropTypes.func, - - /** - * Provide an optional handler that is called whenever the selection - * changes. This method is called with the index of the tab that was - * selected - */ - onSelectionChange: PropTypes.func, - - /** - * Provide the props that describe the right overflow button - */ - rightOverflowButtonProps: PropTypes.object, - - /** - * Optionally provide a delay (in milliseconds) passed to the lodash - * debounce of the onScroll handler. This will impact the responsiveness - * of scroll arrow buttons rendering when scrolling to the first or last tab. - */ - scrollDebounceWait: PropTypes.number, - - /** - * Choose whether or not to automatically scroll to newly selected tabs - * on component rerender - */ - scrollIntoView: PropTypes.bool, - - /** - * Optionally provide an index for the currently selected - */ - selected: PropTypes.number, - - /** - * Choose whether or not to automatically change selection on focus - */ - selectionMode: PropTypes.oneOf(['automatic', 'manual']), - - /** - * Provide a className that is applied to the components - */ - tabContentClassName: PropTypes.string, - - /** - * Provide the type of Tab - */ - type: PropTypes.oneOf(['default', 'container']), - }; - - static defaultProps = { - type: 'default', - scrollIntoView: true, - selected: 0, - selectionMode: 'automatic', - scrollDebounceWait: 150, - }; - - static contextType = PrefixContext; - - state = { - horizontalOverflow: false, +import { usePressable } from './usePressable'; + +// Used to manage the overall state of the Tabs +const TabsContext = React.createContext(); + +// Used to keep track of position in a tablist +const TabContext = React.createContext(); + +// Used to keep track of position in a list of tab panels +const TabPanelContext = React.createContext(); +function Tabs({ + children, + defaultSelectedIndex = 0, + onChange, + selectedIndex: controlledSelectedIndex, +}) { + const baseId = useId('ccs'); + // The active index is used to track the element which has focus in our tablist + const [activeIndex, setActiveIndex] = useState(defaultSelectedIndex); + // The selected index is used for the tab/panel pairing which is "visible" + const [selectedIndex, setSelectedIndex] = useControllableState({ + value: controlledSelectedIndex, + defaultValue: defaultSelectedIndex, + onChange: (value) => { + if (onChange) { + onChange({ selectedIndex: value }); + } + }, + }); + + const value = { + baseId, + activeIndex, + defaultSelectedIndex, + setActiveIndex, + selectedIndex, + setSelectedIndex, }; - tablist = React.createRef(); - leftOverflowNavButton = React.createRef(); - rightOverflowNavButton = React.createRef(); - // width of the overflow buttons - OVERFLOW_BUTTON_OFFSET = 40; - - static getDerivedStateFromProps({ selected }, state) { - const { prevSelected } = state; - return prevSelected === selected - ? null - : { - selected, - prevSelected: selected, - }; - } + return {children}; +} +Tabs.propTypes = { /** - * `scroll` event handler to save tablist clientWidth, scrollWidth, and - * scrollLeft + * Provide child elements to be rendered inside of the `Tabs`. + * These elements should render either `TabsList` or `TabsPanels` */ - handleScroll = () => { - if (!this.tablist?.current) { - return; - } - const { - clientWidth: tablistClientWidth, - scrollLeft: tablistScrollLeft, - scrollWidth: tablistScrollWidth, - } = this.tablist.current; - this.setState({ - tablistClientWidth, - horizontalOverflow: tablistScrollWidth > tablistClientWidth, - tablistScrollWidth, - tablistScrollLeft, - }); - }; + children: PropTypes.node, /** - * The debounced version of the `resize` event handler. - * @type {Function} - * @private + * Specify which content tab should be initially selected when the component + * is first rendered */ - _debouncedHandleWindowResize = null; + defaultSelectedIndex: PropTypes.number, - _handleWindowResize = this.handleScroll; + /** + * Provide an optional function which is called whenever the state of the + * `Tabs` changes + */ + onChange: PropTypes.func, /** - * The debounced version of the `scroll` event handler. - * @type {Function} - * @private + * Control which content panel is currently selected. This puts the component + * in a controlled mode and should be used along with `onChange` */ - _debouncedHandleScroll = null; + selectedIndex: PropTypes.number, +}; - _handleScroll = this.handleScroll; +/** + * Get the next index for a given keyboard event given a count of the total + * items and the current index + * @param {Event} event + * @param {number} total + * @param {number} index + * @returns {number} + */ +function getNextIndex(event, total, index) { + if (match(event, keys.ArrowRight)) { + return (index + 1) % total; + } else if (match(event, keys.ArrowLeft)) { + return (total + index - 1) % total; + } else if (match(event, keys.Home)) { + return 0; + } else if (match(event, keys.End)) { + return total - 1; + } +} - componentDidMount() { - if (!this._debouncedHandleWindowResize) { - this._debouncedHandleWindowResize = debounce( - this._handleWindowResize, - 200 - ); +function TabList({ + activation = 'automatic', + 'aria-label': label, + children, + className: customClassName, + contained = false, + iconSize, + leftOverflowButtonProps, + light, + rightOverflowButtonProps, + scrollDebounceWait = 200, + scrollIntoView, + ...rest +}) { + const { activeIndex, selectedIndex, setSelectedIndex, setActiveIndex } = + React.useContext(TabsContext); + const prefix = usePrefix(); + const ref = useRef(null); + const previousButton = useRef(null); + const nextButton = useRef(null); + const [isScrollable, setIsScrollable] = useState(false); + const [scrollLeft, setScrollLeft] = useState(null); + const className = cx(`${prefix}--tabs`, customClassName, { + [`${prefix}--tabs--contained`]: contained, + [`${prefix}--tabs--light`]: light, + [`${prefix}--tabs__icon--default`]: iconSize === 'default', + [`${prefix}--tabs__icon--lg`]: iconSize === 'lg', + }); + + // Previous Button + // VISIBLE IF: + // SCROLLABLE + // AND SCROLL_LEFT > 0 + const buttonWidth = 44; + const isPreviousButtonVisible = ref.current + ? isScrollable && scrollLeft > 0 + : false; + // Next Button + // VISIBLE IF: + // SCROLLABLE + // AND SCROLL_LEFT + CLIENT_WIDTH < SCROLL_WIDTH + const isNextButtonVisible = ref.current + ? scrollLeft + buttonWidth + ref.current.clientWidth < + ref.current.scrollWidth + : false; + const previousButtonClasses = cx( + `${prefix}--tab--overflow-nav-button`, + `${prefix}--tab--overflow-nav-button--previous`, + { + [`${prefix}--tab--overflow-nav-button--hidden`]: !isPreviousButtonVisible, } + ); + const nextButtonClasses = cx( + `${prefix}--tab--overflow-nav-button`, + `${prefix}--tab--overflow-nav-button--next`, + { + [`${prefix}--tab--overflow-nav-button--hidden`]: !isNextButtonVisible, + } + ); + + const tabs = useRef([]); + const debouncedOnScroll = useCallback(() => { + return debounce((event) => { + setScrollLeft(event.target.scrollLeft); + }, scrollDebounceWait); + }, [scrollDebounceWait]); + + function onKeyDown(event) { + if ( + matches(event, [keys.ArrowRight, keys.ArrowLeft, keys.Home, keys.End]) + ) { + event.preventDefault(); - this._handleWindowResize(); - window.addEventListener('resize', this._debouncedHandleWindowResize); + const activeTabs = tabs.current.filter((tab) => { + return !tab.disabled; + }); - if (!this._debouncedHandleScroll) { - this._debouncedHandleScroll = debounce( - this._handleScroll, - this.props.scrollDebounceWait + const currentIndex = activeTabs.indexOf( + tabs.current[activation === 'automatic' ? selectedIndex : activeIndex] + ); + const nextIndex = tabs.current.indexOf( + activeTabs[getNextIndex(event, activeTabs.length, currentIndex)] ); - } - // scroll selected tab into view on mount - const { - clientWidth: tablistClientWidth, - scrollLeft: tablistScrollLeft, - scrollWidth: tablistScrollWidth, - } = this.tablist?.current || {}; - const tab = this.getTabAt(this.state.selected); - const horizontalOverflow = tablistScrollWidth > tablistClientWidth; - - if (horizontalOverflow) { - const leftOverflowNavButtonHidden = - tab?.tabAnchor?.getBoundingClientRect().right < - tab?.tabAnchor?.offsetParent.getBoundingClientRect().right; - const rightOverflowNavButtonHidden = - tablistScrollLeft + tablistClientWidth === tablistScrollWidth; - this.props.scrollIntoView && - tab?.tabAnchor?.scrollIntoView({ block: 'nearest', inline: 'nearest' }); - - // account for overflow buttons in scroll position on mount - if (!leftOverflowNavButtonHidden && !rightOverflowNavButtonHidden) { - this.tablist.current.scrollLeft += this.OVERFLOW_BUTTON_OFFSET * 2; + if (activation === 'automatic') { + setSelectedIndex(nextIndex); + } else if (activation === 'manual') { + setActiveIndex(nextIndex); } - } - } - componentWillUnmount() { - if (this._debouncedHandleWindowResize) { - this._debouncedHandleWindowResize.cancel(); + tabs.current[nextIndex].focus(); } - window.removeEventListener('resize', this._debouncedHandleWindowResize); } - componentDidUpdate(_, prevState) { - // compare current tablist properties to current state - const { - clientWidth: tablistClientWidth, - scrollLeft: tablistScrollLeft, - scrollWidth: tablistScrollWidth, - } = this.tablist.current; - const { - tablistClientWidth: currentStateClientWidth, - tablistScrollLeft: currentStateScrollLeft, - tablistScrollWidth: currentStateScrollWidth, - selected, - } = this.state; - - if ( - tablistClientWidth !== currentStateClientWidth || - tablistScrollLeft !== currentStateScrollLeft || - tablistScrollWidth !== currentStateScrollWidth - ) { - this.setState({ - horizontalOverflow: tablistScrollWidth > tablistClientWidth, - tablistClientWidth, - tablistScrollLeft, - tablistScrollWidth, - }); - } - - if (this.props.scrollIntoView && prevState.selected !== selected) { - this.getTabAt(selected)?.tabAnchor?.scrollIntoView({ + useEffectOnce(() => { + const tab = tabs.current[selectedIndex]; + if (scrollIntoView && tab) { + tab.scrollIntoView({ block: 'nearest', inline: 'nearest', }); } - } + }); - getEnabledTabs = () => - React.Children.toArray(this.props.children).reduce( - (enabledTabs, tab, index) => - !tab.props.disabled ? enabledTabs.concat(index) : enabledTabs, - [] - ); + useEffectOnce(() => { + if (tabs.current[selectedIndex].disabled) { + const activeTabs = tabs.current.filter((tab) => { + return !tab.disabled; + }); - getNextIndex = (index, direction) => { - const enabledTabs = this.getEnabledTabs(); - const nextIndex = Math.max( - enabledTabs.indexOf(index) + direction, - // For `tab` not found in `enabledTabs` - -1 - ); - const nextIndexLooped = - nextIndex >= 0 && nextIndex < enabledTabs.length - ? nextIndex - : nextIndex - Math.sign(nextIndex) * enabledTabs.length; - return enabledTabs[nextIndexLooped]; - }; + if (activeTabs.length > 0) { + const tab = activeTabs[0]; + setSelectedIndex(tabs.current.indexOf(tab)); + } + } + }); - getDirection = (evt) => { - if (match(evt, keys.ArrowLeft)) { - return -1; + useIsomorphicEffect(() => { + if (ref.current) { + setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth); } - if (match(evt, keys.ArrowRight)) { - return 1; + + function handler() { + if (ref.current) { + setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth); + } } - return 0; - }; - getTabAt = (index, useFresh) => - (!useFresh && this[`tab${index}`]) || - React.Children.toArray(this.props.children)[index]; + const debouncedHandler = debounce(handler, 200); + window.addEventListener('resize', debouncedHandler); + return () => { + debouncedHandler.cancel(); + window.removeEventListener('resize', debouncedHandler); + }; + }, []); - scrollTabIntoView = (event, { index }) => { - const tab = this.getTabAt(index); - if ( - matches(event, [keys.ArrowLeft, keys.ArrowRight]) || - event.type === 'click' - ) { - const currentScrollLeft = this.state.tablistScrollLeft; - tab?.tabAnchor?.scrollIntoView({ block: 'nearest', inline: 'nearest' }); - tab?.tabAnchor?.focus(); - const newScrollLeft = this.tablist.current.scrollLeft; - if (newScrollLeft > currentScrollLeft) { - this.tablist.current.scrollLeft += this.OVERFLOW_BUTTON_OFFSET; - } + // updates scroll location for all scroll behavior. + useIsomorphicEffect(() => { + if (scrollLeft !== null) { + ref.current.scrollLeft = scrollLeft; } - }; + }, [scrollLeft]); - selectTabAt = (event, { index, onSelectionChange }) => { - this.scrollTabIntoView(event, { index }); - if (this.state.selected !== index) { - this.setState({ - selected: index, - }); - if (typeof onSelectionChange === 'function') { - onSelectionChange(index); - } + useIsomorphicEffect(() => { + if (!isScrollable) { + return; } - }; - handleTabKeyDown = (onSelectionChange) => { - return (index, evt) => { - if (matches(evt, [keys.Enter, keys.Space])) { - this.selectTabAt(evt, { index, onSelectionChange }); + const tab = + activation === 'manual' + ? tabs.current[activeIndex] + : tabs.current[selectedIndex]; + if (tab) { + // The width of the "scroll buttons" + + // The start and end position of the selected tab + const { width: tabWidth } = tab.getBoundingClientRect(); + const start = tab.offsetLeft; + const end = tab.offsetLeft + tabWidth; + + // The start and end of the visible area for the tabs + const visibleStart = ref.current.scrollLeft + buttonWidth; + const visibleEnd = + ref.current.scrollLeft + ref.current.clientWidth - buttonWidth; + + // The beginning of the tab is clipped and not visible + if (start < visibleStart) { + setScrollLeft(start - buttonWidth); } - const nextIndex = (() => { - if (matches(evt, [keys.ArrowLeft, keys.ArrowRight])) { - return this.getNextIndex(index, this.getDirection(evt)); - } - if (match(evt, keys.Home)) { - return 0; - } - if (match(evt, keys.End)) { - return this.getEnabledTabs().pop(); - } - })(); - const tab = this.getTabAt(nextIndex); - - if ( - matches(evt, [keys.ArrowLeft, keys.ArrowRight, keys.Home, keys.End]) - ) { - evt.preventDefault(); - if (this.props.selectionMode !== 'manual') { - this.selectTabAt(evt, { index: nextIndex, onSelectionChange }); - } else { - this.scrollTabIntoView(evt, { index: nextIndex }); - } - tab?.tabAnchor?.focus(); + // The end of teh tab is clipped and not visible + if (end > visibleEnd) { + setScrollLeft(end + buttonWidth - ref.current.clientWidth); } - }; - }; + } + }, [activation, activeIndex, selectedIndex, isScrollable]); + + usePressable(previousButton, { + onPress({ longPress }) { + if (!longPress) { + setScrollLeft( + Math.max( + scrollLeft - (ref.current.scrollWidth / tabs.current.length) * 1.5, + 0 + ) + ); + } + }, + onLongPress() { + return createLongPressBehavior(ref, 'backward', setScrollLeft); + }, + }); + + usePressable(nextButton, { + onPress({ longPress }) { + if (!longPress) { + setScrollLeft( + Math.min( + scrollLeft + (ref.current.scrollWidth / tabs.current.length) * 1.5, + ref.current.scrollWidth - ref.current.clientWidth + ) + ); + } + }, + onLongPress() { + return createLongPressBehavior(ref, 'forward', setScrollLeft); + }, + }); + + return ( +
        + + {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */} +
        + {React.Children.map(children, (child, index) => { + return ( + + {React.cloneElement(child, { + ref: (node) => { + tabs.current[index] = node; + }, + })} + + ); + })} +
        + +
        + ); +} - getTabs = () => React.Children.map(this.props.children, (tab) => tab); +TabList.propTypes = { + /** + * Specify whether the content tab should be activated automatically or + * manually + */ + activation: PropTypes.oneOf(['automatic', 'manual']), - // following functions (handle*) are Props on Tab.js, see Tab.js for parameters - handleTabClick = (onSelectionChange) => (index, evt) => { - evt.preventDefault(); - this.selectTabAt(evt, { index, onSelectionChange }); - }; + /** + * Provide an accessible label to be read when a user interacts with this + * component + */ + 'aria-label': PropTypes.string.isRequired, - setTabAt = (index, tabRef) => { - this[`tab${index}`] = tabRef; - }; + /** + * Provide child elements to be rendered inside of `ContentTabs`. + * These elements should render a `ContentTab` + */ + children: PropTypes.node, - overflowNavInterval = null; + /** + * Specify an optional className to be added to the container node + */ + className: PropTypes.string, - handleOverflowNavClick = (_, { direction, multiplier = 10 }) => { - // account for overflow button appearing and causing tablist width change - const { clientWidth, scrollLeft, scrollWidth } = this.tablist?.current; - if (direction === 1 && !scrollLeft) { - this.tablist.current.scrollLeft += this.OVERFLOW_BUTTON_OFFSET; - } + /** + * Specify whether component is contained type + */ + contained: PropTypes.bool, - this.tablist.current.scrollLeft += direction * multiplier; + /** + * If using `IconTab`, specify the size of the icon being used. + */ + iconSize: PropTypes.oneOf(['default', 'lg']), - const leftEdgeReached = - direction === -1 && scrollLeft < this.OVERFLOW_BUTTON_OFFSET; - const rightEdgeReached = - direction === 1 && - scrollLeft + clientWidth >= scrollWidth - this.OVERFLOW_BUTTON_OFFSET; - if (leftEdgeReached || rightEdgeReached) { - if (leftEdgeReached) { - this.rightOverflowNavButton?.current?.focus(); - } - if (rightEdgeReached) { - this.leftOverflowNavButton?.current?.focus(); - } + /** + * Provide the props that describe the left overflow button + */ + leftOverflowButtonProps: PropTypes.object, + + /** + * Specify whether or not to use the light component variant + */ + light: PropTypes.bool, + + /** + * Provide the props that describe the right overflow button + */ + rightOverflowButtonProps: PropTypes.object, + + /** + * Optionally provide a delay (in milliseconds) passed to the lodash + * debounce of the onScroll handler. This will impact the responsiveness + * of scroll arrow buttons rendering when scrolling to the first or last tab. + */ + scrollDebounceWait: PropTypes.number, + + /** + * Choose whether or not to automatically scroll to newly selected tabs + * on component rerender + */ + scrollIntoView: PropTypes.bool, +}; + +/** + * Helper function to setup the behavior when a button is "long pressed". This + * function will take a ref to the tablist, a direction, and a setter for + * scrollLeft and will update the scroll position within a + * requestAnimationFrame. + * + * It returns a cleanup function to be run when the long press is + * deactivated + * + * @param {RefObject} ref + * @param {'forward' | 'backward'} direction + * @param {Function} setScrollLeft + * @returns {Function} + */ +function createLongPressBehavior(ref, direction, setScrollLeft) { + // We manually override the scroll behavior to be "auto". If it is set as + // smooth, this animation does not update correctly + let defaultScrollBehavior = ref.current.style['scroll-behavior']; + ref.current.style['scroll-behavior'] = 'auto'; + + const scrollDelta = direction === 'forward' ? 5 : -5; + let frameId = null; + + function tick() { + ref.current.scrollLeft = ref.current.scrollLeft + scrollDelta; + frameId = requestAnimationFrame(tick); + } + + frameId = requestAnimationFrame(tick); + + return () => { + // Restore the previous scroll behavior + ref.current.style['scroll-behavior'] = defaultScrollBehavior; + + // Make sure that our `scrollLeft` value is in sync with the existing + // `ref` after our requestAnimationFrame loop above + setScrollLeft(ref.current.scrollLeft); + + if (frameId) { + cancelAnimationFrame(frameId); } }; +} + +const Tab = React.forwardRef(function Tab( + { + as: BaseComponent = 'button', + children, + className: customClassName, + disabled, + onClick, + onKeyDown, + ...rest + }, + ref +) { + const prefix = usePrefix(); + const { selectedIndex, setSelectedIndex, baseId } = + React.useContext(TabsContext); + const index = React.useContext(TabContext); + const id = `${baseId}-tab-${index}`; + const panelId = `${baseId}-tabpanel-${index}`; + const className = cx( + `${prefix}--tabs__nav-item`, + `${prefix}--tabs__nav-link`, + customClassName, + { + [`${prefix}--tabs__nav-item--selected`]: selectedIndex === index, + [`${prefix}--tabs__nav-item--disabled`]: disabled, + } + ); + + return ( + { + if (disabled) { + return; + } + setSelectedIndex(index); + if (onClick) { + onClick(evt); + } + }} + onKeyDown={onKeyDown} + tabIndex={selectedIndex === index ? '0' : '-1'} + type="button"> + {children} + + ); +}); + +Tab.propTypes = { + /** + * Provide a custom element to render instead of the default button + */ + as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), + + /** + * Provide child elements to be rendered inside of `Tab`. + */ + children: PropTypes.node, - handleOverflowNavMouseDown = (event, { direction }) => { - // disregard mouse buttons aside from left mouse button - if (event.buttons !== 1) { + /** + * Specify an optional className to be added to your Tab + */ + className: PropTypes.string, + + /** + * Whether your Tab is disabled. + */ + disabled: PropTypes.bool, + + /** + * Provide a handler that is invoked when a user clicks on the control + */ + onClick: PropTypes.func, + + /** + * Provide a handler that is invoked on the key down event for the control + */ + onKeyDown: PropTypes.func, + + /* + * An optional parameter to allow overriding the anchor rendering. + * Useful for using Tab along with react-router or other client + * side router libraries. + **/ + renderButton: PropTypes.func, +}; + +const IconTab = React.forwardRef(function IconTab( + { + children, + className: customClassName, + defaultOpen = false, + enterDelayMs, + leaveDelayMs, + label, + ...rest + }, + ref +) { + const prefix = usePrefix(); + + const classNames = cx(`${prefix}--tabs__nav-item--icon`, customClassName); + return ( + + + {children} + + + ); +}); + +IconTab.propTypes = { + /** + * Provide an icon to be rendered inside of `IconTab` as the visual label for Tab. + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to your Tab + */ + className: PropTypes.string, + + /** + * Specify whether the tooltip for the icon should be open when it first renders + */ + defaultOpen: PropTypes.bool, + + /** + * Specify the duration in milliseconds to delay before displaying the tooltip for the icon. + */ + enterDelayMs: PropTypes.number, + + /** + * Provide the label to be rendered inside of the Tooltip. The label will use + * `aria-labelledby` and will fully describe the child node that is provided. + * This means that if you have text in the child node it will not be + * announced to the screen reader. + */ + label: PropTypes.node.isRequired, + + /** + * Specify the duration in milliseconds to delay before hiding the tooltip + */ + leaveDelayMs: PropTypes.number, +}; + +const TabPanel = React.forwardRef(function TabPanel( + { children, className: customClassName, ...rest }, + forwardRef +) { + const prefix = usePrefix(); + const panel = useRef(null); + const ref = useMergedRefs([forwardRef, panel]); + + const [tabIndex, setTabIndex] = useState('0'); + const [interactiveContent, setInteractiveContent] = useState(false); + const { selectedIndex, baseId } = React.useContext(TabsContext); + const index = React.useContext(TabPanelContext); + const id = `${baseId}-tabpanel-${index}`; + const tabId = `${baseId}-tab-${index}`; + const className = cx(`${prefix}--tab-content`, customClassName, { + [`${prefix}--tab-content--interactive`]: interactiveContent, + }); + + useEffectOnce(() => { + if (!panel.current) { return; } - this.overflowNavInterval = setInterval(() => { - const { clientWidth, scrollLeft, scrollWidth } = this.tablist?.current; - - // clear interval if scroll reaches left or right edge - const leftEdgeReached = - direction === -1 && scrollLeft < this.OVERFLOW_BUTTON_OFFSET; - const rightEdgeReached = - direction === 1 && - scrollLeft + clientWidth >= scrollWidth - this.OVERFLOW_BUTTON_OFFSET; - if (leftEdgeReached || rightEdgeReached) { - clearInterval(this.overflowNavInterval); - } - // account for overflow button appearing and causing tablist width change - this.handleOverflowNavClick(event, { direction }); - }); - }; + const content = getInteractiveContent(panel.current); + if (content) { + setInteractiveContent(true); + setTabIndex('-1'); + } + }); - handleOverflowNavMouseUp = () => { - clearInterval(this.overflowNavInterval); - }; + // tabindex should only be 0 if no interactive content in children + useEffect(() => { + if (!panel.current) { + return; + } - render() { - const { - className, - type, - light, - onSelectionChange, - scrollDebounceWait, // eslint-disable-line no-unused-vars - scrollIntoView, // eslint-disable-line no-unused-vars - selectionMode, // eslint-disable-line no-unused-vars - tabContentClassName, - leftOverflowButtonProps, - rightOverflowButtonProps, - ...other - } = this.props; - - const prefix = this.context; - - /** - * The tab panel acts like a tab panel when the screen is wider, but acts - * like a select list when the screen is narrow. In the wide case we want - * to allow the user to use the tab key to set the focus in the tab panel - * and then use the left and right arrow keys to navigate the tabs. In the - * narrow case we want to use the tab key to select different options in - * the list. - * - * We set the tab index based on the different states so the browser will treat - * the whole tab panel as a single focus component when it looks like a tab - * panel and separate components when it looks like a select list. - */ - const tabsWithProps = this.getTabs().map((tab, index) => { - const tabIndex = index === this.state.selected ? 0 : -1; - const newTab = React.cloneElement(tab, { - index, - selected: index === this.state.selected, - handleTabClick: this.handleTabClick(onSelectionChange), - tabIndex, - ref: (e) => { - this.setTabAt(index, e); - }, - handleTabKeyDown: this.handleTabKeyDown(onSelectionChange), - }); + const { current: node } = panel; - return newTab; - }); + function callback() { + const content = getInteractiveContent(node); + if (content) { + setInteractiveContent(true); + setTabIndex('-1'); + } else { + setInteractiveContent(false); + setTabIndex('0'); + } + } - const tabContentWithProps = React.Children.map(tabsWithProps, (tab) => { - const { - id: tabId, - children, - selected, - renderContent: Content = TabContent, - } = tab.props; - - return ( - - ); + const observer = new MutationObserver(callback); + + observer.observe(node, { + childList: true, + subtree: true, }); - const leftOverflowNavButtonHidden = - !this.state.horizontalOverflow || !this.state.tablistScrollLeft; - const rightOverflowNavButtonHidden = - !this.state.horizontalOverflow || - this.state.tablistScrollLeft + this.state.tablistClientWidth === - this.state.tablistScrollWidth; - const classes = { - // TODO: remove scrollable from classnames in next major release and uncomment classnames that don't contain scrollable - tabs: classNames( - className, - // `${prefix}--tabs`, - `${prefix}--tabs--scrollable`, - { - // [`${prefix}--tabs--container`]: type === 'container', - [`${prefix}--tabs--scrollable--container`]: type === 'container', - // [`${prefix}--tabs--light`]: light, - [`${prefix}--tabs--scrollable--light`]: light, - } - ), - // TODO: remove scrollable from classnames in next major release and uncomment classnames that don't contain scrollable - tablist: classNames( - // `${prefix}--tabs__nav`, - `${prefix}--tabs--scrollable__nav` - ), - leftOverflowButtonClasses: classNames({ - [`${prefix}--tab--overflow-nav-button`]: this.state.horizontalOverflow, - [`${prefix}--tab--overflow-nav-button--hidden`]: - leftOverflowNavButtonHidden, - }), - rightOverflowButtonClasses: classNames({ - [`${prefix}--tab--overflow-nav-button`]: this.state.horizontalOverflow, - [`${prefix}--tab--overflow-nav-button--hidden`]: - rightOverflowNavButtonHidden, - }), + return () => { + observer.disconnect(node); }; + }, []); + + return ( + + ); +}); + +TabPanel.propTypes = { + /** + * Provide child elements to be rendered inside of `TabPanel`. + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to TabPanel. + */ + className: PropTypes.string, +}; +function TabPanels({ children }) { + return React.Children.map(children, (child, index) => { return ( - <> -
        - - {!leftOverflowNavButtonHidden && ( -
        - )} -
          - {tabsWithProps} -
        - {!rightOverflowNavButtonHidden && ( -
        - )} - -
        - {tabContentWithProps} - + {child} ); - } + }); } + +TabPanels.propTypes = { + /** + * Provide child elements to be rendered inside of `TabPanels`. + */ + children: PropTypes.node, +}; + +export { Tabs, Tab, IconTab, TabPanel, TabPanels, TabList }; diff --git a/packages/react/src/components/Tabs/Tabs.mdx b/packages/react/src/components/Tabs/Tabs.mdx index 3bec34083b50..eb55e496372b 100644 --- a/packages/react/src/components/Tabs/Tabs.mdx +++ b/packages/react/src/components/Tabs/Tabs.mdx @@ -1,6 +1,5 @@ import { Props, Preview, Story } from '@storybook/addon-docs'; -import Tabs from '../Tabs'; -import Tab from '../Tab'; +import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'; # Tabs @@ -13,7 +12,7 @@ import Tab from '../Tab'; ## Table of Contents - [Overview](#overview) - - [Default Tabs](#default-tabs) + - [Line Tabs](#line-tabs) - [Container Tabs](#container-tabs) - [Component API](#component-api) - [Tab `renderContent`](#tab-rendercontent) @@ -22,54 +21,129 @@ import Tab from '../Tab'; ## Overview Use tabs to allow users to navigate easily between views within the same -context. +context. Tabs are now more composable, meaning that you have more flexibility in +what is in rendered inside of `Tab` and `TabPanel`. -### Default Tabs +### Line Tabs - + -### Container Tabs +### Contained Tabs - + + + +### Icon Tabs + + + + + + + ## Component API -### Tab `renderContent` +### Tab - render content on click You will occasionally run into a situation where you only want Tab content to be -loaded when the Tab is clicked. To do this, you can use the `renderContent` prop -like so: +loaded when the Tab is clicked. In v11, to do this, you can this by setting +`activation` to `manual`: ```jsx -const TabContentRenderedOnlyWhenSelected = ({ - selected, - children, - className, - ...other -}) => - !selected ? ( -
        - ) : ( -
        - {children} -
        - ); + + + Tab Label 1 + Tab Label 2 + Tab Label 3 + + + Tab Panel 1 + Tab Panel 2 + Tab Panel 3 + + +``` +## V11 + +### Tabs composition + +Tabs got a big revamp in v11! Tabs are now more composable than ever before, +meaning that you have the flexibity and control on your end to make them look +and act how you want. The biggest difference is that the Tab label and the Tab +content are now separate components. + +Example of Tabs in v10: + +```js - -; + +

        Content for first tab goes here.

        +
        + +

        Content for second tab goes here.

        +
        + +

        Content for third tab goes here.

        +
        + +

        Content for fourth tab goes here.

        +
        + ``` +Those same Tabs, now in v11: + +```js + + + Tab Label 1 + Tab Label 2 + Tab Label 3 + Tab Label 4 shows truncation + + + Content for first tab goes here. + Content for second tab goes here. + Content for third tab goes here. + Content for fourth tab goes here. + + +``` + +### Various updates + +All the same functionality for Tabs is available in v11 and more! Below are the +minor tweaks in naming or implementation. + +- the `type` prop is deprecated. Both "container" and "default" tabs still exist + but now can be called by adding the prop `contained` to the `TabList`. See the + above "Contained Tabs" for an example. +- Default tabs are now referred to as line tabs in our documentation here and on + our website. +- `hidden` prop is no longer needed with the new composable Tabs. You have full + control over tab content and when it's hidden through the `TabPanel` and + `TabPanels` components. +- `selected` prop is now named `selectedIndex`. +- `tabContentClassName` is no longer needed. `TabPanel` (equivalent to tab + content) takes in a className prop on its outermost node. +- For `Tab`, `label` is no longer needed. `children` of `Tab` are now the label. +- Due to its composability, `renderAnchor`, `renderButton`, `renderContent` are + no longer needed on `Tab`. +- `selected` on `Tab` is deprecated in favor or `selectedIndex`, now placed on + `Tabs` instead. +- Because `renderButton` is no longer needed, the associated `tabIndex` prop has + also been deprecated. + ## Feedback Help us improve this component by providing feedback, asking questions on Slack, diff --git a/packages/react/src/components/Tabs/next/Tabs.stories.js b/packages/react/src/components/Tabs/Tabs.stories.js similarity index 72% rename from packages/react/src/components/Tabs/next/Tabs.stories.js rename to packages/react/src/components/Tabs/Tabs.stories.js index b2496eb32731..8c3a1be7fd93 100644 --- a/packages/react/src/components/Tabs/next/Tabs.stories.js +++ b/packages/react/src/components/Tabs/Tabs.stories.js @@ -7,9 +7,9 @@ import React from 'react'; import { Tabs, TabList, Tab, TabPanels, TabPanel, IconTab } from './Tabs'; -import TextInput from '../../TextInput'; -import Checkbox from '../../Checkbox'; -import Button from '../../Button'; +import TextInput from '../TextInput'; +import Checkbox from '../Checkbox'; +import Button from '../Button'; import mdx from './Tabs.mdx'; import TabsSkeleton from './Tabs.Skeleton'; @@ -29,11 +29,18 @@ export default { page: mdx, }, }, + argTypes: { + light: { + table: { + disable: true, + }, + }, + }, }; -export const Default = (args) => ( +export const Default = () => ( - + Tab Label 1 Tab Label 2 Tab Label 3 @@ -54,6 +61,7 @@ export const Default = (args) => ( type="text" labelText="Text input label" helperText="Optional help text" + id="text-input-1" /> @@ -63,9 +71,9 @@ export const Default = (args) => ( ); -export const Manual = (args) => ( +export const Manual = () => ( - + Tab Label 1 Tab Label 2 Tab Label 3 @@ -89,6 +97,7 @@ export const Manual = (args) => ( type="text" labelText="Text input label" helperText="Optional help text" + id="text-input-1" /> @@ -99,9 +108,9 @@ export const Manual = (args) => ( ); -export const Icon20Only = (args) => ( +export const Icon20Only = () => ( - + @@ -120,9 +129,9 @@ export const Icon20Only = (args) => ( ); -export const IconOnly = (args) => ( +export const IconOnly = () => ( - + @@ -141,9 +150,9 @@ export const IconOnly = (args) => ( ); -export const Contained = (args) => ( +export const Contained = () => ( - + Tab Label 1 Tab Label 2 Tab Label 3 @@ -177,10 +186,65 @@ export const Contained = (args) => ( ); -export const Skeleton = (args) => { +export const Skeleton = () => { return ( -
        +
        ); }; + +export const Playground = (args) => ( + + + Tab Label 1 + Tab Label 2 + Tab Label 3 + Tab Label 4 + + + Tab Panel 1 + Tab Panel 2 + Tab Panel 3 + Tab Panel 4 + + +); + +Playground.argTypes = { + automatic: { + control: { type: 'select' }, + options: ['automatic', 'manual'], + }, + contained: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, + iconSize: { + control: { type: 'select' }, + options: ['default', 'lg'], + }, + leftOverflowButtonProps: { + control: { + type: 'object', + }, + }, + rightOverflowButtonProps: { + control: { + type: 'object', + }, + }, + scrollDebounceWait: { + control: { + type: 'number', + }, + defaultValue: 200, + }, + scrollIntoView: { + control: { + type: 'boolean', + }, + }, +}; diff --git a/packages/react/src/components/Tabs/index.js b/packages/react/src/components/Tabs/index.js index c769621ce194..59c9a6dadf3e 100644 --- a/packages/react/src/components/Tabs/index.js +++ b/packages/react/src/components/Tabs/index.js @@ -5,26 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import * as FeatureFlags from '@carbon/feature-flags'; -import { - Tabs as TabsNext, - TabPanel, - TabPanels, - TabList, - IconTab, - Tab, -} from './next/Tabs'; -import { default as TabsClassic } from './Tabs'; -import { default as TabsSkeletonClassic } from './Tabs.Skeleton'; -import { default as TabsSkeletonNext } from './next/Tabs.Skeleton'; - -const Tabs = FeatureFlags.enabled('enable-v11-release') - ? TabsNext - : TabsClassic; - -const TabsSkeleton = FeatureFlags.enabled('enable-v11-release') - ? TabsSkeletonNext - : TabsSkeletonClassic; +import { Tabs, TabPanel, TabPanels, TabList, IconTab, Tab } from './Tabs'; +import { default as TabsSkeleton } from './Tabs.Skeleton'; export { TabsSkeleton, TabPanels, TabPanel, TabList, IconTab, Tab }; diff --git a/packages/react/src/components/Tabs/next/Tabs.Skeleton.js b/packages/react/src/components/Tabs/next/Tabs.Skeleton.js deleted file mode 100644 index 86a56b0065a4..000000000000 --- a/packages/react/src/components/Tabs/next/Tabs.Skeleton.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * 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 cx from 'classnames'; -import { usePrefix } from '../../../internal/usePrefix'; - -function Tab() { - const prefix = usePrefix(); - return ( -
      • -
        - -
        -
      • - ); -} - -function TabsSkeleton({ className, contained, ...rest }) { - const prefix = usePrefix(); - const tabClasses = cx(className, `${prefix}--tabs`, `${prefix}--skeleton`, { - [`${prefix}--tabs--contained`]: contained, - }); - return ( -
        -
          - {Tab()} - {Tab()} - {Tab()} - {Tab()} - {Tab()} -
        -
        - ); -} - -TabsSkeleton.propTypes = { - /** - * Specify an optional className to add. - */ - className: PropTypes.string, - - /** - * Provide the type of Tab - */ - contained: PropTypes.bool, -}; - -export default TabsSkeleton; diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js deleted file mode 100644 index d5abd9353044..000000000000 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ /dev/null @@ -1,734 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * 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 { ChevronLeft, ChevronRight } from '@carbon/icons-react'; -import cx from 'classnames'; -import debounce from 'lodash.debounce'; -import PropTypes from 'prop-types'; -import React, { useCallback, useState, useRef, useEffect } from 'react'; -import { Tooltip } from '../../Tooltip/next'; -import { useControllableState } from '../../../internal/useControllableState'; -import { useEffectOnce } from '../../../internal/useEffectOnce'; -import { useId } from '../../../internal/useId'; -import useIsomorphicEffect from '../../../internal/useIsomorphicEffect'; -import { useMergedRefs } from '../../../internal/useMergedRefs'; -import { getInteractiveContent } from '../../../internal/useNoInteractiveChildren'; -import { usePrefix } from '../../../internal/usePrefix'; -import { keys, match, matches } from '../../../internal/keyboard'; -import { usePressable } from './usePressable'; - -// Used to manage the overall state of the Tabs -const TabsContext = React.createContext(); - -// Used to keep track of position in a tablist -const TabContext = React.createContext(); - -// Used to keep track of position in a list of tab panels -const TabPanelContext = React.createContext(); -function Tabs({ - children, - defaultSelectedIndex = 0, - onChange, - selectedIndex: controlledSelectedIndex, -}) { - const baseId = useId('ccs'); - // The active index is used to track the element which has focus in our tablist - const [activeIndex, setActiveIndex] = useState(defaultSelectedIndex); - // The selected index is used for the tab/panel pairing which is "visible" - const [selectedIndex, setSelectedIndex] = useControllableState({ - value: controlledSelectedIndex, - defaultValue: defaultSelectedIndex, - onChange: (value) => { - if (onChange) { - onChange({ selectedIndex: value }); - } - }, - }); - - const value = { - baseId, - activeIndex, - defaultSelectedIndex, - setActiveIndex, - selectedIndex, - setSelectedIndex, - }; - - return {children}; -} - -Tabs.propTypes = { - /** - * Provide child elements to be rendered inside of the `Tabs`. - * These elements should render either `TabsList` or `TabsPanels` - */ - children: PropTypes.node, - - /** - * Specify which content tab should be initially selected when the component - * is first rendered - */ - defaultSelectedIndex: PropTypes.number, - - /** - * Provide an optional function which is called whenever the state of the - * `Tabs` changes - */ - onChange: PropTypes.func, - - /** - * Control which content panel is currently selected. This puts the component - * in a controlled mode and should be used along with `onChange` - */ - selectedIndex: PropTypes.number, -}; - -/** - * Get the next index for a given keyboard event given a count of the total - * items and the current index - * @param {Event} event - * @param {number} total - * @param {number} index - * @returns {number} - */ -function getNextIndex(event, total, index) { - if (match(event, keys.ArrowRight)) { - return (index + 1) % total; - } else if (match(event, keys.ArrowLeft)) { - return (total + index - 1) % total; - } else if (match(event, keys.Home)) { - return 0; - } else if (match(event, keys.End)) { - return total - 1; - } -} - -function TabList({ - activation = 'automatic', - 'aria-label': label, - children, - className: customClassName, - contained = false, - iconSize, - leftOverflowButtonProps, - light, - rightOverflowButtonProps, - scrollDebounceWait = 200, - scrollIntoView, - ...rest -}) { - const { activeIndex, selectedIndex, setSelectedIndex, setActiveIndex } = - React.useContext(TabsContext); - const prefix = usePrefix(); - const ref = useRef(null); - const previousButton = useRef(null); - const nextButton = useRef(null); - const [isScrollable, setIsScrollable] = useState(false); - const [scrollLeft, setScrollLeft] = useState(null); - const className = cx(`${prefix}--tabs`, customClassName, { - [`${prefix}--tabs--contained`]: contained, - [`${prefix}--tabs--light`]: light, - [`${prefix}--tabs__icon--default`]: iconSize === 'default', - [`${prefix}--tabs__icon--lg`]: iconSize === 'lg', - }); - - // Previous Button - // VISIBLE IF: - // SCROLLABLE - // AND SCROLL_LEFT > 0 - const buttonWidth = 44; - const isPreviousButtonVisible = ref.current - ? isScrollable && scrollLeft > 0 - : false; - // Next Button - // VISIBLE IF: - // SCROLLABLE - // AND SCROLL_LEFT + CLIENT_WIDTH < SCROLL_WIDTH - const isNextButtonVisible = ref.current - ? scrollLeft + buttonWidth + ref.current.clientWidth < - ref.current.scrollWidth - : false; - const previousButtonClasses = cx( - `${prefix}--tab--overflow-nav-button`, - `${prefix}--tab--overflow-nav-button--previous`, - { - [`${prefix}--tab--overflow-nav-button--hidden`]: !isPreviousButtonVisible, - } - ); - const nextButtonClasses = cx( - `${prefix}--tab--overflow-nav-button`, - `${prefix}--tab--overflow-nav-button--next`, - { - [`${prefix}--tab--overflow-nav-button--hidden`]: !isNextButtonVisible, - } - ); - - const tabs = useRef([]); - const debouncedOnScroll = useCallback(() => { - return debounce((event) => { - setScrollLeft(event.target.scrollLeft); - }, scrollDebounceWait); - }, [scrollDebounceWait]); - - function onKeyDown(event) { - if ( - matches(event, [keys.ArrowRight, keys.ArrowLeft, keys.Home, keys.End]) - ) { - event.preventDefault(); - - const activeTabs = tabs.current.filter((tab) => { - return !tab.disabled; - }); - - const currentIndex = activeTabs.indexOf( - tabs.current[activation === 'automatic' ? selectedIndex : activeIndex] - ); - const nextIndex = tabs.current.indexOf( - activeTabs[getNextIndex(event, activeTabs.length, currentIndex)] - ); - - if (activation === 'automatic') { - setSelectedIndex(nextIndex); - } else if (activation === 'manual') { - setActiveIndex(nextIndex); - } - - tabs.current[nextIndex].focus(); - } - } - - useEffectOnce(() => { - const tab = tabs.current[selectedIndex]; - if (scrollIntoView && tab) { - tab.scrollIntoView({ - block: 'nearest', - inline: 'nearest', - }); - } - }); - - useEffectOnce(() => { - if (tabs.current[selectedIndex].disabled) { - const activeTabs = tabs.current.filter((tab) => { - return !tab.disabled; - }); - - if (activeTabs.length > 0) { - const tab = activeTabs[0]; - setSelectedIndex(tabs.current.indexOf(tab)); - } - } - }); - - useIsomorphicEffect(() => { - if (ref.current) { - setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth); - } - - function handler() { - if (ref.current) { - setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth); - } - } - - const debouncedHandler = debounce(handler, 200); - window.addEventListener('resize', debouncedHandler); - return () => { - debouncedHandler.cancel(); - window.removeEventListener('resize', debouncedHandler); - }; - }, []); - - // updates scroll location for all scroll behavior. - useIsomorphicEffect(() => { - if (scrollLeft !== null) { - ref.current.scrollLeft = scrollLeft; - } - }, [scrollLeft]); - - useIsomorphicEffect(() => { - if (!isScrollable) { - return; - } - - const tab = - activation === 'manual' - ? tabs.current[activeIndex] - : tabs.current[selectedIndex]; - if (tab) { - // The width of the "scroll buttons" - - // The start and end position of the selected tab - const { width: tabWidth } = tab.getBoundingClientRect(); - const start = tab.offsetLeft; - const end = tab.offsetLeft + tabWidth; - - // The start and end of the visible area for the tabs - const visibleStart = ref.current.scrollLeft + buttonWidth; - const visibleEnd = - ref.current.scrollLeft + ref.current.clientWidth - buttonWidth; - - // The beginning of the tab is clipped and not visible - if (start < visibleStart) { - setScrollLeft(start - buttonWidth); - } - - // The end of teh tab is clipped and not visible - if (end > visibleEnd) { - setScrollLeft(end + buttonWidth - ref.current.clientWidth); - } - } - }, [activation, activeIndex, selectedIndex, isScrollable]); - - usePressable(previousButton, { - onPress({ longPress }) { - if (!longPress) { - setScrollLeft( - Math.max( - scrollLeft - (ref.current.scrollWidth / tabs.current.length) * 1.5, - 0 - ) - ); - } - }, - onLongPress() { - return createLongPressBehavior(ref, 'backward', setScrollLeft); - }, - }); - - usePressable(nextButton, { - onPress({ longPress }) { - if (!longPress) { - setScrollLeft( - Math.min( - scrollLeft + (ref.current.scrollWidth / tabs.current.length) * 1.5, - ref.current.scrollWidth - ref.current.clientWidth - ) - ); - } - }, - onLongPress() { - return createLongPressBehavior(ref, 'forward', setScrollLeft); - }, - }); - - return ( -
        - - {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */} -
        - {React.Children.map(children, (child, index) => { - return ( - - {React.cloneElement(child, { - ref: (node) => { - tabs.current[index] = node; - }, - })} - - ); - })} -
        - -
        - ); -} - -TabList.propTypes = { - /** - * Specify whether the content tab should be activated automatically or - * manually - */ - activation: PropTypes.oneOf(['automatic', 'manual']), - - /** - * Provide an accessible label to be read when a user interacts with this - * component - */ - 'aria-label': PropTypes.string.isRequired, - - /** - * Provide child elements to be rendered inside of `ContentTabs`. - * These elements should render a `ContentTab` - */ - children: PropTypes.node, - - /** - * Specify an optional className to be added to the container node - */ - className: PropTypes.string, - - /** - * Specify whether component is contained type - */ - contained: PropTypes.bool, - - /** - * If using `IconTab`, specify the size of the icon being used. - */ - iconSize: PropTypes.oneOf(['default', 'lg']), - - /** - * Provide the props that describe the left overflow button - */ - leftOverflowButtonProps: PropTypes.object, - - /** - * Specify whether or not to use the light component variant - */ - light: PropTypes.bool, - - /** - * Provide the props that describe the right overflow button - */ - rightOverflowButtonProps: PropTypes.object, - - /** - * Optionally provide a delay (in milliseconds) passed to the lodash - * debounce of the onScroll handler. This will impact the responsiveness - * of scroll arrow buttons rendering when scrolling to the first or last tab. - */ - scrollDebounceWait: PropTypes.number, - - /** - * Choose whether or not to automatically scroll to newly selected tabs - * on component rerender - */ - scrollIntoView: PropTypes.bool, -}; - -/** - * Helper function to setup the behavior when a button is "long pressed". This - * function will take a ref to the tablist, a direction, and a setter for - * scrollLeft and will update the scroll position within a - * requestAnimationFrame. - * - * It returns a cleanup function to be run when the long press is - * deactivated - * - * @param {RefObject} ref - * @param {'forward' | 'backward'} direction - * @param {Function} setScrollLeft - * @returns {Function} - */ -function createLongPressBehavior(ref, direction, setScrollLeft) { - // We manually override the scroll behavior to be "auto". If it is set as - // smooth, this animation does not update correctly - let defaultScrollBehavior = ref.current.style['scroll-behavior']; - ref.current.style['scroll-behavior'] = 'auto'; - - const scrollDelta = direction === 'forward' ? 5 : -5; - let frameId = null; - - function tick() { - ref.current.scrollLeft = ref.current.scrollLeft + scrollDelta; - frameId = requestAnimationFrame(tick); - } - - frameId = requestAnimationFrame(tick); - - return () => { - // Restore the previous scroll behavior - ref.current.style['scroll-behavior'] = defaultScrollBehavior; - - // Make sure that our `scrollLeft` value is in sync with the existing - // `ref` after our requestAnimationFrame loop above - setScrollLeft(ref.current.scrollLeft); - - if (frameId) { - cancelAnimationFrame(frameId); - } - }; -} - -const Tab = React.forwardRef(function Tab( - { - as: BaseComponent = 'button', - children, - className: customClassName, - disabled, - onClick, - onKeyDown, - ...rest - }, - ref -) { - const prefix = usePrefix(); - const { selectedIndex, setSelectedIndex, baseId } = - React.useContext(TabsContext); - const index = React.useContext(TabContext); - const id = `${baseId}-tab-${index}`; - const panelId = `${baseId}-tabpanel-${index}`; - const className = cx( - `${prefix}--tabs__nav-item`, - `${prefix}--tabs__nav-link`, - customClassName, - { - [`${prefix}--tabs__nav-item--selected`]: selectedIndex === index, - [`${prefix}--tabs__nav-item--disabled`]: disabled, - } - ); - - return ( - { - if (disabled) { - return; - } - setSelectedIndex(index); - if (onClick) { - onClick(evt); - } - }} - onKeyDown={onKeyDown} - tabIndex={selectedIndex === index ? '0' : '-1'} - type="button"> - {children} - - ); -}); - -Tab.propTypes = { - /** - * Provide a custom element to render instead of the default button - */ - as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - - /** - * Provide child elements to be rendered inside of `Tab`. - */ - children: PropTypes.node, - - /** - * Specify an optional className to be added to your Tab - */ - className: PropTypes.string, - - /** - * Whether your Tab is disabled. - */ - disabled: PropTypes.bool, - - /** - * Provide a handler that is invoked when a user clicks on the control - */ - onClick: PropTypes.func, - - /** - * Provide a handler that is invoked on the key down event for the control - */ - onKeyDown: PropTypes.func, - - /* - * An optional parameter to allow overriding the anchor rendering. - * Useful for using Tab along with react-router or other client - * side router libraries. - **/ - renderButton: PropTypes.func, -}; - -const IconTab = React.forwardRef(function IconTab( - { - children, - className: customClassName, - defaultOpen = false, - enterDelayMs, - leaveDelayMs, - label, - ...rest - }, - ref -) { - const prefix = usePrefix(); - - const classNames = cx(`${prefix}--tabs__nav-item--icon`, customClassName); - return ( - - - {children} - - - ); -}); - -IconTab.propTypes = { - /** - * Provide an icon to be rendered inside of `IconTab` as the visual label for Tab. - */ - children: PropTypes.node, - - /** - * Specify an optional className to be added to your Tab - */ - className: PropTypes.string, - - /** - * Specify whether the tooltip for the icon should be open when it first renders - */ - defaultOpen: PropTypes.bool, - - /** - * Specify the duration in milliseconds to delay before displaying the tooltip for the icon. - */ - enterDelayMs: PropTypes.number, - - /** - * Provide the label to be rendered inside of the Tooltip. The label will use - * `aria-labelledby` and will fully describe the child node that is provided. - * This means that if you have text in the child node it will not be - * announced to the screen reader. - */ - label: PropTypes.node.isRequired, - - /** - * Specify the duration in milliseconds to delay before hiding the tooltip - */ - leaveDelayMs: PropTypes.number, -}; - -const TabPanel = React.forwardRef(function TabPanel( - { children, className: customClassName, ...rest }, - forwardRef -) { - const prefix = usePrefix(); - const panel = useRef(null); - const ref = useMergedRefs([forwardRef, panel]); - - const [tabIndex, setTabIndex] = useState('0'); - const [interactiveContent, setInteractiveContent] = useState(false); - const { selectedIndex, baseId } = React.useContext(TabsContext); - const index = React.useContext(TabPanelContext); - const id = `${baseId}-tabpanel-${index}`; - const tabId = `${baseId}-tab-${index}`; - const className = cx(`${prefix}--tab-content`, customClassName, { - [`${prefix}--tab-content--interactive`]: interactiveContent, - }); - - useEffectOnce(() => { - if (!panel.current) { - return; - } - - const content = getInteractiveContent(panel.current); - if (content) { - setInteractiveContent(true); - setTabIndex('-1'); - } - }); - - // tabindex should only be 0 if no interactive content in children - useEffect(() => { - if (!panel.current) { - return; - } - - const { current: node } = panel; - - function callback() { - const content = getInteractiveContent(node); - if (content) { - setInteractiveContent(true); - setTabIndex('-1'); - } else { - setInteractiveContent(false); - setTabIndex('0'); - } - } - - const observer = new MutationObserver(callback); - - observer.observe(node, { - childList: true, - subtree: true, - }); - - return () => { - observer.disconnect(node); - }; - }, []); - - return ( - - ); -}); - -TabPanel.propTypes = { - /** - * Provide child elements to be rendered inside of `TabPanel`. - */ - children: PropTypes.node, - - /** - * Specify an optional className to be added to TabPanel. - */ - className: PropTypes.string, -}; - -function TabPanels({ children }) { - return React.Children.map(children, (child, index) => { - return ( - {child} - ); - }); -} - -TabPanels.propTypes = { - /** - * Provide child elements to be rendered inside of `TabPanels`. - */ - children: PropTypes.node, -}; - -export { Tabs, Tab, IconTab, TabPanel, TabPanels, TabList }; diff --git a/packages/react/src/components/Tabs/next/Tabs.mdx b/packages/react/src/components/Tabs/next/Tabs.mdx deleted file mode 100644 index eb55e496372b..000000000000 --- a/packages/react/src/components/Tabs/next/Tabs.mdx +++ /dev/null @@ -1,151 +0,0 @@ -import { Props, Preview, Story } from '@storybook/addon-docs'; -import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'; - -# Tabs - -[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Tabs) - |  -[Usage guidelines](https://www.carbondesignsystem.com/components/tabs/usage) - |  -[Accessibility](https://www.carbondesignsystem.com/components/tabs/accessibility) - -## Table of Contents - -- [Overview](#overview) - - [Line Tabs](#line-tabs) - - [Container Tabs](#container-tabs) -- [Component API](#component-api) - - [Tab `renderContent`](#tab-rendercontent) -- [Feedback](#feedback) - -## Overview - -Use tabs to allow users to navigate easily between views within the same -context. Tabs are now more composable, meaning that you have more flexibility in -what is in rendered inside of `Tab` and `TabPanel`. - -### Line Tabs - - - - - -### Contained Tabs - - - - - -### Icon Tabs - - - - - - - - - -## Component API - - - -### Tab - render content on click - -You will occasionally run into a situation where you only want Tab content to be -loaded when the Tab is clicked. In v11, to do this, you can this by setting -`activation` to `manual`: - -```jsx - - - Tab Label 1 - Tab Label 2 - Tab Label 3 - - - Tab Panel 1 - Tab Panel 2 - Tab Panel 3 - - -``` - -## V11 - -### Tabs composition - -Tabs got a big revamp in v11! Tabs are now more composable than ever before, -meaning that you have the flexibity and control on your end to make them look -and act how you want. The biggest difference is that the Tab label and the Tab -content are now separate components. - -Example of Tabs in v10: - -```js - - -

        Content for first tab goes here.

        -
        - -

        Content for second tab goes here.

        -
        - -

        Content for third tab goes here.

        -
        - -

        Content for fourth tab goes here.

        -
        -
        -``` - -Those same Tabs, now in v11: - -```js - - - Tab Label 1 - Tab Label 2 - Tab Label 3 - Tab Label 4 shows truncation - - - Content for first tab goes here. - Content for second tab goes here. - Content for third tab goes here. - Content for fourth tab goes here. - - -``` - -### Various updates - -All the same functionality for Tabs is available in v11 and more! Below are the -minor tweaks in naming or implementation. - -- the `type` prop is deprecated. Both "container" and "default" tabs still exist - but now can be called by adding the prop `contained` to the `TabList`. See the - above "Contained Tabs" for an example. -- Default tabs are now referred to as line tabs in our documentation here and on - our website. -- `hidden` prop is no longer needed with the new composable Tabs. You have full - control over tab content and when it's hidden through the `TabPanel` and - `TabPanels` components. -- `selected` prop is now named `selectedIndex`. -- `tabContentClassName` is no longer needed. `TabPanel` (equivalent to tab - content) takes in a className prop on its outermost node. -- For `Tab`, `label` is no longer needed. `children` of `Tab` are now the label. -- Due to its composability, `renderAnchor`, `renderButton`, `renderContent` are - no longer needed on `Tab`. -- `selected` on `Tab` is deprecated in favor or `selectedIndex`, now placed on - `Tabs` instead. -- Because `renderButton` is no longer needed, the associated `tabIndex` prop has - also been deprecated. - -## Feedback - -Help us improve this component by providing feedback, asking questions on Slack, -or updating this file on -[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/react/src/components/Tabs/Tabs.mdx). diff --git a/packages/react/src/components/Tabs/next/usePressable.js b/packages/react/src/components/Tabs/usePressable.js similarity index 100% rename from packages/react/src/components/Tabs/next/usePressable.js rename to packages/react/src/components/Tabs/usePressable.js diff --git a/packages/react/src/components/TextArea/TextArea-story.js b/packages/react/src/components/TextArea/TextArea-story.js deleted file mode 100644 index 47c6a057a6ff..000000000000 --- a/packages/react/src/components/TextArea/TextArea-story.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * 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 React from 'react'; -import { action } from '@storybook/addon-actions'; - -import { withKnobs, boolean, number, text } from '@storybook/addon-knobs'; -import TextArea from '../TextArea'; -import TextAreaSkeleton from '../TextArea/TextArea.Skeleton'; -import mdx from './TextArea.mdx'; -import { FeatureFlags } from '../FeatureFlags'; - -const TextAreaProps = () => ({ - className: 'some-class', - disabled: boolean('Disabled (disabled)', false), - light: boolean('Light variant (light)', false), - hideLabel: boolean('No label (hideLabel)', false), - labelText: text('Label text (labelText)', 'Text Area label'), - invalid: boolean('Show form validation UI (invalid)', false), - invalidText: text( - 'Content of form validation UI (invalidText)', - 'A valid value is required' - ), - helperText: text('Helper text (helperText)', 'Optional helper text.'), - enableCounter: boolean( - 'Enable character counter/limit (enableCounter)', - false - ), - maxCount: number('Character limit (maxCount)', undefined), - id: 'test2', - cols: number('Columns (columns)', 50), - rows: number('Rows (rows)', 4), - onChange: action('onChange'), - onClick: action('onClick'), -}); - -export default { - title: 'Components/TextArea', - component: TextArea, - decorators: [withKnobs], - subcomponents: { - TextAreaSkeleton, - }, - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const Default = () =>