Uniformize folder structure (#693)

* Uniformize folder structure

* Fix icons

* Fix icons

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-16 14:29:28 -07:00
committed by GitHub
parent 900ec5572f
commit 6ced8434bd
462 changed files with 931 additions and 960 deletions

View File

@ -0,0 +1,197 @@
import React from 'react';
import styled from '@emotion/styled';
import { SoonPill } from '@/ui/pill/components/SoonPill';
import { rgba } from '@/ui/themes/colors';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'tertiary'
| 'tertiaryBold'
| 'tertiaryLight'
| 'danger';
export type ButtonSize = 'medium' | 'small';
export type ButtonPosition = 'left' | 'middle' | 'right' | undefined;
export type ButtonProps = {
icon?: React.ReactNode;
title?: string;
fullWidth?: boolean;
variant?: ButtonVariant;
size?: ButtonSize;
position?: ButtonPosition;
soon?: boolean;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<
Pick<ButtonProps, 'fullWidth' | 'variant' | 'size' | 'position' | 'title'>
>`
align-items: center;
background: ${({ theme, variant, disabled }) => {
switch (variant) {
case 'primary':
if (disabled) {
return rgba(theme.color.blue, 0.4);
} else {
return theme.color.blue;
}
case 'secondary':
return theme.background.primary;
default:
return 'transparent';
}
}};
border-color: ${({ theme, variant }) => {
switch (variant) {
case 'primary':
case 'secondary':
return `${theme.background.transparent.medium}`;
case 'tertiary':
default:
return 'none';
}
}};
border-radius: ${({ position }) => {
switch (position) {
case 'left':
return '4px 0px 0px 4px';
case 'right':
return '0px 4px 4px 0px';
case 'middle':
return '0px';
default:
return '4px';
}
}};
border-style: solid;
border-width: ${({ theme, variant, position }) => {
switch (variant) {
case 'primary':
case 'secondary':
return position === 'middle' ? `1px 0 1px 0` : `1px`;
case 'tertiary':
default:
return '0';
}
}};
box-shadow: ${({ theme, variant }) => {
switch (variant) {
case 'primary':
case 'secondary':
return theme.boxShadow.extraLight;
default:
return 'none';
}
}};
color: ${({ theme, variant, disabled }) => {
if (disabled) {
if (variant === 'primary') {
return theme.color.gray0;
} else {
return theme.font.color.extraLight;
}
}
switch (variant) {
case 'primary':
return theme.color.gray0;
case 'tertiaryLight':
return theme.font.color.tertiary;
case 'danger':
return theme.color.red;
default:
return theme.font.color.secondary;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme, variant }) => {
switch (variant) {
case 'tertiary':
case 'tertiaryLight':
return theme.font.weight.regular;
default:
return theme.font.weight.medium;
}
}};
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: flex-start;
padding: ${({ theme, title }) => {
if (!title) {
return `${theme.spacing(1)}`;
}
return `${theme.spacing(2)} ${theme.spacing(3)}`;
}};
transition: background 0.1s ease;
white-space: nowrap;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
&:hover,
&:active {
${({ theme, variant, disabled }) => {
if (disabled) {
return '';
}
switch (variant) {
case 'primary':
return `background: linear-gradient(0deg, ${theme.background.transparent.medium} 0%, ${theme.background.transparent.medium} 100%), ${theme.color.blue}`;
default:
return `background: ${theme.background.tertiary}`;
}
}};
}
&:focus {
outline: none;
${({ theme, variant }) => {
switch (variant) {
case 'tertiaryLight':
case 'tertiaryBold':
case 'tertiary':
return `color: ${theme.color.blue};`;
default:
return '';
}
}};
}
`;
export function Button({
icon,
title,
fullWidth = false,
variant = 'primary',
size = 'medium',
position,
soon = false,
disabled = false,
...props
}: ButtonProps) {
return (
<StyledButton
fullWidth={fullWidth}
variant={variant}
size={size}
position={position}
disabled={soon || disabled}
title={title}
{...props}
>
{icon}
{title}
{soon && <SoonPill />}
</StyledButton>
);
}

View File

@ -0,0 +1,43 @@
import React from 'react';
import styled from '@emotion/styled';
import { ButtonPosition, ButtonProps } from './Button';
const StyledButtonGroupContainer = styled.div`
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
`;
type ButtonGroupProps = Pick<ButtonProps, 'variant' | 'size'> & {
children: React.ReactElement[];
};
export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
return (
<StyledButtonGroupContainer>
{React.Children.map(children, (child, index) => {
let position: ButtonPosition;
if (index === 0) {
position = 'left';
} else if (index === children.length - 1) {
position = 'right';
} else {
position = 'middle';
}
const additionalProps: any = { position };
if (variant) {
additionalProps.variant = variant;
}
if (size) {
additionalProps.size = size;
}
return React.cloneElement(child, additionalProps);
})}
</StyledButtonGroupContainer>
);
}

View File

@ -0,0 +1,144 @@
import React, { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icon/index';
type ButtonProps = React.ComponentProps<'button'>;
export type DropdownOptionType = {
key: string;
label: string;
icon: React.ReactNode;
};
type OwnProps = {
options: DropdownOptionType[];
selectedOptionKey?: string;
onSelection: (value: DropdownOptionType) => void;
} & ButtonProps;
const StyledButton = styled.button<ButtonProps & { isOpen: boolean }>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-bottom-left-radius: ${({ isOpen, theme }) =>
isOpen ? 0 : theme.border.radius.sm};
border-bottom-right-radius: ${({ isOpen, theme }) =>
isOpen ? 0 : theme.border.radius.sm};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const StyledDropdownItem = styled.button<ButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const DropdownContainer = styled.div`
position: relative;
`;
const DropdownMenu = styled.div`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
`;
export function DropdownButton({
options,
selectedOptionKey,
onSelection,
...buttonProps
}: OwnProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<
DropdownOptionType | undefined
>(undefined);
useEffect(() => {
if (selectedOptionKey) {
const option = options.find((option) => option.key === selectedOptionKey);
setSelectedOption(option);
} else {
setSelectedOption(options[0]);
}
}, [selectedOptionKey, options]);
if (!options.length) {
throw new Error('You must provide at least one option.');
}
const handleSelect =
(option: DropdownOptionType) =>
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onSelection(option);
setSelectedOption(option);
setIsOpen(false);
};
return (
<>
{selectedOption && (
<DropdownContainer>
<StyledButton
onClick={() => setIsOpen(!isOpen)}
{...buttonProps}
isOpen={isOpen}
>
{selectedOption.icon}
{selectedOption.label}
{options.length > 1 && <IconChevronDown />}
</StyledButton>
{isOpen && (
<DropdownMenu>
{options
.filter((option) => option.label !== selectedOption.label)
.map((option, index) => (
<StyledDropdownItem
key={index}
onClick={handleSelect(option)}
>
{option.icon}
{option.label}
</StyledDropdownItem>
))}
</DropdownMenu>
)}
</DropdownContainer>
)}
</>
);
}

View File

@ -0,0 +1,130 @@
import React from 'react';
import styled from '@emotion/styled';
export type IconButtonVariant = 'transparent' | 'border' | 'shadow' | 'white';
export type IconButtonSize = 'large' | 'medium' | 'small';
export type ButtonProps = {
icon?: React.ReactNode;
variant?: IconButtonVariant;
size?: IconButtonSize;
} & React.ComponentProps<'button'>;
const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
align-items: center;
background: ${({ theme, variant }) => {
switch (variant) {
case 'shadow':
case 'white':
return theme.background.transparent.primary;
case 'transparent':
case 'border':
default:
return 'transparent';
}
}};
border-color: ${({ theme, variant }) => {
switch (variant) {
case 'border':
return theme.border.color.medium;
case 'shadow':
case 'white':
case 'transparent':
default:
return 'none';
}
}};
border-radius: ${({ theme }) => {
return theme.border.radius.sm;
}};
border-style: solid;
border-width: ${({ variant }) => {
switch (variant) {
case 'border':
return '1px';
case 'shadow':
case 'white':
case 'transparent':
default:
return 0;
}
}};
box-shadow: ${({ theme, variant }) => {
switch (variant) {
case 'shadow':
return theme.boxShadow.light;
case 'border':
case 'white':
case 'transparent':
default:
return 'none';
}
}};
color: ${({ theme, disabled }) => {
if (disabled) {
return theme.font.color.extraLight;
}
return theme.font.color.tertiary;
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-shrink: 0;
height: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
justify-content: center;
padding: 0;
transition: background 0.1s ease;
user-select: none;
&:hover {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.light;
}};
}
width: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
&:active {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.medium;
}};
}
`;
export function IconButton({
icon,
title,
variant = 'transparent',
size = 'medium',
disabled = false,
...props
}: ButtonProps) {
return (
<StyledIconButton
variant={variant}
size={size}
disabled={disabled}
{...props}
>
{icon}
</StyledIconButton>
);
}

View File

@ -0,0 +1,88 @@
import React from 'react';
import styled from '@emotion/styled';
type Variant = 'primary' | 'secondary';
type Props = {
icon?: React.ReactNode;
title: string;
fullWidth?: boolean;
variant?: Variant;
soon?: boolean;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<Pick<Props, 'fullWidth' | 'variant'>>`
align-items: center;
background: ${({ theme, variant, disabled }) => {
if (disabled) {
return theme.background.tertiary;
}
switch (variant) {
case 'primary':
return `radial-gradient(
50% 62.62% at 50% 0%,
${theme.font.color.secondary} 0%,
${theme.font.color.primary} 100%
)`;
case 'secondary':
return theme.background.primary;
default:
return theme.background.primary;
}
}};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme, variant, disabled }) => {
if (disabled) {
return theme.font.color.light;
}
switch (variant) {
case 'primary':
return theme.font.color.inverted;
case 'secondary':
return theme.font.color.primary;
default:
return theme.font.color.primary;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
${({ theme, variant }) => {
switch (variant) {
case 'secondary':
return `
&:hover {
background: ${theme.background.tertiary};
}
`;
default:
return '';
}
}};
`;
export function MainButton({
icon,
title,
fullWidth = false,
variant = 'primary',
...props
}: Props) {
return (
<StyledButton fullWidth={fullWidth} variant={variant} {...props}>
{icon}
{title}
</StyledButton>
);
}

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
const StyledIconButton = styled.button`
align-items: center;
background: ${({ theme }) => theme.color.blue};
border: none;
border-radius: 50%;
color: ${({ theme }) => theme.font.color.inverted};
cursor: pointer;
display: flex;
height: 20px;
justify-content: center;
padding: 0;
transition: color 0.1s ease-in-out, background 0.1s ease-in-out;
width: 20px;
&:disabled {
background: ${({ theme }) => theme.background.quaternary};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: default;
}
`;
export function RoundedIconButton({
icon,
...props
}: { icon: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <StyledIconButton {...props}>{icon}</StyledIconButton>;
}

View File

@ -0,0 +1,229 @@
import React from 'react';
import styled from '@emotion/styled';
import { text, withKnobs } from '@storybook/addon-knobs';
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconSearch } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { Button } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
type ButtonProps = React.ComponentProps<typeof Button>;
const StyledContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 800px;
> * + * {
margin-top: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-align: center;
text-transform: uppercase;
`;
const StyledLine = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: space-between;
width: 100%;
`;
const StyledButtonContainer = styled.div`
border: 1px solid ${({ theme }) => theme.color.gray20};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
`;
const meta: Meta<typeof Button> = {
title: 'UI/Buttons/Button',
component: Button,
decorators: [withKnobs],
};
export default meta;
type Story = StoryObj<typeof Button>;
const variants: ButtonProps['variant'][] = [
'primary',
'secondary',
'tertiary',
'tertiaryBold',
'tertiaryLight',
'danger',
];
const clickJestFn = jest.fn();
const states = {
'with-icon': {
description: 'With icon',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-with-icon`,
icon: <IconSearch size={14} />,
}),
},
default: {
description: 'Default',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-default`,
onClick: clickJestFn,
}),
},
hover: {
description: 'Hover',
extraProps: (variant: string) => ({
id: `${variant}-button-hover`,
'data-testid': `${variant}-button-hover`,
}),
},
pressed: {
description: 'Pressed',
extraProps: (variant: string) => ({
id: `${variant}-button-pressed`,
'data-testid': `${variant}-button-pressed`,
}),
},
disabled: {
description: 'Disabled',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-disabled`,
disabled: true,
}),
},
soon: {
description: 'Soon',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-soon`,
soon: true,
}),
},
focus: {
description: 'Focus',
extraProps: (variant: string) => ({
id: `${variant}-button-focus`,
'data-testid': `${variant}-button-focus`,
}),
},
};
const ButtonLine: React.FC<ButtonProps> = ({ variant, ...props }) => (
<>
{Object.entries(states).map(([state, { description, extraProps }]) => (
<StyledButtonContainer key={`${variant}-container-${state}`}>
<StyledDescription>{description}</StyledDescription>
<Button {...props} {...extraProps(variant ?? '')} variant={variant} />
</StyledButtonContainer>
))}
</>
);
const ButtonGroupLine: React.FC<ButtonProps> = ({ variant, ...props }) => (
<>
{Object.entries(states).map(([state, { description, extraProps }]) => (
<StyledButtonContainer key={`${variant}-group-container-${state}`}>
<StyledDescription>{description}</StyledDescription>
<ButtonGroup>
<Button
{...props}
{...extraProps(`${variant}-left`)}
variant={variant}
title="Left"
/>
<Button
{...props}
{...extraProps(`${variant}-center`)}
variant={variant}
title="Center"
/>
<Button
{...props}
{...extraProps(`${variant}-right`)}
variant={variant}
title="Right"
/>
</ButtonGroup>
</StyledButtonContainer>
))}
</>
);
const generateStory = (
size: ButtonProps['size'],
type: 'button' | 'group',
LineComponent: React.ComponentType<ButtonProps>,
): Story => ({
render: getRenderWrapperForComponent(
<StyledContainer>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
<LineComponent
size={size}
variant={variant}
title={text('Text', 'A button title')}
/>
</StyledLine>
</div>
))}
</StyledContainer>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
let button;
if (type === 'group') {
button = canvas.getByTestId(`primary-left-button-default`);
} else {
button = canvas.getByTestId(`primary-button-default`);
}
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
},
parameters: {
pseudo: Object.keys(states).reduce(
(acc, state) => ({
...acc,
[state]: variants.map(
(variant) =>
variant &&
['#left', '#center', '#right'].map(
(pos) => `${pos}-${variant}-${type}-${state}`,
),
),
}),
{},
),
},
});
export const MediumSize = generateStory('medium', 'button', ButtonLine);
export const SmallSize = generateStory('small', 'button', ButtonLine);
export const MediumSizeGroup = generateStory(
'medium',
'group',
ButtonGroupLine,
);
export const SmallSizeGroup = generateStory('small', 'group', ButtonGroupLine);

View File

@ -0,0 +1,153 @@
import React from 'react';
import styled from '@emotion/styled';
import { withKnobs } from '@storybook/addon-knobs';
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconUser } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { IconButton } from '../IconButton';
type IconButtonProps = React.ComponentProps<typeof IconButton>;
const StyledContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 800px;
> * + * {
margin-top: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-align: center;
text-transform: uppercase;
`;
const StyledLine = styled.div`
display: flex;
flex: 1;
flex-direction: row;
`;
const StyledIconButtonContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
width: 50px;
`;
const meta: Meta<typeof IconButton> = {
title: 'UI/Buttons/IconButton',
component: IconButton,
decorators: [withKnobs],
};
export default meta;
type Story = StoryObj<typeof IconButton>;
const variants: IconButtonProps['variant'][] = [
'transparent',
'border',
'shadow',
'white',
];
const clickJestFn = jest.fn();
const states = {
default: {
description: 'Default',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-default`,
onClick: clickJestFn,
}),
},
hover: {
description: 'Hover',
extraProps: (variant: string) => ({
id: `${variant}-button-hover`,
'data-testid': `${variant}-button-hover`,
}),
},
pressed: {
description: 'Pressed',
extraProps: (variant: string) => ({
id: `${variant}-button-pressed`,
'data-testid': `${variant}-button-pressed`,
}),
},
disabled: {
description: 'Disabled',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-disabled`,
disabled: true,
}),
},
};
function IconButtonRow({ variant, size, ...props }: IconButtonProps) {
const iconSize = size === 'small' ? 14 : 16;
return (
<>
{Object.entries(states).map(([state, { description, extraProps }]) => (
<StyledIconButtonContainer key={`${variant}-container-${state}`}>
<StyledDescription>{description}</StyledDescription>
<IconButton
{...props}
{...extraProps(variant ?? '')}
variant={variant}
size={size}
icon={<IconUser size={iconSize} />}
/>
</StyledIconButtonContainer>
))}
</>
);
}
const generateStory = (
size: IconButtonProps['size'],
LineComponent: React.ComponentType<IconButtonProps>,
): Story => ({
render: getRenderWrapperForComponent(
<StyledContainer>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
<LineComponent size={size} variant={variant} />
</StyledLine>
</div>
))}
</StyledContainer>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByTestId(`transparent-button-default`);
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
},
});
export const LargeSize = generateStory('large', IconButtonRow);
export const MediumSize = generateStory('medium', IconButtonRow);
export const SmallSize = generateStory('small', IconButtonRow);

View File

@ -0,0 +1,95 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconBrandGoogle } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { MainButton } from '../MainButton';
const meta: Meta<typeof MainButton> = {
title: 'UI/Buttons/MainButton',
component: MainButton,
};
export default meta;
type Story = StoryObj<typeof MainButton>;
const clickJestFn = jest.fn();
export const DefaultPrimary: Story = {
render: getRenderWrapperForComponent(
<MainButton title="A primary Button" onClick={clickJestFn} />,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};
export const WithIconPrimary: Story = {
render: getRenderWrapperForComponent(
<MainButton
icon={<IconBrandGoogle size={16} stroke={4} />}
title="A primary Button"
/>,
),
};
export const WithIconPrimaryDisabled: Story = {
render: getRenderWrapperForComponent(
<MainButton
icon={<IconBrandGoogle size={16} stroke={4} />}
title="A primary Button"
disabled
/>,
),
};
export const FullWidthPrimary: Story = {
render: getRenderWrapperForComponent(
<MainButton title="A primary Button" fullWidth />,
),
};
export const DefaultSecondary: Story = {
render: getRenderWrapperForComponent(
<MainButton
title="A secondary Button"
onClick={clickJestFn}
variant="secondary"
/>,
),
};
export const WithIconSecondary: Story = {
render: getRenderWrapperForComponent(
<MainButton
icon={<IconBrandGoogle size={16} stroke={4} />}
title="A secondary Button"
variant="secondary"
/>,
),
};
export const WithIconSecondaryDisabled: Story = {
render: getRenderWrapperForComponent(
<MainButton
icon={<IconBrandGoogle size={16} stroke={4} />}
title="A secondary Button"
variant="secondary"
disabled
/>,
),
};
export const FullWidthSecondary: Story = {
render: getRenderWrapperForComponent(
<MainButton title="A secondary Button" variant="secondary" fullWidth />,
),
};

View File

@ -0,0 +1,36 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { RoundedIconButton } from '../RoundedIconButton';
const meta: Meta<typeof RoundedIconButton> = {
title: 'UI/Buttons/RoundedIconButton',
component: RoundedIconButton,
};
export default meta;
type Story = StoryObj<typeof RoundedIconButton>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<RoundedIconButton
onClick={clickJestFn}
icon={<IconArrowRight size={15} />}
/>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};