React Spectrum 进度指示三件套:ProgressBar、ProgressCircle 与 Meter 的 v3 API 设计及 v2 迁移指南
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
本文围绕仓库中 specs/api/Progress.md 这份 API 设计规格展开,系统讲解 React Spectrum v3 中三个进度指示组件——ProgressBar(进度条)、ProgressCircle(环形进度圈)与Meter(计量表)的完整 Props 契约、默认值行为与无障碍实现,并完整保留原文档的 v2 → v3 迁移对照表,结合 useProgressBar Hook、ProgressBarBase 基础组件 等源码逐条印证每个 API 决策背后的命名规范与实现原理。读完本文,你将能够直接按 v3 API 编写这三个组件、把 v2 代码平滑迁移到 v3,并理解 Spectrum 为何如此拆分和命名这些 Props。
一、为什么是"三件套":组件拆分与 API 总览
规格文档开篇以 TypeScript 接口形式给出了三个组件的 v3 API 契约,这是整份文档的核心骨架:
interface ProgressBar { value?: number, minValue?: number, maxValue?: number, size?: 'S' | 'L', label?: ReactNode, 'aria-label'?: string, labelPosition?: 'top' | 'side', showValueLabel?: boolean, // true by default if label, false by default if not formatOptions?: Intl.NumberFormatOptions, // defaults to formatting as a percentage. valueLabel?: ReactNode, // custom value label (e.g. 1 of 4) variant?: 'overBackground', isIndeterminate?: boolean } interface ProgressCircle { value?: number, minValue?: number, maxValue?: number, size?: 'S' | 'M' | 'L', variant?: 'overBackground', isCentered?: boolean, isIndeterminate?: boolean } interface Meter extends ProgressBar { variant: 'positive' | 'warning' | 'critical' }这段契约本身就体现了 Spectrum 的 API 设计准则(见 specs/api/Guidelines.md):
- 组件拆分:Guidelines 中 "Splitting Components" 一节规定"当组件的选项不再适合放在一起时应拆分为独立组件"。v2 中
variant="positive" | "warning" | "critical"本是ProgressBar的变体,但这些颜色变体表达的是"用户行为造成的量"(如存储占用告警),而非"系统操作进度",语义完全不同,因此 v3 将其拆分为独立的Meter组件——Meter extends ProgressBar正体现了"复用进度条的取值 API,叠加语义化变体"的拆分思路。 - 方向无关命名:
labelPosition只接受'top' | 'side'而不接受'left'/'right',因为side在 RTL 布局下可自动翻转方向,这是 Guidelines 中 "Direction Agnostic Naming" 规则的直接落地。 - 布尔状态前缀
is:isIndeterminate表示组件状态,按 "Boolean Props" 规则以is开头。 - 约束型 Props 命名:
minValue/maxValue在名字尾部带上被约束的属性value,而非含糊的min/max,符合 "Prop Restrictions" 规则。
各组件的定位差异
| 组件 | 语义 | 底层实现 |
|---|---|---|
ProgressBar | 系统操作进度(下载、上传、处理),有/无确定进度均可 | ProgressBar.tsx |
ProgressCircle | 同上,但用环形展示,常用于悬浮加载态 | ProgressCircle.tsx |
Meter | 已知范围内的"量"(如磁盘容量),由用户行为决定而非系统操作 | Meter.tsx |
Meter源码中的注释也明确了这一区分:"Meters are visual representations of a quantity or an achievement. Their progress is determined by user actions, rather than system actions."(Meter.tsx L29-L32)
二、ProgressBar:契约、默认值与底层 Hook
2.1 默认值与值域钳制
对照规格契约,ProgressBarBase 中定义了实际的默认值,读者可以据此确认各字段的取值行为:
let { value = 0, minValue = 0, maxValue = 100, size = 'L', label, showValueLabel = !!label, // 关键:有 label 默认显示数值,无 label 默认不显示 labelPosition = 'top', isIndeterminate = false, ... } = props; value = clamp(value, minValue, maxValue); // 越界值会被钳制几个要点值得注意:
showValueLabel的条件默认值:契约中注释 "true by default if label, false by default if not" 在源码中正是showValueLabel = !!label一行实现——只有给了可见标签时才默认展示百分比数值,避免无标签场景下孤悬的数字。value会被钳制(clamp):传入value: -1会得到0,传入value: 1000会得到maxValue。测试用例 ProgressCircle.test.js 中的 "clamps values to 0 / clamps values to 100" 两个it用例专门验证了这一点。- 进度填充宽度的计算:
let range = maxValue - minValue; let percentage = range === 0 ? 0 : (value - minValue) / range; barStyle.width = `${Math.round(percentage * 100)}%`;即填充宽度是(value - minValue) / (maxValue - minValue)四舍五入到整数百分比,并特判了range === 0(minValue === maxValue)时宽度为 0 的除零边界。
2.2 无障碍 Hook:useProgressBar
ProgressBar组件本体很薄(ProgressBar.tsx L24-L44),真正计算 ARIA 属性的逻辑全部在 useProgressBar:
value = clamp(value, minValue, maxValue); let range = maxValue - minValue; let percentage = range === 0 ? 0 : (value - minValue) / range; let formatter = useNumberFormatter(formatOptions); if (!isIndeterminate && !valueLabel) { let valueToFormat = formatOptions.style === 'percent' ? percentage : value; valueLabel = formatter.format(valueToFormat); } return { progressBarProps: mergeProps(domProps, { ...fieldProps, 'aria-valuenow': isIndeterminate ? undefined : value, 'aria-valuemin': minValue, 'aria-valuemax': maxValue, 'aria-valuetext': isIndeterminate ? undefined : (valueLabel as string), role: 'progressbar' }), labelProps };这里印证了规格契约中formatOptions与valueLabel两个 v3 新增字段的完整语义:
formatOptions默认是{ style: 'percent' }(契约注释 "defaults to formatting as a percentage"),此时数值标签对percentage(0~1 的比例)做格式化;若用户传入了其他Intl.NumberFormatOptions(如{ style: 'decimal', maximumFractionDigits: 2 }),则对原始value做格式化——这就是注释里 "others can also be supported" 的实现路径。valueLabel用于 "1 of 4" 这类自定义数值文案:当提供了valueLabel时不再走 formatter,直接把该 ReactNode 作为aria-valuetext输出。isIndeterminate时不输出aria-valuenow与aria-valuetext,符合 WAI-ARIA 对不确定进度条的要求;测试 ProgressCircle.test.js 中 "handles indeterminate" 用例正是断言aria-valuenow属性不存在。- 标签元素使用
labelElementType: 'span'而非<label>,因为进度条不是可聚焦的 HTML 表单控件(见 useProgressBar.ts L89-L94)。
2.3 渲染结构与数值标签的联动
ProgressBarBase.tsx L102-L132 渲染出label → valueLabel → track → fill的结构,其中数值标签直接复用 ARIA 侧算好的结果:
{showValueLabel && barProps && ( <div className={classNames(styles, 'spectrum-BarLoader-percentage')}> {barProps['aria-valuetext']} </div> )}即视觉上显示的百分比文本与aria-valuetext保证一致。labelPosition="side"会切换到spectrum-BarLoader--sideLabel类,size="S"/"L"分别映射--small/--large修饰类。另外注意variant="overBackground"在 v3 源码中已标注@deprecated,推荐使用staticColor('white' | 'black')替代(ProgressBarBase.tsx L46-L52)——这是规格契约中variant字段的一个演进细节。
三、ProgressCircle:双掩膜旋转实现环形填充
ProgressCircle在取值契约上与ProgressBar相同(value/minValue/maxValue,默认 0/0/100,见 ProgressCircle.tsx L22-L45),差异在规格表中列出的两处:
size支持'S' | 'M' | 'L'三档,默认'M'(进度条只有两档,默认'L');- 新增
isCentered与minValue/maxValue(迁移表见第五节)。
源码上最有趣的部分是它没有使用 SVG 弧线,而是用两块 CSS 掩膜旋转来"画出"任意角度的环形填充(ProgressCircle.tsx L94-L107):
let range = maxValue - minValue; let percentage = range === 0 ? 0 : ((value - minValue) / range) * 100; let angle; if (percentage > 0 && percentage <= 50) { angle = -180 + (percentage / 50) * 180; subMask1Style.transform = `rotate(${angle}deg)`; subMask2Style.transform = 'rotate(-180deg)'; } else if (percentage > 50) { angle = -180 + ((percentage - 50) / 50) * 180; subMask1Style.transform = 'rotate(0deg)'; subMask2Style.transform = `rotate(${angle}deg)`; }原理可以推断为:一个 360° 圆环拆成上下两个半圆掩膜(DOM 中对应fillSubMask1/fillSubMask2两个子元素,见 ProgressCircle.tsx L134-L151)。进度在前半圈(0~50%)时只旋转掩膜 1;超过 50% 时掩膜 1 定格在 0°(盖满上半圈),掩膜 2 继续旋转覆盖下半圈。每块掩膜一次最多转 180°,正好对应各自半圆的范围。isIndeterminate为true时不设置任何旋转,交给 Spectrum CSS 的spectrum-CircleLoader--indeterminate类播放旋转动画。
可访问性方面,ProgressCircle复用同一个useProgressBarHook(第 90 行传入{...props, value}),因此 ARIA 输出与进度条完全一致;并且在非生产环境下,若既无aria-label也无aria-labelledby会主动告警(ProgressCircle.tsx L109-L113),进度条侧的等价告警在 ProgressBarBase.tsx L96-L100。
四、Meter:继承 ProgressBar 契约并叠加语义变体
规格定义interface Meter extends ProgressBar { variant: 'positive' | 'warning' | 'critical' }。实际实现上,Meter.tsx 复用了ProgressBarBase作为渲染骨架,只替换了 ARIA Hook 与修饰类:
export interface SpectrumMeterProps extends SpectrumProgressBarBaseProps { variant?: 'informative' | 'positive' | 'warning' | 'critical'; // @default 'informative' } export const Meter = React.forwardRef(function Meter(props, ref) { let {variant = 'informative', ...otherProps} = props; const {meterProps, labelProps} = useMeter(props); return ( <ProgressBarBase {...otherProps} barProps={meterProps} barClassName={classNames(styles, { 'is-positive': variant === 'positive', 'is-warning': variant === 'warning', 'is-critical': variant === 'critical' })} /> ); });两个源码层面的补充说明:
- 默认变体是
'informative'(中性蓝),规格表列出的positive/warning/critical是三个强调状态;informative时不加任何修饰类,走 Spectrum CSS 默认样式。 - ARIA role 使用双值
"meter progressbar":useMeter 内部先调用useProgressBar,再把 role 改写为meter progressbar,源码注释解释了原因——部分浏览器(注释中提到 Chrome 会自动回退、Firefox 当时不支持 meter、Safari 13+ 支持)对meterrole 支持不一,写两个 role 值可以让屏幕阅读器在新旧环境下都能得到正确语义。
Meter 的测试见 Meter.test.js 与 SSR 场景下的 Meter.ssr.test.js。
五、v2 → v3 迁移对照表(完整继承自规格文档)
5.1 ProgressBar Changes
| v2 | v3 | Notes |
|---|---|---|
<Progress> | <ProgressBar> | |
size="M" | size="L" | spectrum calls it large, not medium |
labelPosition="left" | labelPosition="side" | rtl support |
labelPosition="bottom" | - | not supported. |
showPercent | showValueLabel | default changed to true if label is specified, false if not. |
| - | numberFormatter | added. default is percentage, but others can also be supported. |
| - | valueLabel | custom value label, e.g. "1 of 4" |
| - | isIndeterminate | added |
min | minValue | |
max | maxValue | |
variant="positive" | <Meter variant="positive"> | |
variant="warning" | <Meter variant="warning"> | |
variant="critical" | <Meter variant="critical"> |
逐条解读这些改动的动机:
size="M"→size="L":Spectrum 设计系统把进度条的两档尺寸命名为 small/large,v2 的 "M" 在 v3 词汇表中对应 "L",直接改名避免语义错位。labelPosition="left"→"side":side是方向无关术语,RTL 下自动指右侧,这正是第五节开头提到的 Guidelines 规则;"bottom"则被整体取消支持。showPercent→showValueLabel:改名后语义更泛——显示的不只是百分比(受formatOptions影响),默认值逻辑也变为"有 label 默认开、无 label 默认关",与 ProgressBarBase 中showValueLabel = !!label完全对应。numberFormatter→ 源码中的formatOptions:规格表中 v3 新增项写作numberFormatter("added. default is percentage, but others can also be supported"),当前源码将其演进为formatOptions?: Intl.NumberFormatOptions,直接透传给Intl.NumberFormat,默认{ style: 'percent' },与规格意图一致且接口更标准。min/max→minValue/maxValue:约束型 Props 带上被约束属性名,消除歧义。variant三兄弟整体迁往<Meter>:语义拆分,ProgressBar保留variant: 'overBackground'(叠加在彩色背景上的变体,现已标记 deprecated,推荐staticColor)。
5.2 ProgressCircle Changes
| v2 | v3 | Notes |
|---|---|---|
<Wait> | <ProgressCircle> | |
variant="indeterminate" | isIndeterminate | |
centered | isCentered | |
| - | minValue | added |
| - | maxValue | added |
<Wait>→<ProgressCircle>:组件从"等待指示器"升级为通用进度指示器,因此可以像ProgressBar一样表达确定进度(value/minValue/maxValue)。variant="indeterminate"→isIndeterminate:不确定态是组件"状态"而非"视觉风格",按 Guidelines 以is前缀的布尔属性表达,variant一词留给真正的视觉变体('overBackground')。centered→isCentered:同类改名,布尔状态统一is前缀。- 新增
minValue/maxValue:与ProgressBar的取值契约对齐,源码中 ProgressCircle 的取值段 与进度条完全同构。
六、安装与使用:入口、导出与最小示例
三个组件通过@adobe/react-spectrum主包导出,导出映射见 exports/ProgressBar.ts 与 exports/Meter.ts;独立子包入口 packages/@react-spectrum/progress/src/index.ts 则额外导出了可复用的ProgressBarBase与类型,供二次封装时复用同一渲染骨架。
import {Provider, ProgressBar, ProgressCircle, Meter} from '@adobe/react-spectrum'; export function Example() { return ( <Provider> {/* 确定进度:0-100,默认按百分比显示数值标签 */} <ProgressBar label="正在上传照片…" value={72} /> {/* 自定义范围与数值文案:"1 of 4" 风格 */} <ProgressBar minValue={1} maxValue={4} value={2} valueLabel="第 2 步,共 4 步" label="安装进度" /> {/* 不确定进度:不渲染 aria-valuenow,播放滑动动画 */} <ProgressBar aria-label="正在加载" isIndeterminate /> {/* 环形加载器,需自行提供可达性标签 */} <ProgressCircle aria-label="处理中" value={30} size="L" /> {/* 计量表:磁盘空间告警 */} <Meter label="存储空间" value={82} variant="critical" /> </Provider> ); }上述示例中的行为均可在源码中找到依据:value={72}会被钳制后映射为 72% 填充宽度;valueLabel同时驱动视觉文本与aria-valuetext;isIndeterminate时aria-valuenow为undefined;Meter的variant="critical"落到is-critical修饰类。
配套资源
- 官方文档页:ProgressBar.mdx、ProgressCircle.mdx、Meter.mdx
- 交互示例(Storybook):stories/progress、stories/meter
- 视觉回归用例:chromatic/progress、chromatic/meter
- 测试:ProgressBar.test.js(含负值钳制、缺失 aria-label 告警、自定义 DOM props 透传等用例)、ProgressCircle.test.js、Meter.test.js
七、小结
specs/api/Progress.md 用不到 70 行定义清晰划定了 v3 进度指示组件族的 API 边界:ProgressBar承载系统操作进度(含不确定态与可定制数值格式化)、ProgressCircle提供环形形态与三档尺寸、Meter从进度条契约中派生出面向"量"的语义变体。对照源码可以确认,这份契约并非纸面设计——每个字段(从showValueLabel的条件默认值、clamp钳制,到role="meter progressbar"的浏览器兼容策略)都能在 ProgressBarBase、useProgressBar、useMeter 中找到逐行对应的实现,且被 test/progress 与 test/meter 下的测试用例持续验证。对于仍在 v2 上的项目,按第五节两张迁移表逐项替换(组件改名、min/max补全为minValue/maxValue、variant强调态迁往Meter、centered/indeterminate补is前缀)即可完成平滑升级。
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考