-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: useActiveOption.test.tsを復活させる
- Loading branch information
Showing
3 changed files
with
225 additions
and
42 deletions.
There are no files selected for viewing
93 changes: 93 additions & 0 deletions
93
packages/smarthr-ui/src/components/ComboBox/__tests__/useActiveOption.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { act, renderHook } from '@testing-library/react' | ||
|
||
import { useActiveOption } from '../useActiveOption' | ||
|
||
describe('useActiveOption', () => { | ||
const initialProps = { | ||
options: [ | ||
{ id: 'id1', selected: false, isNew: false, item: { label: 'label1', value: 'value1' } }, | ||
{ id: 'id2', selected: false, isNew: false, item: { label: 'label2', value: 'value2' } }, | ||
{ | ||
id: 'id3', | ||
selected: false, | ||
isNew: false, | ||
item: { label: 'label3', value: 'value3', disabled: true }, | ||
}, | ||
{ id: 'id4', selected: false, isNew: false, item: { label: 'label3', value: 'value3' } }, | ||
], | ||
} | ||
const resultDummy = renderHook((props) => useActiveOption(props), { | ||
initialProps, | ||
}) | ||
|
||
let result: (typeof resultDummy)['result'] | ||
let rerender: (typeof resultDummy)['rerender'] | ||
beforeEach(() => { | ||
const renderHookResult = renderHook((props) => useActiveOption(props), { initialProps }) | ||
result = renderHookResult.result | ||
rerender = renderHookResult.rerender | ||
}) | ||
|
||
it('options が変更されても activeOption が維持されること', () => { | ||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, 1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[0]) | ||
rerender({ | ||
options: [ | ||
...initialProps.options, | ||
{ id: 'id4', selected: false, isNew: false, item: { label: 'label4', value: 'value4' } }, | ||
], | ||
}) | ||
expect(result.current.activeOption).toEqual(initialProps.options[0]) | ||
}) | ||
|
||
it('options から activeOption と一致する option が消えたとき、activeOption がリセットされること', () => { | ||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, 1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[0]) | ||
rerender({ | ||
options: initialProps.options.slice(1), | ||
}) | ||
expect(result.current.activeOption).toBeNull() | ||
}) | ||
|
||
it('activeOption をセットできること', () => { | ||
act(() => result.current.setActiveOption(initialProps.options[2])) | ||
expect(result.current.activeOption).toEqual(initialProps.options[2]) | ||
}) | ||
|
||
describe('moveActiveOptionIndex(xxx, 1)', () => { | ||
it('activeOption が未設定のとき、最初に先頭のアイテムが選択されること', () => { | ||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, 1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[0]) | ||
}) | ||
|
||
it('disabled な option が飛ばされること', () => { | ||
act(() => result.current.setActiveOption(initialProps.options[1])) | ||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, 1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[3]) | ||
}) | ||
|
||
it('末尾の option から 先頭の option にループすること', () => { | ||
act(() => | ||
result.current.setActiveOption(initialProps.options[initialProps.options.length - 1]), | ||
) | ||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, 1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[0]) | ||
}) | ||
}) | ||
|
||
describe('moveActiveOptionIndex(xxx, - 1)', () => { | ||
it('activeOption を変更できること', () => { | ||
expect(result.current.activeOption).toBeNull() | ||
|
||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, -1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[3]) | ||
|
||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, -1)) | ||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, -1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[0]) | ||
|
||
act(() => result.current.moveActiveOptionIndex(result.current.activeOption, -1)) | ||
expect(result.current.activeOption).toEqual(initialProps.options[3]) | ||
}) | ||
}) | ||
}) |
130 changes: 130 additions & 0 deletions
130
packages/smarthr-ui/src/components/ComboBox/useActiveOption.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import React, { | ||
KeyboardEvent, | ||
ReactNode, | ||
RefObject, | ||
useCallback, | ||
useEffect, | ||
useId, | ||
useMemo, | ||
useRef, | ||
useState, | ||
} from 'react' | ||
import { tv } from 'tailwind-variants' | ||
|
||
import { type DecoratorsType, useDecorators } from '../../hooks/useDecorators' | ||
import { useEnhancedEffect } from '../../hooks/useEnhancedEffect' | ||
import { usePortal } from '../../hooks/usePortal' | ||
import { spacing } from '../../themes' | ||
import { FaInfoCircleIcon } from '../Icon' | ||
import { Loader } from '../Loader' | ||
import { VisuallyHiddenText } from '../VisuallyHiddenText' | ||
|
||
import { ListBoxItemButton } from './ListBoxItemButton' | ||
import { ComboBoxItem, ComboBoxOption } from './types' | ||
import { usePartialRendering } from './usePartialRendering' | ||
|
||
type Props<T> = { | ||
options: Array<ComboBoxOption<T>> | ||
dropdownHelpMessage?: ReactNode | ||
dropdownWidth?: string | number | ||
onAdd?: (label: string) => void | ||
onSelect: (item: ComboBoxItem<T>) => void | ||
isExpanded: boolean | ||
isLoading?: boolean | ||
triggerRef: RefObject<HTMLElement> | ||
decorators?: DecoratorsType<DecoratorKeyTypes> | ||
} | ||
|
||
type Rect = { | ||
top: number | ||
left: number | ||
height?: number | ||
} | ||
|
||
const DECORATOR_DEFAULT_TEXTS = { | ||
noResultText: '一致する選択肢がありません', | ||
loadingText: '処理中', | ||
} as const | ||
type DecoratorKeyTypes = keyof typeof DECORATOR_DEFAULT_TEXTS | ||
|
||
const KEY_DOWN_REGEX = /^(Arrow)?Down$/ | ||
const KEY_UP_REGEX = /^(Arrow)?Up/ | ||
|
||
const classNameGenerator = tv({ | ||
slots: { | ||
wrapper: 'shr-absolute', | ||
dropdownList: [ | ||
'smarthr-ui-ComboBox-dropdownList', | ||
'shr-absolute shr-z-overlap shr-box-border shr-min-w-full shr-overflow-y-auto shr-rounded-m shr-bg-white shr-py-0.5 shr-shadow-layer-3', | ||
/* 縦スクロールに気づきやすくするために8個目のアイテムが半分見切れるように max-height を算出 | ||
= (アイテムのフォントサイズ + アイテムの上下padding) * 7.5 + コンテナの上padding */ | ||
'shr-max-h-[calc((theme(fontSize.base)_+_theme(spacing[0.5])_*_2)_*_7.5_+_theme(spacing[0.5]))]', | ||
'aria-hidden:shr-hidden', | ||
], | ||
helpMessage: | ||
'shr-whitespace-[initial] shr-border-b-shorthand shr-mx-0.5 shr-mb-0.5 shr-mt-0 shr-px-0.5 shr-pb-0.5 shr-pt-0 shr-text-sm', | ||
loaderWrapper: 'shr-flex shr-items-center shr-justify-center shr-p-1', | ||
noItems: 'smarthr-ui-ComboBox-noItems shr-my-0 shr-bg-white shr-px-1 shr-py-0.5 shr-text-base', | ||
}, | ||
}) | ||
|
||
export const useListBox = <T>({ | ||
options, | ||
dropdownHelpMessage, | ||
dropdownWidth, | ||
onAdd, | ||
onSelect, | ||
isExpanded, | ||
isLoading, | ||
triggerRef, | ||
decorators, | ||
}: Props<T>) => { | ||
const [activeOption, setActiveOption] = useState<ComboBoxOption<T> | null>(null) | ||
|
||
useEffect(() => { | ||
// props の変更によって activeOption の状態が変わりうるので、実態を反映する | ||
setActiveOption((current) => { | ||
if (current === null) { | ||
return null | ||
} | ||
|
||
return options.find((option) => current.id === option.id) ?? null | ||
}) | ||
}, [options]) | ||
|
||
const moveActiveOptionIndex = useCallback( | ||
(currentActive: ComboBoxOption<T> | null, delta: -1 | 1) => { | ||
if (options.every((option) => option.item.disabled)) { | ||
return | ||
} | ||
|
||
const currentActiveIndex = | ||
currentActive === null ? -1 : options.findIndex((option) => option.id === currentActive.id) | ||
let nextIndex = 0 | ||
|
||
if (currentActiveIndex !== -1) { | ||
nextIndex = (currentActiveIndex + delta + options.length) % options.length | ||
} else if (delta !== 1) { | ||
nextIndex = options.length - 1 | ||
} | ||
|
||
const nextActive = options[nextIndex] | ||
|
||
if (nextActive) { | ||
if (nextActive.item.disabled) { | ||
// skip disabled item | ||
moveActiveOptionIndex(nextActive, delta) | ||
} else { | ||
setActiveOption(nextActive) | ||
} | ||
} | ||
}, | ||
[options], | ||
) | ||
|
||
return { | ||
activeOption, | ||
setActiveOption, | ||
moveActiveOptionIndex, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters