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

feat: アイテムが選択されたときに選択済みかどうかを判定するコールバック関数をオプションで渡せるようにする #4346

Merged
merged 4 commits into from
Mar 1, 2024
7 changes: 7 additions & 0 deletions src/components/ComboBox/MultiComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ type Props<T> = BaseProps<T> & {
destroyButtonIconAlt?: (text: string) => string
selectedListAriaLabel?: (text: string) => string
}
/**
* アイテムが選択されたときに選択済みかどうかを判定するコールバック関数/
*/
isItemSelected?: (targetItem: ComboBoxItem<T>, selectedItems: Array<ComboBoxItem<T>>) => boolean

/**
* input 要素の属性
*/
Expand Down Expand Up @@ -185,6 +190,7 @@ const ActualMultiComboBox = <T,>(
onBlur,
onKeyPress,
decorators,
isItemSelected,
inputAttributes,
style,
...props
Expand All @@ -206,6 +212,7 @@ const ActualMultiComboBox = <T,>(
selected: selectedItems,
creatable,
inputValue,
isItemSelected,
})
const handleDelete = useCallback(
(item: ComboBoxItem<T>) => {
Expand Down
116 changes: 116 additions & 0 deletions src/components/ComboBox/__tests__/useOptions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,40 @@ describe('useOptions', () => {
expect(options3.length).toBe(0)
})

describe('isItemSelectedが渡されたとき', () => {
const isItemSelected = jest.fn((_targetItem, _selectedItems) => true)
it('selectedかどうかの判定の際にitemの数だけisItemSelectedが呼ばれること', () => {
const selected = [{ label: 'label2', value: 'value2' }]
const initialProps = {
items: [
{ label: 'label1', value: 'value1' },
{ label: 'label2', value: 'value2' },
{ label: 'label3', value: 'value3' },
],
selected,
creatable: false,
isItemSelected,
}
renderHook((props) => useOptions(props), { initialProps })
expect(isItemSelected).toHaveBeenNthCalledWith(
1,
{ label: 'label1', value: 'value1' },
selected,
)
expect(isItemSelected).toHaveBeenNthCalledWith(
2,
{ label: 'label2', value: 'value2' },
selected,
)
expect(isItemSelected).toHaveBeenNthCalledWith(
3,
{ label: 'label3', value: 'value3' },
selected,
)
expect(isItemSelected).toHaveBeenCalledTimes(3)
})
})

describe('ReactNode を含むオプションの場合', () => {
it('オプションが取得できること', () => {
const element = (
Expand Down Expand Up @@ -138,5 +172,87 @@ describe('useOptions', () => {
expect(options[0].item).toEqual({ label: element, value: 'value3' })
})
})

describe('すべてのオプションがReactNodeの場合', () => {
const labelElement1 = (
<div>
label<span>1</span>
</div>
)
const labelElement2 = (
<div>
label<span>2</span>
</div>
)
const labelElement3 = (
<div>
label<span>3</span>
</div>
)
it('オプションが取得できること', () => {
const initialProps = {
items: [
{ label: labelElement1, value: 'value1' },
{ label: labelElement2, value: 'value2' },
{ label: labelElement3, value: 'value3' },
],
selected: [{ label: labelElement3, value: 'value3' }],
creatable: false,
}
const { result } = renderHook((props) => useOptions(props), { initialProps })
const options = result.current.options

expect(options.length).toBe(3)
expect(options[0].item).toEqual({ label: labelElement1, value: 'value1' })
expect(options[0].selected).toBeFalsy()
expect(options[0].isNew).toBeFalsy()

expect(options[1].item).toEqual({ label: labelElement2, value: 'value2' })
expect(options[1].selected).toBeFalsy()
expect(options[1].isNew).toBeFalsy()

expect(options[2].item).toEqual({ label: labelElement3, value: 'value3' })
expect(options[2].selected).toBeTruthy()
expect(options[2].isNew).toBeFalsy()
})

it('入力によって options がフィルタリングされること', () => {
const initialProps = {
items: [
{ label: labelElement1, value: 'value1' },
{ label: labelElement2, value: 'value2' },
{ label: labelElement3, value: 'value3' },
],
selected: [{ label: labelElement3, value: 'value3' }],
creatable: false,
inputValue: 'label3',
}
const { result } = renderHook((props) => useOptions(props), { initialProps })
const options = result.current.options

expect(options.length).toBe(1)
expect(options[0].item).toEqual({ label: labelElement3, value: 'value3' })
})

it('isItemSelectedが渡されていなくてoptionのインスタンスが違うとき、selectedにならないこと', () => {
const newLabelElement1 = (
<div>
label<span>1</span>
</div>
)
const initialProps = {
items: [{ label: labelElement1, value: 'value1' }],
selected: [{ label: newLabelElement1, value: 'value1' }],
creatable: false,
}
const { result } = renderHook((props) => useOptions(props), { initialProps })
const options = result.current.options

expect(options.length).toBe(1)
expect(options[0].item).toEqual({ label: labelElement1, value: 'value1' })
expect(options[0].selected).toBeFalsy()
expect(options[0].isNew).toBeFalsy()
})
})
})
})
19 changes: 13 additions & 6 deletions src/components/ComboBox/useOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ import { useId } from '../../hooks/useId'
import { convertMatchableString } from './comboBoxHelper'
import { ComboBoxItem, ComboBoxOption } from './types'

const defaultIsItemSelected = <T>(
targetItem: ComboBoxItem<T>,
selectedItems: Array<ComboBoxItem<T>>,
) =>
selectedItems.find(
(selectedItem) =>
selectedItem.label === targetItem.label && selectedItem.value === targetItem.value,
) !== undefined

export function useOptions<T>({
items,
selected,
creatable,
inputValue = '',
isFilteringDisabled = false,
isItemSelected = defaultIsItemSelected,
}: {
items: Array<ComboBoxItem<T>>
selected: (ComboBoxItem<T> | null) | Array<ComboBoxItem<T>>
creatable: boolean
inputValue?: string
isFilteringDisabled?: boolean
isItemSelected?: (targetItem: ComboBoxItem<T>, selectedItems: Array<ComboBoxItem<T>>) => boolean
}) {
const isInputValueAddable = useMemo(
() => creatable && inputValue !== '' && !items.some((item) => item.label === inputValue),
Expand All @@ -34,16 +45,12 @@ export function useOptions<T>({
const isSelected = useCallback(
(item: ComboBoxItem<T>) => {
if (Array.isArray(selected)) {
return (
selected.find(
(_selected) => _selected.label === item.label && _selected.value === item.value,
) !== undefined
)
return isItemSelected(item, selected)
} else {
return selected !== null && selected.label === item.label
}
},
[selected],
[isItemSelected, selected],
)

const allOptions: Array<ComboBoxOption<T>> = useMemo(() => {
Expand Down
Loading