Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
type TabProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
Icon?: IconComponent;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-color: ${({ theme, active }) =>
|
||||
active ? theme.border.color.inverted : 'transparent'};
|
||||
color: ${({ theme, active, disabled }) =>
|
||||
active
|
||||
? theme.font.color.primary
|
||||
: disabled
|
||||
? theme.font.color.light
|
||||
: theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
margin-bottom: -1px;
|
||||
padding: ${({ theme }) => theme.spacing(2) + ' 0'};
|
||||
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
|
||||
`;
|
||||
|
||||
const StyledHover = styled.span`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
&:active {
|
||||
background: ${({ theme }) => theme.background.quaternary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tab = ({
|
||||
id,
|
||||
title,
|
||||
Icon,
|
||||
active = false,
|
||||
onClick,
|
||||
className,
|
||||
disabled,
|
||||
}: TabProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledTab
|
||||
onClick={onClick}
|
||||
active={active}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
data-testid={'tab-' + id}
|
||||
>
|
||||
<StyledHover>
|
||||
{Icon && <Icon size={theme.icon.size.md} />}
|
||||
{title}
|
||||
</StyledHover>
|
||||
</StyledTab>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { activeTabIdScopedState } from '../states/activeTabIdScopedState';
|
||||
|
||||
import { Tab } from './Tab';
|
||||
|
||||
type SingleTabProps = {
|
||||
title: string;
|
||||
Icon?: IconComponent;
|
||||
id: string;
|
||||
hide?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type TabListProps = {
|
||||
tabs: SingleTabProps[];
|
||||
context: React.Context<string | null>;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: 40px;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const TabList = ({ tabs, context }: TabListProps) => {
|
||||
const initialActiveTabId = tabs[0].id;
|
||||
|
||||
const [activeTabId, setActiveTabId] = useRecoilScopedState(
|
||||
activeTabIdScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setActiveTabId(initialActiveTabId);
|
||||
}, [initialActiveTabId, setActiveTabId]);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{tabs
|
||||
.filter((tab) => !tab.hide)
|
||||
.map((tab) => (
|
||||
<Tab
|
||||
id={tab.id}
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
Icon={tab.Icon}
|
||||
active={tab.id === activeTabId}
|
||||
onClick={() => {
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
disabled={tab.disabled}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,64 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { IconCheckbox } from '@/ui/display/icon';
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { CatalogStory } from '~/testing/types';
|
||||
|
||||
import { Tab } from '../Tab';
|
||||
|
||||
const meta: Meta<typeof Tab> = {
|
||||
title: 'UI/Layout/Tab/Tab',
|
||||
component: Tab,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Tab>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Tab title',
|
||||
active: false,
|
||||
Icon: IconCheckbox,
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof Tab> = {
|
||||
args: { title: 'Tab title', Icon: IconCheckbox },
|
||||
argTypes: {
|
||||
active: { control: false },
|
||||
disabled: { control: false },
|
||||
onClick: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
pseudo: { hover: ['.hover'], active: ['.active'] },
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'states',
|
||||
values: ['default', 'hover', 'active'],
|
||||
props: (state: string) =>
|
||||
state === 'default' ? {} : { className: state },
|
||||
},
|
||||
{
|
||||
name: 'Active',
|
||||
values: ['true', 'false'],
|
||||
labels: (active: string) =>
|
||||
active === 'true' ? 'active' : 'inactive',
|
||||
props: (active: string) => ({ active: active === 'true' }),
|
||||
},
|
||||
{
|
||||
name: 'Disabled',
|
||||
values: ['true', 'false'],
|
||||
labels: (disabled: string) =>
|
||||
disabled === 'true' ? 'disabled' : 'enabled',
|
||||
props: (disabled: string) => ({ disabled: disabled === 'true' }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
@ -0,0 +1,68 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
|
||||
import { IconCheckbox } from '@/ui/display/icon';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { TabList } from '../TabList';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tab1',
|
||||
Icon: IconCheckbox,
|
||||
hide: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tab2',
|
||||
Icon: IconCheckbox,
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Tab3',
|
||||
Icon: IconCheckbox,
|
||||
hide: false,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Tab4',
|
||||
Icon: IconCheckbox,
|
||||
hide: false,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof TabList> = {
|
||||
title: 'UI/Layout/Tab/TabList',
|
||||
component: TabList,
|
||||
args: {
|
||||
tabs: tabs,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<RecoilScope>
|
||||
<Story />
|
||||
</RecoilScope>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof TabList>;
|
||||
|
||||
export const TabListDisplay: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const submitButton = canvas.queryByText('Tab1');
|
||||
expect(submitButton).toBeNull();
|
||||
expect(await canvas.findByText('Tab2')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Tab3')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Tab4')).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user