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"; import type { ProgressStatus } from "./progress-root-context.ts"; export type { ProgressStatus }; /** * Options for {@link createProgress}. Every reactive input is a * `MaybeAccessor`; pass an accessor for live updates. * * Headless primitive — no Label/track/value/indicator parts. The wrapper * `` layers the label-id registry, the NVDA-pump span, and * the per-part adapters on top. */ export interface CreateProgressOptions { /** Current value (`null` = indeterminate). */ 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}. Defaults to runtime locale. */ locale?: MaybeAccessor; /** * Compute `aria-valuetext`. Receives the formatted value (empty string * when indeterminate) and the raw numeric value. Default returns * `"indeterminate progress"` when null, otherwise the formatted string * (or `"%"` if no `format` given). */ getAriaValueText?: ( formattedValue: string | null, value: number | null, ) => string; } /** Slot bag for the `
` element. */ export interface ProgressRootSlotProps { role: "progressbar"; "aria-valuemin": number; "aria-valuemax": number; "aria-valuenow": number | undefined; "aria-valuetext": string; } /** Public api returned by {@link createProgress}. */ export interface ProgressApi { /** Current value (`null` = indeterminate). */ value: Accessor; /** Minimum (defaults to 0). */ min: Accessor; /** Maximum (defaults to 100). */ max: Accessor; /** Formatted value string (empty string when null). */ formattedValue: Accessor; /** `"indeterminate" | "progressing" | "complete"`. */ status: Accessor; /** Resolved `aria-valuetext`. */ ariaValueText: Accessor; /** Slot bag for the root element. */ root: ( userProps?: U, ) => ProgressRootSlotProps & (U extends object ? U : object); /** * Context provider. Wrap a subtree to make the api discoverable via * `useProgress()`. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } const ProgressContext = createContext(null); /** * Read the nearest {@link ProgressApi} provided by `c.Provider`. Throws if * called outside a Provider. */ export function useProgress(): ProgressApi { const ctx = useContext(ProgressContext); if (ctx == null) { throw new Error( "Base UI: useProgress() must be called inside a .", ); } return ctx; } function defaultGetAriaValueText( formattedValue: string | null, value: number | null, ): string { if (value == null) return "indeterminate progress"; return formattedValue || `${value}%`; } /** * Solid-native headless progress primitive. Pure derivations from the input * value/min/max — no internal state. Pair with `` for the * structural wrapper, or use `c.root()` directly for a fully custom shell. */ export function createProgress(options: CreateProgressOptions): ProgressApi { const value = createMemo(() => readOption(options.value), { name: "progress.value", }); const min = createMemo(() => readOption(options.min) ?? 0, { name: "progress.min", }); const max = createMemo(() => readOption(options.max) ?? 100, { name: "progress.max", }); const status = createMemo( () => { const v = value(); if (v == null || !Number.isFinite(v)) return "indeterminate"; return v === max() ? "complete" : "progressing"; }, { name: "progress.status" }, ); const formattedValue = createMemo( () => formatNumberValue( value(), readOption(options.locale), readOption(options.format), ), { name: "progress.formattedValue" }, ); const ariaValueText = createMemo( () => { const fn = options.getAriaValueText ?? defaultGetAriaValueText; return fn(formattedValue(), value()); }, { name: "progress.ariaValueText" }, ); const rootInternal = { role: "progressbar" as const, get "aria-valuemin"() { return min(); }, get "aria-valuemax"() { return max(); }, get "aria-valuenow"() { const v = value(); return v == null ? undefined : v; }, get "aria-valuetext"() { return ariaValueText(); }, }; const root = (userProps?: U) => mergeRootProps(rootInternal, userProps) as ProgressRootSlotProps & (U extends object ? U : object); const api: ProgressApi = { value, min, max, formattedValue, status, ariaValueText, root, Provider: (props) => ( {props.children} ), }; (api as ProgressApi & Snapshottable)[SNAPSHOT_KEYS] = PROGRESS_SNAPSHOT_KEYS; return api; } const PROGRESS_SNAPSHOT_KEYS = snapshotKeys( "value", "min", "max", "formattedValue", "status", "ariaValueText", );