import type { ListBoxItemProps } from 'react-aria-components'; import { ComboBox as AriaComboBox, ListBox, ListBoxItem, Popover, Input, Label, Text, Button, TagGroup, TagList, Tag } from 'react-aria-components'; import { useMemo, useRef, createContext, useContext } from 'react'; import type { RefObject } from 'react'; import { findOrCreateContainerElement } from '@coldwired/react'; import * as s from 'superstruct'; import { useLabelledBy, useDispatchChangeEvent, useMultiList, useSingleList, useRemoteList, useOnFormReset, createLoader, type ComboBoxProps } from './react-aria/hooks'; import { type Item, SingleComboBoxProps, MultiComboBoxProps, RemoteComboBoxProps } from './react-aria/props'; const getPortal = () => findOrCreateContainerElement('rac-portal'); export function ComboBox({ children, label, description, className, inputRef, ...props }: ComboBoxProps & { inputRef?: RefObject }) { return ( {label ? : null} {description ? ( {description} ) : null}
{children}
); } export function ComboBoxItem(props: ListBoxItemProps) { return ; } export function SingleComboBox({ children, ...maybeProps }: SingleComboBoxProps) { const { 'aria-labelledby': ariaLabelledby, items: defaultItems, selectedKey: defaultSelectedKey, emptyFilterKey, name, formValue, form, data, ...props } = useMemo(() => s.create(maybeProps, SingleComboBoxProps), [maybeProps]); const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); const { selectedItem, onReset, ...comboBoxProps } = useSingleList({ defaultItems, defaultSelectedKey, emptyFilterKey, onChange: dispatch }); return ( <> {(item) => {item.label}} {children || name ? ( {name ? ( ) : null} {children} ) : null} ); } export function MultiComboBox(maybeProps: MultiComboBoxProps) { const { 'aria-labelledby': ariaLabelledby, items: defaultItems, selectedKeys: defaultSelectedKeys, name, form, formValue, allowsCustomValue, valueSeparator, className, ...props } = useMemo(() => s.create(maybeProps, MultiComboBoxProps), [maybeProps]); const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); const inputRef = useRef(null); const { selectedItems, hiddenInputValues, onRemove, onReset, ...comboBoxProps } = useMultiList({ defaultItems, defaultSelectedKeys, onChange: dispatch, formValue, allowsCustomValue, valueSeparator, focusInput: () => { inputRef.current?.focus(); } }); const formResetRef = useOnFormReset(onReset); return (
{selectedItems.length > 0 ? ( {selectedItems.map((item) => ( {item.label} ))} ) : null} {(item) => {item.label}} {name ? ( {hiddenInputValues.length == 0 ? ( ) : ( hiddenInputValues.map((value, i) => ( )) )} ) : null}
); } export function RemoteComboBox({ loader, onChange, children, ...maybeProps }: RemoteComboBoxProps) { const { 'aria-labelledby': ariaLabelledby, items: defaultItems, selectedKey: defaultSelectedKey, allowsCustomValue, minimumInputLength, limit, debounce, coerce, formValue, name, form, data, ...props } = useMemo(() => s.create(maybeProps, RemoteComboBoxProps), [maybeProps]); const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); const load = useMemo( () => typeof loader == 'string' ? createLoader(loader, { minimumInputLength, limit, coerce }) : loader, [loader, minimumInputLength, limit, coerce] ); const { selectedItem, onReset, ...comboBoxProps } = useRemoteList({ allowsCustomValue, defaultItems, defaultSelectedKey, debounce, load, onChange: (item) => { onChange?.(item); dispatch(); } }); return ( <> 0} allowsCustomValue={allowsCustomValue} aria-labelledby={labelledby} {...comboBoxProps} {...props} > {(item) => {item.label}} {children || name ? ( {name ? ( ) : null} {children} ) : null} ); } export function ComboBoxValueSlot({ field, name, form, onReset, data }: { field: 'label' | 'value' | 'data'; name: string; form?: string; onReset?: () => void; data?: Record; }) { const selectedItem = useContext(SelectedItemContext); const value = getSelectedValue(selectedItem, field); const dataProps = Object.fromEntries( Object.entries(data ?? {}).map(([key, value]) => [ `data-${key.replace(/_/g, '-')}`, value ]) ); const ref = useOnFormReset(onReset); return ( ); } const SelectedItemContext = createContext(null); const SelectedItemProvider = SelectedItemContext.Provider; function getSelectedValue( selectedItem: Item | null, field: 'label' | 'value' | 'data' ): string { if (selectedItem == null) { return ''; } else if (field == 'data') { if (typeof selectedItem.data == 'string') { return selectedItem.data; } else if (!selectedItem.data) { return ''; } return JSON.stringify(selectedItem.data); } return selectedItem[field]; }