-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: restore design system Tabs api (#4949)
- Loading branch information
1 parent
96d6884
commit 59cf99b
Showing
7 changed files
with
230 additions
and
77 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
packages/design-system/src/components/Tabs/Primitive/TabPanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,60 @@ | ||
import { describe, it, expect } from '@jest/globals'; | ||
import { axe } from 'jest-axe'; | ||
import { render } from '@testing-library/react'; | ||
import { Tabs, TabPanel, Tab, TabsProvider } from './'; | ||
import { Tabs } from './'; | ||
|
||
jest.mock('@talend/utils', () => { | ||
let i = 0; | ||
return { | ||
// we need stable but different uuid (is fixed to 42 by current mock) | ||
randomUUID: () => `mocked-uuid-${i++}`, | ||
}; | ||
}); | ||
|
||
describe('Tabs', () => { | ||
it('should render accessible html', async () => { | ||
// note we need to add the aria-label to be accessible | ||
// TODO: make it required | ||
const { container } = render( | ||
<TabsProvider defaultActiveKey="profile"> | ||
<Tabs> | ||
<Tab aria-controls="home" title="Home" /> | ||
<Tab aria-controls="profile" title="Profile" /> | ||
<Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs> | ||
<TabPanel id="home">Tab content for Home</TabPanel> | ||
<TabPanel id="profile">Tab content for Profile</TabPanel> | ||
<TabPanel id="contact">Tab content for Contact</TabPanel> | ||
</TabsProvider>, | ||
<Tabs.Container id="kit" defaultActiveKey="profile"> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="home" title="Home" /> | ||
<Tabs.Tab aria-controls="profile" title="Profile" /> | ||
<Tabs.Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs.List> | ||
<Tabs.Panel id="home">Tab content for Home</Tabs.Panel> | ||
<Tabs.Panel id="profile">Tab content for Profile</Tabs.Panel> | ||
<Tabs.Panel id="contact">Tab content for Contact</Tabs.Panel> | ||
</Tabs.Container>, | ||
); | ||
expect(container.firstChild).toMatchSnapshot(); | ||
const results = await axe(document.body); | ||
expect(results).toHaveNoViolations(); | ||
}); | ||
it('should render accessible html with old api', async () => { | ||
render( | ||
<Tabs | ||
id="old" | ||
tabs={[ | ||
{ | ||
tabTitle: 'Tabs 1', | ||
tabContent: <>Tab 1</>, | ||
}, | ||
{ | ||
tabTitle: 'Tabs 2', | ||
tabContent: <>Tab 2</>, | ||
}, | ||
{ | ||
tabTitle: { | ||
title: 'Tabs 3', | ||
icon: 'user', | ||
}, | ||
tabContent: <>Tab 3</>, | ||
}, | ||
]} | ||
/>, | ||
); | ||
const results = await axe(document.body); | ||
expect(results).toHaveNoViolations(); | ||
}); | ||
}); |
4 changes: 3 additions & 1 deletion
4
packages/design-system/src/components/Tabs/__snapshots__/Tabs.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1 @@ | ||
export * from './Primitive/TabsProvider'; | ||
export * from './Primitive/Tabs'; | ||
export * from './Primitive/TabPanel'; | ||
export * from './variants/Tabs'; |
92 changes: 92 additions & 0 deletions
92
packages/design-system/src/components/Tabs/variants/Tabs.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { TabsProvider, TabsProviderPropTypes } from '../Primitive/TabsProvider'; | ||
import { Tabs as TabList, Tab, TabPropTypes } from '../Primitive/Tabs'; | ||
import { TabPanel, TabPanelPropTypes } from '../Primitive/TabPanel'; | ||
import { useEffect, useState } from 'react'; | ||
import { randomUUID } from '@talend/utils'; | ||
|
||
type TabTitlePropTypes = Omit<TabPropTypes, 'aria-controls'> & { | ||
id?: string; | ||
}; | ||
|
||
type TabItemPropTypes = { | ||
tabTitle?: TabTitlePropTypes | string; | ||
tabContent: React.ReactNode; | ||
}; | ||
|
||
export type TabsProps = { | ||
id?: string; | ||
tabs: TabItemPropTypes[]; | ||
selectedId?: string; | ||
size?: 'S' | 'M' | 'L'; | ||
}; | ||
|
||
export function Tabs(props: TabsProps) { | ||
const [ids, setIds] = useState<string[]>([]); | ||
useEffect(() => { | ||
if (ids.length !== props.tabs.length) { | ||
setIds(props.tabs.map(() => randomUUID())); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [props.tabs]); | ||
if (props.tabs) { | ||
const tabProviderProps: Partial<TabsProviderPropTypes> = { | ||
id: props.id, | ||
size: props.size, | ||
defaultActiveKey: props.selectedId, | ||
}; | ||
if (props.tabs.length > 0 && !props.selectedId) { | ||
if (typeof props.tabs[0].tabTitle === 'string') { | ||
tabProviderProps.defaultActiveKey = props.tabs[0].tabTitle; | ||
} else if (typeof props.tabs[0].tabTitle === 'object') { | ||
tabProviderProps.defaultActiveKey = props.tabs[0].tabTitle.id; | ||
} | ||
} | ||
return ( | ||
<TabsProvider {...tabProviderProps}> | ||
<TabList> | ||
{props.tabs.map((tab: TabItemPropTypes, index: number) => { | ||
const tabProps: Partial<TabPropTypes> = {}; | ||
if (typeof tab.tabTitle === 'string') { | ||
tabProps['aria-controls'] = ids[index]; | ||
tabProps.title = tab.tabTitle; | ||
} else if (typeof tab.tabTitle === 'object') { | ||
tabProps['aria-controls'] = tab.tabTitle.id || ids[index]; | ||
tabProps.title = tab.tabTitle.title; | ||
tabProps.icon = tab.tabTitle.icon; | ||
tabProps.tag = tab.tabTitle.tag; | ||
tabProps.tooltip = tab.tabTitle.tooltip; | ||
tabProps.disabled = tab.tabTitle.disabled; | ||
} | ||
return <Tab key={index} {...(tabProps as TabPropTypes)} />; | ||
})} | ||
</TabList> | ||
{props.tabs.map((tab: TabItemPropTypes, index: number) => { | ||
const tabPanelProps: Partial<TabPanelPropTypes> = {}; | ||
if (typeof tab.tabTitle === 'string') { | ||
tabPanelProps.id = ids[index]; | ||
} else if (typeof tab.tabTitle === 'object') { | ||
tabPanelProps.id = tab.tabTitle.id || ids[index]; | ||
} | ||
return ( | ||
<TabPanel key={index} {...(tabPanelProps as TabPanelPropTypes)}> | ||
{tab.tabContent} | ||
</TabPanel> | ||
); | ||
})} | ||
</TabsProvider> | ||
); | ||
} | ||
return null; | ||
} | ||
|
||
Tabs as typeof Tabs & { | ||
Container: typeof TabsProvider; | ||
List: typeof TabList; | ||
Panel: typeof TabPanel; | ||
Tab: typeof Tab; | ||
}; | ||
|
||
Tabs.Container = TabsProvider; | ||
Tabs.List = TabList; | ||
Tabs.Panel = TabPanel; | ||
Tabs.Tab = Tab; |
146 changes: 86 additions & 60 deletions
146
packages/design-system/src/stories/navigation/Tabs.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,102 +1,128 @@ | ||
import { useState } from 'react'; | ||
import { StackHorizontal, StackVertical, Tabs, TabsProvider, Tab, TabPanel } from '../../'; | ||
import { StackHorizontal, StackVertical, Tabs } from '../../'; | ||
|
||
export default { component: Tabs, title: 'Navigation/Tabs' }; | ||
|
||
export const Styles = () => ( | ||
<StackHorizontal gap="M" justify="spaceBetween"> | ||
<StackVertical gap="S" align="center"> | ||
<h2>Default</h2> | ||
<TabsProvider defaultActiveKey="profile"> | ||
<Tabs> | ||
<Tab aria-controls="home" title="Home" /> | ||
<Tab aria-controls="profile" title="Profile" /> | ||
<Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs> | ||
<TabPanel id="home">Tab content for Home</TabPanel> | ||
<TabPanel id="profile">Tab content for Profile</TabPanel> | ||
<TabPanel id="contact">Tab content for Contact</TabPanel> | ||
</TabsProvider> | ||
<Tabs.Container defaultActiveKey="profile"> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="home" title="Home" /> | ||
<Tabs.Tab aria-controls="profile" title="Profile" /> | ||
<Tabs.Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs.List> | ||
<Tabs.Panel id="home">Tab content for Home</Tabs.Panel> | ||
<Tabs.Panel id="profile">Tab content for Profile</Tabs.Panel> | ||
<Tabs.Panel id="contact">Tab content for Contact</Tabs.Panel> | ||
</Tabs.Container> | ||
</StackVertical> | ||
<StackVertical gap="S" align="center"> | ||
<h2>Large</h2> | ||
<TabsProvider size="L" defaultActiveKey="profile"> | ||
<Tabs> | ||
<Tab aria-controls="home" title="Home" /> | ||
<Tab aria-controls="profile" title="Profile" /> | ||
<Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs> | ||
<TabPanel id="home">Tab content for Home</TabPanel> | ||
<TabPanel id="profile">Tab content for Profile</TabPanel> | ||
<TabPanel id="contact">Tab content for Contact</TabPanel> | ||
</TabsProvider> | ||
<Tabs.Container size="L" defaultActiveKey="profile"> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="home" title="Home" /> | ||
<Tabs.Tab aria-controls="profile" title="Profile" /> | ||
<Tabs.Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs.List> | ||
<Tabs.Panel id="home">Tab content for Home</Tabs.Panel> | ||
<Tabs.Panel id="profile">Tab content for Profile</Tabs.Panel> | ||
<Tabs.Panel id="contact">Tab content for Contact</Tabs.Panel> | ||
</Tabs.Container> | ||
</StackVertical> | ||
</StackHorizontal> | ||
); | ||
|
||
export const TabsWithIcon = () => ( | ||
<TabsProvider defaultActiveKey="profile"> | ||
<Tabs> | ||
<Tab aria-controls="user" title="User" icon="user" /> | ||
<Tab aria-controls="calendar" title="Calendar" icon="calendar" /> | ||
<Tab aria-controls="favorite" title="Favorite" icon="star" disabled /> | ||
</Tabs> | ||
<TabPanel id="user">Users tab content</TabPanel> | ||
<TabPanel id="calendar">Calendar tab content</TabPanel> | ||
<TabPanel id="favorite">Favorite tab content</TabPanel> | ||
</TabsProvider> | ||
<Tabs.Container defaultActiveKey="profile"> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="user" title="User" icon="user" /> | ||
<Tabs.Tab aria-controls="calendar" title="Calendar" icon="calendar" /> | ||
<Tabs.Tab aria-controls="favorite" title="Favorite" icon="star" disabled /> | ||
</Tabs.List> | ||
<Tabs.Panel id="user">Users tab content</Tabs.Panel> | ||
<Tabs.Panel id="calendar">Calendar tab content</Tabs.Panel> | ||
<Tabs.Panel id="favorite">Favorite tab content</Tabs.Panel> | ||
</Tabs.Container> | ||
); | ||
|
||
export const TabsWithTag = () => ( | ||
<TabsProvider defaultActiveKey="profile"> | ||
<Tabs> | ||
<Tab aria-controls="user" title="User" icon="user" tag={13} /> | ||
<Tab aria-controls="calendar" title="Calendar" icon="calendar" tag={54} /> | ||
<Tab | ||
<Tabs.Container defaultActiveKey="profile"> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="user" title="User" icon="user" tag={13} /> | ||
<Tabs.Tab aria-controls="calendar" title="Calendar" icon="calendar" tag={54} /> | ||
<Tabs.Tab | ||
aria-controls="favorite" | ||
title="Favorite" | ||
icon="star" | ||
tag="999+" | ||
tooltip="1534 Favorite items" | ||
/> | ||
</Tabs> | ||
<TabPanel id="user">Users tab content</TabPanel> | ||
<TabPanel id="calendar">Calendar tab content</TabPanel> | ||
<TabPanel id="favorite">Favorite tab content</TabPanel> | ||
</TabsProvider> | ||
</Tabs.List> | ||
<Tabs.Panel id="user">Users tab content</Tabs.Panel> | ||
<Tabs.Panel id="calendar">Calendar tab content</Tabs.Panel> | ||
<Tabs.Panel id="favorite">Favorite tab content</Tabs.Panel> | ||
</Tabs.Container> | ||
); | ||
|
||
export const TabsWithLongTitles = () => ( | ||
<TabsProvider defaultActiveKey="user"> | ||
<Tabs> | ||
<Tab aria-controls="user" title="User" icon="user" tag={13} /> | ||
<Tab | ||
<Tabs.Container defaultActiveKey="user"> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="user" title="User" icon="user" tag={13} /> | ||
<Tabs.Tab | ||
aria-controls="notification" | ||
title="A much too long title that will trigger the overflow limit" | ||
icon="information-stroke" | ||
tag="999+" | ||
tooltip="1239 notifications - A much too long title that will trigger the overflow limit" | ||
/> | ||
</Tabs> | ||
<TabPanel id="user">Users tab content</TabPanel> | ||
<TabPanel id="notification"> | ||
</Tabs.List> | ||
<Tabs.Panel id="user">Users tab content</Tabs.Panel> | ||
<Tabs.Panel id="notification"> | ||
<h2>About tab content</h2> | ||
</TabPanel> | ||
</TabsProvider> | ||
</Tabs.Panel> | ||
</Tabs.Container> | ||
); | ||
|
||
export const TabStandaloneControlled = () => { | ||
const [key, setKey] = useState<string>('home'); | ||
return ( | ||
<TabsProvider activeKey={key} onSelect={(e, k) => setKey(k)}> | ||
<Tabs> | ||
<Tab aria-controls="home" title="Home" /> | ||
<Tab aria-controls="profile" title="Profile" /> | ||
<Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs> | ||
<TabPanel id="home">Tab content for Home</TabPanel> | ||
<TabPanel id="profile">Tab content for Profile</TabPanel> | ||
<TabPanel id="contact">Tab content for Contact</TabPanel> | ||
</TabsProvider> | ||
<Tabs.Container activeKey={key} onSelect={(e, k) => setKey(k)}> | ||
<Tabs.List> | ||
<Tabs.Tab aria-controls="home" title="Home" /> | ||
<Tabs.Tab aria-controls="profile" title="Profile" /> | ||
<Tabs.Tab aria-controls="contact" title="Contact" disabled /> | ||
</Tabs.List> | ||
<Tabs.Panel id="home">Tab content for Home</Tabs.Panel> | ||
<Tabs.Panel id="profile">Tab content for Profile</Tabs.Panel> | ||
<Tabs.Panel id="contact">Tab content for Contact</Tabs.Panel> | ||
</Tabs.Container> | ||
); | ||
}; | ||
|
||
export const TabAPI = () => ( | ||
<Tabs | ||
tabs={[ | ||
{ | ||
tabTitle: 'Tabs 1', | ||
tabContent: <>Tab 1</>, | ||
}, | ||
{ | ||
tabTitle: { | ||
title: 'Tabs 2', | ||
}, | ||
tabContent: <>Tab 2</>, | ||
}, | ||
{ | ||
tabTitle: { | ||
title: 'Tabs 3', | ||
icon: 'user', | ||
tag: '999+', | ||
tooltip: '1534 Favorite', | ||
}, | ||
tabContent: <>Tab 3</>, | ||
}, | ||
]} | ||
/> | ||
); |