diff --git a/front/src/modules/ui/button/components/Button.tsx b/front/src/modules/ui/button/components/Button.tsx index 29a8eb3e9..ad90df3bf 100644 --- a/front/src/modules/ui/button/components/Button.tsx +++ b/front/src/modules/ui/button/components/Button.tsx @@ -4,17 +4,25 @@ 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 enum ButtonSize { + Medium = 'medium', + Small = 'small', +} -export type ButtonSize = 'medium' | 'small'; +export enum ButtonPosition { + Left = 'left', + Middle = 'middle', + Right = 'right', +} -export type ButtonPosition = 'left' | 'middle' | 'right' | undefined; +export enum ButtonVariant { + Primary = 'primary', + Secondary = 'secondary', + Tertiary = 'tertiary', + TertiaryBold = 'tertiaryBold', + TertiaryLight = 'tertiaryLight', + Danger = 'danger', +} export type ButtonProps = { icon?: React.ReactNode; @@ -24,6 +32,7 @@ export type ButtonProps = { size?: ButtonSize; position?: ButtonPosition; soon?: boolean; + disabled?: boolean; } & React.ComponentProps<'button'>; const StyledButton = styled.button< @@ -172,8 +181,8 @@ export function Button({ icon, title, fullWidth = false, - variant = 'primary', - size = 'medium', + variant = ButtonVariant.Primary, + size = ButtonSize.Medium, position, soon = false, disabled = false, diff --git a/front/src/modules/ui/button/components/__stories__/Button.stories.tsx b/front/src/modules/ui/button/components/__stories__/Button.stories.tsx index 3a81ce020..783436071 100644 --- a/front/src/modules/ui/button/components/__stories__/Button.stories.tsx +++ b/front/src/modules/ui/button/components/__stories__/Button.stories.tsx @@ -1,145 +1,30 @@ -import styled from '@emotion/styled'; 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 { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; -import { Button } from '../Button'; -import { ButtonGroup } from '../ButtonGroup'; - -type ButtonProps = React.ComponentProps; - -const StyledContainer = styled.div` - display: flex; - flex-direction: column; - padding: 20px; - 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 variants: ButtonProps['variant'][] = [ - 'primary', - 'secondary', - 'tertiary', - 'tertiaryBold', - 'tertiaryLight', - 'danger', -]; - -const states = { - 'with-icon': { - description: 'With icon', - extraProps: (variant: string) => ({ - 'data-testid': `${variant}-button-with-icon`, - icon: , - }), - }, - 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`, - }), - }, -}; +import { Button, ButtonPosition, ButtonSize, ButtonVariant } from '../Button'; const meta: Meta = { title: 'UI/Button/Button', component: Button, - decorators: [ - (Story) => ( - - - - ), - ], - parameters: { - pseudo: Object.keys(states).reduce( - (acc, state) => ({ - ...acc, - [state]: variants.map( - (variant) => - variant && - ['#left', '#center', '#right'].map( - (pos) => `${pos}-${variant}-button-${state}`, - ), - ), - }), - {}, - ), + argTypes: { + icon: { + type: 'boolean', + mapping: { + true: , + false: undefined, + }, + }, + position: { + control: 'radio', + options: [undefined, ...Object.values(ButtonPosition)], + }, }, - argTypes: { icon: { control: false }, variant: { control: false } }, - args: { title: 'A button title' }, + args: { title: 'Lorem ipsum' }, }; export default meta; @@ -147,34 +32,12 @@ type Story = StoryObj; const clickJestFn = jest.fn(); -export const MediumSize: Story = { - args: { size: 'medium' }, - render: (args) => ( - <> - {variants.map((variant) => ( -
- {variant} - - {Object.entries(states).map( - ([state, { description, extraProps }]) => ( - - {description} -
- ))} - - ), +export const Default: Story = { + args: { onClick: clickJestFn }, + decorators: [ComponentDecorator], play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const button = canvas.getByTestId('primary-button-default'); + const button = canvas.getByRole('button'); const numberOfClicks = clickJestFn.mock.calls.length; await userEvent.click(button); @@ -182,54 +45,78 @@ export const MediumSize: Story = { }, }; -export const SmallSize: Story = { - ...MediumSize, - args: { size: 'small' }, -}; - -export const MediumSizeGroup: Story = { - args: { size: 'medium' }, - render: (args) => ( - <> - {variants.map((variant) => ( -
- {variant} - - {Object.entries(states).map( - ([state, { description, extraProps }]) => ( - - {description} - - {['Left', 'Center', 'Right'].map((position) => ( -
- ))} - - ), - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const button = canvas.getByTestId('primary-left-button-default'); - - const numberOfClicks = clickJestFn.mock.calls.length; - await userEvent.click(button); - expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1); +export const Sizes: Story = { + argTypes: { + size: { control: false }, }, + parameters: { + catalog: [ + { + name: 'sizes', + values: Object.values(ButtonSize), + props: (size: ButtonSize) => ({ size }), + }, + ], + }, + decorators: [CatalogDecorator], }; -export const SmallSizeGroup: Story = { - ...MediumSizeGroup, - args: { size: 'small' }, +export const Variants: Story = { + argTypes: { + disabled: { control: false }, + variant: { control: false }, + }, + parameters: { + pseudo: { hover: ['.hover'], active: ['.active'], focus: ['.focus'] }, + catalog: [ + { + name: 'state', + values: ['default', 'disabled', 'hover', 'active', 'focus'], + props: (state: string) => { + if (state === 'disabled') return { disabled: true }; + if (state === 'default') return {}; + return { className: state }; + }, + }, + { + name: 'variants', + values: Object.values(ButtonVariant), + props: (variant: ButtonVariant) => ({ variant }), + }, + ], + }, + decorators: [CatalogDecorator], +}; + +export const Positions: Story = { + argTypes: { + position: { control: false }, + }, + parameters: { + catalog: [ + { + name: 'positions', + values: ['none', ...Object.values(ButtonPosition)], + props: (position: ButtonPosition | 'none') => + position === 'none' ? {} : { position }, + }, + ], + }, + decorators: [CatalogDecorator], +}; + +export const WithAdornments: Story = { + parameters: { + catalog: [ + { + name: 'adornments', + values: ['with icon', 'with soon pill'], + props: (value: string) => + value === 'with icon' + ? { icon: } + : { soon: true }, + }, + ], + }, + decorators: [CatalogDecorator], }; diff --git a/front/src/modules/ui/button/components/__stories__/ButtonGroup.stories.tsx b/front/src/modules/ui/button/components/__stories__/ButtonGroup.stories.tsx new file mode 100644 index 000000000..4ea5703b6 --- /dev/null +++ b/front/src/modules/ui/button/components/__stories__/ButtonGroup.stories.tsx @@ -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 { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { Button, ButtonPosition } from '../Button'; +import { ButtonGroup } from '../ButtonGroup'; + +const clickJestFn = jest.fn(); + +const meta: Meta = { + title: 'UI/Button/ButtonGroup', + component: ButtonGroup, + decorators: [ComponentDecorator], + argTypes: { children: { control: false } }, + args: { + children: Object.values(ButtonPosition).map((position) => ( +