import type { JSX } from "@solidjs/web"; import { type Accessor, createContext, createMemo, useContext } from "solid-js"; import { formatNumberValue } from "../internals/format-number"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { mergeRootProps } from "../internals/merge-root-props"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; /** * Options for {@link createMeter}. Every reactive input is `MaybeAccessor`. * * Headless primitive — `` layers the label-id registry and the * NVDA-pump span on top. */ export interface CreateMeterOptions { /** Current value. */ value: MaybeAccessor; /** Minimum value. @default 0 */ min?: MaybeAccessor; /** Maximum value. @default 100 */ max?: MaybeAccessor; /** `Intl.NumberFormat` options forwarded to {@link formatNumberValue}. */ format?: MaybeAccessor; /** Locale for {@link Intl.NumberFormat}. */ locale?: MaybeAccessor; /** * Compute `aria-valuetext`. Receives the formatted value and the raw * value. Default returns the formatted string when `format` is provided, * otherwise `"%"`. */ getAriaValueText?: (formattedValue: string, value: number) => string; } /** Slot bag for the `
` element. */ export interface MeterRootSlotProps { role: "meter"; "aria-valuemin": number; "aria-valuemax": number; "aria-valuenow": number; "aria-valuetext": string; } /** Public api returned by {@link createMeter}. */ export interface MeterApi { /** Current value. */ value: Accessor; /** Minimum (defaults to 0). */ min: Accessor; /** Maximum (defaults to 100). */ max: Accessor; /** Formatted value string. */ formattedValue: Accessor; /** Resolved `aria-valuetext`. */ ariaValueText: Accessor; /** Slot bag for the root element. */ root: ( userProps?: U, ) => MeterRootSlotProps & (U extends object ? U : object); /** Context provider — pair with {@link useMeter}. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } const MeterContext = createContext(null); /** * Read the nearest {@link MeterApi} provided by `c.Provider`. Throws if * called outside a Provider. */ export function useMeter(): MeterApi { const ctx = useContext(MeterContext); if (ctx == null) { throw new Error( "Base UI: useMeter() must be called inside a .", ); } return ctx; } /** * Solid-native headless meter primitive — pure derivations from * value/min/max + format options. No internal state. */ export function createMeter(options: CreateMeterOptions): MeterApi { const value = createMemo(() => readOption(options.value), { name: "meter.value", }); const min = createMemo(() => readOption(options.min) ?? 0, { name: "meter.min", }); const max = createMemo(() => readOption(options.max) ?? 100, { name: "meter.max", }); const formattedValue = createMemo( () => formatNumberValue( value(), readOption(options.locale), readOption(options.format), ), { name: "meter.formattedValue" }, ); const ariaValueText = createMemo( () => { const fn = options.getAriaValueText; if (fn) return fn(formattedValue(), value()); if (readOption(options.format)) return formattedValue(); return `${value()}%`; }, { name: "meter.ariaValueText" }, ); const rootInternal = { role: "meter" as const, get "aria-valuemin"() { return min(); }, get "aria-valuemax"() { return max(); }, get "aria-valuenow"() { return value(); }, get "aria-valuetext"() { return ariaValueText(); }, }; const root = (userProps?: U) => mergeRootProps(rootInternal, userProps) as MeterRootSlotProps & (U extends object ? U : object); const api: MeterApi = { value, min, max, formattedValue, ariaValueText, root, Provider: (props) => ( {props.children} ), }; (api as MeterApi & Snapshottable)[SNAPSHOT_KEYS] = METER_SNAPSHOT_KEYS; return api; } const METER_SNAPSHOT_KEYS = snapshotKeys( "value", "min", "max", "formattedValue", "ariaValueText", );