UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

92 lines (90 loc) 3.61 kB
'use client'; import * as React from 'react'; import { visuallyHidden } from '@base-ui/utils/visuallyHidden'; import { formatNumber } from "../../utils/formatNumber.mjs"; import { valueToPercent } from "../../utils/valueToPercent.mjs"; import { clamp } from "../../internals/clamp.mjs"; import { useRenderElement } from "../../internals/useRenderElement.mjs"; import { ProgressRootContext } from "./ProgressRootContext.mjs"; import { progressStateAttributesMapping } from "./stateAttributesMapping.mjs"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Groups all parts of the progress bar and provides the task completion status to screen readers. * Renders a `<div>` element. * * Documentation: [Base UI Progress](https://base-ui.com/react/components/progress) */ export const ProgressRoot = /*#__PURE__*/React.forwardRef(function ProgressRoot(componentProps, forwardedRef) { const { format, getAriaValueText, locale, max = 100, min = 0, value, render, className, children, style, ...elementProps } = componentProps; const [labelId, setLabelId] = React.useState(); // `value === null` (or any non-finite value) keeps Progress indeterminate. Otherwise compute a // single clamped value and normalized percentage so completion status, `aria-valuenow`, the // formatted text, the default `aria-valuetext`, and the indicator width all stay in sync for any // `min`/`max` (not just the default 0–100). let status = 'indeterminate'; let percentageValue = null; let clampedValue = null; let formattedValue = ''; // Derived alongside `status` so the indeterminate condition is not restated anywhere else. let defaultAriaValueText = 'indeterminate progress'; if (value != null && Number.isFinite(value)) { const rawPercentage = valueToPercent(value, min, max); percentageValue = clamp(Number.isNaN(rawPercentage) ? 0 : rawPercentage, 0, 100); clampedValue = clamp(value, min, max); status = clampedValue === max ? 'complete' : 'progressing'; // Format the clamped value so visible and accessible text stay in sync with `aria-valuenow` and // the indicator fill. The raw value remains available as the second `getAriaValueText` argument. formattedValue = format ? formatNumber(clampedValue, locale, format) : formatNumber(percentageValue / 100, locale, { style: 'percent' }); defaultAriaValueText = formattedValue; } const state = React.useMemo(() => ({ status }), [status]); const defaultProps = { 'aria-labelledby': labelId, 'aria-valuemax': max, 'aria-valuemin': min, 'aria-valuenow': clampedValue ?? undefined, 'aria-valuetext': getAriaValueText ? getAriaValueText(formattedValue, value) : defaultAriaValueText, role: 'progressbar', children: /*#__PURE__*/_jsxs(React.Fragment, { children: [children, /*#__PURE__*/_jsx("span", { role: "presentation", style: visuallyHidden, children: "x" })] }) }; const contextValue = React.useMemo(() => ({ formattedValue, percentageValue, setLabelId, state, value }), [formattedValue, percentageValue, setLabelId, state, value]); const element = useRenderElement('div', componentProps, { state, ref: forwardedRef, props: [defaultProps, elementProps], stateAttributesMapping: progressStateAttributesMapping }); return /*#__PURE__*/_jsx(ProgressRootContext.Provider, { value: contextValue, children: element }); }); if (process.env.NODE_ENV !== "production") ProgressRoot.displayName = "ProgressRoot";