'use client'; import type { ListCollection } from '@chakra-ui/react'; import { Box, Select as ChakraSelect, createListCollection, Flex, Portal, Icon, useSelectContext } from '@chakra-ui/react'; import { useDebounce } from '@uidotdev/usehooks'; import * as React from 'react'; import ArrowIcon from 'icons/arrows/east-mini.svg'; import CheckIcon from 'icons/check.svg'; import { FilterInput } from '../components/filters/FilterInput'; import { CloseButton } from './close-button'; import { Skeleton } from './skeleton'; export type ViewMode = 'default' | 'compact'; export interface SelectOption { label: string; renderLabel?: (place: 'item' | 'value-text') => React.ReactNode; value: Value; icon?: React.ReactNode; afterElement?: React.ReactNode; }; export interface SelectControlProps extends ChakraSelect.ControlProps { noIndicator?: boolean; triggerProps?: ChakraSelect.TriggerProps; loading?: boolean; } export const SelectControl = React.forwardRef< HTMLButtonElement, SelectControlProps >(function SelectControl(props, ref) { // NOTE: here defaultValue means the "default" option of the select, not its initial value const { children, noIndicator, triggerProps, loading, defaultValue, ...rest } = props; const context = useSelectContext(); const isDefaultValue = Array.isArray(defaultValue) ? context.value.every((item) => defaultValue.includes(item)) : context.value === defaultValue; return ( { children } { (!noIndicator) && ( { !noIndicator && ( ) } ) } ); }); export const SelectClearTrigger = React.forwardRef< HTMLButtonElement, ChakraSelect.ClearTriggerProps >(function SelectClearTrigger(props, ref) { return ( ); }); interface SelectContentProps extends ChakraSelect.ContentProps { portalled?: boolean; portalRef?: React.RefObject; } export const SelectContent = React.forwardRef< HTMLDivElement, SelectContentProps >(function SelectContent(props, ref) { const { portalled = true, portalRef, ...rest } = props; return ( ); }); export interface SelectItemProps extends ChakraSelect.ItemProps { item: SelectOption; } export const SelectItem = React.forwardRef< HTMLDivElement, SelectItemProps >(function SelectItem(props, ref) { const { item, children, ...rest } = props; const { icon, ...itemProps } = item; return ( { icon } { children } ); }); interface SelectValueTextProps extends Omit { children?(items: Array): React.ReactNode; size?: SelectRootProps['size']; required?: boolean; invalid?: boolean; errorText?: string; mode?: ViewMode; } export const SelectValueText = React.forwardRef< HTMLSpanElement, SelectValueTextProps >(function SelectValueText(props, ref) { const { children, size, required, invalid, errorText, mode, ...rest } = props; const context = useSelectContext(); const content = (() => { const items = context.selectedItems; const placeholder = `${ props.placeholder }${ required ? '*' : '' }${ invalid && errorText ? ` - ${ errorText }` : '' }`; if (items.length === 0) return placeholder; if (children) return children(items); if (items.length === 1) { const item = items[0] as SelectOption; if (!item) return placeholder; const label = size === 'lg' ? ( { placeholder } ) : null; return ( <> { label } { item.icon } { mode !== 'compact' && ( { item.renderLabel ? item.renderLabel('value-text') : context.collection.stringifyItem(item) } ) } ); } // FIXME: we don't have multiple selection in the select yet return `${ items.length } selected`; })(); return ( { content } ); }); export interface SelectRootProps extends ChakraSelect.RootProps {} export const SelectRoot = React.forwardRef< HTMLDivElement, ChakraSelect.RootProps >(function SelectRoot(props, ref) { const { lazyMount = true, unmountOnExit = true, ...rest } = props; return ( { props.asChild ? ( props.children ) : ( <> { props.children } ) } ); }) as ChakraSelect.RootComponent; interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps { label: React.ReactNode; } export const SelectItemGroup = React.forwardRef< HTMLDivElement, SelectItemGroupProps >(function SelectItemGroup(props, ref) { const { children, label, ...rest } = props; return ( { label } { children } ); }); export const SelectLabel = ChakraSelect.Label; export const SelectItemText = ChakraSelect.ItemText; export interface SelectProps extends SelectRootProps { collection: ListCollection; placeholder: string; portalled?: boolean; loading?: boolean; errorText?: string; contentProps?: SelectContentProps; contentHeader?: React.ReactNode; itemFilter?: (item: SelectOption) => boolean; mode?: ViewMode; } export const Select = React.forwardRef((props, ref) => { const { collection, placeholder, portalled = true, loading, errorText, contentProps, contentHeader, itemFilter, mode, ...rest } = props; return ( { contentHeader } { collection.items .filter(itemFilter ?? (() => true)) .map((item: SelectOption) => ( { item.renderLabel ? item.renderLabel('item') : item.label } { item.afterElement } )) } ); }); export interface SelectAsyncProps extends Omit { placeholder: string; portalled?: boolean; loading?: boolean; loadOptions: (input: string, currentValue: Array) => Promise>; extraControls?: React.ReactNode; mode?: ViewMode; } export const SelectAsync = React.forwardRef((props, ref) => { const { placeholder, portalled = true, loading, loadOptions, extraControls, onValueChange, errorText, mode, contentHeader, ...rest } = props; const [ collection, setCollection ] = React.useState>(createListCollection({ items: [] })); const [ inputValue, setInputValue ] = React.useState(''); const [ value, setValue ] = React.useState>([]); const debouncedInputValue = useDebounce(inputValue, 300); React.useEffect(() => { loadOptions(debouncedInputValue, value).then(setCollection); }, [ debouncedInputValue, loadOptions, value ]); const handleFilterChange = React.useCallback((value: string) => { setInputValue(value); }, [ ]); const handleValueChange = React.useCallback(({ value, items }: { value: Array; items: Array }) => { setValue(value); onValueChange?.({ value, items }); }, [ onValueChange ]); return ( { extraControls } { contentHeader } { collection.items.map((item) => ( { item.renderLabel ? item.renderLabel('item') : item.label } )) } ); });