import type { JSX } from "@solidjs/web"; /** * Value-to-label resolution for ``. Mirrors Base UI React's * `packages/react/src/internals/resolveValueLabel.ts` — trimmed to the * subset the Solid port wires: * * - `stringifyAsValue(value, itemToStringValue)` → string form for form * submission + hidden-input `value`. * - `stringifyAsLabel(value, itemToStringLabel)` → string form for * trigger display text. * - `resolveSelectedLabel` / `resolveMultipleLabels` — select display * resolution with optional `items` registry support. * - `hasNullItemLabel` — detects `null` entries in the items registry. */ /** Group shape accepted in the `items` prop. */ export interface Group { items: | Record | ReadonlyArray<{ label: JSX.Element; value: Value }>; label?: JSX.Element; } export type ItemsInput = | Record | ReadonlyArray<{ label: JSX.Element; value: Value }> | ReadonlyArray> | undefined; function isGroup(item: unknown): item is Group { return ( typeof item === "object" && item !== null && "items" in item && !("value" in item) ); } function isRecord(items: unknown): items is Record { return typeof items === "object" && items !== null && !Array.isArray(items); } function stringifyValue(value: unknown): string { if (value == null) return ""; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (typeof value === "object") { // Object-with-value shortcut. if ("value" in (value as object)) { return stringifyValue((value as { value: unknown }).value); } try { return JSON.stringify(value); } catch { return ""; } } return ""; } export function stringifyAsValue( value: unknown, itemToStringValue?: ((value: unknown) => string) | undefined, ): string { if (itemToStringValue && value != null) { try { return itemToStringValue(value); } catch { /* fall through */ } } return stringifyValue(value); } export function stringifyAsLabel( value: unknown, itemToStringLabel?: ((value: unknown) => string) | undefined, ): string { if (itemToStringLabel && value != null) { try { return itemToStringLabel(value); } catch { /* fall through */ } } if (typeof value === "object" && value !== null && "label" in value) { const label = (value as { label: unknown }).label; if (typeof label === "string") return label; } return stringifyValue(value); } export function resolveSelectedLabel( value: Value, items: ItemsInput, itemToStringLabel?: ((value: Value) => string) | undefined, ): JSX.Element { function fallback(): JSX.Element { // `stringifyAsLabel` only ever calls this with the same `value`, so // widening the parameter to `unknown` is sound (function contravariance). return stringifyAsLabel( value, itemToStringLabel as ((value: unknown) => string) | undefined, ); } if (itemToStringLabel && value != null) { try { return itemToStringLabel(value); } catch { /* fall through */ } } if (typeof value === "object" && value !== null && "label" in value) { const label = (value as { label: unknown }).label; if (label != null) { return (value as { label: JSX.Element }).label; } } if (isRecord(items)) { const match = (items as Record)[value as string]; if (match != null) return match; } else if (Array.isArray(items)) { for (const entry of items) { if (isGroup(entry)) { if (isRecord(entry.items)) { const key = stringifyValue(value); if (key in entry.items) return entry.items[key]; } else { for (const item of entry.items) { if (Object.is(item.value, value)) return item.label; } } } else if ( typeof entry === "object" && entry !== null && "value" in entry && Object.is((entry as { value: Value }).value, value) ) { return (entry as { label: JSX.Element }).label; } } } return fallback(); } export function resolveMultipleLabels( values: ReadonlyArray, items: ItemsInput, itemToStringLabel?: ((value: Value) => string) | undefined, ): JSX.Element { if (!values || values.length === 0) return null; const labels = values.map( (v) => resolveSelectedLabel(v, items, itemToStringLabel) ?? "", ); // Join with ", " by default. Consumers who want custom formatting use a // children render function on ``. return labels .map((label) => (typeof label === "string" ? label : stringifyValue(label))) .filter(Boolean) .join(", "); } export function hasNullItemLabel(items: ItemsInput): boolean { if (!items) return false; if (isRecord(items)) return "null" in items; for (const entry of items) { if (isGroup(entry)) { if (hasNullItemLabel(entry.items as ItemsInput)) return true; } else if ( typeof entry === "object" && entry !== null && "value" in entry && (entry as { value: unknown }).value == null ) { return true; } } return false; }