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

SOV-580: vertical tabs component #30

Merged
merged 17 commits into from
Oct 19, 2022
Merged
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
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