diff --git a/src/components/experimental/Tabs/Tabs.spec.tsx b/src/components/experimental/Tabs/Tabs.spec.tsx new file mode 100644 index 00000000..c5cc3ccd --- /dev/null +++ b/src/components/experimental/Tabs/Tabs.spec.tsx @@ -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( + + + Tab 1 + Tab 2 + Tab 3 + + Content of Tab 1 + Content of Tab 2 + Content of Tab 3 + + ); + + 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( + + + Tab 1 + + Tab 2 + + + Founding. + Monarchy. + + ); + + 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'); + }); +}); diff --git a/src/components/experimental/Tabs/Tabs.tsx b/src/components/experimental/Tabs/Tabs.tsx new file mode 100644 index 00000000..1a0b7215 --- /dev/null +++ b/src/components/experimental/Tabs/Tabs.tsx @@ -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)` + display: flex; + gap: ${get('space.4')}; + + &[data-orientation='vertical'] { + flex-direction: row; + } + + &[data-orientation='horizontal'] { + flex-direction: column; + } +`; + +const StyledTabList = styled(BaseTabList as ComponentType>>)` + display: flex; + gap: ${get('space.4')}; + + &[data-orientation='vertical'] { + flex-direction: column; + } + + &[data-orientation='horizontal'] { + flex-direction: row; + } +`; + +const StyledTab = styled(BaseTab as ComponentType)` + 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)` + outline: none; + ${textStyles.variants.body1}; +`; + +function Tabs(props: TabsProps): ReactElement { + return ; +} + +function TabList>(props: TabListProps): ReactElement { + return ; +} + +function Tab(props: TabProps): ReactElement { + return ; +} + +function TabPanel(props: TabPanelProps): ReactElement { + return ; +} + +export { Tabs, TabList, Tab, TabPanel }; diff --git a/src/components/experimental/Tabs/docs/Tabs.stories.tsx b/src/components/experimental/Tabs/docs/Tabs.stories.tsx new file mode 100644 index 00000000..eab5e13c --- /dev/null +++ b/src/components/experimental/Tabs/docs/Tabs.stories.tsx @@ -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; + +export const Default: Story = { + render: args => ( + + + Tab 1 + Tab 2 + Tab 3 + + Content of Tab 1 + Content of Tab 2 + Content of Tab 3 + + ) +}; + +export const DisabledTab: Story = { + render: args => ( + + + Tab 1 + + Tab 2 + + Tab 3 + + Content of Tab 1 + Content of Tab 2 + Content of Tab 3 + + ) +}; + +export const DisabledTabs: Story = { + args: { + isDisabled: true + }, + render: args => ( + + + Tab 1 + Tab 2 + Tab 3 + + Content of Tab 1 + Content of Tab 2 + Content of Tab 3 + + ) +}; diff --git a/src/components/experimental/index.ts b/src/components/experimental/index.ts index a283f728..b9395bba 100644 --- a/src/components/experimental/index.ts +++ b/src/components/experimental/index.ts @@ -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';