diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox.test.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiComboBox.test.tsx similarity index 99% rename from packages/smarthr-ui/src/components/ComboBox/MultiComboBox.test.tsx rename to packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiComboBox.test.tsx index 47fb166f30..1f54ec1f2f 100644 --- a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox.test.tsx +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiComboBox.test.tsx @@ -2,10 +2,9 @@ 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 { MultiComboBox } from './MultiComboBox' -import { SingleComboBox } from './SingleComboBox' describe('SingleComboBox', () => { beforeEach(() => { diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiComboBox.tsx similarity index 97% rename from packages/smarthr-ui/src/components/ComboBox/MultiComboBox.tsx rename to packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiComboBox.tsx index e5f6d49aaf..e304b25962 100644 --- a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox.tsx +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiComboBox.tsx @@ -15,19 +15,19 @@ import { useId } from 'react' import innerText from 'react-innertext' import { tv } from 'tailwind-variants' -import { useOuterClick } from '../../hooks/useOuterClick' -import { genericsForwardRef } from '../../libs/util' -import { textColor } from '../../themes' -import { FaCaretDownIcon } from '../Icon' +import { useOuterClick } from '../../../hooks/useOuterClick' +import { genericsForwardRef } from '../../../libs/util' +import { textColor } from '../../../themes' +import { FaCaretDownIcon } from '../../Icon' +import { useFocusControl } from '../useFocusControl' +import { useListBox } from '../useListBox' +import { useOptions } from '../useOptions' import { MultiSelectedItem } from './MultiSelectedItem' import { hasParentElementByClassName } from './multiComboBoxHelper' -import { useFocusControl } from './useFocusControl' -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 = BaseProps & { /** diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiSelectedItem.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiSelectedItem.tsx similarity index 95% rename from packages/smarthr-ui/src/components/ComboBox/MultiSelectedItem.tsx rename to packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiSelectedItem.tsx index 8bd78be713..92b07f3213 100644 --- a/packages/smarthr-ui/src/components/ComboBox/MultiSelectedItem.tsx +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiSelectedItem.tsx @@ -1,12 +1,12 @@ import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { tv } from 'tailwind-variants' -import { UnstyledButton } from '../Button' -import { Chip } from '../Chip' -import { FaTimesCircleIcon } from '../Icon' +import { UnstyledButton } from '../../Button' +import { Chip } from '../../Chip' +import { FaTimesCircleIcon } from '../../Icon' +import { ComboBoxItem } from '../types' import { MultiSelectedItemTooltip } from './MultiSelectedItemTooltip' -import { ComboBoxItem } from './types' export type Props = { item: ComboBoxItem & { deletable?: boolean } diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiSelectedItemTooltip.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiSelectedItemTooltip.tsx similarity index 91% rename from packages/smarthr-ui/src/components/ComboBox/MultiSelectedItemTooltip.tsx rename to packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiSelectedItemTooltip.tsx index f6429214c9..1511022b10 100644 --- a/packages/smarthr-ui/src/components/ComboBox/MultiSelectedItemTooltip.tsx +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/MultiSelectedItemTooltip.tsx @@ -1,6 +1,6 @@ import React, { FC, PropsWithChildren, ReactNode } from 'react' -import { Tooltip } from '../Tooltip' +import { Tooltip } from '../../Tooltip' type Props = PropsWithChildren<{ needsTooltip: boolean diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/index.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/index.tsx new file mode 100644 index 0000000000..68ef677601 --- /dev/null +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/index.tsx @@ -0,0 +1 @@ +export { MultiComboBox } from './MultiComboBox' diff --git a/packages/smarthr-ui/src/components/ComboBox/multiComboBoxHelper.ts b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/multiComboBoxHelper.ts similarity index 100% rename from packages/smarthr-ui/src/components/ComboBox/multiComboBoxHelper.ts rename to packages/smarthr-ui/src/components/ComboBox/MultiComboBox/multiComboBoxHelper.ts diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/MultiComboBox.stories.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/MultiComboBox.stories.tsx new file mode 100644 index 0000000000..90a6097fc0 --- /dev/null +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/MultiComboBox.stories.tsx @@ -0,0 +1,158 @@ +/* eslint-disable smarthr/a11y-input-in-form-control */ +import { useArgs } from '@storybook/preview-api' +import { Meta, StoryObj } from '@storybook/react' +import React from 'react' + +import { Stack } from '../../../Layout' +import { Text } from '../../../Text' +import { MultiComboBox } from '../MultiComboBox' + +// eslint-disable-next-line storybook/prefer-pascal-case +export const defaultItems = { + 'option 1': { + label: 'option 1', + value: 'value-1', + data: { + name: 'test', + age: 23, + }, + }, + 'option 2': { + label: 'option 2', + value: 'value-2', + data: { + name: 'test 2', + age: 34, + }, + }, + 'option 3': { + label: 'option 3', + value: 'value-3', + disabled: true, + }, + 'option 4': { + label: 'option 4', + value: 'value-4', + }, + 'option 5': { + label: 'option 5', + value: 'value-5', + }, + 'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)': { + label: 'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)', + value: 'value-6', + }, + アイテムのラベルがReactNodeの場合: { + label: ( + + アイテムのラベルがReactNodeの場合 + (ダミーテキストダミーテキストダミーテキストダミーテキスト) + + ), + value: 'value-7', + }, +} + +export default { + title: 'Forms(フォーム)/MultiComboBox', + component: MultiComboBox, + render: (args) => { + const [_, setArgs] = useArgs() + return ( + + setArgs({ + selectedItems: args.selectedItems.filter( + (selectedItem) => selectedItem.value !== item.value, + ), + }) + } + onSelect={(item) => + setArgs({ + selectedItems: [...args.selectedItems, item], + }) + } + /> + ) + }, + args: { + items: Object.values(defaultItems), + selectedItems: [], + }, + argTypes: { + items: { control: 'object' }, + selectedItems: { control: 'object' }, + dropdownHelpMessage: { + control: { type: 'select' }, + options: ['文字列', 'ReactNode'], + mapping: { + 文字列: 'ヘルプメッセージ', + ReactNode: React Nodeを渡したメッセージ, + }, + }, + }, + parameters: { + chromatic: { disableSnapshot: true }, + }, + excludeStories: ['defaultItems'], +} as Meta> + +export const Playground: StoryObj = {} + +export const SelectedItems: StoryObj = { + name: 'selectedItems', + args: { + selectedItems: [defaultItems['option 1'], defaultItems['option 4']], + }, +} + +export const Disabled: StoryObj = { + name: 'disabled', + args: { + disabled: true, + }, +} + +export const Error: StoryObj = { + name: 'error', + args: { + error: true, + }, +} + +export const Creatable: StoryObj = { + name: 'creatable', + args: { + creatable: true, + dropdownHelpMessage: '新しいアイテムを追加できます。', + }, +} + +export const IsLoading: StoryObj = { + name: 'isLoading', + args: { + isLoading: true, + }, +} + +export const Width: StoryObj = { + name: 'width', + args: { + width: '20rem', + }, +} + +export const DropdownHelpMessage: StoryObj = { + name: 'dropdownHelpMessage', + args: { + dropdownHelpMessage: 'ヘルプメッセージ', + }, +} + +export const DropdownWidth: StoryObj = { + name: 'dropdownWidth', + args: { + dropdownWidth: '30rem', + }, +} diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/VRTMultiCombobox.stories.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/VRTMultiCombobox.stories.tsx new file mode 100644 index 0000000000..f9ba23193a --- /dev/null +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/VRTMultiCombobox.stories.tsx @@ -0,0 +1,110 @@ +/* eslint-disable smarthr/a11y-input-in-form-control */ +import { Meta, StoryObj } from '@storybook/react' +import { userEvent, within } from '@storybook/test' +import React, { useState } from 'react' + +import { Stack } from '../../../Layout' +import { ComboBoxItem } from '../../types' +import { MultiComboBox } from '../MultiComboBox' + +import { defaultItems } from './MultiComboBox.stories' + +/* + * pict multiComboBox.pict + * disabled error width selectedItems + * false true なし なし + * false false あり 複数 + * true true あり 一つ + * false false なし 一つ + * true true なし 複数 + * true false あり なし + */ + +const _cases: Array[0], 'items'>> = [ + { disabled: false, error: true, width: undefined, selectedItems: [] }, + { + disabled: false, + error: false, + width: '15em', + selectedItems: [defaultItems['option 1'], defaultItems['option 4']], + }, + { disabled: true, error: true, width: '15em', selectedItems: [defaultItems['option 3']] }, + { + disabled: false, + error: false, + width: undefined, + selectedItems: [defaultItems['アイテムのラベルがReactNodeの場合']], + }, + { + disabled: true, + error: true, + width: undefined, + selectedItems: [ + defaultItems['option 2'], + defaultItems[ + 'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)' + ], + ], + }, + { disabled: true, error: false, width: '15em', selectedItems: [] }, +] + +const waitForRAF = () => + new Promise((resolve) => { + requestAnimationFrame(() => { + resolve() + }) + }) +const playMulti = async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const canvas = within(canvasElement) + const comboboxes = await canvas.findAllByRole('combobox') + comboboxes[comboboxes.length - 1].focus() + const body = canvasElement.ownerDocument.body + const option1 = await within(body).findByRole('option', { name: 'option 1' }) + await userEvent.click(option1) + await waitForRAF() + const option2 = await within(body).findByRole('option', { name: 'option 2' }) + await userEvent.click(option2) + await waitForRAF() + const helpMessage = await within(body).findAllByText('入力でフィルタリングできます。') + await userEvent.click(helpMessage[0]) // カーソルの点滅によるVRTのフレーキーを避けるためにフォーカスを移動する +} + +export default { + title: 'Forms(フォーム)/MultiComboBox/VRT', + component: MultiComboBox, + render: (args) => { + const items = Object.values(defaultItems) + const [selectedItems, setSelectedItems] = useState>>([]) + return ( + + {_cases.map((props, i) => ( + + ))} + setSelectedItems(its)} + /> + + ) + }, + play: playMulti, + parameters: { + withTheming: true, + chromatic: { disableSnapshot: false }, + }, + tags: ['!autodocs', 'skip-test-runner'], +} as Meta + +export const VRT: StoryObj = {} + +export const VRTForcedColors: StoryObj = { + ...VRT, + parameters: { + chromatic: { forcedColors: 'active' }, + }, +} diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/multiComboBox.pict b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/multiComboBox.pict new file mode 100644 index 0000000000..5c13a6fc32 --- /dev/null +++ b/packages/smarthr-ui/src/components/ComboBox/MultiComboBox/stories/multiComboBox.pict @@ -0,0 +1,4 @@ +disabled: true, false +error: true, false +width: あり, なし +selectedItems: 一つ, 複数, なし diff --git a/packages/smarthr-ui/src/components/ComboBox/MultiCombobox.stories.tsx b/packages/smarthr-ui/src/components/ComboBox/MultiCombobox.stories.tsx deleted file mode 100644 index ebd09626b2..0000000000 --- a/packages/smarthr-ui/src/components/ComboBox/MultiCombobox.stories.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { action } from '@storybook/addon-actions' -import { StoryFn } from '@storybook/react' -import React, { ReactNode, useCallback, useState } from 'react' - -import { FormControl } from '../FormControl' -import { Stack } from '../Layout' - -import { MultiComboBox } from '.' - -export default { - title: 'Forms(フォーム)/MultiComboBox', - component: MultiComboBox, - parameters: { - docs: { - source: { type: 'dynamic' }, - }, - }, -} - -const defaultItems = [ - { - label: 'option 1', - value: 'value-1', - data: { - name: 'test', - age: 23, - }, - }, - { - label: 'option 2', - value: 'value-2', - data: { - name: 'test 2', - age: 34, - }, - }, - { - label: 'option 3', - value: 'value-3', - disabled: true, - }, - { - label: 'option 4', - value: 'value-4', - }, - { - label: 'option 5', - value: 'value-5', - }, - { - label: 'アイテムのラベルが長い場合(ダミーテキストダミーテキストダミーテキストダミーテキスト)', - value: 'value-6', - }, - { - label: ( - - アイテムのラベルがReactNodeの場合 - (ダミーテキストダミーテキストダミーテキストダミーテキスト) - - ), - value: 'value-7', - }, -] - -const manyItems = Array.from({ length: 2000 }).map((_, i) => ({ - label: `option ${i}`, - value: `option ${i}`, -})) - -type Item = { label: ReactNode; value: string; disabled?: boolean; data?: any } - -export const MultiCombobox: StoryFn = () => { - const [items, setItems] = useState(defaultItems) - const [selectedItems, setSelectedItems] = useState([]) - const [seq, setSeq] = useState(0) - const [controlledInputValue, setControlledInputValue] = useState('') - - const handleSelectItem = useCallback( - (item: Item) => { - action('onSelect')(item) - setSelectedItems([...selectedItems, item]) - }, - [selectedItems], - ) - const handleDelete = useCallback( - (deleted: Item) => { - action('onDelete')() - setSelectedItems(selectedItems.filter((item) => item.value !== deleted.value)) - }, - [selectedItems], - ) - const handleAddItem = useCallback( - (label: string) => { - action('onAdd')(label) - const newItem = { - label, - value: label, - } - setItems([...items, newItem]) - setSelectedItems([...selectedItems, newItem]) - setSeq(seq + 1) - }, - [items, selectedItems, seq], - ) - - return ( - - - { - action('onChangeSelected')(selected) - setSelectedItems(selected) - }} - onFocus={action('onFocus')} - onBlur={action('onBlur')} - data-test="multi-combobox-default" - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - `no result.(${text})`, - destroyButtonIconAlt: (text) => `destroy.(${text})`, - selectedListAriaLabel: (text) => `selected item list.(${text})`, - }} - /> - - - ({ ...item, deletable: false }))} - dropdownHelpMessage="入力でフィルタリングできます。" - onDelete={handleDelete} - onSelect={handleSelectItem} - data-test="multi-combobox-undeletable" - /> - - - { - action('onChangeSelected')(selected) - setSelectedItems(selected) - }} - inputValue={controlledInputValue} - onChangeInput={(e) => { - setControlledInputValue(e.target.value) - }} - onBlur={() => setControlledInputValue('')} - /> - - - - - - - - - - - - ) -} diff --git a/packages/smarthr-ui/src/components/ComboBox/VRTMultiCombobox.stories.tsx b/packages/smarthr-ui/src/components/ComboBox/VRTMultiCombobox.stories.tsx deleted file mode 100644 index f8713f1100..0000000000 --- a/packages/smarthr-ui/src/components/ComboBox/VRTMultiCombobox.stories.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { StoryFn } from '@storybook/react' -import { userEvent, within } from '@storybook/test' -import React from 'react' - -import { InformationPanel } from '../InformationPanel' -import { Stack } from '../Layout' - -import { MultiCombobox as StoriesMultiComboBox } from './MultiCombobox.stories' - -import { MultiComboBox } from '.' - -export default { - title: 'Forms(フォーム)/MultiComboBox', - component: MultiComboBox, - parameters: { - withTheming: true, - }, -} - -export const VRTMultiCombobox: StoryFn = () => ( - - - Multiコンボボックスのリストを展開して1つ目と2つ目の項目を選択した状態で表示されます - - {/* eslint-disable-next-line smarthr/a11y-input-has-name-attribute */} - - -) - -const waitForRAF = () => - new Promise((resolve) => { - requestAnimationFrame(() => { - resolve() - }) - }) -const playMulti = async ({ canvasElement }: { canvasElement: HTMLElement }) => { - const canvas = within(canvasElement) - const comboboxes = await canvas.findAllByRole('combobox') - comboboxes[0].focus() - const body = canvasElement.ownerDocument.body - const option1 = await within(body).findByText('option 1') - await userEvent.click(option1) - await waitForRAF() - const option2 = await within(body).findByText('option 2') - await userEvent.click(option2) - await waitForRAF() - const helpMessage = await within(body).findAllByText('入力でフィルタリングできます。') - await userEvent.click(helpMessage[0]) // カーソルの点滅によるVRTのフレーキーを避けるためにフォーカスを移動する -} -VRTMultiCombobox.play = playMulti - -export const VRTForcedColorsMultiCombobox: StoryFn = () => ( - - - Chromatic 上では強制カラーモードで表示されます{' '} - - {/* eslint-disable-next-line smarthr/a11y-input-has-name-attribute */} - - -) -VRTForcedColorsMultiCombobox.play = playMulti -VRTForcedColorsMultiCombobox.parameters = { - chromatic: { forcedColors: 'active' }, -}