diff --git a/src/Tabs/TabPanel.tsx b/src/Tabs/TabPanel.tsx index 47517d54..ac8a9885 100644 --- a/src/Tabs/TabPanel.tsx +++ b/src/Tabs/TabPanel.tsx @@ -10,13 +10,18 @@ export function TabPanel(props: TabPanelProps) { context.registerTab({ id: props.id, title: props.title, + order: props.order, }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.id, props.title, props.order]); - return () => { + useEffect( + () => () => { context.unregisterTab(props.id); - }; + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.id, props.title]); + [props.id], + ); return (
= { @@ -33,7 +34,12 @@ function reducer( (tab) => tab.id === action.newTab.id, ); if (tabWasAlreadyRegistered) { - return state; + return { + ...state, + tabs: state.tabs.map((tab) => + tab.id === action.newTab.id ? action.newTab : tab, + ), + }; } return { @@ -46,14 +52,13 @@ function reducer( const tabsWithoutUnregistered = state.tabs.filter( (tab) => tab.id !== action.tabID, ); - const firstTab = tabsWithoutUnregistered[0]; + if (state.activeTabID !== action.tabID) { + return { ...state, tabs: tabsWithoutUnregistered }; + } return { tabs: tabsWithoutUnregistered, - activeTabID: - state.activeTabID === action.tabID && firstTab - ? firstTab.id - : undefined, + activeTabID: sortedByOrder(tabsWithoutUnregistered)[0]?.id, }; } case 'onSelected': { diff --git a/src/Tabs/TabsHeader.tsx b/src/Tabs/TabsHeader.tsx index 7214487e..ada916bb 100644 --- a/src/Tabs/TabsHeader.tsx +++ b/src/Tabs/TabsHeader.tsx @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import styled, { css } from 'styled-components'; import { TabsContext } from './TabsContext'; +import { sortedByOrder } from './sortedByOrder'; import { TabPanelProps } from './types'; const TitleList = styled.ul` @@ -43,7 +44,7 @@ export function TabsHeader() { return ( - {context.tabs.map((tab: TabPanelProps) => { + {sortedByOrder(context.tabs).map((tab: TabPanelProps) => { const isActiveTab = context.activeTabID === tab.id; return ( diff --git a/src/Tabs/index.test.tsx b/src/Tabs/index.test.tsx index 1bf744f5..718e5e84 100644 --- a/src/Tabs/index.test.tsx +++ b/src/Tabs/index.test.tsx @@ -50,6 +50,77 @@ describe('Tabs', () => { expect(tabContent2).toBeVisible(); }); + it('shows tabs in ascending order, no matter when they mount', () => { + const { rerender } = render( + + + + , + ); + + rerender( + + + + + , + ); + + expect( + screen.getAllByRole('listitem').map((title) => title.textContent), + ).toEqual(['first', 'second', 'third']); + }); + + it('keeps the active tab when another panel changes its props', () => { + const { rerender } = render( + + + content of first + + + content of second + + , + ); + + rerender( + + + content of first + + + content of second + + , + ); + + expect(screen.getByText('content of first')).toBeVisible(); + expect( + screen.getAllByRole('listitem').map((title) => title.textContent), + ).toEqual(['first', 'renamed']); + }); + + it('activates the first displayed tab when the active one unmounts', () => { + const { rerender } = render( + + + + + , + ); + + fireEvent.click(screen.getByText('third')); + + rerender( + + + + , + ); + + expect(screen.getByText('first')).toHaveAttribute('aria-selected', 'true'); + }); + it('set active tab to last tabID', () => { const title1 = 'My tab title1'; const content1 = 'My tab content1'; diff --git a/src/Tabs/sortedByOrder.ts b/src/Tabs/sortedByOrder.ts new file mode 100644 index 00000000..e8b957c6 --- /dev/null +++ b/src/Tabs/sortedByOrder.ts @@ -0,0 +1,7 @@ +import { TabPanelProps } from './types'; + +export function sortedByOrder( + tabs: Array>, +): Array> { + return [...tabs].sort((tab, other) => (tab.order ?? 0) - (other.order ?? 0)); +} diff --git a/src/Tabs/types.ts b/src/Tabs/types.ts index 8947ccdf..54b28d7f 100644 --- a/src/Tabs/types.ts +++ b/src/Tabs/types.ts @@ -11,5 +11,9 @@ export type TabsProps = { export type TabPanelProps = { id: TTabID; title: ReactNode; + + /** Ascending, panels sharing an order keep the order they mounted in. */ + order?: number; + children?: ReactNode; };