import type { JSX } from "@solidjs/web"; import { type Accessor, createContext, createMemo, useContext } from "solid-js"; import { type BaseUIChangeEventDetails, type BaseUIEventReasons, createChangeEventDetails, REASONS, } from "../internals/change-event-details"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; import type { Orientation } from "../internals/types"; import type { AccordionValue } from "./accordion-root-context"; export type { AccordionValue }; export type AccordionChangeReason = BaseUIEventReasons["none"]; export type AccordionChangeEventDetails = BaseUIChangeEventDetails; /** * Options for {@link createAccordion}. Every reactive input is * `MaybeAccessor`. Pure function: no internal state. * * **Solid-native shape (2026-04-30).** `value` is required and the consumer * always owns the signal; the primitive reads through the accessor and * dispatches changes via `onValueChange`. */ export interface CreateAccordionOptions { /** Open values. Empty array → "no item open". Consumer-owned signal. */ value: MaybeAccessor>; /** * Whether multiple items can be open at the same time. * @default false */ multiple?: MaybeAccessor; /** Whether the whole accordion is disabled. @default false */ disabled?: MaybeAccessor; /** Layout orientation. @default "vertical" */ orientation?: MaybeAccessor; /** * Pre-change callback. Called with the new value array + a cancel-capable * details bag. The consumer's signal write is the only writer — the * primitive holds no internal state. */ onValueChange?: ( value: AccordionValue, details: AccordionChangeEventDetails, ) => void; } /** Public api returned by {@link createAccordion}. */ export interface AccordionApi { /** Currently-open values (always an array). */ value: Accessor>; /** Whether multi-expand mode is on. */ multiple: Accessor; /** Whether the whole accordion is disabled. */ disabled: Accessor; /** Layout orientation. */ orientation: Accessor; /** Selection check for a given item value. */ isOpen: (itemValue: Value) => boolean; /** * Toggle an item open or closed. In single-expand mode, opening one item * implicitly closes any other open item; in multi-expand mode, the new * item is added to / removed from the open set. * * `nextOpen` is the desired state. Pass `event` if dispatching from a DOM * handler — it's threaded through the details bag for cancellation. */ toggle: (itemValue: Value, nextOpen: boolean, event?: Event) => void; /** Context provider — pair with {@link useAccordion}. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } const AccordionContext = createContext | null>(null); /** * Read the nearest {@link AccordionApi} provided by `c.Provider`. Throws if * called outside a Provider. */ export function useAccordion(): AccordionApi { const ctx = useContext(AccordionContext); if (ctx == null) { throw new Error( "Base UI: useAccordion() must be called inside a .", ); } return ctx as AccordionApi; } /** * Solid-native headless accordion primitive. Owns the value-array * normalization + single/multi toggle logic; the structural wrapper * `` layers the CompositeList (sibling-trigger keyboard * navigation) and the region role on top. */ export function createAccordion( options: CreateAccordionOptions, ): AccordionApi { const value = createMemo>( () => readOption(options.value) ?? ([] as AccordionValue), { name: "accordion.value" }, ); const multiple = createMemo( () => Boolean(readOption(options.multiple)), { name: "accordion.multiple" }, ); const disabled = createMemo( () => Boolean(readOption(options.disabled)), { name: "accordion.disabled" }, ); const orientation = createMemo( () => (readOption(options.orientation) ?? "vertical") as Orientation, { name: "accordion.orientation" }, ); const isOpen = (itemValue: Value): boolean => value().includes(itemValue); const toggle = (itemValue: Value, nextOpen: boolean, event?: Event): void => { const current = value(); let nextArr: AccordionValue; if (!multiple()) { // Single-expand: toggling the already-open item closes it; otherwise // the new value becomes the sole open item. nextArr = ( current[0] === itemValue ? [] : [itemValue] ) as AccordionValue; } else if (nextOpen) { nextArr = [...current, itemValue] as AccordionValue; } else { nextArr = current.filter((v) => v !== itemValue) as AccordionValue; } const details = createChangeEventDetails(REASONS.none, event); options.onValueChange?.(nextArr, details); }; const api: AccordionApi = { value, multiple, disabled, orientation, isOpen, toggle, Provider: (props) => ( }> {props.children} ), }; (api as AccordionApi & Snapshottable)[SNAPSHOT_KEYS] = ACCORDION_SNAPSHOT_KEYS; return api; } const ACCORDION_SNAPSHOT_KEYS = snapshotKeys>( "value", "multiple", "disabled", "orientation", );