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 type { DynamicProps, HeadlessPropsWithRef, ValidConstructor, } from '../../utils/dynamic-prop'; import { createForwardRef } from '../../utils/dynamic-prop'; import { createARIADisabledState, createDisabledState, createHasActiveState, createHasSelectedState, } from '../../utils/state-props'; import type { Prettify } from '../../utils/types'; import useEventListener from '../../utils/use-event-listener'; import { AccordionContext, createAccordionFocusNavigator } from './AccordionContext'; import { ACCORDION_TAG } from './tags'; export type AccordionSingleControlledBaseProps = Prettify< SingleSelectStateControlledOptions & SelectStateRenderProps >; export type AccordionSingleControlledProps< V, T extends ValidConstructor = 'div', > = HeadlessPropsWithRef>; export type AccordionSingleUncontrolledBaseProps = Prettify< SingleSelectStateUncontrolledOptions & SelectStateRenderProps >; export type AccordionSingleUncontrolledProps< V, T extends ValidConstructor = 'div', > = HeadlessPropsWithRef>; export type AccordionMultipleControlledBaseProps = Prettify< MultipleSelectStateControlledOptions & SelectStateRenderProps >; export type AccordionMultipleControlledProps< V, T extends ValidConstructor = 'div', > = HeadlessPropsWithRef>; export type AccordionMultipleUncontrolledBaseProps = Prettify< MultipleSelectStateUncontrolledOptions & SelectStateRenderProps >; export type AccordionMultipleUncontrolledProps< V, T extends ValidConstructor = 'div', > = HeadlessPropsWithRef>; export type AccordionProps = | AccordionSingleControlledProps | AccordionSingleUncontrolledProps | AccordionMultipleControlledProps | AccordionMultipleUncontrolledProps; function isAccordionUncontrolled( props: AccordionProps, ): props is AccordionSingleUncontrolledProps | AccordionMultipleUncontrolledProps { return 'defaultValue' in props; } function isAccordionMultiple( props: AccordionProps, ): props is AccordionMultipleUncontrolledProps | AccordionMultipleControlledProps { return !!props.multiple; } /** * A set of vertically stacked sections, each of which can be expanded to * reveal its content. Wraps a select state, so `defaultValue`/`value` hold the * id of the open section, or an array of ids when `multiple` is set. * * Renders a `
` by default. * * @see {@link https://github.com/lxsmnsyc/terracotta/blob/main/docs/components/accordion.md} */ export function Accordion( props: AccordionProps, ): JSX.Element { return createMemo(() => { const state = isAccordionMultiple(props) ? createMultipleSelectState(props) : createSingleSelectState(props); const controller = createAccordionFocusNavigator(); const [ref, setRef] = createForwardRef(props); 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': { e.preventDefault(); controller.setPrevChecked(true); break; } case 'ArrowDown': { e.preventDefault(); controller.setNextChecked(true); break; } case 'Home': { e.preventDefault(); controller.setFirstChecked(); break; } case 'End': { e.preventDefault(); controller.setLastChecked(); break; } default: break; } } }); useEventListener(current, 'focusin', (e) => { if (e.target && e.target !== current) { controller.setCurrent(e.target as HTMLElement); } }); } }); return createComponent(AccordionContext.Provider, { value: controller, get children() { return createDynamic( () => props.as ?? ('div' as T), mergeProps( isAccordionUncontrolled(props) ? omitProps(props, [ 'as', 'by', 'children', 'defaultValue', 'disabled', 'multiple', 'onChange', 'ref', 'toggleable', ]) : omitProps(props, [ 'as', 'by', 'children', 'value', 'disabled', 'multiple', 'onChange', 'ref', 'toggleable', ]), ACCORDION_TAG, { ref: setRef, id: controller.getId(), }, createDisabledState(() => state.disabled()), createARIADisabledState(() => state.disabled()), createHasSelectedState(() => state.hasSelected()), createHasActiveState(() => state.hasActive()), { get children() { return createComponent(SelectStateProvider, { state, get children() { return props.children; }, }); }, }, ) as DynamicProps, ); }, }); }) as unknown as JSX.Element; }