diff --git a/packages/ods-react/src/components/switch/src/components/switch/Switch.tsx b/packages/ods-react/src/components/switch/src/components/switch/Switch.tsx index d2e0ae0c1..d28f52620 100644 --- a/packages/ods-react/src/components/switch/src/components/switch/Switch.tsx +++ b/packages/ods-react/src/components/switch/src/components/switch/Switch.tsx @@ -1,6 +1,6 @@ import { ToggleGroup, type ToggleGroupValueChangeDetails } from '@ark-ui/react/toggle-group'; import classNames from 'classnames'; -import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react'; +import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useEffect } from 'react'; import { SWITCH_SIZE, type SwitchSize } from '../../constants/switch-size'; import style from './switch.module.scss'; @@ -40,6 +40,10 @@ const Switch: FC = forwardRef(({ value, ...props }, ref): JSX.Element => { + useEffect(() => { + console.warn('[DEPRECATED]: Switch component is not recommended anymore and will be removed in a next major release. Check the documentation for correct alternative.'); + }, []); + function onSwitchValueChange(detail: ToggleGroupValueChangeDetails): void { if (onValueChange) { onValueChange({ value: detail.value[0] }); 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 569711a02..34423c909 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 @@ -4,6 +4,7 @@ import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useCallback, import { debounce } from '../../../../../utils/debounce'; import { BUTTON_SIZE, BUTTON_VARIANT, Button } from '../../../../button/src'; import { ICON_NAME, Icon } from '../../../../icon/src'; +import { TABS_VARIANT } from '../../constants/tabs-variant'; import { useTabs } from '../../contexts/useTabs'; import style from './tabList.module.scss'; @@ -14,10 +15,11 @@ const TabList: FC = forwardRef(({ className, ...props }, ref): JSX.Element => { - const { withArrows, setScrollContainerRef } = useTabs(); + const { setScrollContainerRef, size, variant, withArrows } = useTabs(); const [isLeftButtonDisabled, setIsLeftButtonDisabled] = useState(false); const [isRightButtonDisabled, setIsRightButtonDisabled] = useState(false); const scrollRef = useRef(null); + const arrowSize = variant === TABS_VARIANT.default ? BUTTON_SIZE.xs : size; useEffect(() => { if (setScrollContainerRef) { @@ -27,7 +29,7 @@ const TabList: FC = forwardRef(({ const updateScrollButtonState = useCallback(() => { setIsLeftButtonDisabled(scrollRef.current ? scrollRef.current.scrollLeft === 0 : false); - setIsRightButtonDisabled(scrollRef.current ? scrollRef.current.scrollLeft === scrollRef.current.scrollWidth - scrollRef.current.offsetWidth : false); + setIsRightButtonDisabled(scrollRef.current ? Math.ceil(scrollRef.current.scrollLeft) >= scrollRef.current.scrollWidth - scrollRef.current.offsetWidth : false); }, []); const debouncedUpdateScrollButtonState = useMemo(() => { @@ -73,7 +75,10 @@ const TabList: FC = forwardRef(({ return (
@@ -81,12 +86,13 @@ const TabList: FC = forwardRef(({ withArrows &&
); -export const CustomStyle = () => ( - - - Tab 1 - Tab 2 - Tab 3 - - -

Tab 1 Content

-
- -

Tab 2 Content

-
- -

Tab 3 Content

-
-
+export const Size = () => ( + <> +

MD

+ + + 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 + + + +

+ + + + 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 + + + +

SM

+ + + 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 + + + +

+ + + + 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 + + + +

XS

+ + + 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 + + + +

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

Default rendering

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

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

With arrows

+ + + + 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 + + + +

+ + + + 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 + + + +

Disabled

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

+ + + + Tab 1 + Tab 2 + Tab 3 + + + ); export const WithArrows = () => ( diff --git a/packages/ods-react/src/components/tabs/src/index.ts b/packages/ods-react/src/components/tabs/src/index.ts index 555c9062f..7dafb1b05 100644 --- a/packages/ods-react/src/components/tabs/src/index.ts +++ b/packages/ods-react/src/components/tabs/src/index.ts @@ -2,4 +2,6 @@ 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 { TABS_SIZE, TABS_SIZES, type TabsSize } from './constants/tabs-size'; +export { TABS_VARIANT, TABS_VARIANTS, type TabsVariant } from './constants/tabs-variant'; export { type TabsValueChangeEvent } from './contexts/useTabs'; diff --git a/packages/storybook/.storybook/manager.tsx b/packages/storybook/.storybook/manager.tsx index c63431061..1eb4bf827 100644 --- a/packages/storybook/.storybook/manager.tsx +++ b/packages/storybook/.storybook/manager.tsx @@ -1,4 +1,4 @@ -import { type BADGE_COLOR, Badge } from '@ovhcloud/ods-react'; +import { BADGE_COLOR, Badge } from '@ovhcloud/ods-react'; import { addons } from '@storybook/manager-api'; import classNames from 'classnames'; import React from 'react'; @@ -10,12 +10,23 @@ import { light } from './ods.theme'; import styles from './manager.module.css'; function renderLabel(name: string, tags: string[], isDoc: boolean) { - let badge = ''; + let badge; if (tags.indexOf('beta') > -1) { - badge = 'beta'; + badge = { + color: BADGE_COLOR.beta, + label: 'beta', + } } else if (tags.indexOf('new') > -1) { - badge = 'new'; + badge = { + color: BADGE_COLOR.new, + label: 'new', + } + } else if (tags.indexOf('deprecated') > -1) { + badge = { + color: BADGE_COLOR.warning, + label: 'deprecated', + } } if (!badge) { @@ -33,9 +44,9 @@ function renderLabel(name: string, tags: string[], isDoc: boolean) { - { badge.toUpperCase() } + { badge.label.toUpperCase() }
); diff --git a/packages/storybook/stories/components/switch/Deprecated.tsx b/packages/storybook/stories/components/switch/Deprecated.tsx new file mode 100644 index 000000000..e50247e0d --- /dev/null +++ b/packages/storybook/stories/components/switch/Deprecated.tsx @@ -0,0 +1,27 @@ +import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react'; +import React from 'react'; +import { StorybookLink } from '../../../src/components/storybookLink/StorybookLink'; +import { REACT_COMPONENTS_TITLE, STORY } from '../../../src/constants/meta'; + +const Deprecated = () => ( + + + + + Component is now deprecated and will be removed in a future major release. + You can use different components instead depending on your use-case: +
    +
  • managing navigation: move to Tabs using the switch variant.
  • +
  • managing option activation: move to a Button Group.
  • +
  • as a form element: move to a Radio Group.
  • +
+
+
+); + +export { + Deprecated, +}; diff --git a/packages/storybook/stories/components/switch/documentation.mdx b/packages/storybook/stories/components/switch/documentation.mdx index 48452e246..96de8e4db 100644 --- a/packages/storybook/stories/components/switch/documentation.mdx +++ b/packages/storybook/stories/components/switch/documentation.mdx @@ -8,6 +8,7 @@ import { Canvas } from '../../../src/components/canvas/Canvas'; import { ExternalLink } from '../../../src/components/externalLink/ExternalLink'; import { Heading } from '../../../src/components/heading/Heading'; import { IdentityCard } from '../../../src/components/identityCard/IdentityCard'; +import { Deprecated } from './Deprecated'; @@ -17,6 +18,8 @@ _A **Switch** allows users to quickly and easily switch between several states, + + = { argTypes: excludeFromDemoControls(['defaultValue', 'onValueChange', 'value']), component: Switch, subcomponents: { SwitchItem }, + tags: ['deprecated'], title: 'React Components/Switch', }; diff --git a/packages/storybook/stories/components/switch/technical-information.mdx b/packages/storybook/stories/components/switch/technical-information.mdx index 1cd77efa3..20f719f2c 100644 --- a/packages/storybook/stories/components/switch/technical-information.mdx +++ b/packages/storybook/stories/components/switch/technical-information.mdx @@ -6,6 +6,7 @@ import { Banner } from '../../../src/components/banner/Banner'; import { Canvas } from '../../../src/components/canvas/Canvas'; import { Heading } from '../../../src/components/heading/Heading'; import { TechnicalSpecification } from '../../../src/components/technicalSpecification/TechnicalSpecification'; +import { Deprecated } from './Deprecated'; import * as SwitchStories from './switch.stories'; @@ -16,6 +17,8 @@ import * as SwitchStories from './switch.stories'; + + diff --git a/packages/storybook/stories/components/tabs/documentation.mdx b/packages/storybook/stories/components/tabs/documentation.mdx index 86ee4cf4f..1c036ce95 100644 --- a/packages/storybook/stories/components/tabs/documentation.mdx +++ b/packages/storybook/stories/components/tabs/documentation.mdx @@ -41,11 +41,14 @@ They can also be used to filter content via some common denominator. '- Don\'t overload the interface with too many tabs. Use dropdowns if you need more than 5 tabs', '- Don\'t use long or verbose labels', '- Don\'t use external links or actions as tab triggers, Tabs should only control the visibility of in-page content', + '- Avoid using the switch variant for main navigation or complex page segmentation. Prefer the default Tabs for those cases instead', ]} dos={[ '- Use Tabs to group related content under the same page when it fits in a shared context', '- Keep Tab labels short, clear, and scannable, use nouns or very short phrases (1–3 words max)', '- Use Tabs for horizontal navigation, not hierarchical structure or progressive steps', + '- Do use default Tabs for main navigation', + '- Do use switch variant when Tabs act as a mode selector or view switcher (e.g., toggling between "List" and "Grid" views)', ]} /> @@ -86,6 +89,19 @@ 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. + + + + +Used for displaying and organizing related content within the same page or view. +The default **Tabs** variant follows the standard horizontal layout, with a clear underline or border to indicate the active state. +It is ideal for primary navigation within a section or module. + + + +The switch variant is visually distinct of **Tabs**, designed for switching between sub-views or modes within the same context. +It behaves as a lightweight navigation switch between views. + diff --git a/packages/storybook/stories/components/tabs/tabs.stories.tsx b/packages/storybook/stories/components/tabs/tabs.stories.tsx index be28e382f..80511a3b6 100644 --- a/packages/storybook/stories/components/tabs/tabs.stories.tsx +++ b/packages/storybook/stories/components/tabs/tabs.stories.tsx @@ -1,12 +1,11 @@ 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 { TABS_SIZE, TABS_SIZES, TABS_VARIANT, TABS_VARIANTS, Tabs, TabList, Tab, TabContent, type TabsProp, type TabsValueChangeEvent } from '../../../../ods-react/src/components/tabs/src'; 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, @@ -20,8 +19,8 @@ export default meta; export const Demo: Story = { render: (arg) => ( + defaultValue="tab1" + { ...arg }> Tab 1 Tab 2 @@ -57,6 +56,22 @@ export const Demo: Story = { ), argTypes: orderControls({ + size: { + table: { + category: CONTROL_CATEGORY.design, + type: { summary: 'TABS_SIZE' } + }, + control: { type: 'select' }, + options: TABS_SIZES, + }, + variant: { + table: { + category: CONTROL_CATEGORY.design, + type: { summary: 'TABS_VARIANT' } + }, + control: { type: 'select' }, + options: TABS_VARIANTS, + }, withArrows: { table: { category: CONTROL_CATEGORY.design, @@ -120,7 +135,7 @@ export const Default: Story = { }, tags: ['!dev'], render: ({}) => ( - + Tab 1 Tab 2 @@ -167,6 +182,70 @@ export const Overflow: Story = { ), }; +export const Size: Story = { + globals: { + imports: `import { TABS_SIZE, TABS_VARIANT, Tabs, TabList, Tab } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + <> +

MD

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

SM

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

XS

+ + + Tab 1 + Tab 2 + Tab 3 + + + + ), +}; + +export const Variant: Story = { + globals: { + imports: `import { TABS_VARIANT, Tabs, TabList, Tab } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + + + Tab 1 + Tab 2 + Tab 3 + + + ), +}; + export const WithArrows: Story = { globals: { imports: `import { Tabs, 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 efead480e..cb821a8eb 100644 --- a/packages/storybook/stories/components/tabs/technical-information.mdx +++ b/packages/storybook/stories/components/tabs/technical-information.mdx @@ -62,6 +62,14 @@ import * as TabsStories from './tabs.stories'; + + + + + + + +