Skip to content

Commit

Permalink
SOV-580: vertical tabs component (#30)
Browse files Browse the repository at this point in the history
* feat(vertical-tabs): desktop component for vertical tabs

* feat(vertical-tabs): change indicator

* feat(vertical-tabs): add test cases

* chore: move Tabs to molecules and update layout id (#31)

* chore(actions): automation for package releases SOV-863 (#33)

* chore: configure changesets (#34)

* chore(versions): configure changesets

* chore(versions): add default changeset for ui package

* chore: prevent duplicated tests

* Version Packages (#36)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: review comments

* chore: add changeset

* fix: remove leadings

* fix: add hover state

* fix: modify storybook controls

* fix: selectedIndex in the storybook

Co-authored-by: soulBit <[email protected]>
Co-authored-by: Pietro Maximoff <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
5 people authored Oct 19, 2022
1 parent 274b915 commit 32ac879
Show file tree
Hide file tree
Showing 15 changed files with 501 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-bananas-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sovryn/ui': patch
---

SOV-580: add vertical tabs component
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
build/
node_modules/
package-lock.json
yarn.lock
yarn.lock
**/build
**/dist
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix --max-warnings=0",
"eslint --fix --max-warnings=0 --ignore-pattern !packages/ui/.storybook",
"prettier --write"
]
},
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
root: true,
// This tells ESLint to load the config from the package `eslint-config-custom`
extends: ['@sovryn/eslint-config-custom'],
ignorePatterns: ['build/', 'dist/'],
};
2 changes: 1 addition & 1 deletion packages/ui/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const parameters = {
},
})),
},
layout: 'fullscreen',
layout: 'padded',
options: {
storySort: {
order: [
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/.storybook/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
}
}

.sb-main-fullscreen #root {
@apply h-screen;
}

.sb-show-main #root {
@apply relative block p-4;
@apply relative block;
}

.docs-story .innerZoomElementWrapper {
Expand Down
25 changes: 25 additions & 0 deletions packages/ui/src/2_molecules/VerticalTabs/VerticalTabs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.container {
@apply w-full min-h-full flex flex-row justify-center items-start relative;
}

.aside {
@apply h-full min-h-full bg-gray-80 p-[1.25rem] flex-shrink-0 flex-grow-0 w-[19.25rem] relative
flex flex-col justify-between items-start;
transition: clip-path 0.3s ease-in-out;
}

.header {
@apply pt-[2.875rem] pb-[4.5rem] w-full relative;
}

.footer {
@apply pt-[2.875rem] w-full relative;
}

.tabs {
@apply w-full flex flex-col justify-center items-center gap-y-10;
}

.content {
@apply w-full h-full p-[3.25rem] relative overflow-y-auto;
}
116 changes: 116 additions & 0 deletions packages/ui/src/2_molecules/VerticalTabs/VerticalTabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useArgs } from '@storybook/client-api';
import { Story } from '@storybook/react';

import { ComponentProps, useCallback, useReducer, useState } from 'react';

import { Button, Heading } from '../../1_atoms';
import { Dialog } from '../Dialog/Dialog';
import { DialogSize } from '../Dialog/Dialog.types';
import { VerticalTabs } from './VerticalTabs';

const EXCLUDED_CONTROLS = ['header', 'footer', 'onChange'];

export default {
title: 'Molecule/VerticalTabs',
component: VerticalTabs,
parameters: {
layout: 'fullscreen',
controls: {
exclude: EXCLUDED_CONTROLS,
},
},
};

const Template: Story<ComponentProps<typeof VerticalTabs>> = args => {
const [, updateArgs] = useArgs();
const handleOnChange = useCallback(
(index: number) => updateArgs({ selectedIndex: index }),
[updateArgs],
);

return <VerticalTabs {...args} onChange={handleOnChange} />;
};

const DialogTemplate: Story<ComponentProps<typeof VerticalTabs>> = args => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [isDialogOpen, toggle] = useReducer(a => !a, false);
return (
<>
<Button onClick={toggle} text="Open Dialog" />
<Dialog isOpen={isDialogOpen} width={DialogSize.xl2}>
<VerticalTabs
{...args}
selectedIndex={selectedIndex}
onChange={setSelectedIndex}
footer={() => (
<button onClick={toggle} className="text-primary-20 text-xs">
Close
</button>
)}
/>
</Dialog>
</>
);
};

export const Basic = Template.bind({});
Basic.args = {
items: [
{ label: 'Tab 1', content: 'Tab 1 Content' },
{ label: 'Tab 2', content: 'Tab 2 Content' },
{ label: 'Tab 3', content: 'Tab 3 Content' },
{
label: 'Tab4 ',
infoText: 'Example with long content',
content: (
<div>
<Heading>Long List</Heading>
<ol>
{new Array(100).fill('Row').map((item, index) => (
<li key={index}>{item}</li>
))}
</ol>
</div>
),
},
],
selectedIndex: 0,
header: props => (
<Heading>Selected: {props.items[props.selectedIndex].label}</Heading>
),
footer: () => <div>Footer</div>,
className: '',
tabsClassName: '',
contentClassName: '',
};

export const InDialog = DialogTemplate.bind({});
InDialog.args = {
items: [
{
label: 'Hardware Wallet',
infoText: 'Select the hardware wallet you want to connect',
content: 'Content of HW tab',
},
{
label: 'Browser Wallet',
infoText: 'Select the web3 wallet you want to connect',
content: 'Tab 2 Content',
},
{
label: "Don't have a wallet",
infoText: 'Read the following instructions',
content: 'Tab 3 Content',
},
],
header: () => <Heading>Connect Wallet</Heading>,
tabsClassName: 'rounded-l-lg',
className: 'rounded-lg',
};

InDialog.parameters = {
layout: 'centered',
controls: {
exclude: [...EXCLUDED_CONTROLS, 'selectedIndex'],
},
};
160 changes: 160 additions & 0 deletions packages/ui/src/2_molecules/VerticalTabs/VerticalTabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import React, { useState } from 'react';

import { VerticalTabs } from './VerticalTabs';
import { VerticalTabsItem } from './VerticalTabs.types';

const INITIAL_TAB = 2;
const DISABLED_TAB = 1;
const TAB_ITEMS: VerticalTabsItem[] = [
{
label: 'Tab 1',
infoText: 'Info text 1',
content: 'Content 1',
},
{
label: 'Tab 2',
disabled: true,
content: 'Content 2',
},
{
label: 'Tab 3',
content: 'Content 3',
},
];

const TestComponent = () => {
const [index, setIndex] = useState(INITIAL_TAB);
return (
<VerticalTabs
items={TAB_ITEMS}
selectedIndex={index}
onChange={setIndex}
header={() => <h1>This is header</h1>}
footer={() => <p>This is footer</p>}
/>
);
};

describe('VerticalTabs', () => {
it('renders all of the tabs', () => {
const { getByText } = render(<TestComponent />);

TAB_ITEMS.forEach(item => {
expect(getByText(item.label as string)).toBeInTheDocument();
});
});

it('renders initial content', () => {
const { getByText } = render(<TestComponent />);
expect(
getByText(TAB_ITEMS[INITIAL_TAB].content as string),
).toBeInTheDocument();
});

it('renders header', () => {
const { getByText } = render(<TestComponent />);
expect(getByText('This is header')).toBeInTheDocument();
});

it('renders footer', () => {
const { getByText } = render(<TestComponent />);
expect(getByText('This is footer')).toBeInTheDocument();
});

it('does not render non selected tab content', () => {
const { getByText } = render(<TestComponent />);
expect(() => getByText(TAB_ITEMS[0].content as string)).toThrow();
});

it('switches tab content when clicked', () => {
const { getByText } = render(<TestComponent />);

userEvent.click(getByText(TAB_ITEMS[0].label as string));
expect(getByText(TAB_ITEMS[0].content as string)).toBeInTheDocument();
});

it('does not switch to content of disabled tab', () => {
const { getByText } = render(<TestComponent />);

userEvent.click(getByText(TAB_ITEMS[DISABLED_TAB].label as string));
expect(
getByText(TAB_ITEMS[INITIAL_TAB].content as string),
).toBeInTheDocument();
expect(() =>
getByText(TAB_ITEMS[DISABLED_TAB].content as string),
).toThrow();
});

it('triggers onChange callback when tab item is clicked', () => {
const mockFunction = jest.fn();

const { getByText } = render(
<VerticalTabs
items={TAB_ITEMS}
selectedIndex={0}
onChange={mockFunction}
/>,
);

userEvent.click(getByText(TAB_ITEMS[0].label as string));
expect(mockFunction).toHaveBeenCalledTimes(1);
});

it('does not trigger onChange callback when disabled tab item is clicked', () => {
const mockFunction = jest.fn();

const { getByText } = render(
<VerticalTabs
items={TAB_ITEMS}
selectedIndex={0}
onChange={mockFunction}
/>,
);

userEvent.click(getByText(TAB_ITEMS[DISABLED_TAB].label as string));
expect(mockFunction).toHaveBeenCalledTimes(0);
});

it('adds className to vertical tabs root wrapper', () => {
const { container } = render(
<VerticalTabs
items={TAB_ITEMS}
selectedIndex={0}
className="test-class"
/>,
);
// adding "container" to make sure correct element is found
expect(container.firstChild).toHaveClass('container test-class');
});

it('adds className to tab list wrapper', () => {
const { container } = render(
<VerticalTabs
items={TAB_ITEMS}
selectedIndex={0}
tabsClassName="test-class"
/>,
);

// adding "aside" to make sure correct element is found
expect(container.firstChild?.childNodes[0]).toHaveClass('aside test-class');
});

it('adds className to tab content wrapper', () => {
const { container } = render(
<VerticalTabs
items={TAB_ITEMS}
selectedIndex={0}
contentClassName="test-class"
/>,
);

// adding "content" to make sure correct element is found
expect(container.firstChild?.childNodes[1]).toHaveClass(
'content test-class',
);
});
});
Loading

0 comments on commit 32ac879

Please sign in to comment.