diff --git a/packages/ods-react/jest.config.ts b/packages/ods-react/jest.config.ts new file mode 100644 index 0000000000..00249b0b60 --- /dev/null +++ b/packages/ods-react/jest.config.ts @@ -0,0 +1,13 @@ +export default { + collectCoverage: false, + testPathIgnorePatterns: [ + 'components', + 'node_modules/', + 'dist/', + ], + testRegex: 'tests\\/.*\\.spec\\.(ts|tsx)$', + transform: { + '\\.(ts|tsx)$': 'ts-jest', + }, + verbose: true, +}; diff --git a/packages/ods-react/package.json b/packages/ods-react/package.json index 7ead346c76..8293da6fe5 100644 --- a/packages/ods-react/package.json +++ b/packages/ods-react/package.json @@ -28,7 +28,9 @@ "build:prod": "rimraf dist && tsc -b && vite build", "clean": "rimraf dist", "lint:scss": "stylelint 'src/style/*.scss'", - "lint:ts": "eslint '{src,tests}/!(components)/**/*.{ts,tsx}'" + "lint:ts": "eslint '{src,tests}/!(components)/**/*.{ts,tsx}'", + "test:spec": "jest --passWithNoTests", + "test:spec:ci": "npm run test:spec" }, "dependencies": { "@ark-ui/react": "5.12.0", diff --git a/packages/ods-react/src/components/tabs/src/components/tab-content/TabContent.tsx b/packages/ods-react/src/components/tabs/src/components/tab-content/TabContent.tsx index e396099f05..be4e0f04b0 100644 --- a/packages/ods-react/src/components/tabs/src/components/tab-content/TabContent.tsx +++ b/packages/ods-react/src/components/tabs/src/components/tab-content/TabContent.tsx @@ -1,6 +1,6 @@ import { Tabs } from '@ark-ui/react/tabs'; import classNames from 'classnames'; -import { type ComponentPropsWithRef, type FC, type JSX } from 'react'; +import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react'; import style from './tabContent.module.scss'; interface TabContentProp extends ComponentPropsWithRef<'div'> { @@ -10,22 +10,23 @@ interface TabContentProp extends ComponentPropsWithRef<'div'> { value: string, } -const TabContent: FC = ({ +const TabContent: FC = forwardRef(({ children, className, value, ...props -}): JSX.Element => { +}, ref): JSX.Element => { return ( { children } ); -}; +}); export { TabContent, diff --git a/packages/ods-react/src/components/tabs/src/components/tab-list/TabList.tsx b/packages/ods-react/src/components/tabs/src/components/tab-list/TabList.tsx index 01368a4ddf..3825ae029d 100644 --- a/packages/ods-react/src/components/tabs/src/components/tab-list/TabList.tsx +++ b/packages/ods-react/src/components/tabs/src/components/tab-list/TabList.tsx @@ -1,24 +1,122 @@ import { Tabs } from '@ark-ui/react/tabs'; import classNames from 'classnames'; -import { type ComponentPropsWithRef, type FC, type JSX } from 'react'; +import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { debounce } from '../../../../../utils/debounce'; +import { BUTTON_SIZE, BUTTON_VARIANT, Button } from '../../../../button/src'; +import { ICON_NAME, Icon } from '../../../../icon/src'; +import { useTabs } from '../../contexts/useTabs'; import style from './tabList.module.scss'; interface TabListProp extends ComponentPropsWithRef<'div'> {} -const TabList: FC = ({ +const TabList: FC = forwardRef(({ children, className, ...props -}): JSX.Element => { +}, ref): JSX.Element => { + const { withArrows } = useTabs(); + const [isLeftButtonDisabled, setIsLeftButtonDisabled] = useState(false); + const [isRightButtonDisabled, setIsRightButtonDisabled] = useState(false); + const scrollRef = useRef(null); + + const updateScrollButtonState = useCallback(() => { + setIsLeftButtonDisabled(scrollRef.current ? scrollRef.current.scrollLeft === 0 : false); + setIsRightButtonDisabled(scrollRef.current ? scrollRef.current.scrollLeft === scrollRef.current.scrollWidth - scrollRef.current.offsetWidth : false); + }, []); + + const debouncedUpdateScrollButtonState = useMemo(() => { + return debounce(updateScrollButtonState, 50); + }, [updateScrollButtonState]); + + useEffect(() => { + updateScrollButtonState(); + + if (scrollRef.current) { + const observer = new ResizeObserver(() => { + updateScrollButtonState(); + }); + + observer.observe(scrollRef.current); + + return () => { + observer.disconnect(); + }; + } + }, [updateScrollButtonState]); + + function scroll(left: number): void { + scrollRef.current?.scrollBy({ + behavior: 'smooth', + left, + }); + } + + function onLeftScrollClick(): void { + if (!scrollRef.current) { + return; + } + scroll(-scrollRef.current.offsetWidth); + } + + function onRightScrollClick(): void { + if (!scrollRef.current) { + return; + } + scroll(scrollRef.current.offsetWidth); + } + return ( - { children } + { + withArrows && +
+ +
+ } + +
+
+ { children } +
+
+ + { + withArrows && +
+ +
+ }
); -}; +}); export { TabList, diff --git a/packages/ods-react/src/components/tabs/src/components/tab-list/tabList.module.scss b/packages/ods-react/src/components/tabs/src/components/tab-list/tabList.module.scss index a7bca73a53..eabf9bdf0e 100644 --- a/packages/ods-react/src/components/tabs/src/components/tab-list/tabList.module.scss +++ b/packages/ods-react/src/components/tabs/src/components/tab-list/tabList.module.scss @@ -1,9 +1,73 @@ +@use '../../../../../style/button'; +@use '../../../../../style/scroll'; + $tab-list-border-bottom-size: 2px; @layer ods-molecules { .tab-list { - display: inline-flex; - border-bottom: solid $tab-list-border-bottom-size var(--ods-color-neutral-100); - min-width: 100%; + display: flex; + position: relative; + align-items: baseline; + + &__left-arrow, + &__right-arrow { + display: flex; + position: relative; + align-items: center; + align-self: stretch; + height: auto; + + &::after { + position: absolute; + top: 0; + bottom: 0; + transition: opacity ease .3s; + opacity: 0; + z-index: 1; + width: 60px; + height: 100%; + content: ''; + pointer-events: none; + } + } + + &__left-arrow { + &::after { + background: linear-gradient(270deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%); + } + + &--active { + &::after { + left: button.$ods-button-size-xs; + opacity: 1; + } + } + } + + &__right-arrow { + &::after { + background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%); + } + + &--active { + &::after { + right: button.$ods-button-size-xs; + opacity: 1; + } + } + } + + &__container { + @include scroll.hide-scrollbar(); + + display: inline-block; + position: relative; + overflow: auto hidden; + white-space: nowrap; + + &__tabs { + display: flex; + } + } } } diff --git a/packages/ods-react/src/components/tabs/src/components/tab/Tab.tsx b/packages/ods-react/src/components/tabs/src/components/tab/Tab.tsx index 6208cc896e..ac8c4fc1d3 100644 --- a/packages/ods-react/src/components/tabs/src/components/tab/Tab.tsx +++ b/packages/ods-react/src/components/tabs/src/components/tab/Tab.tsx @@ -1,6 +1,6 @@ -import { Tabs } from '@ark-ui/react/tabs'; +import { Tabs, useTabsContext } from '@ark-ui/react/tabs'; import classNames from 'classnames'; -import { type ComponentPropsWithRef, type FC, type JSX } from 'react'; +import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import style from './tab.module.scss'; interface TabProp extends ComponentPropsWithRef<'button'> { @@ -10,22 +10,37 @@ interface TabProp extends ComponentPropsWithRef<'button'> { value: string, } -const Tab: FC = ({ +const Tab: FC = forwardRef(({ children, className, value, ...props -}): JSX.Element => { +}, ref): JSX.Element => { + const { focusedValue } = useTabsContext(); + const innerRef = useRef(null); + + useImperativeHandle(ref, () => innerRef.current!, [innerRef]); + + useEffect(() => { + if (innerRef.current && focusedValue === value) { + innerRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + } + }, [focusedValue, value]); + return ( { children } ); -}; +}); export { Tab, diff --git a/packages/ods-react/src/components/tabs/src/components/tab/tab.module.scss b/packages/ods-react/src/components/tabs/src/components/tab/tab.module.scss index ffdea497e3..6e21ba8fb6 100644 --- a/packages/ods-react/src/components/tabs/src/components/tab/tab.module.scss +++ b/packages/ods-react/src/components/tabs/src/components/tab/tab.module.scss @@ -5,14 +5,12 @@ @layer ods-molecules { .tab { display: block; - margin-bottom: -#{tab-list.$tab-list-border-bottom-size}; border-top: none; border-right: none; - border-bottom: solid tab-list.$tab-list-border-bottom-size transparent; + border-bottom: solid tab-list.$tab-list-border-bottom-size var(--ods-color-neutral-100); border-left: none; border-top-left-radius: var(--ods-border-radius-md); border-top-right-radius: var(--ods-border-radius-md); - border-color: transparent; background: none; cursor: pointer; padding: 2px 16px; @@ -27,11 +25,12 @@ @include focus.ods-focus(); z-index: 1; - outline-offset: 0; + outline-offset: calc(var(--ods-outline-offset) * -1); } &:focus-visible:not([disabled], [aria-selected="true"]), &:hover:not([disabled], [aria-selected="true"]) { + border-color: var(--ods-color-neutral-050); background-color: var(--ods-color-neutral-050); } diff --git a/packages/ods-react/src/components/tabs/src/components/tabs/Tabs.tsx b/packages/ods-react/src/components/tabs/src/components/tabs/Tabs.tsx index 9477a2aa07..296817166b 100644 --- a/packages/ods-react/src/components/tabs/src/components/tabs/Tabs.tsx +++ b/packages/ods-react/src/components/tabs/src/components/tabs/Tabs.tsx @@ -1,26 +1,11 @@ import { Tabs as VendorTabs } from '@ark-ui/react/tabs'; -import classNames from 'classnames'; -import { type ComponentPropsWithRef, type FC, type JSX, type Ref, forwardRef } from 'react'; -import style from './tabs.module.scss'; +import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react'; +import { TabsProvider, type TabsRootProp } from '../../contexts/useTabs'; -interface TabsValueChangeEvent { - value: string; -} - -interface TabsProp extends ComponentPropsWithRef<'div'> { - /** - * The initial value of the selected tab. Use when you don't need to control the value of the tabs. - */ - defaultValue?: string; - /** - * Callback fired when the state of selected tab changes. - */ - onValueChange?: (event: TabsValueChangeEvent) => void; - /** - * The controlled value of the selected tab. - */ - value?: string; -} +/** + * @inheritDoc TabsRootProp + */ +interface TabsProp extends Omit, 'defaultValue'>, TabsRootProp {} const Tabs: FC = forwardRef(({ children, @@ -28,20 +13,22 @@ const Tabs: FC = forwardRef(({ defaultValue, onValueChange, value, + withArrows, ...props -}, ref: Ref): JSX.Element => { - +}, ref): JSX.Element => { return ( - - { children } - + + + { children } + + ); }); @@ -50,5 +37,4 @@ Tabs.displayName = 'Tabs'; export { Tabs, type TabsProp, - type TabsValueChangeEvent, }; diff --git a/packages/ods-react/src/components/tabs/src/components/tabs/tabs.module.scss b/packages/ods-react/src/components/tabs/src/components/tabs/tabs.module.scss deleted file mode 100644 index 4969599a38..0000000000 --- a/packages/ods-react/src/components/tabs/src/components/tabs/tabs.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -@use '../../../../../style/scroll'; - -@layer ods-molecules { - .tabs { - @include scroll.hide-scrollbar(); - - display: block; - padding: var(--ods-outline-width); - width: 100%; - overflow-x: auto; - } -} diff --git a/packages/ods-react/src/components/tabs/src/contexts/useTabs.tsx b/packages/ods-react/src/components/tabs/src/contexts/useTabs.tsx new file mode 100644 index 0000000000..231a9da57f --- /dev/null +++ b/packages/ods-react/src/components/tabs/src/contexts/useTabs.tsx @@ -0,0 +1,51 @@ +import { type JSX, type ReactNode, createContext, useContext } from 'react'; + +interface TabsValueChangeEvent { + value: string, +} + +interface TabsRootProp { + /** + * The initial value of the selected tab. Use when you don't need to control the value of the tabs. + */ + defaultValue?: string, + /** + * Callback fired when the state of selected tab changes. + */ + onValueChange?: (event: TabsValueChangeEvent) => void, + /** + * The controlled value of the selected tab. + */ + value?: string, + /** + * Whether the component displays navigation arrows around the tabs. + */ + withArrows?: boolean, +} + +type TabsContextType = Pick; + +interface TabsProviderProp extends TabsContextType { + children: ReactNode; +} + +const TabsContext = createContext({}); + +function TabsProvider({ children, withArrows }: TabsProviderProp): JSX.Element { + return ( + + { children } + + ); +} + +function useTabs(): TabsContextType { + return useContext(TabsContext); +} + +export { + TabsProvider, + type TabsRootProp, + type TabsValueChangeEvent, + useTabs, +}; diff --git a/packages/ods-react/src/components/tabs/src/dev.stories.tsx b/packages/ods-react/src/components/tabs/src/dev.stories.tsx index 7dbd097500..284177e3c6 100644 --- a/packages/ods-react/src/components/tabs/src/dev.stories.tsx +++ b/packages/ods-react/src/components/tabs/src/dev.stories.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Tab, TabContent, TabList, Tabs } from '.'; import style from './dev.module.css'; @@ -95,3 +96,117 @@ export const CustomStyle = () => ( ); + +export const WithArrows = () => ( + <> +

No container size

+ + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 + Tab 12 + Tab 13 + Tab 14 + Tab 15 + + + +

In limited container

+ + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 + Tab 12 + Tab 13 + Tab 14 + Tab 15 + + + +

No overflow

+ + + Tab 1 + Tab 2 + Tab 3 + + + +

Long tab text

+ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Tab 2 + ed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. + + + +); + +export const WithArrowsDynamicResize = () => { + const [width, setWidth] = useState(400); + + function updateWidth() { + setWidth(width === 400 ? 200: 400); + } + + return ( + <> + + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 + Tab 12 + Tab 13 + Tab 14 + Tab 15 + + + + + + ); +}; diff --git a/packages/ods-react/src/components/tabs/src/index.ts b/packages/ods-react/src/components/tabs/src/index.ts index c898762185..555c9062f2 100644 --- a/packages/ods-react/src/components/tabs/src/index.ts +++ b/packages/ods-react/src/components/tabs/src/index.ts @@ -1,4 +1,5 @@ -export { Tabs, type TabsProp, type TabsValueChangeEvent } from './components/tabs/Tabs'; +export { Tabs, type TabsProp } from './components/tabs/Tabs'; export { TabList, type TabListProp } from './components/tab-list/TabList'; export { Tab, type TabProp } from './components/tab/Tab'; export { TabContent, type TabContentProp } from './components/tab-content/TabContent'; +export { type TabsValueChangeEvent } from './contexts/useTabs'; diff --git a/packages/ods-react/src/style/_button.scss b/packages/ods-react/src/style/_button.scss index a3bcfd82fe..f1bf10bc52 100644 --- a/packages/ods-react/src/style/_button.scss +++ b/packages/ods-react/src/style/_button.scss @@ -3,6 +3,7 @@ $ods-button-border-radius-xs: var(--ods-border-radius-xs); $ods-button-border-radius-md: var(--ods-border-radius-md); +$ods-button-border-radius-width: var(--ods-border-width-sm); $ods-button-size-xs: 24px; $ods-button-size-sm: 32px; $ods-button-size-md: 40px; @@ -53,7 +54,7 @@ $ods-button-size-md: 40px; } @mixin ods-button-variant-default($color) { - border-width: 1px; + border-width: $ods-button-border-radius-width; border-style: solid; &:not(:disabled) { @@ -152,7 +153,7 @@ $ods-button-size-md: 40px; } @mixin ods-button-variant-ghost($color) { - border: 1px solid transparent; + border: $ods-button-border-radius-width solid transparent; &:disabled { background-color: inherit; @@ -248,7 +249,7 @@ $ods-button-size-md: 40px; } @mixin ods-button-variant-outline($color) { - border-width: 1px; + border-width: $ods-button-border-radius-width; border-style: solid; &:not(:disabled) { diff --git a/packages/ods-react/src/utils/debounce.ts b/packages/ods-react/src/utils/debounce.ts new file mode 100644 index 0000000000..99906885e2 --- /dev/null +++ b/packages/ods-react/src/utils/debounce.ts @@ -0,0 +1,12 @@ +function debounce) => ReturnType>(callback: F, waitFor: number): (...args: Parameters) => void { + let timeout: ReturnType; + + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => callback(...args), waitFor); + }; +} + +export { + debounce, +}; diff --git a/packages/ods-react/tests/utils/debounce.spec.ts b/packages/ods-react/tests/utils/debounce.spec.ts new file mode 100644 index 0000000000..2037c5cc3e --- /dev/null +++ b/packages/ods-react/tests/utils/debounce.spec.ts @@ -0,0 +1,46 @@ +import { debounce } from '../../src/utils/debounce'; + +describe('debounce', () => { + beforeAll(() => jest.useFakeTimers()); + + afterAll(() => jest.useRealTimers()); + + beforeEach(jest.clearAllMocks); + + it('should delay the callback', () => { + const callback = jest.fn(); + const debouncedFn = debounce(callback, 1000); + + debouncedFn(); + + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should call callback once', () => { + const callback = jest.fn(); + const debouncedFn = debounce(callback, 1000); + + debouncedFn(); + jest.advanceTimersByTime(500); + debouncedFn(); + jest.advanceTimersByTime(500); + debouncedFn(); + jest.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should call callback with parameters', () => { + const callback = jest.fn(); + const debouncedFn = debounce(callback, 1000); + + debouncedFn('hello', 42); + jest.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledWith('hello', 42); + }); +}); diff --git a/packages/ods-react/tsconfig.json b/packages/ods-react/tsconfig.json index bf2be10484..c23897cb4e 100644 --- a/packages/ods-react/tsconfig.json +++ b/packages/ods-react/tsconfig.json @@ -16,13 +16,13 @@ "target": "ES2020", "useDefineForClassFields": true }, - "include": ["src"], + "include": ["src", "tests"], "exclude": [ "dist", "node_modules", + "src/**/tests", "**/**/.storybook", "**/**/node_modules", - "**/**/tests", "**/**/*.config.ts", "**/**/*.stories.tsx" ] diff --git a/packages/storybook/assets/components/tabs/anatomy.png b/packages/storybook/assets/components/tabs/anatomy.png index c49985820f..376d9be85d 100644 Binary files a/packages/storybook/assets/components/tabs/anatomy.png and b/packages/storybook/assets/components/tabs/anatomy.png differ diff --git a/packages/storybook/stories/components/tabs/documentation.mdx b/packages/storybook/stories/components/tabs/documentation.mdx index b987f14fa3..b9d99216b1 100644 --- a/packages/storybook/stories/components/tabs/documentation.mdx +++ b/packages/storybook/stories/components/tabs/documentation.mdx @@ -51,8 +51,9 @@ They can also be used to filter content via some common denominator. ![Component anatomy](components/tabs/anatomy.png "Component anatomy") 1. **Tabs** -2. **Active tab** -3. **Unselected tab** +2. **Scroll Buttons (previous/next) - optional** +3. **Active tab** +4. **Unselected tab** @@ -70,6 +71,18 @@ Each **Tab** has a panel displaying content. Selecting a **Tab** displays the co The panel does not depend on the **Tab** component, it is in the developer's hands. + + +When the number of **Tabs** exceeds the visible container width, left and right scroll buttons can be displayed to allow horizontal navigation. + +Clicking a button scrolls to the next or previous group of **Tabs** that were not visible. + +When at the first tab, the left button is displayed but disabled. + +When at the last tab, the right button is displayed but disabled. + +Scroll buttons do not affect tab selection. They only change the visible portion of the **Tabs** list. + @@ -90,6 +103,10 @@ Pressing `Home` / `fn` + `Arrow Left` moves focus to the first **Tab** and activ Pressing `End` / `fn` + `Arrow Right` moves focus to the last **Tab** and activates its content. +Pressing `Home` / `End` jumps to the first or last **Tab**, even if these **Tabs** are not currently visible. In this case, the **Tabs** list automatically scrolls to bring the selected **Tab** into view. + +Arrow navigation moves focus to the next or previous **Tab**, even if it is not currently visible. In this case, the **Tabs** list automatically scrolls to bring the focused **Tab** into view. + This component complies with the Tabs WAI-ARIA design pattern. diff --git a/packages/storybook/stories/components/tabs/tabs.stories.tsx b/packages/storybook/stories/components/tabs/tabs.stories.tsx index df3b04403c..c39ae819c0 100644 --- a/packages/storybook/stories/components/tabs/tabs.stories.tsx +++ b/packages/storybook/stories/components/tabs/tabs.stories.tsx @@ -1,30 +1,69 @@ import { type Meta, type StoryObj } from '@storybook/react'; import React, { useState } from 'react'; import { Tabs, TabList, Tab, TabContent, type TabsProp, type TabsValueChangeEvent } from '../../../../ods-react/src/components/tabs/src'; -import { excludeFromDemoControls } from '../../../src/helpers/controls'; +import { excludeFromDemoControls, orderControls } from '../../../src/helpers/controls'; import { staticSourceRenderConfig } from '../../../src/helpers/source'; +import { CONTROL_CATEGORY } from '../../../src/constants/controls'; type Story = StoryObj; +// type DemoArg = Partial; const meta: Meta = { component: Tabs, subcomponents: { TabList, Tab, TabContent }, - argTypes: excludeFromDemoControls(['defaultValue', 'value', 'onValueChange']), + argTypes: excludeFromDemoControls(['defaultValue', 'onValueChange', 'value']), title: 'React Components/Tabs', }; export default meta; export const Demo: Story = { - render: () => ( - - + render: (arg) => ( + + Tab 1 Tab 2 Tab 3 + Tab 4 + Tab 5 + Tab 6 + + +

Tab 1 Content

+
+ + +

Tab 2 Content

+
+ + +

Tab 3 Content

+
+ + +

Tab 4 Content

+
+ + +

Tab 5 Content

+
+ + +

Tab 6 Content

+
), + argTypes: orderControls({ + withArrows: { + table: { + category: CONTROL_CATEGORY.design, + }, + control: { type: 'boolean' }, + }, + }), } export const Overview: Story = { @@ -128,6 +167,36 @@ export const Overflow: Story = { ), }; +export const WithArrows: Story = { + globals: { + imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 + Tab 12 + Tab 13 + Tab 14 + Tab 15 + + + ), +}; + export const WithContent: Story = { globals: { imports: `import { Tabs, TabContent, TabList, Tab } from '@ovhcloud/ods-react';`, diff --git a/packages/storybook/stories/components/tabs/technical-information.mdx b/packages/storybook/stories/components/tabs/technical-information.mdx index 3362261318..1de1ba6e2c 100644 --- a/packages/storybook/stories/components/tabs/technical-information.mdx +++ b/packages/storybook/stories/components/tabs/technical-information.mdx @@ -74,3 +74,7 @@ import * as TabsStories from './tabs.stories'; + + + +