import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { createMemo, createSignal, omit } from "solid-js"; import { userRef } from "../../internals/compose-refs"; import { type MaybeAccessor, readOption } from "../../internals/maybe-accessor"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import { VISUALLY_HIDDEN_STYLE } from "../../internals/visually-hidden-style"; import { createProgress } from "../create-progress.tsx"; import { ProgressRootContext, type ProgressRootContextValue, type ProgressRootState, type ProgressStatus, } from "../progress-root-context.ts"; export type { ProgressRootState, ProgressStatus }; export interface ProgressRootProps extends Omit< BaseUIComponentProps<"div", ProgressRootState>, | "role" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-labelledby" > { /** * Tag/component to render. Defaults to `"div"`. Replaces the * pre-spike `render` prop (axis G). */ as?: ValidComponent; "aria-valuetext"?: string | undefined; format?: MaybeAccessor; getAriaValueText?: | ((formattedValue: string | null, value: number | null) => string) | undefined; locale?: MaybeAccessor; max?: MaybeAccessor; min?: MaybeAccessor; value: MaybeAccessor; } /** * Groups all parts of the progress bar and provides the task completion * status to screen readers. Renders a `
` (or `as` override). * * ยง1 wave 2: replaces `renderElement` with `dynamic(() => props.as ?? "div")`. * The slot factory `createProgress().root()` carries `role="progressbar"` and * the `aria-valuemin`/`aria-valuemax`/`aria-valuenow`/`aria-valuetext` * getters; status data-attrs (`data-indeterminate` / `data-progressing` / * `data-complete`) and `aria-labelledby` are emitted as inline reactive * getters on the wrapper's internal slot bag. */ export function ProgressRoot(props: ProgressRootProps): JSX.Element { const c = createProgress({ value: () => readOption(props.value), min: () => readOption(props.min), max: () => readOption(props.max), format: () => readOption(props.format), locale: () => readOption(props.locale), // Getter, not an eager read: createProgress reads this inside its // `ariaValueText` memo (tracked), and an eager `props` read here is // untracked in the component body (`[STRICT_READ_UNTRACKED]`). get getAriaValueText() { return props.getAriaValueText; }, }); const value = c.value; const min = c.min; const max = c.max; const status = c.status; const formattedValue = c.formattedValue; const state = createMemo((): ProgressRootState => ({ status: status() })); const [labelId, setLabelId] = createSignal(undefined, { ownedWrite: true, }); const contextValue: ProgressRootContextValue = { formattedValue, max, min, value, state, status, setLabelId, labelId, }; // Resolve `class`/`style` so function values (stateful styling) are invoked // with the progress state instead of being spread to the DOM raw (which // would drop the class entirely). const userClassStyle = resolveClassStyle(props, () => state()); const userProps = omit( props, "as", "children", "class", "style", "attrs", "format", "getAriaValueText", "locale", "max", "min", "value", "aria-valuetext", ); const internalSlot = { get "aria-labelledby"(): string { // Emit empty string instead of undefined when no label is registered // so the key stays in `ownKeys` from render 1 and Solid's spread // effect sets up the reactive binding for late-mounting labels. return labelId() ?? ""; }, get "aria-valuetext"(): string { return props["aria-valuetext"] ?? c.ariaValueText(); }, get "data-indeterminate"(): "" | undefined { return status() === "indeterminate" ? "" : undefined; }, get "data-progressing"(): "" | undefined { return status() === "progressing" ? "" : undefined; }, get "data-complete"(): "" | undefined { return status() === "complete" ? "" : undefined; }, }; let merged: Record = mergeRootProps(c.root(), internalSlot); merged = mergeRootProps(merged, userClassStyle); merged = mergeRootProps(merged, userProps); const Tag = dynamic(() => props.as ?? "div"); return ( {props.children} {/* force NVDA to read the label https://github.com/mui/base-ui/issues/4184 */} x ); }