import { type Accessor, createContext, useContext } from "solid-js"; /** * Registry for description/error message ids rendered inside a ``. * * `` and `` each generate a unique id, register * it here on setup, and unregister on cleanup. `` reads * `getMessageIds()` and composes them into `aria-describedby` so assistive tech * announces the related help text / error messages. * * Keyed by a per-consumer `symbol` so multiple simultaneous messages (e.g. a * description AND an error) can coexist without stepping on each other. * * Registration order is preserved: `getMessageIds()` returns ids in the order * they were registered, which matches render order for typical * "description before error" layouts. * * ### Why a dedicated registry? * * The shared `` already exposes a `registerLabelId` / label * registry (used by ``), plus a `controlId` slot. React's version * had a third slot — `messageIds` — that Description/Error wrote into. The * Solid port's `LabelableProvider` hasn't followed that design: keeping the * message-id channel here keeps the labelable provider focused on the * labelled/controlId pair, and avoids touching shared internals. */ export interface FieldMessagesContextValue { /** * Register a description/error id. Pass `undefined` to unregister. Multiple * calls with the same `source` replace the prior registration. Safe to * invoke from owned scope (the backing signal opts into `ownedWrite`). */ registerMessageId: (source: symbol, id: string | undefined) => void; /** * Snapshot of currently registered message ids, in registration order. * Reading this accessor subscribes to message registrations — when a * description/error mounts or unmounts, the consuming memo/effect re-runs. */ getMessageIds: Accessor; /** Marker — `true` when this context was provided by a real ``. */ readonly present: boolean; } const NOOP = (): void => {}; const EMPTY_IDS: readonly string[] = Object.freeze([]); /** * Returned when no `` is above a `` / * `` — the registry is a no-op and the registered ids list is * empty. */ export const NOOP_FIELD_MESSAGES_CONTEXT: FieldMessagesContextValue = { registerMessageId: NOOP, getMessageIds: () => EMPTY_IDS, present: false, }; export const FieldMessagesContext = createContext( NOOP_FIELD_MESSAGES_CONTEXT, ); /** * Read the nearest `` message-id registry. Returns the noop * context when no `` is above — callers can branch on `.present`. */ export function useFieldMessagesContext(): FieldMessagesContextValue { return useContext(FieldMessagesContext); }