diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 0614b8e597fe..4b3b24cb5375 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -7441,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/src/components/FormLabel/FormLabel-story.js b/packages/react/src/components/FormLabel/FormLabel.stories.js similarity index 95% rename from packages/react/src/components/FormLabel/FormLabel-story.js rename to packages/react/src/components/FormLabel/FormLabel.stories.js index 4f55d622fe20..62bfdfeae962 100644 --- a/packages/react/src/components/FormLabel/FormLabel-story.js +++ b/packages/react/src/components/FormLabel/FormLabel.stories.js @@ -52,7 +52,8 @@ export const WithTooltip = (controls) => { lowContrast inline className="notification" - aria-label="Accessiblity note on form labels"> + aria-label="Accessibility note on form labels" + actionButtonLabel="Accessibility button note on form labels">

Note:   It is not recommended to include interactive items, such as diff --git a/packages/react/src/components/Grid/next/FlexGrid.mdx b/packages/react/src/components/Grid/FlexGrid.mdx similarity index 100% rename from packages/react/src/components/Grid/next/FlexGrid.mdx rename to packages/react/src/components/Grid/FlexGrid.mdx diff --git a/packages/react/src/components/Grid/next/FlexGrid.stories.js b/packages/react/src/components/Grid/FlexGrid.stories.js similarity index 87% rename from packages/react/src/components/Grid/next/FlexGrid.stories.js rename to packages/react/src/components/Grid/FlexGrid.stories.js index 3050fedb41e6..f48d6035b3ec 100644 --- a/packages/react/src/components/Grid/next/FlexGrid.stories.js +++ b/packages/react/src/components/Grid/FlexGrid.stories.js @@ -1,6 +1,6 @@ import './FlexGrid.stories.scss'; import React from 'react'; -import { FlexGrid, Row, Column } from '../'; +import { FlexGrid, Row, Column } from './'; import mdx from './FlexGrid.mdx'; export default { @@ -16,6 +16,13 @@ export default { page: mdx, }, }, + argTypes: { + columns: { + table: { + disable: true, + }, + }, + }, }; function DemoContent({ children }) { @@ -289,3 +296,49 @@ export const MixedGridModes = () => ( ); + +export const Playground = (args) => ( + + + + 1/4 + + + 1/4 + + + 1/4 + + + 1/4 + + + +); + +Playground.argTypes = { + as: { + control: { + type: 'text', + }, + defaultValue: 'div', + }, + fullWidth: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, + narrow: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, + condensed: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, +}; diff --git a/packages/react/src/components/Grid/next/FlexGrid.stories.scss b/packages/react/src/components/Grid/FlexGrid.stories.scss similarity index 100% rename from packages/react/src/components/Grid/next/FlexGrid.stories.scss rename to packages/react/src/components/Grid/FlexGrid.stories.scss diff --git a/packages/react/src/components/Grid/Grid-story.js b/packages/react/src/components/Grid/Grid-story.js deleted file mode 100644 index c8da68f464f6..000000000000 --- a/packages/react/src/components/Grid/Grid-story.js +++ /dev/null @@ -1,337 +0,0 @@ -import './Grid-story.scss'; -import React from 'react'; -import { Grid, Row, Column } from './'; -import mdx from './Grid.mdx'; -import { FeatureFlags } from '../FeatureFlags'; -import { Heading } from '../Heading'; - -export default { - title: 'Components/Grid', - component: Grid, - subcomponents: { - Row, - Column, - }, - decorators: [(storyFn) =>

{storyFn()}
], - parameters: { - docs: { - page: mdx, - }, - }, -}; - -function DemoContent({ children }) { - return ( -
-
{children}
-
- ); -} - -export const ExperimentalCSSGrid = () => ( - - Wide - - - Column - - - Column - - - Column - - - Column - - - - Subgrid - - - Subgrid - - - - - Condensed - - - Column - - - Column - - - Column - - - Column - - - -); - -export const AutoColumns = () => ( - - - - Span 25% - - - Span 25% - - - Span 25% - - - Span 25% - - - -); - -export const ResponsiveGrid = () => ( - - - - -

Small: Span 2 of 4

-

Medium: Span 4 of 8

-

Large: Span 6 of 12

-
-
- - -

Small: Span 2 of 4

-

Medium: Span 2 of 8

-

Large: Span 3 of 12

-
-
- - -

Small: Span 0 of 4

-

Medium: Span 2 of 8

-

Large: Span 3 of 12

-
-
-
-
-); - -export const Offset = () => ( - - - - Small: offset 3 - - - Small: offset 2 - - - Small: offset 1 - - - Small: offset 0 - - - -); - -export const Condensed = () => ( - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - -); - -export const CondensedColumns = () => ( - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - -); - -export const Narrow = () => ( - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - -); - -export const NarrowColumns = () => ( - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - -); - -export const FullWidth = () => ( - - - - 1/4 - - - 1/4 - - - 1/4 - - - 1/4 - - - -); - -export const MixedGridModes = () => ( - - - - Wide - - - 1/4 - - - 1/4 - - - 1/4 - - - - - Narrow - - - 1/4 - - - 1/4 - - - 1/4 - - - - - Condensed - - - 1/4 - - - 1/4 - - - 1/4 - - - -); diff --git a/packages/react/src/components/Grid/Grid-story.scss b/packages/react/src/components/Grid/Grid-story.scss deleted file mode 100644 index 8baede7964fb..000000000000 --- a/packages/react/src/components/Grid/Grid-story.scss +++ /dev/null @@ -1,89 +0,0 @@ -@use '../../../../grid/scss/modules/css-grid'; - -@import '~carbon-components/scss/globals/scss/colors'; - -// base of project work area -#root > div:first-child > div:first-child { - width: 100%; -} - -// grid styles -.bx--grid .outside { - min-height: 80px; - height: 100%; -} - -.bx--grid .inside { - min-height: 80px; - height: 100%; -} - -// hack to enable zoom feature to trigger -.bx--grid--full-width { - max-width: 100%; -} - -.default .bx--col code { - display: flex; - justify-content: center; -} - -// template hard-coded styles -#templates .inside { - background-color: $blue-10; -} - -#templates .bx--grid [class*='col'] { - background-color: $blue-20; - outline: 1px dashed $blue-40; -} - -#templates .bx--grid--condensed, -#templates .bx--row--condensed { - background-color: $warm-gray-100; - color: $gray-10; -} - -#templates .bx--grid--condensed [class*='col'], -#templates .bx--row--condensed [class*='col'] { - background: none; - outline: none; -} - -#templates .bx--grid--condensed .outside, -#templates .bx--row--condensed .outside { - background-color: $gray-80; - outline: none; -} - -#templates .bx--grid--condensed .inside, -#templates .bx--row--condensed .inside { - background: none; -} - -#templates .bx--grid--narrow .inside, -#templates .bx--row--narrow .inside { - background-color: $teal-10; -} - -#templates .bx--grid--narrow [class*='col'], -#templates .bx--row--narrow [class*='col'] { - background-color: $teal-20; - outline: 1px dashed $teal-40; -} - -// css grid -#templates > .bx--css-grid { - border: 1px dashed black; -} - -#templates .bx--css-grid .bx--subgrid [class*='col'] { - background-color: rgba(#ffe056, 0.25); - outline: 1px solid #ffe056; -} - -#templates .bx--css-grid [class*='col'] { - background-color: #edf4ff; - outline: 1px solid #a6c8ff; - min-height: 80px; -} diff --git a/packages/react/src/components/Grid/Grid.mdx b/packages/react/src/components/Grid/Grid.mdx index 7502c150a446..fa7cf9d4defc 100644 --- a/packages/react/src/components/Grid/Grid.mdx +++ b/packages/react/src/components/Grid/Grid.mdx @@ -12,11 +12,12 @@ import { Story, Preview, Props } from '@storybook/addon-docs'; ## Table of Contents - [Overview](#overview) + - [Debugging](#debugging) - [Grid modes](#grid-modes) - [Wide grid](#wide-grid) - - [Narrow grid](#narrow-grid) - [Condensed grid](#condensed-grid) - - [Mix-and-match](#mix-and-match) +- [Subgrid](#subgrid) +- [Mixed grid modes](#mixed-grid-modes) - [Auto columns](#auto-columns) - [Offset columns](#offset-columns) - [Component API](#component-api) @@ -31,16 +32,15 @@ import { Story, Preview, Props } from '@storybook/addon-docs'; Carbon's grid components help developers use the [2x Grid](https://www.carbondesignsystem.com/guidelines/2x-grid/overview). The -project provides `Grid`, `Row`, and `Column` components which can be used to -build a variety of layouts. You can import these components from -`carbon-components-react`: +project provides `Grid` and `Column` components which can be used to build a +variety of layouts. You can import these components from `@carbon/react`: ```js -import { Grid, Row, Column } from 'carbon-components-react'; +import { Grid, Column } from '@carbon/react'; ``` - + ## Overview @@ -49,9 +49,9 @@ Every layout starts with the `Grid` component. You can specify a `Grid` at the top-level of your project, or at different depths provided that it can span 100% width of its container. -Next, you will use a combination of `Row` and `Column`. You can have multiple -`Row` components in a `Grid`, and multiple `Column` components in a `Row`. Each -`Row` will contain all the `Column` components provided to it, as long as they +Next, you will use a combination of `Column` and `Grid`. You can have multiple +`Column` components in a `Grid`, and nest `Grid` components in a `Column`. Each +`Grid` will contain all the `Column` components provided to it, as long as they don't span more columns than the total number of columns in the grid. To specify how many columns the `Column` component should span, you can use the @@ -63,26 +63,21 @@ number `4` to specify that each `Column` component should span 4 columns at that breakpoint. ```js -import { Grid, Row, Column } from 'carbon-components-react'; +import { Grid, Column } from '@carbon/react'; function MyComponent() { return ( - - Span 4 of 12 - Span 4 of 12 - Span 4 of 12 - Span 4 of 12 - + Span 4 of 16 + Span 4 of 16 + Span 4 of 16 + Span 4 of 16 ); } ``` -_Note: by default, `carbon-components` ships with a 12 column grid. You can -enable a 16 column grid, which will be the default grid in the next major -version, by using our -[feature flags in Sass](https://github.com/carbon-design-system/carbon/blob/main/docs/guides/sass.md#feature-flags)._ +_Note: by default, `@carbon/styles` ships with a 16 column grid._ You can pair up multiple breakpoint props to specify how many columns the `Column` component should span at different break points. In the example below, @@ -90,65 +85,85 @@ we will use the `sm`, `md`, and `lg` prop to specify how many columns the `Column` components should span at the small, medium, and large breakpoints. - + +### Debugging + +It is incredibly helpful when debugging CSS Grid to use the browser developer +tools' css grid mode to view the grid definition. Depending on the browser, these +typically provide a toggle for overlaying a schematic showing the column and +grid gap definition. With this overlay, it's much easier to visually understand +if grid modes are configured and set properly on the grid. + +Documentation on these features is available for +[Chrome](https://developer.chrome.com/docs/devtools/css/grid/), +[Firefox](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Examine_grid_layouts), +and +[Safari](https://webkit.org/blog/11588/introducing-css-grid-inspector/#:~:text=If%20you're%20using%20Safari,%23css%2Dgrid%2Ddemo%20.), +among others. + ## Grid modes There are several [grid modes](https://www.carbondesignsystem.com/guidelines/2x-grid/implementation/#grid-modes) that you can use depending on the layout effect you're looking to achieve. By default, `Grid` uses the wide grid mode with a 32px gutter. However, you can use -the `narrow` or `condensed` props to enable the narrow and condensed grid modes, -respectively. +the `condensed` prop to enable the condensed grid mode. -The same way that you can pass in `narrow` or `condensed` to the `Grid` -component, you can also pass in `narrow` or `condensed` to a `Row` component to -enable a certain grid mode but only for a row. This can be useful when you need -to mix-and-match certain grid modes to achieve a particular layout. - -### Wide grid +### Full width grid - + -### Narrow grid +### Condensed grid - + -### Condensed grid +## Subgrid + +`Grid` components can be nested within one another to achieve more advanced +layout configurations. When a `Grid` is a child of another `Grid`, the child +will always be automatically defined as a subgrid. Subgrids should always be +contained within a `Column` to ensure that the column amount/definition is +properly configured for the subgrid to inherit. Additionally, wrapping subgrids +in a `Column` enables you to define responsive parameters for the column (`sm`, +`md`, etc) that the subgrid will inherit and be bound to. - + -### Mix-and-match +## Mixed grid modes + +The same way that you can pass in `condensed` to a root `Grid`, you can also +pass in `condensed` to a nested `Grid` (subgrid) to enable a certain grid mode +for only that subgrid. This can be useful when you need to mix-and-match certain +grid modes to achieve a particular layout. - + ## Auto columns -In some situations, you may want your content to span a specific proportion of -the grid without having to calculate it across every breakpoint. A common -use-case for this is if you have a row of four cards and want each to span 25% -width each. +Each column by default spans one single column as defined by the parent grid's +parameters. -In these situations, you can make use of the auto columns feature of the -`Column` component. Auto columns is enabled when you do not provide any -breakpoint props, and it will automatically set each column to a percentage of -the total available width. +The default track sizing functions of the grid columns are defined by the parent +grid's `grid-template-columns` property. This declares that there should be +`--cds-grid-columns` number of columns, and each column should by default span a +`minmax()` of `0` columns minimum, or a maximum of `--cds-grid-column-size` +(`1fr`). -For example, if you have one `Column` component it would span 100%, two `Column` -components would span 50% each, four `Column` components would span 25% each, -and so on. +The values of these custom properties can be changed to modify the default +behavior of columns. - + ## Offset columns @@ -169,7 +184,7 @@ breakpoint. At the medium breakpoint, it will be offset by two columns. ``` - + ## Component API @@ -178,26 +193,24 @@ breakpoint. At the medium breakpoint, it will be offset by two columns. ### Using the `as` prop -By default, `Grid`, `Row`, and `Column` will render as a `div`. However, you can -use the `as` prop to change this to another HTML element, or a custom component -from your project. +By default, `Grid` and `Column` will render as a `div`. However, you can use the +`as` prop to change this to another HTML element, or a custom component from +your project. -In the example below, we use the `as` prop on `Row` to change it from a `div` to -a `section`. Similarly, we use the `as` prop on `Column` to change it from a +In the example below, we use the `as` prop on `Column` to change it from a `div` +to a `section`. Similarly, we use the `as` prop on `Column` to change it from a `div` to an `article`. ```jsx -import { Grid, Row, Column } from 'carbon-components-react'; +import { Grid, Column } from '@carbon/react'; function MyComponent() { return ( - - Example content - Example content - Example content - Example content - + Example content + Example content + Example content + Example content ); } @@ -207,7 +220,7 @@ You can also provide a custom component to the `as` prop. This custom component should accept all props passed to it, like a class name for the column. ```jsx -import { Grid, Row, Column } from 'carbon-components-react'; +import { Grid, Column } from '@carbon/react'; function Article({ children, ...rest }) { return
{children}
; @@ -220,12 +233,10 @@ function CustomColumn({ children, ...rest }) { function MyComponent() { return ( - - Example content - Example content - Example content - Example content - + Example content + Example content + Example content + Example content ); } @@ -247,13 +258,11 @@ at the small breakpoint. ### Can I nest grid components? -At the moment, there is no ability to nest grid components. In the future, we -may take advantage of CSS Grid and -[subgrid](https://caniuse.com/#feat=css-subgrid) alongside but at the moment we -are limited in terms of what we can support for nesting. - -If you would like to contribute nesting for the grid components available, we -would love for you to get in touch! +Yes! While the CSS Grid `subgrid` property is still +[not well supported](https://caniuse.com/#feat=css-subgrid), css custom +properties are used to enable nested grids with inherited column definitions. +View the subgrid story documentation for more information on how this works and +how to use it. ## Feedback diff --git a/packages/react/src/components/Grid/next/Grid.stories.js b/packages/react/src/components/Grid/Grid.stories.js similarity index 91% rename from packages/react/src/components/Grid/next/Grid.stories.js rename to packages/react/src/components/Grid/Grid.stories.js index ae2afc29d7a3..8d549699edc7 100644 --- a/packages/react/src/components/Grid/next/Grid.stories.js +++ b/packages/react/src/components/Grid/Grid.stories.js @@ -8,7 +8,7 @@ import './Grid.stories.scss'; import React from 'react'; -import { Grid, Column, ColumnHang } from '../../Grid'; +import { Grid, Column, ColumnHang } from '../Grid'; import mdx from './Grid.mdx'; export default { @@ -272,3 +272,42 @@ export const Offset = () => ( /> ); + +export const Playground = (args) => ( + + + + + + +); + +Playground.argTypes = { + as: { + control: { + type: 'text', + }, + defaultValue: 'div', + }, + fullWidth: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, + narrow: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, + condensed: { + control: { + type: 'boolean', + }, + defaultValue: false, + }, + columns: { + control: { type: 'number' }, + }, +}; diff --git a/packages/react/src/components/Grid/next/Grid.stories.scss b/packages/react/src/components/Grid/Grid.stories.scss similarity index 100% rename from packages/react/src/components/Grid/next/Grid.stories.scss rename to packages/react/src/components/Grid/Grid.stories.scss diff --git a/packages/react/src/components/Grid/next/Grid.mdx b/packages/react/src/components/Grid/next/Grid.mdx deleted file mode 100644 index 6ef149363138..000000000000 --- a/packages/react/src/components/Grid/next/Grid.mdx +++ /dev/null @@ -1,271 +0,0 @@ -import { Story, Preview, Props } from '@storybook/addon-docs'; - -# Grid - -[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Grid) - |  -[Usage guidelines](https://www.carbondesignsystem.com/guidelines/2x-grid/overview) - - - - -## Table of Contents - -- [Overview](#overview) - - [Debugging](#debugging) -- [Grid modes](#grid-modes) - - [Wide grid](#wide-grid) - - [Condensed grid](#condensed-grid) -- [Subgrid](#subgrid) -- [Mixed grid modes](#mixed-grid-modes) -- [Auto columns](#auto-columns) -- [Offset columns](#offset-columns) -- [Component API](#component-api) - - [Using the `as` prop](#using-the-as-prop) -- [FAQ](#faq) - - [How can I hide columns at a certain breakpoint?](#how-can-i-hide-columns-at-a-certain-breakpoint) - - [Can I nest grid components?](#can-i-nest-grid-components) -- [Feedback](#feedback) - - - - -Carbon's grid components help developers use the -[2x Grid](https://www.carbondesignsystem.com/guidelines/2x-grid/overview). The -project provides `Grid` and `Column` components which can be used to build a -variety of layouts. You can import these components from `@carbon/react`: - -```js -import { Grid, Column } from '@carbon/react'; -``` - - - - - -## Overview - -Every layout starts with the `Grid` component. You can specify a `Grid` at the -top-level of your project, or at different depths provided that it can span 100% -width of its container. - -Next, you will use a combination of `Column` and `Grid`. You can have multiple -`Column` components in a `Grid`, and nest `Grid` components in a `Column`. Each -`Grid` will contain all the `Column` components provided to it, as long as they -don't span more columns than the total number of columns in the grid. - -To specify how many columns the `Column` component should span, you can use the -`sm`, `md`, `lg`, `xlg`, or `max` props. These props are shorthand versions of -the names given to each of the breakpoints defined by the -[2x Grid](https://www.carbondesignsystem.com/guidelines/2x-grid/implementation/#responsive-options). -In the example below, we will use the `lg` prop for the large breakpoint and the -number `4` to specify that each `Column` component should span 4 columns at that -breakpoint. - -```js -import { Grid, Column } from '@carbon/react'; - -function MyComponent() { - return ( - - Span 4 of 16 - Span 4 of 16 - Span 4 of 16 - Span 4 of 16 - - ); -} -``` - -_Note: by default, `@carbon/styles` ships with a 16 column grid._ - -You can pair up multiple breakpoint props to specify how many columns the -`Column` component should span at different break points. In the example below, -we will use the `sm`, `md`, and `lg` prop to specify how many columns the -`Column` components should span at the small, medium, and large breakpoints. - - - - - -### Debugging - -It is incredibly helpful when debugging CSS Grid to use the browser developer -tools css grid mode to view the grid definition. Depending on the browser these -typically provide a toggle for overlaying a schematic showing the column and -grid gap definition. With this overlay it's much easier to visually understand -if grid modes are configured and set properly on the grid. - -Documentation on these features is available for -[Chrome](https://developer.chrome.com/docs/devtools/css/grid/), -[Firefox](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Examine_grid_layouts), -and -[Safari](https://webkit.org/blog/11588/introducing-css-grid-inspector/#:~:text=If%20you're%20using%20Safari,%23css%2Dgrid%2Ddemo%20.), -among others. - -## Grid modes - -There are several -[grid modes](https://www.carbondesignsystem.com/guidelines/2x-grid/implementation/#grid-modes) -that you can use depending on the layout effect you're looking to achieve. By -default, `Grid` uses the wide grid mode with a 32px gutter. However, you can use -the `condensed` prop to enable the condensed grid mode. - -### Full width grid - - - - - -### Condensed grid - - - - - -## Subgrid - -`Grid` components can be nested within one another to achieve more advanced -layout configurations. When a `Grid` is a child of another `Grid`, the child -will always be automatically defined as a subgrid. Subgrids should always be -contained within a `Column` to ensure that the column amount/definition is -properly configured for the subgrid to inherit. Additionally, wrapping subgrids -in a `Column` enables you to define responsive parameters for the column (`sm`, -`md`, etc) that the subgrid will inherit and be bound to. - - - - - -## Mixed grid modes - -The same way that you can pass in `condensed` to a root `Grid`, you can also -pass in `condensed` to a nested `Grid` (subgrid) to enable a certain grid mode -for only that subgrid. This can be useful when you need to mix-and-match certain -grid modes to achieve a particular layout. - - - - - -## Auto columns - -Each column by default spans one single column as defined by the parent grid's -parameters. - -The default track sizing functions of the grid columns are defined by the parent -grid's `grid-template-columns` property. This declares that there should be -`--cds-grid-columns` number of columns, and each column should by default span a -`minmax()` of `0` columns minimum, or a maximum of `--cds-grid-column-size` -(`1fr`). - -The values of these custom properties can be changed to modify the default -behavior of columns. - - - - - -## Offset columns - -You can offset your `Column` components by a specific amount of columns using -the object form for each breakpoint prop. This specific prop type allows you to -pass in an object to each breakpoint prop and this object has two keys, `span` -and `offset`, which allow you to specify the total numbers of columns the -`Column` component spans, and how many columns to offset it by. - -You can specify either prop in this object form, and can mix-and-match across -breakpoints. For example, the following snippet will have the `Column` component -span two columns at the small breakpoint and then four columns at the medium -breakpoint. At the medium breakpoint, it will be offset by two columns. - -```jsx - -``` - - - - - -## Component API - - - -### Using the `as` prop - -By default, `Grid` and `Column` will render as a `div`. However, you can use the -`as` prop to change this to another HTML element, or a custom component from -your project. - -In the example below, we use the `as` prop on `Column` to change it from a `div` -to a `section`. Similarly, we use the `as` prop on `Column` to change it from a -`div` to an `article`. - -```jsx -import { Grid, Column } from '@carbon/react'; - -function MyComponent() { - return ( - - Example content - Example content - Example content - Example content - - ); -} -``` - -You can also provide a custom component to the `as` prop. This custom component -should accept all props passed to it, like a class name for the column. - -```jsx -import { Grid, Column } from '@carbon/react'; - -function Article({ children, ...rest }) { - return
{children}
; -} - -function CustomColumn({ children, ...rest }) { - return {children} - Example content - Example content
- Example content - Example content - - ); -} -``` - -## FAQ - -### How can I hide columns at a certain breakpoint? - -To hide a column at a specific breakpoint, you should specify 0 for the span of -the column at that particular breakpoint. For example, you can use the following -two forms for specifying column span and pass in 0 to either to hide the column -at the small breakpoint. - -```jsx - - -``` - -### Can I nest grid components? - -Yes! While the CSS Grid `subgrid` property is still -[not well supported](https://caniuse.com/#feat=css-subgrid), css custom -properties are used to enable nested grids with inherited column definitions. -View the subgrid story documentation for more information on how this works and -how to use it. - -## 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/Grid/Grid.mdx). diff --git a/packages/react/src/components/Tab/index.js b/packages/react/src/components/Tab/index.js index 6e0e9f938ad1..e5bb3be0d422 100644 --- a/packages/react/src/components/Tab/index.js +++ b/packages/react/src/components/Tab/index.js @@ -6,7 +6,7 @@ */ import * as FeatureFlags from '@carbon/feature-flags'; -import { Tab as TabNext } from '../Tabs/next/Tabs'; +import { Tab as TabNext } from '../Tabs/Tabs'; import { default as TabClassic } from './Tab'; const Tab = FeatureFlags.enabled('enable-v11-release') ? TabNext : TabClassic; diff --git a/packages/react/src/components/Tabs/Tabs-story.js b/packages/react/src/components/Tabs/Tabs-story.js deleted file mode 100644 index 056ff21854f3..000000000000 --- a/packages/react/src/components/Tabs/Tabs-story.js +++ /dev/null @@ -1,242 +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, - select, - text, -} from '@storybook/addon-knobs'; -import classNames from 'classnames'; -import './Tabs-story.scss'; -import CodeSnippet from '../CodeSnippet'; -import Button from '../Button'; -import Tabs from './Tabs'; -import Tab from '../Tab'; -import TabsSkeleton from '../Tabs/Tabs.Skeleton'; -import mdx from './Tabs.mdx'; - -const selectionModes = { - 'Change selection automatically upon focus (automatic)': 'automatic', - 'Change selection on explicit gesture (manual)': 'manual', -}; - -const types = { - Default: 'default', - Container: 'container', -}; - -const prefix = 'cds'; -const props = { - tabs: () => ({ - type: select('Type of Tabs (type)', types, 'default'), - className: 'some-class', - light: boolean('Light variant (light)', false), - selected: number('The index of the selected tab (selected in )', 1), - onSelectionChange: action('onSelectionChange'), - tabContentClassName: text( - 'The className for the child `` components', - 'tab-content' - ), - scrollIntoView: boolean( - 'Scroll to selected tab on component rerender (scrollIntoView)', - true - ), - selectionMode: select( - 'Selection mode (selectionMode)', - selectionModes, - 'automatic' - ), - }), - tab: () => ({ - disabled: boolean('Disabled (disabled in )', false), - onClick: action('onClick'), - onKeyDown: action('onKeyDown'), - }), -}; - -const CustomLabel = ({ text }) => text; - -const CodeSnippetExample = () => ( - - {`@mixin grid-container { - width: 100%; - padding-right: padding(mobile); - padding-left: padding(mobile); - @include breakpoint(bp--xs--major) { - padding-right: padding(xs); - padding-left: padding(xs); - } -} -$z-indexes: ( - modal : 9000, - overlay : 8000, - dropdown : 7000, - header : 6000, - footer : 5000, - hidden : - 1, - overflowHidden: - 1, - floating: 10000 -);`} - -); - -const TabContentRenderedOnlyWhenSelected = ({ - selected, - children, - className, - ...other -}) => - !selected ? ( -
- ) : ( -
- {children} -
- ); - -export default { - title: 'Components/Tabs', - component: Tabs, - decorators: [withKnobs], - subcomponents: { - Tab, - TabsSkeleton, - }, - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const Default = () => ( - - -

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.

-
-
-); - -export const Playground = () => ( -
- - -
-

Content for first tab goes here.

-
-
- -
-

Content for second tab goes here.

-
-
- -
-

Content for third tab goes here.

-
-
- -
-

Content for fourth tab goes here.

-

- This example uses the  - renderContent prop to - re-render content when the tab is selected. -

- -
-
- }> -
-

Content for fifth tab goes here.

-
-
-
-
-); - -export const Container = () => ( - - -

Content for first tab goes here.

-
- -

Content for second tab goes here.

-
- -

Content for third tab goes here.

-
- Custom Label}> -

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 = () =>