Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Introduce Tabs component #2678

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
121 changes: 121 additions & 0 deletions src/components/tabs/Tabs.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as React from 'react';
import {fireEvent, render, screen} from '@testing-library/react';
import Tabs, {Tab} from './Tabs';

const hiddenPanelClass = 'sg-tabs__panel--hidden';

describe('<Tabs />', () => {
it('shows by default all tabs and only first panel', () => {
render(
<Tabs>
<Tab.Header>
<Tab.List>
<Tab>First tab</Tab>
<Tab>Second tab</Tab>
</Tab.List>
</Tab.Header>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
</Tabs>
);
expect(screen.getByText('First tab')).toBeInTheDocument();
expect(screen.getByText('Second tab')).toBeInTheDocument();
expect(screen.getByText('Content 1')).toBeInTheDocument();
expect(screen.getByText('Content 2')).toHaveClass(hiddenPanelClass);
});

it('shows panel corresponding to startIndex', () => {
render(
<Tabs startIndex={1}>
<Tab.Header>
<Tab.List>
<Tab>First tab</Tab>
<Tab>Second tab</Tab>
</Tab.List>
</Tab.Header>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
</Tabs>
);
expect(screen.getByText('Content 1')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 2')).not.toHaveClass(hiddenPanelClass);
});

it('correctly handles tab change', () => {
const mockOnChange = jest.fn();

render(
<Tabs onTabChange={mockOnChange}>
<Tab.Header>
<Tab.List>
<Tab>First tab</Tab>
<Tab>Second tab</Tab>
</Tab.List>
</Tab.Header>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
</Tabs>
);
expect(screen.getByText('Content 1')).not.toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 2')).toHaveClass(hiddenPanelClass);
const secondTab = screen.getByText('Second tab');

fireEvent.click(secondTab);
expect(screen.getByText('Content 1')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 2')).not.toHaveClass(hiddenPanelClass);
expect(mockOnChange).toBeCalledWith(
screen.getByText('Second tab').parentElement
);
});

it('makes component controlled if correct props is passed', () => {
const mockActiveIndex = 2;
const mockOnChange = jest.fn();
const {rerender} = render(
<Tabs onTabChange={mockOnChange} activeIndex={mockActiveIndex}>
<Tab.Header>
<Tab.List>
<Tab>First tab</Tab>
<Tab>Second tab</Tab>
<Tab>Third tab</Tab>
</Tab.List>
</Tab.Header>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel>
</Tabs>
);

expect(screen.getByText('Content 1')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 2')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 3')).not.toHaveClass(hiddenPanelClass);

const secondTab = screen.getByText('Second tab');

fireEvent.click(secondTab);

expect(mockOnChange).not.toHaveBeenCalled();
expect(screen.getByText('Content 1')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 2')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 3')).not.toHaveClass(hiddenPanelClass);

rerender(
<Tabs onTabChange={mockOnChange} activeIndex={mockActiveIndex - 1}>
<Tab.Header>
<Tab.List>
<Tab>First tab</Tab>
<Tab>Second tab</Tab>
<Tab>Third tab</Tab>
</Tab.List>
</Tab.Header>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel>
</Tabs>
);

expect(screen.getByText('Content 1')).toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 2')).not.toHaveClass(hiddenPanelClass);
expect(screen.getByText('Content 3')).toHaveClass(hiddenPanelClass);
});
});
209 changes: 209 additions & 0 deletions src/components/tabs/Tabs.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import Button from '../buttons/Button';
import Flex from '../flex/Flex';
import Icon from '../icons/Icon';
import Text from '../text/Text';
import Checkbox from '../form-elements/checkbox/Checkbox';
import PageHeader from 'blocks/PageHeader';
import {useState} from 'react';
import classnames from 'classnames';
import {TabHeaderProps} from './components';
import Tabs, {Tab, TabsProps} from './Tabs.tsx';
import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs';
import TabsA11y from './stories/Tabs.a11y.mdx';

<Meta
title="Components/Tabs"
component={Tabs}
subcomponents={{
Tab,
'Tab.Panel': Tab.Panel,
'Tab.Header': Tab.Header,
'Tab.List': Tab.List,
'Tab.ActiveIndicator': Tab.ActiveIndicator,
}}
argTypes={{
children: {
control: {
disable: true,
},
},
onTabChange: {
table: {
type: {
summary: 'function',
},
},
control: {
type: 'function',
},
},
startIndex: {
description: '(Responsive)',
table: {
type: {
summary: 'number',
},
},
control: {
type: 'number',
},
},
activeIndex: {
description: '(Responsive)',
table: {
type: {
summary: 'number',
},
},
control: {
type: 'number',
},
},
}}
/>

<PageHeader>Tabs</PageHeader>

- [Stories](#stories)
- [Accessibility](#accessibility)

## Overview

<Canvas>
<Story name="Default">
{args => (
<Tabs {...args}>
<Tab.Header>
<Tab.List>
<Tab>Tab name</Tab>
<Tab>Tab name 2</Tab>
<Tab>Tab name with a very very very long name</Tab>
<Tab>Tab</Tab>
<Tab.ActiveIndicator />
</Tab.List>
</Tab.Header>
<Tab.Panel>
<Flex marginTop="xs">Content 1</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 2</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 3</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 4</Flex>
</Tab.Panel>
</Tabs>
)}
</Story>
</Canvas>

<ArgsTable story="Default" />

## Stories

### External Control

<Canvas>
<Story name="External control">
{args => {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Flex style={{gap: 8}}>
<Button
variant="solid-inverted"
iconOnly
icon={<Icon size={16} color="adaptive" type="arrow_left" />}
onClick={() => {
setActiveIndex(activeIndex - 1);
}}
/>
<Button
variant="solid-inverted"
iconOnly
icon={<Icon size={16} color="adaptive" type="arrow_right" />}
onClick={() => {
setActiveIndex(activeIndex + 1);
}}
/>
</Flex>
<Tabs activeIndex={activeIndex}>
<Tab.Header>
<Tab.List>
<Tab disabled>Tab name</Tab>
<Tab disabled>Tab name 2</Tab>
<Tab disabled>Tab name with a very very very long name</Tab>
<Tab disabled>Tab</Tab>
<Tab.ActiveIndicator />
</Tab.List>
</Tab.Header>
<Tab.Panel>
<Flex marginTop="xs">Content 1</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 2</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 3</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 4</Flex>
</Tab.Panel>
</Tabs>
</>
);
}}
</Story>
</Canvas>

### Custom header

<Canvas>
<Story name="Custom header">
{args => {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Tabs startIndex={1}>
<Tab.Header>
<Flex fullHeight fullWidth alignItems="center">
<Tab.List style={{marginRight: 'auto'}} fullHeight>
<Tab>Tab name</Tab>
<Tab>Tab name 2</Tab>
<Tab>Tab name with a very very very long name</Tab>
<Tab>Tab</Tab>
<Tab.ActiveIndicator />
</Tab.List>
<Button
size="s"
variant="solid-inverted"
icon={<Icon size={16} color="adaptive" type="trash" />}
>
Delete all
</Button>
</Flex>
</Tab.Header>
<Tab.Panel>
<Flex marginTop="xs">Content 1</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 2</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 3</Flex>
</Tab.Panel>
<Tab.Panel>
<Flex marginTop="xs">Content 4</Flex>
</Tab.Panel>
</Tabs>
</>
);
}}
</Story>
</Canvas>

## Accessibility

<TabsA11y />
Loading