import type { JSX } from 'solid-js'; import { createComponent, createEffect, createMemo, mergeProps, onCleanup, } from 'solid-js'; import { omitProps } from 'solid-use/props'; import type { MultipleSelectStateControlledOptions, MultipleSelectStateUncontrolledOptions, SelectStateRenderProps, SingleSelectStateControlledOptions, SingleSelectStateUncontrolledOptions, } from '../../states/create-select-state'; import { SelectStateProvider, createMultipleSelectState, createSingleSelectState, } from '../../states/create-select-state'; import createDynamic from '../../utils/create-dynamic'; import createTypeAhead from '../../utils/create-type-ahead'; import type { DynamicProps, HeadlessPropsWithRef, ValidConstructor, } from '../../utils/dynamic-prop'; import { createForwardRef } from '../../utils/dynamic-prop'; import { SELECTED_NODE } from '../../utils/namespace'; import { createARIADisabledState, createDisabledState, createHasActiveState, createHasSelectedState, } from '../../utils/state-props'; import type { Prettify } from '../../utils/types'; import useEventListener from '../../utils/use-event-listener'; import { SelectContext, createSelectOptionFocusNavigator, } from './SelectContext'; import { SELECT_TAG } from './tags'; export interface SelectBaseProps { horizontal?: boolean; } export type SingleSelectControlledBaseProps = Prettify< SelectBaseProps & SingleSelectStateControlledOptions & SelectStateRenderProps >; export type SingleSelectControlledProps< V, T extends ValidConstructor = 'ul', > = HeadlessPropsWithRef>; export type SingleSelectUncontrolledBaseProps = Prettify< SelectBaseProps & SingleSelectStateUncontrolledOptions & SelectStateRenderProps >; export type SingleSelectUncontrolledProps< V, T extends ValidConstructor = 'ul', > = HeadlessPropsWithRef>; export type SingleSelectProps = | SingleSelectControlledProps | SingleSelectUncontrolledProps; export type MultipleSelectControlledBaseProps = Prettify< SelectBaseProps & MultipleSelectStateControlledOptions & SelectStateRenderProps >; export type MultipleSelectControlledProps< V, T extends ValidConstructor = 'ul', > = HeadlessPropsWithRef>; export type MultipleSelectUncontrolledBaseProps = Prettify< SelectBaseProps & MultipleSelectStateUncontrolledOptions & SelectStateRenderProps >; export type MultipleSelectUncontrolledProps< V, T extends ValidConstructor = 'ul', > = HeadlessPropsWithRef>; export type MultipleSelectProps = | MultipleSelectControlledProps | MultipleSelectUncontrolledProps; export type SelectProps = | SingleSelectProps | MultipleSelectProps; function isSelectMultiple( props: SelectProps, ): props is MultipleSelectProps { return !!props.multiple; } function isSelectUncontrolled( props: SelectProps, ): props is | SingleSelectUncontrolledProps | MultipleSelectUncontrolledProps { return 'defaultValue' in props; } export function Select( props: SelectProps, ): JSX.Element { return createMemo(() => { const controller = createSelectOptionFocusNavigator(); const [ref, setRef] = createForwardRef(props); const state = isSelectMultiple(props) ? createMultipleSelectState(props) : createSingleSelectState(props); const pushCharacter = createTypeAhead(value => { controller.setFirstMatch(value); }); createEffect(() => { const current = ref(); if (current instanceof HTMLElement) { controller.setRef(current); onCleanup(() => { controller.clearRef(); }); useEventListener(current, 'keydown', e => { if (!state.disabled()) { switch (e.key) { case 'ArrowUp': { if (!props.horizontal) { e.preventDefault(); controller.setPrevChecked(true); } break; } case 'ArrowLeft': { if (props.horizontal) { e.preventDefault(); controller.setPrevChecked(true); } break; } case 'ArrowDown': { if (!props.horizontal) { e.preventDefault(); controller.setNextChecked(true); } break; } case 'ArrowRight': { if (props.horizontal) { e.preventDefault(); controller.setNextChecked(true); } break; } case 'Home': { e.preventDefault(); controller.setFirstChecked(); break; } case 'End': { e.preventDefault(); controller.setLastChecked(); break; } case ' ': case 'Enter': { e.preventDefault(); break; } default: { if (e.key.length === 1) { pushCharacter(e.key); } break; } } } }); useEventListener(current, 'focus', () => { if (state.hasSelected()) { controller.setFirstChecked(SELECTED_NODE); } else { controller.setFirstChecked(); } }); useEventListener(current, 'focusin', e => { if (e.target && e.target !== current) { controller.setCurrent(e.target as HTMLElement); } }); } }); return createComponent(SelectContext.Provider, { value: { controller, get horizontal() { return !!props.horizontal; }, }, get children() { return createDynamic( () => props.as || ('ul' as T), mergeProps( SELECT_TAG, { id: controller.getId(), role: 'listbox', get 'aria-multiselectable'() { return props.multiple; }, ref: setRef, get 'aria-orientation'() { return props.horizontal ? 'horizontal' : 'vertical'; }, get tabindex() { return state.hasActive() ? -1 : 0; }, }, createDisabledState(() => state.disabled()), createARIADisabledState(() => state.disabled()), createHasSelectedState(() => state.hasSelected()), createHasActiveState(() => state.hasActive()), isSelectUncontrolled(props) ? omitProps(props, [ 'as', 'by', 'children', 'defaultValue', 'disabled', 'horizontal', 'multiple', 'onChange', 'ref', 'toggleable', ]) : omitProps(props, [ 'as', 'by', 'children', 'value', 'disabled', 'horizontal', 'multiple', 'onChange', 'ref', 'toggleable', ]), { get children() { return createComponent(SelectStateProvider, { state, get children() { return props.children; }, }); }, }, ) as DynamicProps, ); }, }); }) as unknown as JSX.Element; }