import { onCleanup, untrack } from "solid-js"; import { useBaseUiId } from "../base-ui-id"; import { useLabelableContext } from "./labelable-context"; export interface UseLabelParameters { /** * Explicit id override for the label. When omitted, {@link useBaseUiId} * generates a stable id for the component's lifetime. */ id?: string | undefined; /** * Fallback control id used when no labelable context is present (or the * context hasn't seen a control registration yet). Consumers typically * pass the id they'd have used for their own control so `htmlFor` still * lines up in the no-provider case. */ fallbackControlId?: string | null | undefined; } export interface UseLabelReturnValue { /** * The id assigned to the label element. Read once during setup — the value * is stable for the component's lifetime (matching React's `useBaseUiId`). */ id: string; /** * The id of the control this label is associated with, if any. Reactive — * tracks context registrations. `undefined` when nothing has registered, * `null` when the association is implicit, a string otherwise. */ controlId: () => string | null | undefined; } /** * Register a label id with the surrounding `` and pull the * control id back out so the caller can render `htmlFor` / click handlers. * * Lifecycle (mirrors `use-registered-label-id.ts` but against the multi-label * registry): * * - On setup, generates (or accepts an override for) an id via * {@link useBaseUiId} and registers it under a per-call `Symbol()` source * key. No `ref` dance — the symbol is a plain local binding. * - On cleanup (component disposed), unregisters the id so the context's * `aria-labelledby` composition drops this label cleanly. * - Returns the generated id plus a reactive `controlId` accessor that * reflects the currently-registered control id. * * Note the relationship to `src/internals/use-registered-label-id.ts`: that * hook is the **tier-1 single-label** path already used by progress/meter. * This hook is the **tier-2 multi-label** superset. Both are kept alive on * purpose — tier-1 consumers don't need a full LabelableProvider. */ export function useLabel(params: UseLabelParameters = {}): UseLabelReturnValue { const context = useLabelableContext(); // `untrack` silences the STRICT_READ_UNTRACKED warning (RFC 08) for the // one-time prop read. The id is consumed exactly once by `useBaseUiId` — // later changes to `params.id` are deliberately ignored (matches React's // stable-id behavior via `useBaseUiId`). const idOverride = untrack(() => params.id); const id = useBaseUiId(idOverride); // One-shot source key per `useLabel` call. A plain `const source = Symbol()` // is enough — the binding outlives setup via closure and is consulted at // cleanup time. const source = Symbol("labelable-label"); // Register synchronously during setup so the context's `getLabelIds()` is // populated by the time downstream JSX reads it (same-frame). The // underlying signal opts into `ownedWrite`, so this owned-scope call is // permitted. context.registerLabelId(source, id); onCleanup(() => { context.registerLabelId(source, undefined); }); const controlId = (): string | null | undefined => context.controlId(); return { id, controlId }; }