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

docs: SingleComboBox の Story を見直し #5143

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/smarthr-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@storybook/blocks": "^8.4.7",
"@storybook/cli": "^8.4.7",
"@storybook/manager-api": "^8.4.7",
"@storybook/preview-api": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/react-webpack5": "^8.4.7",
"@storybook/source-loader": "^8.4.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { fireEvent, userEvent, within } from '@storybook/test'
import { userEvent, within } from '@storybook/test'
import dayjs from 'dayjs'
import React from 'react'

import { Cluster, Stack } from '../../Layout'
import { Cluster } from '../../Layout'
import { Calendar } from '../Calendar'

import type { Meta, StoryObj } from '@storybook/react'
Expand All @@ -11,17 +11,17 @@ export default {
title: 'Data Display(データ表示)/Calendar/VRT',
render: (args) => (
<Cluster gap="S">
<Calendar value={dayjs('2024/11/06').toDate()} onSelectDate={args.onSelectDate} />
<Calendar value={dayjs('2024/10/06').toDate()} onSelectDate={args.onSelectDate} />
<Calendar
from={dayjs('2024/11/03').toDate()}
value={dayjs('2024/11/06').toDate()}
to={dayjs('2024/11/09').toDate()}
from={dayjs('2024/10/03').toDate()}
value={dayjs('2024/10/06').toDate()}
to={dayjs('2024/10/09').toDate()}
onSelectDate={args.onSelectDate}
/>
<Calendar
from={dayjs('2010/11/03').toDate()}
value={dayjs('2020/11/06').toDate()}
to={dayjs('2022/11/09').toDate()}
from={dayjs('2010/10/03').toDate()}
value={dayjs('2020/10/06').toDate()}
to={dayjs('2022/10/09').toDate()}
onSelectDate={args.onSelectDate}
/>
</Cluster>
Expand Down Expand Up @@ -49,9 +49,9 @@ export const VRTFocusVisible: StoryObj<typeof Calendar> = {
...VRT,
render: (args) => (
<Calendar
from={dayjs('2024/11/03').toDate()}
value={dayjs('2024/11/06').toDate()}
to={dayjs('2024/11/09').toDate()}
from={dayjs('2024/10/03').toDate()}
value={dayjs('2024/10/06').toDate()}
to={dayjs('2024/10/09').toDate()}
onSelectDate={args.onSelectDate}
/>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { userEvent } from '@storybook/test'
import { render, screen } from '@testing-library/react'
import React, { ComponentProps, act } from 'react'

import { FormControl } from '../FormControl'
import { FormControl } from '../../FormControl'

import { SingleComboBox } from './SingleComboBox'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ import React, {
import innerText from 'react-innertext'
import { tv } from 'tailwind-variants'

import { useClick } from '../../hooks/useClick'
import { genericsForwardRef } from '../../libs/util'
import { textColor } from '../../themes'
import { UnstyledButton } from '../Button'
import { FaCaretDownIcon, FaCircleXmarkIcon } from '../Icon'
import { Input } from '../Input'
import { useClick } from '../../../hooks/useClick'
import { genericsForwardRef } from '../../../libs/util'
import { textColor } from '../../../themes'
import { UnstyledButton } from '../../Button'
import { FaCaretDownIcon, FaCircleXmarkIcon } from '../../Icon'
import { Input } from '../../Input'
import { useListBox } from '../useListBox'
import { useOptions } from '../useOptions'

import { useListBox } from './useListBox'
import { useOptions } from './useOptions'

import type { BaseProps, ComboBoxItem } from './types'
import type { DecoratorsType } from '../../types'
import type { DecoratorsType } from '../../../types'
import type { BaseProps, ComboBoxItem } from '../types'

type Props<T> = BaseProps<T> & {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SingleComboBox } from './SingleComboBox'
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/* eslint-disable smarthr/a11y-input-in-form-control */
import { useArgs } from '@storybook/preview-api'
import { Meta, StoryObj } from '@storybook/react'
import React, { useState } from 'react'

import { FaCirclePlusIcon } from '../../../Icon'
import { Stack } from '../../../Layout'
import { Text } from '../../../Text'
import { SingleComboBox } from '../SingleComboBox'

// eslint-disable-next-line storybook/prefer-pascal-case
export const defaultItems = {
'option 1': {
label: 'option 1',
value: 'value-1',
data: {
option: 'option 1',
},
},
'option 2': {
label: 'option 2',
value: 'value-2',
data: {
option: 'option 2',
},
},
'option 3': {
label: 'option 3',
value: 'value-3',
disabled: true,
data: {
option: 'option 3',
},
},
'option 4': {
label: 'option 4',
value: 'value-4',
data: {
option: 'option 4',
},
},
'option 5': {
label: 'option 5',
value: 'value-5',
data: {
option: 'option 5',
},
},
'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)': {
label: 'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)',
value: 'value-6',
data: {
option:
'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)',
},
},
アイテムのラベルがReactNodeの場合: {
label: (
<Stack as="span" gap={0.25}>
<span>アイテムのラベルがReactNodeの場合</span>
<span>(ダミーテキストダミーテキストダミーテキストダミーテキスト)</span>
</Stack>
),
value: 'value-7',
data: {
option: 'アイテムのラベルがReactNodeの場合',
},
},
}

// eslint-disable-next-line storybook/prefer-pascal-case
export const prefixes = { なし: '', あり: <FaCirclePlusIcon /> }

export default {
title: 'Forms(フォーム)/SingleComboBox',
component: SingleComboBox,
render: (args) => {
const [, setArgs] = useArgs()
return (
<SingleComboBox
{...args}
onClearClick={() => setArgs({ selectedItem: null })}
onSelect={(item) => setArgs({ selectedItem: item.data?.option })}
/>
)
},
args: {
items: Object.values(defaultItems),
selectedItem: null,
},
argTypes: {
items: { control: 'object' },
selectedItem: {
control: { type: 'select' },
options: Object.keys(defaultItems),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nits]

細かすぎますが、Controls 上でいずれかのアイテムを選択状態にしちゃうと、それをクリアする手段がなくなってちょっとだけ困りました (単にブラウザリロードしてもURLに状態が保持されちゃうので…。)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s-sasaki-0529 消えた方が使い方がわかっていいかな〜と思ったので、消えたり選択できるようにしてしまいました!
20bc809

mapping: defaultItems,
},
defaultItem: {
control: { type: 'select' },
options: Object.keys(defaultItems),
mapping: defaultItems,
},
prefix: {
control: 'radio',
options: Object.keys(prefixes),
mapping: prefixes,
},
dropdownHelpMessage: {
control: { type: 'select' },
options: ['文字列', 'ReactNode'],
mapping: {
文字列: 'ヘルプメッセージ',
ReactNode: <Text className="shr-text-danger">React Nodeを渡したメッセージ</Text>,
},
},
},
parameters: {
chromatic: { disableSnapshot: true },
},
excludeStories: ['defaultItems', 'prefixes'],
} as Meta<typeof SingleComboBox<{ option: string }>>

export const Playground: StoryObj<typeof SingleComboBox> = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nits]

dropdownHelpMessage を Controls で編集しようとするとクラッシュしました。
ReactNode を取るコンポーネント全般で起こる問題なので Storybook が悪いのではって気はしますが。

Copy link
Contributor Author

@misako0927 misako0927 Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s-sasaki-0529 クラッシュするのは使いづらそうなので選択式に直しました!

0b6d0bb


export const SelectedItem: StoryObj<typeof SingleComboBox> = {
name: 'selectedItem',
args: {
selectedItem: defaultItems['option 2'],
},
}

export const DefaultItem: StoryObj<typeof SingleComboBox> = {
uknmr marked this conversation as resolved.
Show resolved Hide resolved
name: 'defaultItem',
render: (args) => {
const [selectItem, setSelectItem] = useState(args.defaultItem)
return (
<SingleComboBox
{...args}
selectedItem={selectItem ?? null}
onSelect={(item) => setSelectItem(item)}
/>
)
},
args: {
defaultItem: defaultItems['option 4'],
},
}

export const Prefix: StoryObj<typeof SingleComboBox> = {
name: 'prefix',
render: (args) => (
<Stack gap={1}>
<SingleComboBox {...args} prefix={prefixes['なし']} />
<SingleComboBox {...args} prefix={prefixes['あり']} />
</Stack>
),
}

export const Disabled: StoryObj<typeof SingleComboBox> = {
name: 'disabled',
args: {
disabled: true,
},
}

export const Error: StoryObj<typeof SingleComboBox> = {
name: 'error',
args: {
error: true,
},
}

export const Creatable: StoryObj<typeof SingleComboBox> = {
name: 'creatable',
args: {
creatable: true,
dropdownHelpMessage: '新しいアイテムを追加できます。',
},
}

export const IsLoading: StoryObj<typeof SingleComboBox> = {
name: 'isLoading',
args: {
isLoading: true,
},
}

export const Width: StoryObj<typeof SingleComboBox> = {
name: 'width',
args: {
width: '20rem',
},
}

export const DropdownHelpMessage: StoryObj<typeof SingleComboBox> = {
name: 'dropdownHelpMessage',
args: {
dropdownHelpMessage: 'ヘルプメッセージ',
},
}

export const DropdownWidth: StoryObj<typeof SingleComboBox> = {
name: 'dropdownWidth',
args: {
dropdownWidth: '30rem',
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* eslint-disable smarthr/a11y-input-in-form-control */
import { Meta, StoryObj } from '@storybook/react'
import { userEvent, within } from '@storybook/test'
import React from 'react'

import { Stack } from '../../../Layout'
import { SingleComboBox } from '../SingleComboBox'

import { defaultItems, prefixes } from './SingleComboBox.stories'

/* pict singleComboBox.pict
* disabled error width prefix selectedItem
* false false なし なし あり
* false true あり あり なし
* true true あり なし あり
* true false あり あり なし
* true true なし なし なし
* true false なし あり あり
*/

const _cases: Array<Omit<Parameters<typeof SingleComboBox>[0], 'items'>> = [
{
disabled: false,
error: false,
width: undefined,
prefix: undefined,
selectedItem: defaultItems['option 1'],
},
{ disabled: false, error: true, width: '15em', prefix: prefixes['あり'], selectedItem: null },
{
disabled: true,
error: true,
width: '15em',
prefix: undefined,
selectedItem:
defaultItems[
'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)'
],
},
{ disabled: true, error: false, width: '15em', prefix: prefixes['あり'], selectedItem: null },
{ disabled: true, error: true, width: undefined, prefix: undefined, selectedItem: null },
{
disabled: true,
error: false,
width: undefined,
prefix: prefixes['あり'],
selectedItem: defaultItems['アイテムのラベルがReactNodeの場合'],
},
]

const playSingle = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const canvas = within(canvasElement)
const textboxes = await canvas.findAllByRole('combobox')
await textboxes[textboxes.length - 1].focus()
const body = canvasElement.ownerDocument.body
const option = await within(body).findByText('option 1')
await userEvent.hover(option)
const helpMessage = await within(body).findAllByText('入力でフィルタリングできます。')
await userEvent.click(helpMessage[0]) // カーソルの点滅によるVRTのフレーキーを避けるためにフォーカスを移動する
}

export default {
title: 'Forms(フォーム)/SingleComboBox/VRT',
component: SingleComboBox,
render: (args) => (
<Stack align="flex-start" gap={2} className="shr-h-screen">
{_cases.map((props, i) => (
<SingleComboBox {...args} {...props} items={Object.values(defaultItems)} key={i} />
))}
<SingleComboBox
{...args}
name="default"
items={Object.values(defaultItems)}
dropdownHelpMessage="入力でフィルタリングできます。"
selectedItem={null}
/>
</Stack>
),
play: playSingle,
parameters: {
withTheming: true,
chromatic: { disableSnapshot: false },
},
tags: ['!autodocs', 'skip-test-runner'],
} as Meta<typeof SingleComboBox>

export const VRT: StoryObj<typeof SingleComboBox> = {}

export const VRTForcedColors: StoryObj<typeof SingleComboBox> = {
...VRT,
parameters: {
chromatic: { forcedColors: 'active' },
},
}
Loading
Loading