Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/components/experimental/Tabs/Tabs.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Tabs, TabList, Tab, TabPanel } from './Tabs';

describe('Experimental: Tabs', () => {
it('renders tabs and switch between them', async () => {
const user = userEvent.setup();
render(
<Tabs>
<TabList aria-label="Tabs">
<Tab id="T1">Tab 1</Tab>
<Tab id="T2">Tab 2</Tab>
<Tab id="T3">Tab 3</Tab>
</TabList>
<TabPanel id="T1">Content of Tab 1</TabPanel>
<TabPanel id="T2">Content of Tab 2</TabPanel>
<TabPanel id="T3">Content of Tab 3</TabPanel>
</Tabs>
);

expect(screen.getByRole('tab', { name: 'Tab 1' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByText('Content of Tab 1')).toBeInTheDocument();

await user.click(screen.getByRole('tab', { name: 'Tab 3' }));

expect(screen.getByRole('tab', { name: 'Tab 3' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByText('Content of Tab 3')).toBeInTheDocument();
expect(screen.queryByText('Content of Tab 1')).not.toBeInTheDocument();
});

it('handles disabled tabs', async () => {
const user = userEvent.setup();
render(
<Tabs>
<TabList aria-label="Tabs">
<Tab id="T1">Tab 1</Tab>
<Tab id="T2" isDisabled>
Tab 2
</Tab>
</TabList>
<TabPanel id="T1">Founding.</TabPanel>
<TabPanel id="T2">Monarchy.</TabPanel>
</Tabs>
);

await user.click(screen.getByRole('tab', { name: 'Tab 2' }));
expect(screen.getByRole('tab', { name: 'Tab 1' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByRole('tab', { name: 'Tab 2' })).toHaveAttribute('aria-selected', 'false');
});
});
125 changes: 125 additions & 0 deletions src/components/experimental/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { ReactElement, ComponentType } from 'react';
import {
Tabs as BaseTabs,
TabList as BaseTabList,
Tab as BaseTab,
TabPanel as BaseTabPanel,
TabsProps,
TabListProps,
TabProps,
TabPanelProps
} from 'react-aria-components';
import styled from 'styled-components';
import { get } from '../../../utils/experimental/themeGet';
import { getSemanticValue } from '../../../essentials/experimental';
import { textStyles } from '../Text/Text';

const StyledTabs = styled(BaseTabs as ComponentType<TabsProps>)`
display: flex;
gap: ${get('space.4')};

&[data-orientation='vertical'] {
flex-direction: row;
}

&[data-orientation='horizontal'] {
flex-direction: column;
}
`;

const StyledTabList = styled(BaseTabList as ComponentType<TabListProps<Record<string, unknown>>>)`
display: flex;
gap: ${get('space.4')};

&[data-orientation='vertical'] {
flex-direction: column;
}

&[data-orientation='horizontal'] {
flex-direction: row;
}
`;

const StyledTab = styled(BaseTab as ComponentType<TabProps>)`
position: relative;
cursor: pointer;
outline: none;
padding: ${get('space.2')} 0;
${textStyles.variants.label1};
color: ${getSemanticValue('on-surface-variant')};
transition: color 200ms ease;

display: flex;
align-items: center;
justify-content: center;

&[data-hovered] {
color: ${getSemanticValue('on-surface')};
}

&[data-selected] {
color: ${getSemanticValue('accent')};
}

&[data-disabled] {
color: ${getSemanticValue('on-surface-variant')};
opacity: 0.38;
cursor: default;
}

&::after {
content: '';
position: absolute;
background: ${getSemanticValue('accent')};
opacity: 0;
transition: opacity 200ms ease;
}

[data-orientation='vertical'] &::after {
top: 50%;
transform: translateY(-50%);
right: -1px;
width: 2px;
height: 85%;
}

[data-orientation='horizontal'] &::after {
left: 50%;
transform: translateX(-50%);
bottom: -1px;
height: 2px;
width: 85%;
}

&[data-selected]::after {
opacity: 1;
}

&[data-focus-visible] {
outline: 0.125rem solid ${getSemanticValue('accent')};
outline-offset: 0.125rem;
}
`;

const StyledTabPanel = styled(BaseTabPanel as ComponentType<TabPanelProps>)`
outline: none;
${textStyles.variants.body1};
`;

function Tabs(props: TabsProps): ReactElement {
return <StyledTabs {...props} />;
}

function TabList<T extends Record<string, unknown>>(props: TabListProps<T>): ReactElement {
return <StyledTabList {...props} />;
}

function Tab(props: TabProps): ReactElement {
return <StyledTab {...props} />;
}

function TabPanel(props: TabPanelProps): ReactElement {
return <StyledTabPanel {...props} />;
}

export { Tabs, TabList, Tab, TabPanel };
78 changes: 78 additions & 0 deletions src/components/experimental/Tabs/docs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { StoryObj, Meta } from '@storybook/react';
import { Tabs, TabList, Tab, TabPanel } from '../Tabs';

const meta: Meta = {
title: 'Experimental/Components/Tabs',
component: Tabs,
parameters: {
layout: 'centered'
},
argTypes: {
keyboardActivation: {
control: 'radio',
options: ['automatic', 'manual']
},
orientation: {
control: 'radio',
options: ['horizontal', 'vertical']
},
isDisabled: {
control: 'boolean'
}
}
};

export default meta;

type Story = StoryObj<typeof Tabs>;

export const Default: Story = {
render: args => (
<Tabs {...args}>
<TabList aria-label="Tabs">
<Tab id="T1">Tab 1</Tab>
<Tab id="T2">Tab 2</Tab>
<Tab id="T3">Tab 3</Tab>
</TabList>
<TabPanel id="T1">Content of Tab 1</TabPanel>
<TabPanel id="T2">Content of Tab 2</TabPanel>
<TabPanel id="T3">Content of Tab 3</TabPanel>
</Tabs>
)
};

export const DisabledTab: Story = {
render: args => (
<Tabs {...args}>
<TabList aria-label="Tabs">
<Tab id="T1">Tab 1</Tab>
<Tab id="T2" isDisabled>
Tab 2
</Tab>
<Tab id="T3">Tab 3</Tab>
</TabList>
<TabPanel id="T1">Content of Tab 1</TabPanel>
<TabPanel id="T2">Content of Tab 2</TabPanel>
<TabPanel id="T3">Content of Tab 3</TabPanel>
</Tabs>
)
};

export const DisabledTabs: Story = {
args: {
isDisabled: true
},
render: args => (
<Tabs {...args}>
<TabList aria-label="Tabs">
<Tab id="T1">Tab 1</Tab>
<Tab id="T2">Tab 2</Tab>
<Tab id="T3">Tab 3</Tab>
</TabList>
<TabPanel id="T1">Content of Tab 1</TabPanel>
<TabPanel id="T2">Content of Tab 2</TabPanel>
<TabPanel id="T3">Content of Tab 3</TabPanel>
</Tabs>
)
};
1 change: 1 addition & 0 deletions src/components/experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { Search } from './Search/Search';
export { Select } from './Select/Select';
export { Snackbar, SnackbarProps } from './Snackbar/Snackbar';
export { Table, Row, Cell, Skeleton, Column, TableBody, TableHeader } from './Table/Table';
export { Tabs, TabList, Tab, TabPanel } from './Tabs/Tabs';
export { Text } from './Text/Text';
export { TextField } from './TextField/TextField';
export { TimeField } from './TimeField/TimeField';
Expand Down
Loading