import type { JSX } from 'solid-js'; import { createComponent, createEffect, createMemo, createSignal, createUniqueId, mergeProps, } from 'solid-js'; import { omitProps } from 'solid-use/props'; import type { MultipleAutocompleteStateControlledOptions, MultipleAutocompleteStateUncontrolledOptions, SingleAutocompleteStateControlledOptions, SingleAutocompleteStateUncontrolledOptions, } from '../../states/create-autocomplete-state'; import { AutocompleteStateProvider, createMultipleAutocompleteState, createSingleAutocompleteState, } from '../../states/create-autocomplete-state'; import type { DisclosureStateControlledOptions, DisclosureStateUncontrolledOptions, } from '../../states/create-disclosure-state'; import { DisclosureStateProvider, createDisclosureState, } from '../../states/create-disclosure-state'; import createDynamic from '../../utils/create-dynamic'; import type { DynamicProps, HeadlessProps, ValidConstructor, } from '../../utils/dynamic-prop'; import { createARIADisabledState, createDisabledState, createExpandedState, createHasActiveState, createHasSelectedState, } from '../../utils/state-props'; import type { Prettify } from '../../utils/types'; import { ComboboxContext, createComboboxOptionFocusNavigator, } from './ComboboxContext'; import { COMBOBOX_TAG } from './tags'; export interface ComboboxBaseProps { onDisclosureChange?: (value: boolean) => void; } export interface ComboboxMultipleBaseProps { onSelectChange?: (value: V[]) => void; } export interface ComboboxSingleBaseProps { onSelectChange?: (value?: V) => void; } // SCSCD = Single, Controlled Select, Controlled Disclosure export type ComboboxSCSCDBaseProps = Prettify< ComboboxBaseProps & ComboboxSingleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxSCSCDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; // SCSCD = Single, Controlled Select, Uncontrolled Disclosure export type ComboboxSCSUDBaseProps = Prettify< ComboboxBaseProps & ComboboxSingleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxSCSUDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; // SCSCD = Single, Uncontrolled Select, Controlled Disclosure export type ComboboxSUSCDBaseProps = Prettify< ComboboxBaseProps & ComboboxSingleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxSUSCDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; // SCSCD = Single, Uncontrolled Select, Uncontrolled Disclosure export type ComboboxSUSUDBaseProps = Prettify< ComboboxBaseProps & ComboboxSingleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxSUSUDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; export type ComboboxSingleProps = | ComboboxSCSCDProps | ComboboxSCSUDProps | ComboboxSUSCDProps | ComboboxSUSUDProps; // MCSCD = Multiple, Controlled Select, Controlled Disclosure export type ComboboxMCSCDBaseProps = Prettify< ComboboxBaseProps & ComboboxMultipleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxMCSCDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; // MCSCD = Multiple, Controlled Select, Uncontrolled Disclosure export type ComboboxMCSUDBaseProps = Prettify< ComboboxBaseProps & ComboboxMultipleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxMCSUDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; // MCSCD = Multiple, Uncontrolled Select, Controlled Disclosure export type ComboboxMUSCDBaseProps = Prettify< ComboboxBaseProps & ComboboxMultipleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxMUSCDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; // MCSCD = Multiple, Uncontrolled Select, Uncontrolled Disclosure export type ComboboxMUSUDBaseProps = Prettify< ComboboxBaseProps & ComboboxMultipleBaseProps & Omit, 'onChange'> & Omit & { children?: JSX.Element; } >; export type ComboboxMUSUDProps< V, T extends ValidConstructor = 'div', > = HeadlessProps>; export type ComboboxMultipleProps = | ComboboxMCSCDProps | ComboboxMCSUDProps | ComboboxMUSCDProps | ComboboxMUSUDProps; type ComboboxSelectUncontrolledProps = | ComboboxMUSCDProps | ComboboxMUSUDProps | ComboboxSUSCDProps | ComboboxSUSUDProps; type ComboboxDisclosureUncontrolledProps< V, T extends ValidConstructor = 'div', > = | ComboboxMCSUDProps | ComboboxMUSUDProps | ComboboxSCSUDProps | ComboboxSUSUDProps; export type ComboboxProps = | ComboboxMultipleProps | ComboboxSingleProps; function isComboboxMultiple( props: ComboboxProps, ): props is ComboboxMultipleProps { return !!props.multiple; } function isComboboxSelectUncontrolled( props: ComboboxProps, ): props is ComboboxSelectUncontrolledProps { return 'defaultValue' in props; } function isComboboxDisclosureUncontrolled< V, T extends ValidConstructor = 'div', >( props: ComboboxProps, ): props is ComboboxDisclosureUncontrolledProps { return 'defaultOpen' in props; } function getProps( props: ComboboxProps, ): DynamicProps { if (isComboboxSelectUncontrolled(props)) { if (isComboboxDisclosureUncontrolled(props)) { return omitProps(props, [ 'as', 'by', 'children', 'defaultOpen', 'defaultValue', 'disabled', 'matchBy', 'multiple', 'onClose', 'onDisclosureChange', 'onOpen', 'onSelectChange', 'toggleable', ]) as DynamicProps; } return omitProps(props, [ 'as', 'by', 'children', 'defaultValue', 'disabled', 'isOpen', 'matchBy', 'multiple', 'onClose', 'onDisclosureChange', 'onOpen', 'onSelectChange', 'toggleable', ]) as DynamicProps; } if (isComboboxDisclosureUncontrolled(props)) { return omitProps(props, [ 'as', 'by', 'children', 'defaultOpen', 'disabled', 'matchBy', 'multiple', 'onClose', 'onDisclosureChange', 'onOpen', 'onSelectChange', 'toggleable', 'value', ]) as DynamicProps; } return omitProps(props, [ 'as', 'by', 'children', 'disabled', 'isOpen', 'matchBy', 'multiple', 'onClose', 'onDisclosureChange', 'onOpen', 'onSelectChange', 'toggleable', 'value', ]) as DynamicProps; } export function Combobox( props: ComboboxProps, ): JSX.Element { return createMemo(() => { const labelID = createUniqueId(); const inputID = createUniqueId(); const optionsID = createUniqueId(); const disclosureState = createDisclosureState( mergeProps(props, { onChange(value: boolean) { if (props.onDisclosureChange) { props.onDisclosureChange(value); } }, }), ); const autocompleteState = isComboboxMultiple(props) ? createMultipleAutocompleteState( mergeProps(props, { onChange(value: V[]) { if (props.onSelectChange) { props.onSelectChange(value); } }, }), ) : createSingleAutocompleteState( mergeProps(props, { onChange(value?: V) { if (props.onSelectChange) { props.onSelectChange(value); } }, }), ); const controller = createComboboxOptionFocusNavigator(); const [activeDescendant, setActiveDescendant] = createSignal(); const [selectedDescendant, setSelectedDescendant] = createSignal< string | undefined >(undefined, { equals: false, }); createEffect(() => { if (!autocompleteState.hasActive()) { setActiveDescendant(undefined); } }); return createComponent(ComboboxContext.Provider, { value: { get multiple() { return props.multiple; }, labelID, inputID, optionsID, controller, inputHovering: false, optionsHovering: false, get activeDescendant() { return activeDescendant(); }, set activeDescendant(value: string | undefined) { setActiveDescendant(value); }, get selectedDescendant() { return selectedDescendant(); }, set selectedDescendant(value: string | undefined) { setSelectedDescendant(value); }, }, get children() { return createComponent(AutocompleteStateProvider, { state: autocompleteState, get children() { return createComponent(DisclosureStateProvider, { state: disclosureState, get children() { return createDynamic( () => props.as || 'div', mergeProps( COMBOBOX_TAG, { 'aria-labelledby': labelID, get children() { return props.children; }, }, createDisabledState(() => autocompleteState.disabled()), createARIADisabledState(() => autocompleteState.disabled()), createHasSelectedState(() => autocompleteState.hasSelected(), ), createHasActiveState(() => autocompleteState.hasActive()), createExpandedState(() => disclosureState.isOpen()), getProps(props), ) as DynamicProps, ); }, }); }, }); }, }); }) as unknown as JSX.Element; }