mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 04:37:51 +00:00
137 lines
4.1 KiB
TypeScript
137 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Combobox,
|
|
ComboboxButton,
|
|
ComboboxInput,
|
|
ComboboxOption,
|
|
ComboboxOptions
|
|
} from '@headlessui/react';
|
|
import { ChevronDownIcon } from '@heroicons/react/16/solid';
|
|
import Spinner from 'components/spinner';
|
|
import { useDebounce } from 'hooks/use-debounce';
|
|
import get from 'lodash.get';
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { useInView } from 'react-intersection-observer';
|
|
|
|
type FilterFieldProps<T extends { [key: string]: unknown }> = {
|
|
options: T[];
|
|
selectedValue: T | null;
|
|
// eslint-disable-next-line no-unused-vars
|
|
onChange: (value: T | null) => void;
|
|
label: string;
|
|
displayKey?: string;
|
|
// eslint-disable-next-line no-unused-vars
|
|
getId: (option: T) => string;
|
|
disabled?: boolean;
|
|
autoFocus?: boolean;
|
|
isLoading?: boolean;
|
|
// eslint-disable-next-line no-unused-vars
|
|
loadMore?: (reset?: boolean) => void;
|
|
hasNextPage?: boolean;
|
|
};
|
|
|
|
const FilterField = <T extends { [key: string]: unknown }>({
|
|
options,
|
|
selectedValue,
|
|
onChange,
|
|
label,
|
|
displayKey = 'name',
|
|
getId,
|
|
disabled,
|
|
isLoading,
|
|
autoFocus = false,
|
|
loadMore,
|
|
hasNextPage
|
|
}: FilterFieldProps<T>) => {
|
|
const [query, setQuery] = useState('');
|
|
const getDisplayValue = useCallback(
|
|
(option: T | null) => {
|
|
if (!option) return '';
|
|
|
|
if (typeof option[displayKey] === 'string') {
|
|
return option[displayKey] as string;
|
|
}
|
|
|
|
return get(option, `${displayKey}.value`) as string;
|
|
},
|
|
[displayKey]
|
|
);
|
|
const [scrollTrigger, isInView] = useInView();
|
|
|
|
const filteredOptions =
|
|
query === ''
|
|
? options
|
|
: options.filter((option) => {
|
|
return getDisplayValue(option).toLocaleLowerCase().includes(query.toLowerCase());
|
|
});
|
|
|
|
const loadMoreFnRef = useRef<Function>();
|
|
|
|
useEffect(() => {
|
|
loadMoreFnRef.current = loadMore;
|
|
}, [loadMore]);
|
|
|
|
useEffect(() => {
|
|
if (isInView && hasNextPage) {
|
|
loadMoreFnRef.current?.();
|
|
}
|
|
}, [isInView, hasNextPage]);
|
|
|
|
const debouncedQuery = useDebounce(query);
|
|
|
|
useEffect(() => {
|
|
if (debouncedQuery && !filteredOptions.length) {
|
|
loadMoreFnRef.current?.(true);
|
|
}
|
|
}, [debouncedQuery, filteredOptions.length]);
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<Combobox
|
|
value={selectedValue}
|
|
by={displayKey}
|
|
onChange={onChange}
|
|
onClose={() => setQuery('')}
|
|
immediate
|
|
disabled={disabled || isLoading}
|
|
>
|
|
<div className="relative">
|
|
<ComboboxInput
|
|
aria-label={label}
|
|
displayValue={getDisplayValue}
|
|
placeholder={`Select ${label}`}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
className="w-full rounded border border-gray-200 py-1.5 pl-3 pr-8 text-sm ring-2 ring-transparent focus:outline-none focus-visible:outline-none data-[disabled]:cursor-not-allowed data-[autofocus]:border-0 data-[focus]:border-transparent data-[disabled]:opacity-50 data-[focus]:ring-2 data-[autofocus]:ring-secondary data-[focus]:ring-secondary data-[focus]:ring-offset-0"
|
|
autoFocus={autoFocus}
|
|
/>
|
|
<ComboboxButton className="group absolute inset-y-0 right-0 px-2.5 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
|
|
{isLoading ? (
|
|
<Spinner className="fill-black/60" />
|
|
) : (
|
|
<ChevronDownIcon className="fill-black/60 group-data-[hover]:fill-black size-5" />
|
|
)}
|
|
</ComboboxButton>
|
|
</div>
|
|
<ComboboxOptions
|
|
anchor="bottom"
|
|
className="z-10 w-[var(--input-width)] rounded-xl border border-gray-200 bg-white p-1 [--anchor-gap:6px] empty:hidden"
|
|
>
|
|
{filteredOptions.map((option) => (
|
|
<ComboboxOption
|
|
key={getId(option)}
|
|
value={option}
|
|
className="flex cursor-default select-none items-center gap-2 rounded-lg px-3 py-1.5 text-sm/6 data-[focus]:bg-secondary/10"
|
|
ref={scrollTrigger}
|
|
>
|
|
{getDisplayValue(option)}
|
|
</ComboboxOption>
|
|
))}
|
|
</ComboboxOptions>
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FilterField;
|