From ed179a157324f3857c805ea3465728ca56a56a2e Mon Sep 17 00:00:00 2001 From: Eirik Backer Date: Wed, 18 Sep 2024 15:12:08 +0200 Subject: [PATCH] fix(Tabs): api alignment (#2448) - Part of #2221 - Renames `Tabs.Root` to `Tabs` - Renames `Tabs.Content` to `Tabs.Panel` as this aligns with ARIA-standard and is a more common convention --- .changeset/gold-chairs-jog.md | 8 ++ .../Previews/Components/Components.tsx | 4 +- packages/css/tabs.css | 2 +- .../src/components/Badge/Badge.stories.tsx | 10 +-- packages/react/src/components/Tabs/Tab.tsx | 40 ---------- .../react/src/components/Tabs/TabList.tsx | 39 --------- packages/react/src/components/Tabs/Tabs.mdx | 26 +++--- .../src/components/Tabs/Tabs.stories.tsx | 34 ++++---- .../react/src/components/Tabs/Tabs.test.tsx | 30 +++---- packages/react/src/components/Tabs/Tabs.tsx | 78 ++++++++++++++++++ .../react/src/components/Tabs/TabsList.tsx | 38 +++++++++ .../Tabs/{TabContent.tsx => TabsPanel.tsx} | 18 ++--- .../react/src/components/Tabs/TabsRoot.tsx | 79 ------------------- .../react/src/components/Tabs/TabsTab.tsx | 41 ++++++++++ packages/react/src/components/Tabs/index.ts | 49 +++++------- packages/react/src/components/Tabs/useTab.ts | 32 -------- packages/react/stories/showcase.stories.tsx | 4 +- 17 files changed, 248 insertions(+), 284 deletions(-) create mode 100644 .changeset/gold-chairs-jog.md delete mode 100644 packages/react/src/components/Tabs/Tab.tsx delete mode 100644 packages/react/src/components/Tabs/TabList.tsx create mode 100644 packages/react/src/components/Tabs/Tabs.tsx create mode 100644 packages/react/src/components/Tabs/TabsList.tsx rename packages/react/src/components/Tabs/{TabContent.tsx => TabsPanel.tsx} (61%) delete mode 100644 packages/react/src/components/Tabs/TabsRoot.tsx create mode 100644 packages/react/src/components/Tabs/TabsTab.tsx delete mode 100644 packages/react/src/components/Tabs/useTab.ts diff --git a/.changeset/gold-chairs-jog.md b/.changeset/gold-chairs-jog.md new file mode 100644 index 0000000000..9351938447 --- /dev/null +++ b/.changeset/gold-chairs-jog.md @@ -0,0 +1,8 @@ +--- +"@digdir/designsystemet-css": patch +"@digdir/designsystemet-react": patch +--- + +Tabs: +- Renames `Tabs.Root` to `Tabs` +- Renames `Tabs.Content` to `Tabs.Panel` diff --git a/apps/theme/components/Previews/Components/Components.tsx b/apps/theme/components/Previews/Components/Components.tsx index 2576db1663..6d32accf0e 100644 --- a/apps/theme/components/Previews/Components/Components.tsx +++ b/apps/theme/components/Previews/Components/Components.tsx @@ -309,13 +309,13 @@ export const Components = () => { />
- + Min profil Tjenester Innstillinger - + For å kunne bli registrert i{' '} diff --git a/packages/css/tabs.css b/packages/css/tabs.css index 6484086774..21fc16d5b6 100644 --- a/packages/css/tabs.css +++ b/packages/css/tabs.css @@ -17,7 +17,7 @@ } } -.ds-tabs__content { +.ds-tabs__panel { padding: var(--dsc-tabs-content-padding); color: var(--dsc-tabs-content-color); } diff --git a/packages/react/src/components/Badge/Badge.stories.tsx b/packages/react/src/components/Badge/Badge.stories.tsx index 17e2f12f1f..f17e17c960 100644 --- a/packages/react/src/components/Badge/Badge.stories.tsx +++ b/packages/react/src/components/Badge/Badge.stories.tsx @@ -134,7 +134,7 @@ export const Status: Story = (args) => ( ); export const InTabs: Story = (args) => ( - + @@ -148,10 +148,10 @@ export const InTabs: Story = (args) => ( - content 1 - content 2 - content 3 - + content 1 + content 2 + content 3 + ); export const InButton: Story = (args) => ( diff --git a/packages/react/src/components/Tabs/Tab.tsx b/packages/react/src/components/Tabs/Tab.tsx deleted file mode 100644 index 04f0ce0538..0000000000 --- a/packages/react/src/components/Tabs/Tab.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import cl from 'clsx/lite'; -import type { HTMLAttributes } from 'react'; -import { forwardRef } from 'react'; - -import { RovingFocusItem } from '../../utilities/RovingFocus/RovingFocusItem'; -import { Paragraph } from '../Typography'; - -import { useTabItem } from './useTab'; - -export type TabProps = { - /** Value that will be set in the `Tabs` components state when the tab is activated */ - value: string; -} & Omit, 'value'>; - -/** - * A single item in a Tabs component. - * @example - * Tab 1 - */ -export const Tab = forwardRef((props, ref) => { - const { children, className, ...rest } = props; - const { size, ...useTabRest } = useTabItem(props); - - return ( - - - - - - ); -}); - -Tab.displayName = 'Tab'; diff --git a/packages/react/src/components/Tabs/TabList.tsx b/packages/react/src/components/Tabs/TabList.tsx deleted file mode 100644 index 8fb84e0549..0000000000 --- a/packages/react/src/components/Tabs/TabList.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import cl from 'clsx/lite'; -import type { HTMLAttributes } from 'react'; -import { forwardRef, useContext } from 'react'; - -import { RovingFocusRoot } from '../../utilities/RovingFocus'; - -import { TabsContext } from './TabsRoot'; - -/** - * The container for all `Tab` components. - * @example - * ```tsx - * - * Tab 1 - * Tab 2 - * - * ``` - */ -export const TabList = forwardRef< - HTMLDivElement, - HTMLAttributes ->(({ children, className, ...rest }, ref) => { - const { value } = useContext(TabsContext); - - return ( - - {children} - - ); -}); - -TabList.displayName = 'TabList'; diff --git a/packages/react/src/components/Tabs/Tabs.mdx b/packages/react/src/components/Tabs/Tabs.mdx index 18b1bef7b7..9956e549e0 100644 --- a/packages/react/src/components/Tabs/Tabs.mdx +++ b/packages/react/src/components/Tabs/Tabs.mdx @@ -1,8 +1,8 @@ import { Meta, Canvas, Controls, Primary, ArgTypes } from '@storybook/blocks'; -import { TabsRoot } from './TabsRoot'; -import { Tab } from './Tab'; -import { TabContent } from './TabContent'; +import { Tabs } from './Tabs'; +import { TabsTab } from './TabsTab'; +import { TabsPanel } from './TabsPanel'; import * as TabsStories from './Tabs.stories'; @@ -19,16 +19,16 @@ Tabs lar brukerne navigere mellom relaterte deler av innholdet, der én del vise ```tsx import { Tabs } from '@digdir/designsystemet-react'; - + Tab 1 Tab 2 Tab 3 - content 1 - content 2 - content 3 -; + content 1 + content 2 + content 3 +; ``` ## Icons only @@ -41,17 +41,17 @@ import { Tabs } from '@digdir/designsystemet-react'; ### Props -#### `Tabs.Root` +#### `Tabs` - + #### `Tabs.Tab` - + -#### `Tabs.Content` +#### `Tabs.Panel` - + ### Størrelse på ikon diff --git a/packages/react/src/components/Tabs/Tabs.stories.tsx b/packages/react/src/components/Tabs/Tabs.stories.tsx index 8f1d54f857..435b8df1f8 100644 --- a/packages/react/src/components/Tabs/Tabs.stories.tsx +++ b/packages/react/src/components/Tabs/Tabs.stories.tsx @@ -14,20 +14,20 @@ import { Tabs } from '.'; export default { title: 'Komponenter/Tabs', - component: Tabs.Root, + component: Tabs, } as Meta; -export const Preview: StoryFn = (args) => ( - +export const Preview: StoryFn = (args) => ( + Tab 1 Tab 2 Tab 3 - content 1 - content 2 - content 3 - + content 1 + content 2 + content 3 + ); Preview.args = { @@ -36,7 +36,7 @@ Preview.args = { }; export const IconsOnly: StoryFn = () => ( - + @@ -48,10 +48,10 @@ export const IconsOnly: StoryFn = () => ( - content 1 - content 2 - content 3 - + content 1 + content 2 + content 3 + ); export const Controlled: StoryFn = () => { @@ -63,7 +63,7 @@ export const Controlled: StoryFn = () => { Choose Tab 3
- + @@ -78,10 +78,10 @@ export const Controlled: StoryFn = () => { Tab 3 - content 1 - content 2 - content 3 - + content 1 + content 2 + content 3 + ); }; diff --git a/packages/react/src/components/Tabs/Tabs.test.tsx b/packages/react/src/components/Tabs/Tabs.test.tsx index d88de3d439..57145bb752 100644 --- a/packages/react/src/components/Tabs/Tabs.test.tsx +++ b/packages/react/src/components/Tabs/Tabs.test.tsx @@ -8,14 +8,14 @@ const user = userEvent.setup(); describe('Tabs', () => { test('can navigate tabs with keyboard', async () => { render( - + Tab 1 Tab 2 - content 1 - content 2 - , + content 1 + content 2 + , ); const tab1 = screen.getByRole('tab', { name: 'Tab 1' }); @@ -30,14 +30,14 @@ describe('Tabs', () => { test('renders content based on value', async () => { render( - + Tab 1 Tab 2 - content 1 - content 2 - , + content 1 + content 2 + , ); expect(screen.queryByText('content 1')).toBeVisible(); @@ -49,12 +49,12 @@ describe('Tabs', () => { test('item renders with correct aria attributes', async () => { render( - + Tab 1 Tab 2 - , + , ); const tab = screen.getByRole('tab', { name: 'Tab 2' }); @@ -63,13 +63,13 @@ describe('Tabs', () => { expect(tab).toHaveAttribute('aria-selected', 'true'); }); - test('renders ReactNodes as children when TabContents value is selected', () => { + test('renders ReactNodes as children when TabsPanels value is selected', () => { render( - - + +
content 1
-
-
, + + , ); const content = screen.queryByText('content 1'); diff --git a/packages/react/src/components/Tabs/Tabs.tsx b/packages/react/src/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..129dfbf441 --- /dev/null +++ b/packages/react/src/components/Tabs/Tabs.tsx @@ -0,0 +1,78 @@ +import cl from 'clsx/lite'; +import type { HTMLAttributes } from 'react'; +import { createContext, forwardRef, useState } from 'react'; + +export type TabsProps = { + /** Controlled state for `Tabs` component. */ + value?: string; + /** Default value. */ + defaultValue?: string; + /** Callback with selected `TabItem` `value` */ + onChange?: (value: string) => void; + /** + * Changes items size and paddings + * @default md + */ + size?: 'sm' | 'md' | 'lg'; +} & Omit, 'onChange' | 'value'>; + +export type ContextProps = { + value?: string; + defaultValue?: string; + onChange?: (value: string) => void; + size?: TabsProps['size']; +}; + +export const Context = createContext({}); + +/** + * Display a group of tabs that can be toggled between. + * @example + * ```tsx + * console.log(value)}> + * + * Tab 1 + * Tab 2 + * Tab 3 + * + * content 1 + * content 2 + * content 3 + * + * ``` + */ +export const Tabs = forwardRef(function Tabs( + { size = 'md', value, defaultValue, className, onChange, ...rest }, + ref, +) { + const isControlled = value !== undefined; + const [uncontrolledValue, setUncontrolledValue] = useState< + string | undefined + >(defaultValue); + + let onValueChange = onChange; + if (!isControlled) { + onValueChange = (newValue: string) => { + setUncontrolledValue(newValue); + onChange?.(newValue); + }; + value = uncontrolledValue; + } + return ( + +
+ + ); +}); diff --git a/packages/react/src/components/Tabs/TabsList.tsx b/packages/react/src/components/Tabs/TabsList.tsx new file mode 100644 index 0000000000..821c97374a --- /dev/null +++ b/packages/react/src/components/Tabs/TabsList.tsx @@ -0,0 +1,38 @@ +import cl from 'clsx/lite'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useContext } from 'react'; + +import { RovingFocusRoot } from '../../utilities/RovingFocus'; + +import { Context } from './Tabs'; + +export type TabsListProps = HTMLAttributes; + +/** + * The container for all `Tab` components. + * @example + * ```tsx + * + * Tab 1 + * Tab 2 + * + * ``` + */ +export const TabsList = forwardRef( + function TabsList({ children, className, ...rest }, ref) { + const { value } = useContext(Context); + + return ( + + {children} + + ); + }, +); diff --git a/packages/react/src/components/Tabs/TabContent.tsx b/packages/react/src/components/Tabs/TabsPanel.tsx similarity index 61% rename from packages/react/src/components/Tabs/TabContent.tsx rename to packages/react/src/components/Tabs/TabsPanel.tsx index 4f3fdc3e6d..50230c37bd 100644 --- a/packages/react/src/components/Tabs/TabContent.tsx +++ b/packages/react/src/components/Tabs/TabsPanel.tsx @@ -4,10 +4,10 @@ import { forwardRef, useContext } from 'react'; import { Paragraph } from '../Typography'; -import { TabsContext } from './TabsRoot'; +import { Context } from './Tabs'; -export type TabContentProps = { - /** When this value is selected as the current state, render this `TabContent` component*/ +export type TabsPanelProps = { + /** When this value is selected as the current state, render this `TabsPanel` component*/ value: string; } & Omit, 'value'>; @@ -15,12 +15,12 @@ export type TabContentProps = { * A single content item in a Tabs component. * @example * ```tsx - * content 1 + * content 1 * ``` */ -export const TabContent = forwardRef( - ({ children, value, className, ...rest }, ref) => { - const { value: tabsValue, size } = useContext(TabsContext); +export const TabsPanel = forwardRef( + function TabsPanel({ children, value, className, ...rest }, ref) { + const { value: tabsValue, size } = useContext(Context); const active = value === tabsValue; return ( @@ -28,7 +28,7 @@ export const TabContent = forwardRef( {active && (
@@ -40,5 +40,3 @@ export const TabContent = forwardRef( ); }, ); - -TabContent.displayName = 'TabContent'; diff --git a/packages/react/src/components/Tabs/TabsRoot.tsx b/packages/react/src/components/Tabs/TabsRoot.tsx deleted file mode 100644 index 83df12f752..0000000000 --- a/packages/react/src/components/Tabs/TabsRoot.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import cl from 'clsx/lite'; -import type { HTMLAttributes } from 'react'; -import { createContext, forwardRef, useState } from 'react'; - -export type TabsProps = { - /** Controlled state for `Tabs` component. */ - value?: string; - /** Default value. */ - defaultValue?: string; - /** Callback with selected `TabItem` `value` */ - onChange?: (value: string) => void; - /** - * Changes items size and paddings - * @default md - */ - size?: 'sm' | 'md' | 'lg'; -} & Omit, 'onChange' | 'value'>; - -export type TabsContextProps = { - value?: string; - defaultValue?: string; - onChange?: (value: string) => void; - size?: TabsProps['size']; -}; - -export const TabsContext = createContext({}); - -/** - * Display a group of tabs that can be toggled between. - * @example - * ```tsx - * console.log(value)}> - * - * Tab 1 - * Tab 2 - * Tab 3 - * - * content 1 - * content 2 - * content 3 - * - * ``` - */ -export const TabsRoot = forwardRef( - ({ size = 'md', value, defaultValue, className, onChange, ...rest }, ref) => { - const isControlled = value !== undefined; - const [uncontrolledValue, setUncontrolledValue] = useState< - string | undefined - >(defaultValue); - - let onValueChange = onChange; - if (!isControlled) { - onValueChange = (newValue: string) => { - setUncontrolledValue(newValue); - onChange?.(newValue); - }; - value = uncontrolledValue; - } - return ( - -
- - ); - }, -); - -TabsRoot.displayName = 'TabsRoot'; diff --git a/packages/react/src/components/Tabs/TabsTab.tsx b/packages/react/src/components/Tabs/TabsTab.tsx new file mode 100644 index 0000000000..adbe97248d --- /dev/null +++ b/packages/react/src/components/Tabs/TabsTab.tsx @@ -0,0 +1,41 @@ +import cl from 'clsx/lite'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useContext, useId } from 'react'; + +import { RovingFocusItem } from '../../utilities/RovingFocus/RovingFocusItem'; +import { Paragraph } from '../Typography'; +import { Context } from './Tabs'; + +export type TabsTabProps = { + /** Value that will be set in the `Tabs` components state when the tab is activated */ + value: string; +} & Omit, 'value'>; + +/** + * A single item in a Tabs component. + * @example + * Tab 1 + */ +export const TabsTab = forwardRef( + function TabsTab({ className, value, ...rest }, ref) { + const tabs = useContext(Context); + const buttonId = `tab-${useId()}`; + + return ( + + +
- + Min profil Tjenester Innstillinger - + For å kunne bli registrert i{' '}