Skip to content

Commit

Permalink
feat: add Tabs component (#24)
Browse files Browse the repository at this point in the history
* feat: add Tabs component

* chore: change tabs props name

* resolve: PR issues

* resole: PR issues

* fix: tabs css format

* fix: tabs divider

* fix: update Tabs component
  • Loading branch information
Rickk137 authored Oct 14, 2022
1 parent bccc22f commit bfef8e2
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 1 deletion.
5 changes: 4 additions & 1 deletion packages/tailwindcss-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module.exports = {
'primary-20': '#F5A549',
'primary-10': '#FFDAA7',

negative: '#FF6868',
negative: '#FF0D39',
positive: '#72EADE',

error: '#FF0000',
Expand Down Expand Up @@ -186,6 +186,9 @@ module.exports = {
scale: {
85: '.85',
},
opacity: {
65: '.65',
},
},
},
variants: {
Expand Down
17 changes: 17 additions & 0 deletions packages/ui/src/1_atoms/Tabs/Tabs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.wrapper {
@apply inline-flex flex-col;

.tabs {
@apply inline-flex rounded-t items-center flex-nowrap overflow-auto;

&.primary {
@apply border border-b-0 border-gray-50 bg-gray-90;
}
}

.content {
&.primary {
@apply bg-gray-90 rounded-b text-gray-10 border-b border-x border-gray-50;
}
}
}
88 changes: 88 additions & 0 deletions packages/ui/src/1_atoms/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Story } from '@storybook/react';

import React, { ComponentProps, useState } from 'react';

import { Tabs } from './Tabs';
import { TabSize, TabType } from './Tabs.types';

export default {
title: 'Atoms/Tabs',
component: Tabs,
};

const Template: Story<ComponentProps<typeof Tabs>> = args => {
const [index, setIndex] = useState(args.index);
return <Tabs {...args} onChange={setIndex} index={index} />;
};

export const Primary = Template.bind();
Primary.args = {
items: [
{
label: 'Default',
content: <div>Default</div>,
activeClassName: 'border-t-primary-30',
dataActionId: 'default',
},
{
label: 'Buy',
content: <div>Buy</div>,
activeClassName: 'border-t-positive',
dataActionId: 'buy',
},
{
label: 'Sell',
content: <div>Sell</div>,
activeClassName: 'border-t-negative',
dataActionId: 'sell',
},
{
label: 'Disabled',
content: <div>Disabled</div>,
disabled: true,
activeClassName: '',
dataActionId: 'disabled',
},
],
index: 0,
contentClassName: 'p-6',
className: '',
size: TabSize.normal,
type: TabType.primary,
};

export const Secondary = Template.bind();
Secondary.args = {
items: [
{
label: 'Default',
content: <div>Default</div>,
activeClassName: 'text-primary-20',
dataActionId: 'default',
},
{
label: 'Buy',
content: <div>Buy</div>,
activeClassName: 'text-positive',
dataActionId: 'buy',
},
{
label: 'Sell',
content: <div>Sell</div>,
activeClassName: 'text-negative',
dataActionId: 'sell',
},
{
label: 'Disabled',
content: <div>Disabled</div>,
disabled: true,
activeClassName: '',
dataActionId: 'disabled',
},
],
index: 0,
contentClassName: 'p-4',
className: '',
size: TabSize.normal,
type: TabType.secondary,
};
54 changes: 54 additions & 0 deletions packages/ui/src/1_atoms/Tabs/Tabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import React from 'react';

import { Tabs } from './Tabs';

const items = [
{
label: '0',
content: 'content-0',
},
{
label: '1',
content: 'content-1',
},
{
label: 'disabled',
content: 'disabled-content',
disabled: true,
},
];

describe('Tabs', () => {
it('renders tabs', () => {
const { getByText } = render(<Tabs items={items} index={1} />);

expect(getByText('0')).toBeInTheDocument();
expect(getByText('1')).toBeInTheDocument();
expect(getByText('content-1')).toBeInTheDocument();
});

it('switch between tabs', () => {
const onChange = jest.fn();
const { getByText } = render(
<Tabs items={items} index={1} onChange={onChange} />,
);

userEvent.click(getByText('0'));

expect(onChange).toBeCalledWith(0);
});

it('does not switch to a disabled tab', () => {
const onChange = jest.fn();
const { getByText } = render(
<Tabs items={items} index={1} onChange={onChange} />,
);

userEvent.click(getByText('disabled'));

expect(onChange).not.toBeCalled();
});
});
73 changes: 73 additions & 0 deletions packages/ui/src/1_atoms/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useMemo, useCallback } from 'react';

import classNames from 'classnames';

import styles from './Tabs.module.css';
import { TabSize, TabType } from './Tabs.types';
import { Tab } from './components/Tab/Tab';

interface ITabItem {
label: React.ReactNode;
content: React.ReactNode;
disabled?: boolean;
dataActionId?: string;
activeClassName?: string;
}

type TabsProps = {
className?: string;
contentClassName?: string;
index: number;
items: ITabItem[];
onChange?: (index: number) => void;
type?: TabType;
size?: TabSize;
};

export const Tabs: React.FC<TabsProps> = ({
items,
index,
onChange,
className = '',
contentClassName,
type = TabType.primary,
size = TabSize.normal,
}) => {
const selectTab = useCallback(
(item: ITabItem, index: number) => {
if (!item.disabled) {
onChange?.(index);
}
},
[onChange],
);

const content = useMemo(() => items[index]?.content, [index, items]);

return (
<div className={classNames(styles.wrapper, className)}>
<div className={classNames(styles.tabs, styles[type])}>
{items.map((item, i) => (
<Tab
key={i}
active={index === i}
index={i}
disabled={item.disabled}
onClick={() => selectTab(item, i)}
content={item.label}
dataActionId={item.dataActionId}
type={type}
size={size}
activeIndex={index}
activeClassName={item.activeClassName}
/>
))}
</div>
<div
className={classNames(styles.content, styles[type], contentClassName)}
>
{content}
</div>
</div>
);
};
9 changes: 9 additions & 0 deletions packages/ui/src/1_atoms/Tabs/Tabs.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum TabSize {
small = 'small',
normal = 'normal',
}

export enum TabType {
primary = 'primary',
secondary = 'secondary',
}
64 changes: 64 additions & 0 deletions packages/ui/src/1_atoms/Tabs/components/Tab/Tab.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.button {
@apply cursor-pointer inline-flex box-border items-center justify-center text-center rounded cursor-pointer transition-colors relative;

&.primary {
@apply border-t-2 border-t-transparent border-b text-base text-gray-10/50 rounded-b-none h-12;
min-width: 10rem;

&.normal {
@apply font-medium;
}

&:not(:disabled):not(.active):hover {
@apply text-gray-10;
}

&.active {
@apply text-gray-10 bg-gray-90 border-x border-x-gray-50 pb-0.5 border-b-transparent;
}

&:last-child {
@apply border-r-0;
}

&:first-child {
@apply border-l-0;
}

&:not(.active) {
@apply bg-gray-80 border-b-gray-50 border-r border-r-gray-50 rounded-none border-t-0;

&:last-child,
&.noRightBorder {
@apply border-r-0;
}
}
}

&.secondary {
&.normal {
@apply font-semibold text-xs px-0.5 py-2 mx-0.5;
min-width: 3.5rem;
}

&.small {
@apply font-semibold text-tiny px-0.5 py-1 mx-0.5;
min-width: 2.5rem;
}

&.active {
@apply bg-gray-70;
}

&:not(.active) {
@apply text-gray-10/75;
&:not(:disabled):hover {
@apply text-gray-10;
}
}
}

&:disabled {
@apply cursor-not-allowed;
}
}
54 changes: 54 additions & 0 deletions packages/ui/src/1_atoms/Tabs/components/Tab/Tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';

import classNames from 'classnames';

import { TabSize, TabType } from '../../Tabs.types';
import styles from './Tab.module.css';

type TabProps = {
content: React.ReactNode;
disabled?: boolean;
active: boolean;
onClick: () => void;
className?: string;
dataActionId?: string;
type: TabType;
size: TabSize;
activeClassName?: string;
index: number;
activeIndex: number;
};

export const Tab: React.FC<TabProps> = ({
content,
onClick,
disabled,
className,
dataActionId,
type,
size,
active,
activeClassName = '',
activeIndex,
index,
}) => (
<button
type="button"
className={classNames(
{
[styles.active]: active,
[activeClassName]: active,
[styles.noRightBorder]: activeIndex - 1 === index,
},
className,
styles.button,
styles[size],
styles[type],
)}
onClick={onClick}
data-action-id={dataActionId}
disabled={disabled}
>
{content}
</button>
);

0 comments on commit bfef8e2

Please sign in to comment.