Skip to content
Open
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
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