import { type Accessor, createMemo } from "solid-js"; import { useLabelableContext } from "./labelable-context"; /** * Compose an `aria-labelledby` string from: * * 1. the caller-supplied external `aria-labelledby` value (e.g. from a * consumer prop), and * 2. every label id currently registered with the nearest * ``, in **registration order**. * * Returns an `Accessor` — `undefined` when neither source * contributes anything so the attribute can be omitted entirely (per RFC 07, * Solid 2 treats `undefined` as "absent" for presence/absence attributes). * * ### Why registration order * * DOM order requires observing the rendered label elements, which would need * a MutationObserver or post-mount reconciliation pass. Registration order is * a deterministic stand-in: Base UI labels register during setup, which in * practice proceeds in source/render order (and therefore in DOM order for * the typical "labels before control" layout). If a consumer genuinely needs * a different order, they can pass the external `aria-labelledby` string to * override — that string wins position-wise (it comes first in the composed * result). * * ### Solid 2 notes * * - The `external` argument is accepted as either a plain `string | undefined` * (read once during setup, won't react) or an `Accessor` * (tracks). Most consumers already work with accessors in Solid 2, so this * dual shape is a thin convenience. * - Built on `createMemo`, so joining only re-runs when either the external * value or the registered-ids array changes — not on every read. */ export function useAriaLabelledBy( external?: string | undefined | Accessor, ): Accessor { const context = useLabelableContext(); const readExternal = (): string | undefined => { if (typeof external === "function") return external(); return external; }; return createMemo(() => { const externalValue = readExternal(); const registered = context.getLabelIds(); if (!externalValue && registered.length === 0) { return undefined; } // External wins position-wise — consumers that pass an explicit // `aria-labelledby` expect that to come first. Empty/whitespace-only // registered ids are skipped to stay tidy. const parts: string[] = []; if (externalValue) parts.push(externalValue); for (const id of registered) { if (id) parts.push(id); } return parts.length > 0 ? parts.join(" ") : undefined; }); }