-
-
-
-
-);
-
-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 ? (
-
- ) : (
-
{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 (
+
+ );
+}
- 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 (
-
- {children}
-
- );
+ 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 (
+
+ {children}
+
+ );
+});
+
+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 1Tab Label 2Tab 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 1Tab Label 2Tab 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 1Tab Label 2Tab Label 3
@@ -177,10 +186,65 @@ export const Contained = (args) => (
);
-export const Skeleton = (args) => {
+export const Skeleton = () => {
return (
-
- );
-}
-
-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 (
-
- {children}
-
- );
-});
-
-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 = () => ;
-
-export const Skeleton = () => ;
-
-export const ClassNameChangeTest = () => (
- <>
-
-
-
-
-
- >
-);
diff --git a/packages/react/src/components/TextArea/TextArea.stories.js b/packages/react/src/components/TextArea/TextArea.stories.js
new file mode 100644
index 000000000000..5a89e26b4738
--- /dev/null
+++ b/packages/react/src/components/TextArea/TextArea.stories.js
@@ -0,0 +1,150 @@
+/**
+ * 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 { default as TextArea, TextAreaSkeleton } from './';
+import { Layer } from '../Layer';
+
+export default {
+ title: 'Components/TextArea',
+ component: TextArea,
+ subcomponents: {
+ TextAreaSkeleton,
+ },
+};
+
+export const Default = () => (
+
+);
+
+export const WithLayer = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export const Skeleton = () => ;
+
+export const Playground = (args) => ;
+
+Playground.argTypes = {
+ className: {
+ control: {
+ type: 'text',
+ },
+ },
+ cols: {
+ control: {
+ type: 'number',
+ },
+ defaultValue: 50,
+ },
+ defaultValue: {
+ control: {
+ type: 'text',
+ },
+ },
+ value: {
+ control: {
+ type: 'text',
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ enableCounter: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ helperText: {
+ control: {
+ type: 'text',
+ },
+ },
+ hideLabel: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ },
+ invalid: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ invalidText: {
+ control: {
+ type: 'text',
+ },
+ },
+ labelText: {
+ control: {
+ type: 'text',
+ },
+ },
+ light: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+ maxCount: {
+ control: {
+ type: 'number',
+ },
+ },
+};
+
+Playground.args = {
+ enableCounter: true,
+ helperText: 'TextArea helper text',
+ labelText: 'TextArea label',
+ maxCount: 500,
+};
diff --git a/packages/react/src/components/TextArea/next/TextArea.stories.js b/packages/react/src/components/TextArea/next/TextArea.stories.js
deleted file mode 100644
index a8b2b0d34c5a..000000000000
--- a/packages/react/src/components/TextArea/next/TextArea.stories.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 { default as TextArea, TextAreaSkeleton } from '../';
-import { Layer } from '../../Layer';
-
-export default {
- title: 'Components/TextArea',
- component: TextArea,
- subcomponents: {
- TextAreaSkeleton,
- },
- argTypes: {
- light: {
- table: {
- disable: true,
- },
- },
- },
-};
-
-export const Default = () => (
-
-);
-
-export const WithLayer = () => {
- return (
- <>
-
-
-
-
-
-
-
- >
- );
-};
-
-export const Skeleton = () => ;
-
-export const Playground = (args) => (
-
-);
diff --git a/packages/react/src/components/UnorderedList/UnorderedList-story.js b/packages/react/src/components/UnorderedList/UnorderedList.stories.js
similarity index 81%
rename from packages/react/src/components/UnorderedList/UnorderedList-story.js
rename to packages/react/src/components/UnorderedList/UnorderedList.stories.js
index d62a10761df5..77f289b6b9d4 100644
--- a/packages/react/src/components/UnorderedList/UnorderedList-story.js
+++ b/packages/react/src/components/UnorderedList/UnorderedList.stories.js
@@ -61,3 +61,20 @@ export const Nested = () => {
};
Nested.storyName = 'nested';
+
+export const Playground = (args) => (
+
+ Unordered List level 1
+ Unordered List level 1
+ Unordered List level 1
+
+);
+
+Playground.argTypes = {
+ isExpressive: {
+ control: {
+ type: 'boolean',
+ },
+ defaultValue: false,
+ },
+};