import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { createMemo, createSignal, omit } from "solid-js"; import { useDirection } from "../direction-provider/direction-provider-context.ts"; import type { CompositeMetadata } from "../internals/composite/composite-context.ts"; import { CompositeList } from "../internals/composite/composite-list.tsx"; import { useCompositeRoot } from "../internals/composite/composite-root.ts"; import { mergeRootProps } from "../internals/merge-root-props"; import { resolveClassStyle } from "../internals/resolve-class-style"; import type { BaseUIComponentProps, Orientation } from "../internals/types"; import { ToolbarRootContext, type ToolbarRootContextValue, type ToolbarRootItemMetadata, } from "./toolbar-root-context.ts"; // Audited 2026-04-20: // // React's `` keeps a `useState>` and // projects `disabledIndices` from it. That single map is populated by // ``, so the "registry" and the // composite-list sorted map are the same thing. // // Here we do the same: the `>` below // already carries `focusableWhenDisabled` (supplied via each item's // `useCompositeListItem({ metadata })` call) along with the resolved `index`. // Capturing the map from `onMapChange` and deriving `disabledIndices` from // that single source is enough — no auxiliary per-node registry needed. export type ToolbarRootOrientation = Orientation; export interface ToolbarRootState { disabled: boolean; orientation: Orientation; } export interface ToolbarRootProps extends BaseUIComponentProps<"div", ToolbarRootState> { /** Tag/component to render. Defaults to `"div"`. */ as?: ValidComponent; /** * Whether the toolbar (and every descendant part) ignores user interaction. * @default false */ disabled?: boolean | undefined; /** * Navigation orientation — drives arrow-key dispatch and the * `data-orientation` / `aria-orientation` attributes. * @default "horizontal" */ orientation?: Orientation | undefined; /** * Whether arrow-key navigation loops focus at the ends. * @default true */ loopFocus?: boolean | undefined; } /** * A container for grouping a set of controls — buttons, toggle groups, * separators, links. Renders a `
` (or `as` override) with * arrow-key navigation between focusable items. */ export function ToolbarRoot(props: ToolbarRootProps): JSX.Element { const disabled = (): boolean => props.disabled === true; const orientation = (): Orientation => props.orientation ?? "horizontal"; const loopFocus = (): boolean => props.loopFocus ?? true; const direction = useDirection(); const [sortedMap, setSortedMap] = createSignal< Map | null> >(new Map(), { name: "toolbar.sortedMap", ownedWrite: true }); const onMapChange = ( map: Map | null>, ): void => { setSortedMap(map); }; const disabledIndices = createMemo( (): number[] => { const map = sortedMap(); if (map.size === 0) return []; const out: number[] = []; for (const entry of map.values()) { const idx = entry?.index; if ( entry != null && entry.focusableWhenDisabled === false && typeof idx === "number" ) { out.push(idx); } } return out; }, { name: "toolbar.disabledIndices" }, ); const composite = useCompositeRoot({ direction, get orientation() { return orientation(); }, get loopFocus() { return loopFocus(); }, enableHomeAndEndKeys: true, disabledIndices: () => disabledIndices(), }); const contextValue: ToolbarRootContextValue = { disabled, orientation, present: true, }; const userClassStyle = resolveClassStyle(props, () => ({ disabled: disabled(), orientation: orientation(), })); const userHtmlProps = omit( props, "as", "class", "style", "attrs", "children", "disabled", "loopFocus", "orientation", ) as Record; const internalProps = { ref: composite.rootRef, role: "toolbar" as const, get "aria-orientation"() { return orientation(); }, get "data-orientation"() { return orientation(); }, get "data-disabled"(): "" | undefined { return disabled() ? "" : undefined; }, }; const merged = mergeRootProps( mergeRootProps( mergeRootProps(internalProps, composite.props), userClassStyle, ), userHtmlProps, ); const Tag = dynamic(() => props.as ?? "div"); return ( elementsRef={composite.elementsRef} onMapChange={(map) => { onMapChange( map as Map< Element, CompositeMetadata | null >, ); composite.onMapChange(map); }} > {props.children} ); }