diff --git a/docs/migrations/web-react/MIGRATION-v2.md b/docs/migrations/web-react/MIGRATION-v2.md index f897a35fe6..ca53ca58ac 100644 --- a/docs/migrations/web-react/MIGRATION-v2.md +++ b/docs/migrations/web-react/MIGRATION-v2.md @@ -20,6 +20,7 @@ Introducing version 2 of the _spirit-web-react_ package - [Modal: ModalDialog `isExpandedOnMobile` Prop](#modal-modaldialog-isexpandedonmobile-prop) - [Modal: ModalDialog `isScrollable` Prop](#modal-modaldialog-isscrollable-prop) - [Modal: ModalDialog Uniform Appearance](#modal-modaldialog-uniform-appearance) + - [Tabs: TabItem and TabPane Props](#tabs-tabitem-and-tabpane-props) - [TextField: `label` prop](#textfield-label-prop) - [Tooltip: `off` Placement](#tooltip-off-placement) - [Tooltip: Refactored](#tooltip-refactored) @@ -281,6 +282,26 @@ See [Codemods documentation][readme-codemods] for more details. Or manually add `isDockedOnMobile` prop to the `ModalDialog` component. +### Tabs: TabItem and TabPane Props + +TabItem `forTab` prop is renamed to `forTabPane`. +TabPane `tabId` prop is renamed to `id`. + +#### Migration Guide + +Use codemod to automatically update your codebase. + +```sh +npx @lmc-eu/spirit-codemods -p -t v2/web-react/tabs-tabitem-tabpane-props +``` + +See [Codemods documentation][readme-codemods] for more details. + +Or manually replace the props in your project. + +- `` → `` +- `` → `` + ### TextField: `label` prop The `label` prop is now required. diff --git a/packages/codemods/src/transforms/v2/web-react/__testfixtures__/tabs-tabitem-tabpane-props.input.tsx b/packages/codemods/src/transforms/v2/web-react/__testfixtures__/tabs-tabitem-tabpane-props.input.tsx new file mode 100644 index 0000000000..fcafc9fa32 --- /dev/null +++ b/packages/codemods/src/transforms/v2/web-react/__testfixtures__/tabs-tabitem-tabpane-props.input.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +// @ts-ignore: No declaration -- The library is not installed; we don't need to install it for fixtures. +import { TabItem, TabPane } from '@lmc-eu/spirit-web-react'; + +export const MyComponent = () => ( + <> + + + +); diff --git a/packages/codemods/src/transforms/v2/web-react/__testfixtures__/tabs-tabitem-tabpane-props.output.tsx b/packages/codemods/src/transforms/v2/web-react/__testfixtures__/tabs-tabitem-tabpane-props.output.tsx new file mode 100644 index 0000000000..63dff51076 --- /dev/null +++ b/packages/codemods/src/transforms/v2/web-react/__testfixtures__/tabs-tabitem-tabpane-props.output.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +// @ts-ignore: No declaration -- The library is not installed; we don't need to install it for fixtures. +import { TabItem, TabPane } from '@lmc-eu/spirit-web-react'; + +export const MyComponent = () => ( + <> + + + +); diff --git a/packages/codemods/src/transforms/v2/web-react/__tests__/tabs-tabitem-tabpane-props.test.ts b/packages/codemods/src/transforms/v2/web-react/__tests__/tabs-tabitem-tabpane-props.test.ts new file mode 100644 index 0000000000..a36eba14d8 --- /dev/null +++ b/packages/codemods/src/transforms/v2/web-react/__tests__/tabs-tabitem-tabpane-props.test.ts @@ -0,0 +1,3 @@ +import { testTransform } from '../../../../../tests/testUtils'; + +testTransform(__dirname, 'tabs-tabitem-tabpane-props'); diff --git a/packages/codemods/src/transforms/v2/web-react/tabs-tabitem-tabpane-props.ts b/packages/codemods/src/transforms/v2/web-react/tabs-tabitem-tabpane-props.ts new file mode 100644 index 0000000000..66f97ad8b1 --- /dev/null +++ b/packages/codemods/src/transforms/v2/web-react/tabs-tabitem-tabpane-props.ts @@ -0,0 +1,80 @@ +import { API, FileInfo } from 'jscodeshift'; + +const transform = (fileInfo: FileInfo, api: API) => { + const j = api.jscodeshift; + const root = j(fileInfo.source); + + // Find import statements for the specific module and TabItem or TabPane specifier + const importStatements = root.find(j.ImportDeclaration, { + source: { + value: (value: string) => /^@lmc-eu\/spirit-web-react(\/.*)?$/.test(value), + }, + }); + + // Check if the module is imported + if (importStatements.length > 0) { + const tabItemComponentSpecifier = importStatements.find(j.ImportSpecifier, { + imported: { + type: 'Identifier', + name: 'TabItem', + }, + }); + + const tabPaneComponentSpecifier = importStatements.find(j.ImportSpecifier, { + imported: { + type: 'Identifier', + name: 'TabPane', + }, + }); + + // Check if TabItem specifier is present + if (tabItemComponentSpecifier.length > 0) { + // Find TabItem components in the module + const components = root.find(j.JSXOpeningElement, { + name: { + type: 'JSXIdentifier', + name: 'TabItem', + }, + }); + + // Rename 'forTab' attribute to 'forTabPane' + components + .find(j.JSXAttribute, { + name: { + type: 'JSXIdentifier', + name: 'forTab', + }, + }) + .forEach((attributePath) => { + attributePath.node.name.name = 'forTabPane'; + }); + } + + // Check if TabPane specifier is present + if (tabPaneComponentSpecifier.length > 0) { + // Find TabPane components in the module + const components = root.find(j.JSXOpeningElement, { + name: { + type: 'JSXIdentifier', + name: 'TabPane', + }, + }); + + // Rename 'tabId' attribute to 'id' + components + .find(j.JSXAttribute, { + name: { + type: 'JSXIdentifier', + name: 'tabId', + }, + }) + .forEach((attributePath) => { + attributePath.node.name.name = 'id'; + }); + } + } + + return root.toSource(); +}; + +export default transform; diff --git a/packages/web-react/src/components/Tabs/README.md b/packages/web-react/src/components/Tabs/README.md index 8e51c1cbb0..4acb961aa3 100644 --- a/packages/web-react/src/components/Tabs/README.md +++ b/packages/web-react/src/components/Tabs/README.md @@ -6,20 +6,20 @@ groups of information in tabbable regions. ## Tab ```jsx -const [selectedTabId, setSelectedTab] = useState(1); +const [selectedId, setSelectedTab] = useState(1); -const selectTab = useCallback((tabId) => { - setSelectedTab(tabId); +const selectTab = useCallback((id) => { + setSelectedTab(id); }, []); - + - Item Selected - Item + Item Selected + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ; ``` @@ -27,22 +27,22 @@ const selectTab = useCallback((tabId) => { ## Tab with Links ```jsx -const [selectedTabId, setSelectedTab] = useState(1); +const [selectedId, setSelectedTab] = useState(1); -const selectTab = useCallback((tabId) => { - setSelectedTab(tabId); +const selectTab = useCallback((id) => { + setSelectedTab(id); }, []); - + - Item Selected - Item + Item Selected + Item Item Link Item Link Only Desktop - Pane 1 - Pane 2 + Pane 1 + Pane 2 ; ``` @@ -52,12 +52,12 @@ const selectTab = useCallback((tabId) => { ```jsx - Item Selected - Item + Item Selected + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ``` @@ -66,12 +66,12 @@ const selectTab = useCallback((tabId) => { #### API -| Name | Type | Default | Required | Description | -| ------------------- | ------------------------ | ------- | -------- | -------------------------------------------- | -| `selectedTab` | [`string` \| `number`] | — | ✔ | Identification of the selected tab | -| `toogle` | `Function` | — | ✔ | Toggle function which accept tab ID as input | -| `children` | `any` | — | ✕ | Child component | -| `onSelectionChange` | `(tabId: TabId) => void` | — | ✕ | When the state of the selected panel changes | +| Name | Type | Default | Required | Description | +| ------------------- | ---------------------- | ------- | -------- | -------------------------------------------- | +| `selectedTab` | [`string` \| `number`] | — | ✔ | Identification of the selected tab | +| `toogle` | `Function` | — | ✔ | Toggle function which accept tab ID as input | +| `children` | `any` | — | ✕ | Child component | +| `onSelectionChange` | `(id: TabId) => void` | — | ✕ | When the state of the selected panel changes | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -81,11 +81,11 @@ and [escape hatches][readme-escape-hatches]. #### API -| Name | Type | Default | Required | Description | -| -------------------- | ------------------------ | ------- | -------- | -------------------------------------------- | -| `defaultSelectedTab` | [`string` \| `number`] | — | ✔ | Identification of default selected tab | -| `children` | `any` | — | ✕ | Child component | -| `onSelectionChange` | `(tabId: TabId) => void` | — | ✕ | When the state of the selected panel changes | +| Name | Type | Default | Required | Description | +| -------------------- | ---------------------- | ------- | -------- | -------------------------------------------- | +| `defaultSelectedTab` | [`string` \| `number`] | — | ✔ | Identification of default selected tab | +| `children` | `any` | — | ✕ | Child component | +| `onSelectionChange` | `(id: TabId) => void` | — | ✕ | When the state of the selected panel changes | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -111,10 +111,10 @@ Tab list item #### API -| Name | Type | Default | Required | Description | -| ---------- | ---------------------- | ------- | -------- | --------------------- | -| `forTab` | [`string` \| `number`] | — | ✔ | Identification of tab | -| `children` | `any` | — | ✕ | Child component | +| Name | Type | Default | Required | Description | +| ------------ | ---------------------- | ------- | -------- | --------------------- | +| `forTabPane` | [`string` \| `number`] | — | ✔ | Identification of tab | +| `children` | `any` | — | ✕ | Child component | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -158,7 +158,7 @@ Tab content item | Name | Type | Default | Required | Description | | ---------- | ---------------------- | ------- | -------- | --------------------- | -| `tabId` | [`string` \| `number`] | — | ✔ | Identification of tab | +| `id` | [`string` \| `number`] | — | ✔ | Identification of tab | | `children` | `any` | — | ✕ | Child component | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. diff --git a/packages/web-react/src/components/Tabs/TabContent.tsx b/packages/web-react/src/components/Tabs/TabContent.tsx index c083c3e8ae..c725c2e0fa 100644 --- a/packages/web-react/src/components/Tabs/TabContent.tsx +++ b/packages/web-react/src/components/Tabs/TabContent.tsx @@ -1,8 +1,6 @@ import React from 'react'; import { useStyleProps } from '../../hooks'; -import { ChildrenProps, TransferProps } from '../../types'; - -export type TabContentProps = ChildrenProps & TransferProps; +import { TabContentProps } from '../../types'; const TabContent = ({ children, ...restProps }: TabContentProps): JSX.Element => { const { styleProps, props: transferProps } = useStyleProps(restProps); diff --git a/packages/web-react/src/components/Tabs/TabContext.tsx b/packages/web-react/src/components/Tabs/TabContext.tsx index cb50dfd1c6..25dbd3148c 100644 --- a/packages/web-react/src/components/Tabs/TabContext.tsx +++ b/packages/web-react/src/components/Tabs/TabContext.tsx @@ -1,20 +1,12 @@ import { createContext, useContext } from 'react'; -import { TabId } from '../../types'; - -type TabsToggler = (tabId: TabId) => void; - -type TabsContextType = { - selectedTabId: TabId; - selectTab: TabsToggler; - onSelectionChange?: (tabId: TabId) => void; -}; +import { TabId, TabsContextType, TabsToggler } from '../../types'; const defaultContext: TabsContextType = { - selectedTabId: '', + selectedId: '', // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - selectTab: (tabId: TabId) => {}, + selectTab: (id: TabId) => {}, // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - onSelectionChange: (tabId: TabId) => {}, + onSelectionChange: (id: TabId) => {}, }; const TabsContext = createContext(defaultContext); diff --git a/packages/web-react/src/components/Tabs/TabItem.tsx b/packages/web-react/src/components/Tabs/TabItem.tsx index 1a52c3152a..23e5452a32 100644 --- a/packages/web-react/src/components/Tabs/TabItem.tsx +++ b/packages/web-react/src/components/Tabs/TabItem.tsx @@ -1,28 +1,24 @@ import React from 'react'; import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; -import { ChildrenProps, TabId, TransferProps, ClickEvents, ClickEvent } from '../../types'; +import { ClickEvent, TabItemProps } from '../../types'; import { useTabContext } from './TabContext'; import { useTabsStyleProps } from './useTabsStyleProps'; -export interface TabItemProps extends ChildrenProps, TransferProps, ClickEvents { - forTab: TabId; -} - -const TabItem = ({ children, forTab, onClick, ...restProps }: TabItemProps): JSX.Element => { - const { selectTab, selectedTabId, onSelectionChange } = useTabContext(); - const { classProps } = useTabsStyleProps({ forTab, selectedTabId }); +const TabItem = ({ children, forTabPane, onClick, ...restProps }: TabItemProps): JSX.Element => { + const { selectTab, selectedId, onSelectionChange } = useTabContext(); + const { classProps } = useTabsStyleProps({ forTabPane, selectedId }); const { styleProps, props: transferProps } = useStyleProps(restProps); const handleClick = (event: ClickEvent) => { - selectTab(forTab); + selectTab(forTabPane); if (onClick) { onClick(event); } if (onSelectionChange) { - onSelectionChange(selectedTabId); + onSelectionChange(selectedId); } }; @@ -34,9 +30,9 @@ const TabItem = ({ children, forTab, onClick, ...restProps }: TabItemProps): JSX type="button" className={classNames(classProps.link, styleProps.className)} role="tab" - aria-selected={selectedTabId === forTab} - id={`${forTab}-tab`} - aria-controls={forTab.toString()} + aria-selected={selectedId === forTabPane} + id={`${forTabPane}-tab`} + aria-controls={forTabPane.toString()} onClick={handleClick} > {children} diff --git a/packages/web-react/src/components/Tabs/TabList.tsx b/packages/web-react/src/components/Tabs/TabList.tsx index 3e857aa434..352f8dde9a 100644 --- a/packages/web-react/src/components/Tabs/TabList.tsx +++ b/packages/web-react/src/components/Tabs/TabList.tsx @@ -1,11 +1,9 @@ import React from 'react'; import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; -import { ChildrenProps, TransferProps } from '../../types'; +import { TabListProps } from '../../types'; import { useTabsStyleProps } from './useTabsStyleProps'; -export type TabListProps = ChildrenProps & TransferProps; - const TabList = ({ children, ...restProps }: TabListProps): JSX.Element => { const { classProps } = useTabsStyleProps(); const { styleProps, props: transferProps } = useStyleProps(restProps); diff --git a/packages/web-react/src/components/Tabs/TabPane.tsx b/packages/web-react/src/components/Tabs/TabPane.tsx index d433217cb9..8a6096a456 100644 --- a/packages/web-react/src/components/Tabs/TabPane.tsx +++ b/packages/web-react/src/components/Tabs/TabPane.tsx @@ -1,27 +1,23 @@ import React from 'react'; import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; -import { ChildrenProps, TabId, TransferProps } from '../../types'; +import { TabPaneProps } from '../../types'; import { useTabContext } from './TabContext'; import { useTabsStyleProps } from './useTabsStyleProps'; -export interface TabPaneProps extends ChildrenProps, TransferProps { - tabId: TabId; -} - -const TabPane = ({ children, tabId, ...restProps }: TabPaneProps): JSX.Element | null => { - const { selectedTabId } = useTabContext(); - const { classProps } = useTabsStyleProps({ tabId, selectedTabId }); +const TabPane = ({ children, id, ...restProps }: TabPaneProps): JSX.Element | null => { + const { selectedId } = useTabContext(); + const { classProps } = useTabsStyleProps({ id, selectedId }); const { styleProps, props: transferProps } = useStyleProps(restProps); - return selectedTabId === tabId ? ( + return selectedId === id ? (
{children}
diff --git a/packages/web-react/src/components/Tabs/Tabs.tsx b/packages/web-react/src/components/Tabs/Tabs.tsx index d1081b4e44..f744f845b8 100644 --- a/packages/web-react/src/components/Tabs/Tabs.tsx +++ b/packages/web-react/src/components/Tabs/Tabs.tsx @@ -3,7 +3,7 @@ import { TabsProvider } from './TabContext'; import { TabsProps } from '../../types'; const Tabs = ({ children, selectedTab, toggle: selectTab, onSelectionChange }: TabsProps): JSX.Element => ( - {children} + {children} ); export default Tabs; diff --git a/packages/web-react/src/components/Tabs/UncontrolledTabs.tsx b/packages/web-react/src/components/Tabs/UncontrolledTabs.tsx index 5f6175a7f2..23effd5992 100644 --- a/packages/web-react/src/components/Tabs/UncontrolledTabs.tsx +++ b/packages/web-react/src/components/Tabs/UncontrolledTabs.tsx @@ -1,18 +1,12 @@ import React from 'react'; -import { ChildrenProps, TabId, TransferProps } from '../../types'; +import { UncontrolledTabsProps } from '../../types'; import { TabsProvider } from './TabContext'; import { useTab } from './useTabs'; -interface TabsProps extends ChildrenProps, TransferProps { - defaultSelectedTab: TabId; - // eslint-disable-next-line react/require-default-props - onSelectionChange?: (tabId: TabId) => void; -} +const Tabs = ({ children, defaultSelectedTab, onSelectionChange }: UncontrolledTabsProps): JSX.Element => { + const { selectedId, selectTab } = useTab(defaultSelectedTab); -const Tabs = ({ children, defaultSelectedTab, onSelectionChange }: TabsProps): JSX.Element => { - const { selectedTabId, selectTab } = useTab(defaultSelectedTab); - - return {children}; + return {children}; }; export default Tabs; diff --git a/packages/web-react/src/components/Tabs/__tests__/TabItem.test.tsx b/packages/web-react/src/components/Tabs/__tests__/TabItem.test.tsx index 02ef29d1eb..94343ac04a 100644 --- a/packages/web-react/src/components/Tabs/__tests__/TabItem.test.tsx +++ b/packages/web-react/src/components/Tabs/__tests__/TabItem.test.tsx @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; @@ -8,23 +8,25 @@ import { withTabsContext } from '../../../../tests/testUtils/withTabsContext'; import TabItem from '../TabItem'; describe('TabItem', () => { - stylePropsTest((props) => , 'TabItemTestId'); + stylePropsTest((props) => , 'TabItemTestId'); - classNamePrefixProviderTest(() => , 'Tabs__item'); + classNamePrefixProviderTest(() => , 'Tabs__item'); - restPropsTest((props) => , 'button'); + restPropsTest((props) => , 'button'); it('should render button tag when there is no href prop', () => { - const dom = render(); + render(); + + const element = screen.getByRole('tab'); - const element = dom.container.querySelector('button') as HTMLElement; expect(element).toHaveClass('Tabs__link'); }); it('should have ARIA attributtes', () => { - const dom = render(); + render(); + + const element = screen.getByRole('tab'); - const element = dom.container.querySelector('button') as HTMLElement; expect(element).toHaveAttribute('id', 'test-tab'); expect(element).toHaveAttribute('aria-controls', 'test'); expect(element).toHaveAttribute('type', 'button'); @@ -32,9 +34,9 @@ describe('TabItem', () => { it('should handle tab switch when clicked', async () => { const selectTabMock = jest.fn(); - const dom = render(withTabsContext(TabItem, { selectedTabId: 0, selectTab: selectTabMock })({ forTab: 'test' })); + render(withTabsContext(TabItem, { selectedId: 0, selectTab: selectTabMock })({ forTabPane: 'test' })); - fireEvent.click(dom.getByRole('tab') as HTMLElement); + fireEvent.click(screen.getByRole('tab')); await waitFor(() => expect(selectTabMock).toHaveBeenCalled()); }); diff --git a/packages/web-react/src/components/Tabs/__tests__/TabPane.test.tsx b/packages/web-react/src/components/Tabs/__tests__/TabPane.test.tsx index 1538c066be..b18b7be847 100644 --- a/packages/web-react/src/components/Tabs/__tests__/TabPane.test.tsx +++ b/packages/web-react/src/components/Tabs/__tests__/TabPane.test.tsx @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; @@ -15,7 +15,7 @@ describe('TabPane', () => { (props) => ( {}}> - + ), @@ -23,20 +23,30 @@ describe('TabPane', () => { ); classNamePrefixProviderTest( - withTabsContext((props) => , { selectedTabId: 'test' } as TabsContextType), + withTabsContext((props) => , { selectedId: 'test' } as TabsContextType), 'TabsPane', ); restPropsTest( - withTabsContext((props) => , { selectedTabId: 'test' } as TabsContextType), + withTabsContext((props) => , { selectedId: 'test' } as TabsContextType), 'div', ); it('should not render tab pane if tab is not selected', () => { - const dom = render( - withTabsContext(TabPane, { selectedTabId: 'another-tab', selectTab: jest.fn() })({ tabId: 'test' }), - ); + render(withTabsContext(TabPane, { selectedId: 'another-tab', selectTab: jest.fn() })({ id: 'test' })); - expect(dom.container.querySelector('#test') as HTMLElement).toBeNull(); + expect(screen.queryByRole('tabpanel')).not.toBeInTheDocument(); + }); + + it('should render tab pane if tab is selected', () => { + render(withTabsContext(TabPane, { selectedId: 'test', selectTab: jest.fn() })({ id: 'test' })); + + const tabPane = screen.queryByRole('tabpanel'); + + expect(tabPane).toBeInTheDocument(); + expect(tabPane).toHaveAttribute('id', 'test'); + expect(tabPane).toHaveAttribute('aria-labelledby', 'test-tab'); + expect(tabPane).toHaveClass('TabsPane'); + expect(tabPane).toHaveClass('is-selected'); }); }); diff --git a/packages/web-react/src/components/Tabs/__tests__/UncontrolledTabs.test.tsx b/packages/web-react/src/components/Tabs/__tests__/UncontrolledTabs.test.tsx index 5e14f44913..c41ac59c77 100644 --- a/packages/web-react/src/components/Tabs/__tests__/UncontrolledTabs.test.tsx +++ b/packages/web-react/src/components/Tabs/__tests__/UncontrolledTabs.test.tsx @@ -12,18 +12,18 @@ describe('UncontrolledTabs', () => { const tabs = ( <> - + Item Selected - + Item Not Selected - + Pane 1 - + Pane 2 diff --git a/packages/web-react/src/components/Tabs/__tests__/useTab.test.ts b/packages/web-react/src/components/Tabs/__tests__/useTab.test.ts index 45db78c899..756a459c09 100644 --- a/packages/web-react/src/components/Tabs/__tests__/useTab.test.ts +++ b/packages/web-react/src/components/Tabs/__tests__/useTab.test.ts @@ -4,23 +4,23 @@ import { useTab } from '../useTabs'; describe('useTab', () => { it('should return defaults', () => { const { result } = renderHook(() => useTab(1)); - const { selectedTabId, selectTab } = result.current; + const { selectedId, selectTab } = result.current; - expect(selectedTabId).toBe(1); + expect(selectedId).toBe(1); expect(typeof selectTab).toBe('function'); }); it('should selectTab state', () => { const { result } = renderHook(() => useTab(1)); // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { selectedTabId: _, selectTab } = result.current; + const { selectedId: _, selectTab } = result.current; act(() => { selectTab(2); }); - const { selectedTabId } = result.current; + const { selectedId } = result.current; - expect(selectedTabId).toBe(2); + expect(selectedId).toBe(2); }); }); diff --git a/packages/web-react/src/components/Tabs/__tests__/useTabsStyleProps.test.ts b/packages/web-react/src/components/Tabs/__tests__/useTabsStyleProps.test.ts index b612993e64..9403f6a0c4 100644 --- a/packages/web-react/src/components/Tabs/__tests__/useTabsStyleProps.test.ts +++ b/packages/web-react/src/components/Tabs/__tests__/useTabsStyleProps.test.ts @@ -13,21 +13,21 @@ describe('useTabsStyleProps', () => { }); it('should return selected pane', () => { - const props = { selectedTabId: 'test', tabId: 'test' }; + const props = { selectedId: 'test', id: 'test' }; const { result } = renderHook(() => useTabsStyleProps(props)); expect(result.current.classProps.pane).toBe('TabsPane is-selected'); }); it('should return selected link', () => { - const props = { selectedTabId: 'test', forTab: 'test' }; + const props = { selectedId: 'test', forTabPane: 'test' }; const { result } = renderHook(() => useTabsStyleProps(props)); expect(result.current.classProps.link).toBe('Tabs__link is-selected'); }); it('should return unselected link', () => { - const props = { selectedTabId: '', forTab: '', tabId: '' }; + const props = { selectedId: '', forTabPane: '', id: '' }; const { result } = renderHook(() => useTabsStyleProps(props)); expect(result.current.classProps.link).toBe('Tabs__link'); diff --git a/packages/web-react/src/components/Tabs/demo/TabsDefault.tsx b/packages/web-react/src/components/Tabs/demo/TabsDefault.tsx index 8624dcb759..ec500176a0 100644 --- a/packages/web-react/src/components/Tabs/demo/TabsDefault.tsx +++ b/packages/web-react/src/components/Tabs/demo/TabsDefault.tsx @@ -8,25 +8,25 @@ import TabContent from '../TabContent'; import TabPane from '../TabPane'; const TabsDefault = () => { - const [selectedTabId, setSelectedTabId] = useState(1); + const [selectedId, setSelectedId] = useState(1); - const selectTab = (tabId: TabId) => { - setSelectedTabId(tabId); + const selectTab = (id: TabId) => { + setSelectedId(id); }; return ( - - - Item 1 - Item 2 + + + Item 1 + Item 2 Item link Item link, desktop only - Pane 1 content - Pane 2 content + Pane 1 content + Pane 2 content ); diff --git a/packages/web-react/src/components/Tabs/stories/TabContent.stories.tsx b/packages/web-react/src/components/Tabs/stories/TabContent.stories.tsx index 622a779393..b9351a9ec7 100644 --- a/packages/web-react/src/components/Tabs/stories/TabContent.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/TabContent.stories.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { TabId } from '../../../types'; -import { TabContent, TabContentProps, TabItem, TabList, TabPane, Tabs } from '..'; +import { TabId, TabContentProps } from '../../../types'; +import { TabContent, TabItem, TabList, TabPane, Tabs } from '..'; const meta: Meta = { title: 'Components/Tabs', @@ -13,21 +13,21 @@ export default meta; type Story = StoryObj; const TabsWithHooks = (args: TabContentProps) => { - const [selectedTabId, setState] = useState(1); + const [selectedId, setState] = useState(1); - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); return ( - + - Item Selected - Item + Item Selected + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ); diff --git a/packages/web-react/src/components/Tabs/stories/TabItem.stories.tsx b/packages/web-react/src/components/Tabs/stories/TabItem.stories.tsx index 41d37b129f..68670fb3f3 100644 --- a/packages/web-react/src/components/Tabs/stories/TabItem.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/TabItem.stories.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { TabId } from '../../../types'; -import { TabContent, TabItem, TabItemProps, TabList, TabPane, Tabs } from '..'; +import { TabId, TabItemProps } from '../../../types'; +import { TabContent, TabItem, TabList, TabPane, Tabs } from '..'; const meta: Meta = { title: 'Components/Tabs', @@ -13,23 +13,23 @@ export default meta; type Story = StoryObj; const TabsWithHooks = (args: TabItemProps) => { - const [selectedTabId, setState] = useState(1); + const [selectedId, setState] = useState(1); - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); return ( - + - + Item Selected - Item + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ); diff --git a/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx b/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx index af0739e5d3..a2128e9b30 100644 --- a/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx @@ -23,22 +23,22 @@ export default meta; type Story = StoryObj; const TabsWithHooks = (args: TabLinkProps) => { - const [selectedTabId, setState] = useState(1); + const [selectedId, setState] = useState(1); - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); return ( - + - Item Selected - Item + Item Selected + Item Item Link - Pane 1 - Pane 2 + Pane 1 + Pane 2 ); diff --git a/packages/web-react/src/components/Tabs/stories/TabList.stories.tsx b/packages/web-react/src/components/Tabs/stories/TabList.stories.tsx index 94e5201bac..5468dad719 100644 --- a/packages/web-react/src/components/Tabs/stories/TabList.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/TabList.stories.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { TabId } from '../../../types'; -import { TabContent, TabItem, TabList, TabListProps, TabPane, Tabs } from '..'; +import { TabId, TabListProps } from '../../../types'; +import { TabContent, TabItem, TabList, TabPane, Tabs } from '..'; const meta: Meta = { title: 'Components/Tabs', @@ -13,21 +13,21 @@ export default meta; type Story = StoryObj; const TabsWithHooks = (args: TabListProps) => { - const [selectedTabId, setState] = useState(1); + const [selectedId, setState] = useState(1); - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); return ( - + - Item Selected - Item + Item Selected + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ); diff --git a/packages/web-react/src/components/Tabs/stories/TabPane.stories.tsx b/packages/web-react/src/components/Tabs/stories/TabPane.stories.tsx index 41c5443b63..fcfc22ba2a 100644 --- a/packages/web-react/src/components/Tabs/stories/TabPane.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/TabPane.stories.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { TabId } from '../../../types'; -import { TabContent, TabItem, TabList, TabPane, TabPaneProps, Tabs } from '..'; +import { TabId, TabPaneProps } from '../../../types'; +import { TabContent, TabItem, TabList, TabPane, Tabs } from '..'; const meta: Meta = { title: 'Components/Tabs', @@ -13,23 +13,23 @@ export default meta; type Story = StoryObj; const TabsWithHooks = (args: TabPaneProps) => { - const [selectedTabId, setState] = useState(1); + const [selectedId, setState] = useState(1); - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); return ( - + - Item Selected - Item + Item Selected + Item - + Pane 1 - Pane 2 + Pane 2 ); diff --git a/packages/web-react/src/components/Tabs/stories/Tabs.stories.tsx b/packages/web-react/src/components/Tabs/stories/Tabs.stories.tsx index 68fc581fbf..de8717089f 100644 --- a/packages/web-react/src/components/Tabs/stories/Tabs.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/Tabs.stories.tsx @@ -19,21 +19,21 @@ export default meta; type Story = StoryObj; const TabsWithHooks = (args: TabsProps) => { - const [selectedTabId, setState] = useState(1); + const [selectedId, setState] = useState(1); - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); return ( - + - Item Selected - Item + Item Selected + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ); diff --git a/packages/web-react/src/components/Tabs/stories/UncontrolledTabs.stories.tsx b/packages/web-react/src/components/Tabs/stories/UncontrolledTabs.stories.tsx index 80db984a7d..2093cc8cc5 100644 --- a/packages/web-react/src/components/Tabs/stories/UncontrolledTabs.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/UncontrolledTabs.stories.tsx @@ -23,12 +23,12 @@ export const UncontrolledTabsPlayground: Story = { render: (args) => ( - Item Selected - Item + Item Selected + Item - Pane 1 - Pane 2 + Pane 1 + Pane 2 ), diff --git a/packages/web-react/src/components/Tabs/useTabs.ts b/packages/web-react/src/components/Tabs/useTabs.ts index 1f41e80c7f..357fd1ab6f 100644 --- a/packages/web-react/src/components/Tabs/useTabs.ts +++ b/packages/web-react/src/components/Tabs/useTabs.ts @@ -1,13 +1,13 @@ import { useCallback, useState } from 'react'; import { TabId } from '../../types'; -export const useTab = (initialTabId: TabId) => { - const [selectedTabId, setState] = useState(initialTabId); +export const useTab = (initialId: TabId) => { + const [selectedId, setState] = useState(initialId); // Define and memorize toggler function in case we pass down the component, - const selectTab = useCallback((tabId: TabId) => { - setState(tabId); + const selectTab = useCallback((id: TabId) => { + setState(id); }, []); - return { selectedTabId, selectTab }; + return { selectedId, selectTab }; }; diff --git a/packages/web-react/src/components/Tabs/useTabsStyleProps.ts b/packages/web-react/src/components/Tabs/useTabsStyleProps.ts index f3a8203fb8..a922c8e90e 100644 --- a/packages/web-react/src/components/Tabs/useTabsStyleProps.ts +++ b/packages/web-react/src/components/Tabs/useTabsStyleProps.ts @@ -14,8 +14,8 @@ export interface TabsStyles { props: unknown; } -export function useTabsStyleProps(props: SpiritTabsProps = { selectedTabId: '', forTab: '', tabId: '' }): TabsStyles { - const { selectedTabId, forTab, tabId, ...modifiedProps } = props; +export function useTabsStyleProps(props: SpiritTabsProps = { selectedId: '', forTabPane: '', id: '' }): TabsStyles { + const { selectedId, forTabPane, id, ...modifiedProps } = props; const tabsClass = useClassNamePrefix('Tabs'); const tabsItemClass = `${tabsClass}__item`; @@ -27,10 +27,10 @@ export function useTabsStyleProps(props: SpiritTabsProps = { selectedTabId: '', classProps: { item: tabsItemClass, link: classNames(tabsLinkClass, { - [tabsSelectedClass]: !!forTab && !!selectedTabId && selectedTabId === forTab, + [tabsSelectedClass]: !!forTabPane && !!selectedId && selectedId === forTabPane, }), pane: classNames(tabsPaneClass, { - [tabsSelectedClass]: !!tabId && !!selectedTabId && selectedTabId === tabId, + [tabsSelectedClass]: !!id && !!selectedId && selectedId === id, }), root: tabsClass, }, diff --git a/packages/web-react/src/types/tabs.ts b/packages/web-react/src/types/tabs.ts index ae2e0918a5..b896651476 100644 --- a/packages/web-react/src/types/tabs.ts +++ b/packages/web-react/src/types/tabs.ts @@ -1,16 +1,21 @@ -import { ElementType } from 'react'; -import { ChildrenProps, SpiritPolymorphicElementPropsWithRef, StyleProps, TransferProps } from './shared'; -import { TabsToggler } from '../components'; +import { ElementType, HTMLProps } from 'react'; +import { ChildrenProps, ClickEvents, SpiritPolymorphicElementPropsWithRef, StyleProps, TransferProps } from './shared'; export type TabId = string | number; +export type TabListProps = ChildrenProps & TransferProps; + +export interface TabItemProps extends ChildrenProps, TransferProps, ClickEvents { + forTabPane: TabId; +} + export interface SpiritTabsProps { /** Identification of selected tab */ - selectedTabId?: TabId; + selectedId?: TabId; /** Identification of tab */ - tabId?: TabId; + id?: TabId; /** Identification of affected pane */ - forTab?: TabId; + forTabPane?: TabId; } export interface TabsProps extends ChildrenProps, TransferProps { @@ -19,7 +24,7 @@ export interface TabsProps extends ChildrenProps, TransferProps { onSelectionChange?: (tabId: TabId) => void; } -export type TabLinkItemProps = StyleProps & React.HTMLProps; +export type TabLinkItemProps = StyleProps & HTMLProps; export interface TabLinkBaseProps extends ChildrenProps, StyleProps, TransferProps { itemProps?: TabLinkItemProps; @@ -36,3 +41,22 @@ export type TabLinkProps = { export type SpiritTabLinkProps = TabLinkProps & SpiritPolymorphicElementPropsWithRef>; + +export type TabsToggler = (id: TabId) => void; + +export type TabsContextType = { + selectedId: TabId; + selectTab: TabsToggler; + onSelectionChange?: (id: TabId) => void; +}; + +export interface TabPaneProps extends ChildrenProps, TransferProps { + id: TabId; +} + +export type TabContentProps = ChildrenProps & TransferProps; + +export interface UncontrolledTabsProps extends ChildrenProps, TransferProps { + defaultSelectedTab: TabId; + onSelectionChange?: (id: TabId) => void; +} diff --git a/packages/web-react/tests/testUtils/withTabsContext.tsx b/packages/web-react/tests/testUtils/withTabsContext.tsx index d214710dca..282981e355 100644 --- a/packages/web-react/tests/testUtils/withTabsContext.tsx +++ b/packages/web-react/tests/testUtils/withTabsContext.tsx @@ -1,8 +1,8 @@ import React, { ElementType } from 'react'; -import { TabsContextType, TabsProvider } from '../../src/components/Tabs/TabContext'; +import { TabsContextType, TabsProvider } from '../../src'; export const withTabsContext = - (Component: ElementType, value = { selectedTabId: 0, selectTab: jest.fn() } as TabsContextType) => + (Component: ElementType, value = { selectedId: 0, selectTab: jest.fn() } as TabsContextType) => (props: unknown) => (