Skip to content

Commit 32ac879

Browse files
creed-victorsoulBitpietro-maximoffgithub-actions[bot]
authored
SOV-580: vertical tabs component (#30)
* 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 <its.soulBit@gmail.com> Co-authored-by: Pietro Maximoff <74987028+pietro-maximoff@users.noreply.github.com> 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>
1 parent 274b915 commit 32ac879

15 files changed

Lines changed: 501 additions & 5 deletions

.changeset/nice-bananas-deliver.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sovryn/ui': patch
3+
---
4+
5+
SOV-580: add vertical tabs component

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
build/
22
node_modules/
33
package-lock.json
4-
yarn.lock
4+
yarn.lock
5+
**/build
6+
**/dist

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
"lint-staged": {
3535
"*.{js,ts,tsx}": [
36-
"eslint --fix --max-warnings=0",
36+
"eslint --fix --max-warnings=0 --ignore-pattern !packages/ui/.storybook",
3737
"prettier --write"
3838
]
3939
},

packages/ui/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
root: true,
3+
// This tells ESLint to load the config from the package `eslint-config-custom`
4+
extends: ['@sovryn/eslint-config-custom'],
5+
ignorePatterns: ['build/', 'dist/'],
6+
};

packages/ui/.storybook/preview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const parameters = {
4646
},
4747
})),
4848
},
49-
layout: 'fullscreen',
49+
layout: 'padded',
5050
options: {
5151
storySort: {
5252
order: [

packages/ui/.storybook/tailwind.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919
}
2020
}
2121

22+
.sb-main-fullscreen #root {
23+
@apply h-screen;
24+
}
25+
2226
.sb-show-main #root {
23-
@apply relative block p-4;
27+
@apply relative block;
2428
}
2529

2630
.docs-story .innerZoomElementWrapper {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.container {
2+
@apply w-full min-h-full flex flex-row justify-center items-start relative;
3+
}
4+
5+
.aside {
6+
@apply h-full min-h-full bg-gray-80 p-[1.25rem] flex-shrink-0 flex-grow-0 w-[19.25rem] relative
7+
flex flex-col justify-between items-start;
8+
transition: clip-path 0.3s ease-in-out;
9+
}
10+
11+
.header {
12+
@apply pt-[2.875rem] pb-[4.5rem] w-full relative;
13+
}
14+
15+
.footer {
16+
@apply pt-[2.875rem] w-full relative;
17+
}
18+
19+
.tabs {
20+
@apply w-full flex flex-col justify-center items-center gap-y-10;
21+
}
22+
23+
.content {
24+
@apply w-full h-full p-[3.25rem] relative overflow-y-auto;
25+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { useArgs } from '@storybook/client-api';
2+
import { Story } from '@storybook/react';
3+
4+
import { ComponentProps, useCallback, useReducer, useState } from 'react';
5+
6+
import { Button, Heading } from '../../1_atoms';
7+
import { Dialog } from '../Dialog/Dialog';
8+
import { DialogSize } from '../Dialog/Dialog.types';
9+
import { VerticalTabs } from './VerticalTabs';
10+
11+
const EXCLUDED_CONTROLS = ['header', 'footer', 'onChange'];
12+
13+
export default {
14+
title: 'Molecule/VerticalTabs',
15+
component: VerticalTabs,
16+
parameters: {
17+
layout: 'fullscreen',
18+
controls: {
19+
exclude: EXCLUDED_CONTROLS,
20+
},
21+
},
22+
};
23+
24+
const Template: Story<ComponentProps<typeof VerticalTabs>> = args => {
25+
const [, updateArgs] = useArgs();
26+
const handleOnChange = useCallback(
27+
(index: number) => updateArgs({ selectedIndex: index }),
28+
[updateArgs],
29+
);
30+
31+
return <VerticalTabs {...args} onChange={handleOnChange} />;
32+
};
33+
34+
const DialogTemplate: Story<ComponentProps<typeof VerticalTabs>> = args => {
35+
const [selectedIndex, setSelectedIndex] = useState(0);
36+
const [isDialogOpen, toggle] = useReducer(a => !a, false);
37+
return (
38+
<>
39+
<Button onClick={toggle} text="Open Dialog" />
40+
<Dialog isOpen={isDialogOpen} width={DialogSize.xl2}>
41+
<VerticalTabs
42+
{...args}
43+
selectedIndex={selectedIndex}
44+
onChange={setSelectedIndex}
45+
footer={() => (
46+
<button onClick={toggle} className="text-primary-20 text-xs">
47+
Close
48+
</button>
49+
)}
50+
/>
51+
</Dialog>
52+
</>
53+
);
54+
};
55+
56+
export const Basic = Template.bind({});
57+
Basic.args = {
58+
items: [
59+
{ label: 'Tab 1', content: 'Tab 1 Content' },
60+
{ label: 'Tab 2', content: 'Tab 2 Content' },
61+
{ label: 'Tab 3', content: 'Tab 3 Content' },
62+
{
63+
label: 'Tab4 ',
64+
infoText: 'Example with long content',
65+
content: (
66+
<div>
67+
<Heading>Long List</Heading>
68+
<ol>
69+
{new Array(100).fill('Row').map((item, index) => (
70+
<li key={index}>{item}</li>
71+
))}
72+
</ol>
73+
</div>
74+
),
75+
},
76+
],
77+
selectedIndex: 0,
78+
header: props => (
79+
<Heading>Selected: {props.items[props.selectedIndex].label}</Heading>
80+
),
81+
footer: () => <div>Footer</div>,
82+
className: '',
83+
tabsClassName: '',
84+
contentClassName: '',
85+
};
86+
87+
export const InDialog = DialogTemplate.bind({});
88+
InDialog.args = {
89+
items: [
90+
{
91+
label: 'Hardware Wallet',
92+
infoText: 'Select the hardware wallet you want to connect',
93+
content: 'Content of HW tab',
94+
},
95+
{
96+
label: 'Browser Wallet',
97+
infoText: 'Select the web3 wallet you want to connect',
98+
content: 'Tab 2 Content',
99+
},
100+
{
101+
label: "Don't have a wallet",
102+
infoText: 'Read the following instructions',
103+
content: 'Tab 3 Content',
104+
},
105+
],
106+
header: () => <Heading>Connect Wallet</Heading>,
107+
tabsClassName: 'rounded-l-lg',
108+
className: 'rounded-lg',
109+
};
110+
111+
InDialog.parameters = {
112+
layout: 'centered',
113+
controls: {
114+
exclude: [...EXCLUDED_CONTROLS, 'selectedIndex'],
115+
},
116+
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { render } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import React, { useState } from 'react';
5+
6+
import { VerticalTabs } from './VerticalTabs';
7+
import { VerticalTabsItem } from './VerticalTabs.types';
8+
9+
const INITIAL_TAB = 2;
10+
const DISABLED_TAB = 1;
11+
const TAB_ITEMS: VerticalTabsItem[] = [
12+
{
13+
label: 'Tab 1',
14+
infoText: 'Info text 1',
15+
content: 'Content 1',
16+
},
17+
{
18+
label: 'Tab 2',
19+
disabled: true,
20+
content: 'Content 2',
21+
},
22+
{
23+
label: 'Tab 3',
24+
content: 'Content 3',
25+
},
26+
];
27+
28+
const TestComponent = () => {
29+
const [index, setIndex] = useState(INITIAL_TAB);
30+
return (
31+
<VerticalTabs
32+
items={TAB_ITEMS}
33+
selectedIndex={index}
34+
onChange={setIndex}
35+
header={() => <h1>This is header</h1>}
36+
footer={() => <p>This is footer</p>}
37+
/>
38+
);
39+
};
40+
41+
describe('VerticalTabs', () => {
42+
it('renders all of the tabs', () => {
43+
const { getByText } = render(<TestComponent />);
44+
45+
TAB_ITEMS.forEach(item => {
46+
expect(getByText(item.label as string)).toBeInTheDocument();
47+
});
48+
});
49+
50+
it('renders initial content', () => {
51+
const { getByText } = render(<TestComponent />);
52+
expect(
53+
getByText(TAB_ITEMS[INITIAL_TAB].content as string),
54+
).toBeInTheDocument();
55+
});
56+
57+
it('renders header', () => {
58+
const { getByText } = render(<TestComponent />);
59+
expect(getByText('This is header')).toBeInTheDocument();
60+
});
61+
62+
it('renders footer', () => {
63+
const { getByText } = render(<TestComponent />);
64+
expect(getByText('This is footer')).toBeInTheDocument();
65+
});
66+
67+
it('does not render non selected tab content', () => {
68+
const { getByText } = render(<TestComponent />);
69+
expect(() => getByText(TAB_ITEMS[0].content as string)).toThrow();
70+
});
71+
72+
it('switches tab content when clicked', () => {
73+
const { getByText } = render(<TestComponent />);
74+
75+
userEvent.click(getByText(TAB_ITEMS[0].label as string));
76+
expect(getByText(TAB_ITEMS[0].content as string)).toBeInTheDocument();
77+
});
78+
79+
it('does not switch to content of disabled tab', () => {
80+
const { getByText } = render(<TestComponent />);
81+
82+
userEvent.click(getByText(TAB_ITEMS[DISABLED_TAB].label as string));
83+
expect(
84+
getByText(TAB_ITEMS[INITIAL_TAB].content as string),
85+
).toBeInTheDocument();
86+
expect(() =>
87+
getByText(TAB_ITEMS[DISABLED_TAB].content as string),
88+
).toThrow();
89+
});
90+
91+
it('triggers onChange callback when tab item is clicked', () => {
92+
const mockFunction = jest.fn();
93+
94+
const { getByText } = render(
95+
<VerticalTabs
96+
items={TAB_ITEMS}
97+
selectedIndex={0}
98+
onChange={mockFunction}
99+
/>,
100+
);
101+
102+
userEvent.click(getByText(TAB_ITEMS[0].label as string));
103+
expect(mockFunction).toHaveBeenCalledTimes(1);
104+
});
105+
106+
it('does not trigger onChange callback when disabled tab item is clicked', () => {
107+
const mockFunction = jest.fn();
108+
109+
const { getByText } = render(
110+
<VerticalTabs
111+
items={TAB_ITEMS}
112+
selectedIndex={0}
113+
onChange={mockFunction}
114+
/>,
115+
);
116+
117+
userEvent.click(getByText(TAB_ITEMS[DISABLED_TAB].label as string));
118+
expect(mockFunction).toHaveBeenCalledTimes(0);
119+
});
120+
121+
it('adds className to vertical tabs root wrapper', () => {
122+
const { container } = render(
123+
<VerticalTabs
124+
items={TAB_ITEMS}
125+
selectedIndex={0}
126+
className="test-class"
127+
/>,
128+
);
129+
// adding "container" to make sure correct element is found
130+
expect(container.firstChild).toHaveClass('container test-class');
131+
});
132+
133+
it('adds className to tab list wrapper', () => {
134+
const { container } = render(
135+
<VerticalTabs
136+
items={TAB_ITEMS}
137+
selectedIndex={0}
138+
tabsClassName="test-class"
139+
/>,
140+
);
141+
142+
// adding "aside" to make sure correct element is found
143+
expect(container.firstChild?.childNodes[0]).toHaveClass('aside test-class');
144+
});
145+
146+
it('adds className to tab content wrapper', () => {
147+
const { container } = render(
148+
<VerticalTabs
149+
items={TAB_ITEMS}
150+
selectedIndex={0}
151+
contentClassName="test-class"
152+
/>,
153+
);
154+
155+
// adding "content" to make sure correct element is found
156+
expect(container.firstChild?.childNodes[1]).toHaveClass(
157+
'content test-class',
158+
);
159+
});
160+
});

0 commit comments

Comments
 (0)