import { type Accessor, createContext, useContext } from "solid-js"; import type { BaseUIChangeEventDetails, REASONS, } from "../internals/change-event-details"; import type { CompositeMetadata } from "../internals/composite/composite-context.ts"; import type { Orientation } from "../internals/types"; /** * The opaque identifier a consumer attaches to a `` + the matching * ``. `null` disables selection entirely (no panel visible). * Matches React Base UI's `TabsTab.Value` (which is also `any | null`). */ export type TabsTabValue = unknown; /** * Direction of the most recent activation. `"none"` on first render / when * the source or destination tab is missing from the live DOM map. */ export type TabsTabActivationDirection = | "left" | "right" | "up" | "down" | "none"; /** Reason attached to `onValueChange` details. */ export type TabsRootChangeEventReason = typeof REASONS.none; /** * Change-event details passed to `onValueChange`. Extends the standard Base UI * details bag with `activationDirection` so consumers can drive directional * animations from their own handler. */ export type TabsRootChangeEventDetails = BaseUIChangeEventDetails< TabsRootChangeEventReason, { activationDirection: TabsTabActivationDirection } >; /** * Metadata stored per `` in the composite map. `id` is the tab's * DOM id — used to wire `aria-labelledby` on the matching panel. `value` is * what the consumer passed via `props.value`. */ export interface TabsTabMetadata { disabled: boolean; id: string | undefined; value: TabsTabValue; } /** Metadata stored per `` in the composite panel list. */ export interface TabsPanelMetadata { id?: string | undefined; value: TabsTabValue; } /** * State of `` projected to `data-*` attrs + the `class` / * `style` / `render` callback surfaces. */ export interface TabsRootState { /** Layout orientation. */ orientation: Orientation; /** Activation direction of the most recent change. */ tabActivationDirection: TabsTabActivationDirection; } /** * Context shared by every `` part. Accessors mirror React Base UI's * `TabsRootContext` but every slot that's reactive is exposed as an * `Accessor` so descendants stay live without a memo ping-pong. */ export interface TabsRootContextValue { /** Currently selected tab value (`undefined` → defaultValue branch). */ value: Accessor; /** * Request a change. Fires the consumer's `onValueChange` first (with a * cancel-capable details bag) then — if not canceled — writes the * internal signal in the uncontrolled branch. */ onValueChange: ( newValue: TabsTabValue, eventDetails: TabsRootChangeEventDetails, ) => void; /** Layout orientation (`"horizontal"` / `"vertical"`). */ orientation: Accessor; /** Activation direction accessor. */ tabActivationDirection: Accessor; /** Look up a tab element by value — powers the indicator's position math. */ getTabElementBySelectedValue: ( selectedValue: TabsTabValue | undefined, ) => HTMLElement | null; /** Panel `aria-labelledby` lookup — finds the tab id for a given panel value. */ getTabIdByPanelValue: (panelValue: TabsTabValue) => string | undefined; /** Tab `aria-controls` lookup — finds the panel id for a given tab value. */ getTabPanelIdByValue: (tabValue: TabsTabValue) => string | undefined; /** * Panels call this when they mount (or on `keepMounted` register) so the * root can answer `getTabPanelIdByValue(tabValue)` without walking DOM. */ registerMountedTabPanel: (panelValue: TabsTabValue, panelId: string) => void; /** Reverse of {@link registerMountedTabPanel}. */ unregisterMountedTabPanel: ( panelValue: TabsTabValue, panelId: string, ) => void; /** * Exposed to `` so its `` forwards * the sorted tab map up to the root for aria-labelledby / activation- * direction lookups. */ setTabMap: ( map: Map | null>, ) => void; /** Marker — `true` when a real `` provided this context. */ readonly present: boolean; } const NOOP = (): void => {}; export const NOOP_TABS_ROOT_CONTEXT: TabsRootContextValue = { value: () => undefined, onValueChange: NOOP, orientation: () => "horizontal", tabActivationDirection: () => "none", getTabElementBySelectedValue: () => null, getTabIdByPanelValue: () => undefined, getTabPanelIdByValue: () => undefined, registerMountedTabPanel: NOOP, unregisterMountedTabPanel: NOOP, setTabMap: NOOP, present: false, }; export const TabsRootContext = createContext( NOOP_TABS_ROOT_CONTEXT, ); /** * Read the `` context. Throws when no root ancestor is present — * every tabs part requires a root. */ export function useTabsRootContext(): TabsRootContextValue { const context = useContext(TabsRootContext); if (!context.present) { throw new Error( "Base UI: TabsRootContext is missing. Tabs parts must be placed within .", ); } return context; }