UNPKG

@vtex/admin-ui

Version:

> VTEX admin component library

135 lines (115 loc) 3.84 kB
import type { Dispatch, SetStateAction, ReactNode } from 'react' import { useEffect, useMemo, useState } from 'react' import type { ComboboxStateProps as AriakitComboboxStateProps, ComboboxState as AriakitComboboxState, } from 'ariakit/combobox' import { useComboboxState as useAriakitComboboxState } from 'ariakit/combobox' import { useDebounce } from '@vtex/admin-ui-hooks' // TODO: allow dev to customize the matches func const matchesSearch = (itemValue: string, inputValue: string) => { return itemValue.indexOf(inputValue) > -1 } export function useComboboxState<T>( props: ComboboxStateProps<T> = {} ): ComboboxState<T> { const { timeoutMs = 250, list, getOptionValue = (item: T) => typeof item === 'string' ? item : JSON.stringify(item), renderOption = (item: T) => typeof item === 'string' ? item : JSON.stringify(item), ...comboboxProps } = props const state = useAriakitComboboxState({ gutter: 4, ...comboboxProps }) const [deferredValue] = useDebounce(state.value, timeoutMs) // replacing ariakits matches for a custom one of any type const [matches, setMatches] = useState<T[]>(list || []) const [selectedItem, setSelectedItem] = useState<T>() // data states const [loading, setLoading] = useState(false) const [error, setError] = useState(false) // filters matches when input changes useEffect(() => { if (!list) return const sanitizedValue = state.value.toLocaleLowerCase() if (sanitizedValue === '') { setMatches(list) } else { setMatches( list.filter((item) => matchesSearch( getOptionValue(item).toLocaleLowerCase(), sanitizedValue ) ) ) } }, [state.value]) const status = useMemo<Status>(() => { if (loading) { return 'loading' } if (error) { return 'error' } const noMatches = !matches.length if (noMatches && deferredValue === '') { return 'empty-search' } if (noMatches) { return 'no-result' } return 'ready' }, [loading, error, matches, state.value]) return { ...state, deferredValue, setError, setLoading, status, getOptionValue, renderOption, setSelectedItem, selectedItem, setMatches, matches, } } type Status = 'loading' | 'error' | 'empty-search' | 'no-result' | 'ready' export type ComboboxStateProps<T> = Pick< AriakitComboboxStateProps, 'virtualFocus' > & { /** Initial list of values that will be filtered on search, should be set unless the input will be controlled */ list?: T[] /** Function for transforming item shape into a string value, no need to use it on string[] lists */ getOptionValue?: (item: T) => string /** Function for transforming item shape into renderable node, no need to use it on string[] lists */ renderOption?: (item: T) => ReactNode /** Debounce interval */ timeoutMs?: number } export type ComboboxState<T> = Omit<AriakitComboboxState, 'matches'> & { /** Debounced value. */ deferredValue: string /** Sets component state to error */ setError: Dispatch<SetStateAction<boolean>> /** Sets component state to loading */ setLoading: Dispatch<SetStateAction<boolean>> /** Component status */ status: Status /** Function that gets text value from the items in matches or list */ getOptionValue: (item: T) => string /** Function that render items from matches or list */ renderOption: (item: T) => ReactNode /** Array of options that matched with the input value */ matches: T[] /** Setter for array of options that matched with the input value */ setMatches: (arg: T[]) => void /** Full value of current selected item */ selectedItem: T | undefined /** Setter for current selected item */ setSelectedItem: (arg?: T) => void }