import { useCallback, useEffect, useReducer, useRef } from 'react'; import { useClickOutside } from '../../hooks/useClickOutside'; import { useUpdateEffect } from '../../hooks/useUpdateEffect'; enum Mode { Resting, Open } type State = { value: string | string[] | undefined; focused: string | undefined; mode: Mode; }; type Action = | { type: 'OPEN' } | { type: 'CLOSE' } | { type: 'SELECT'; payload: { value: string | string[] } } | { type: 'FOCUS'; payload: { value: string } }; const reducer = (state: State, action: Action): State => { switch (action.type) { case 'OPEN': return { ...state, mode: Mode.Open }; case 'CLOSE': return { ...state, mode: Mode.Resting, focused: undefined }; case 'SELECT': return { ...state, value: action.payload.value, focused: undefined, mode: Mode.Resting }; case 'FOCUS': return { ...state, focused: action.payload.value }; } }; interface UseSelectProps { value: string | string[] | undefined; defaultValue: string | string[] | undefined; } export const useSelect = ({ value, defaultValue }: UseSelectProps) => { const selectRef = useRef(null); const buttonRef = useRef(null); const initialValue = value ?? defaultValue; const [state, dispatch] = useReducer(reducer, { value: initialValue, focused: typeof initialValue == 'object' ? initialValue?.[0] : initialValue, mode: Mode.Resting }); const handleKeyDown = useCallback((event: KeyboardEvent) => { switch (event.key) { case 'Escape': { event.preventDefault(); dispatch({ type: 'CLOSE' }); buttonRef.current?.focus(); break; } default: break; } }, []); const handleClickOutside = useCallback(() => dispatch({ type: 'CLOSE' }), []); useClickOutside(selectRef, handleClickOutside); useEffect(() => { const el = selectRef.current; if (!el) return; el.addEventListener('keydown', handleKeyDown); return () => { el.removeEventListener('keydown', handleKeyDown); }; }, [handleKeyDown]); useUpdateEffect(() => { dispatch({ type: 'SELECT', payload: { value: value ?? '' } }); }, [value]); return { state, dispatch, Mode, selectRef, buttonRef }; };