react-spectrum NumberField API 设计详解:本地化数字解析、步进行为与 v2 到 v3 的迁移
发布时间:2026/9/14 15:09:47 锦皓数字建站

react-spectrum NumberField API 设计详解本地化数字解析、步进行为与 v2 到 v3 的迁移【免费下载链接】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本文基于 react-spectrum 仓库中的 API 规范文档 NumberField.md系统讲解 NumberField 组件的接口设计、v2 到 v3 的属性迁移、多语言数字解析的实现原理以及聚焦提交commit on blur、步进按钮、移动端软键盘等关键行为的源码级实现。读完本文你既能完整理解该组件的 API 契约与迁移方式也能结合 react-aria 的 useNumberField、react-stately 的 useNumberFieldState 和 Spectrum 主题组件实现 掌握其底层状态机与无障碍细节。接口定义从规范到现状规范文档定义的接口如下interface NumberField extends InputBase, TextInputBase, ValueBasenumber, RangeInputBasenumber, Labelable, DOMProps, StyleProps { isQuiet?: boolean, decrementAriaLabel?: string, incrementAriaLabel?: string, hideStepper?: boolean, formatOptions?: Intl.NumberFormatOptions }这些 mixin 各自承载一类能力ValueBasenumber说明输入/输出的值是number类型RangeInputBasenumber提供minValue/maxValue/stepTextInputBase提供inputValue与onInputLabelable提供label、description、errorMessage。核心专属属性中isQuiet是否使用 quiet无边框样式decrementAriaLabel/incrementAriaLabel步进按钮的无障碍标签缺省时使用本地化的 Decrement / IncrementhideStepperv3 新增隐藏步进按钮只保留纯文本输入formatOptionsv3 新增标准的Intl.NumberFormatOptions控制显示格式如percent、currency、maximumFractionDigits同时决定用户允许输入哪些字符。从当前源码看这一规范在落地过程中继续演进。useNumberFieldState 中的NumberFieldProps还增加了commitBehaviorsnap|validate默认snap用于控制失焦提交时是否将值钳制到 min/max 并对齐 stepuseNumberField 中的AriaNumberFieldProps又补充了isWheelDisabled是否禁止滚轮修改值。而 Spectrum 主题的 SpectrumNumberFieldProps 则是在 Aria 层之上叠加isQuiet与hideStepper并接入SpectrumLabelableProps与StyleProps。也就是说规范文档描述的是组件的核心契约各层 hook 在此基础上分头扩展了状态行为、无障碍行为与视觉主题三组属性。v2 到 v3 的 API 迁移规范文档给出了完整的迁移对照表从 v2react-spectrum 3.x 之前升级到 v3 时需按此调整v2v3NotesNumberInputNumberField组件更名minminValuemaxmaxValuedecrementTitledecrementAriaLabelincrementTitleincrementAriaLabel-hideStepperv3 新增-formatOptionsv3 新增几点迁移说明组件从NumberInput更名为NumberField与 HTML 表单控件语义及 react-aria 的useNumberFieldhook 命名对齐min/max改为minValue/maxValueminValue/maxValue也是 useNumberFieldState 中状态对象对外暴露的字段名属性与状态一一对应按钮的title语义鼠标悬浮提示被替换为decrementAriaLabel/incrementAriaLabel无障碍标签因为步进按钮的可读性主要服务于屏幕阅读器而非悬浮提示。国际化数字解析为什么不能用 parseFloat规范文档的 Implementation details 部分明确给出了核心设计决策我们需要支持很多不同的 locale所以不能使用只适用于特定 locale 的parseFloat/Number()。应参考 localized number parsing 的做法来处理解析这样也能处理阿拉伯数字等我们支持的其他数字系统同时还能帮我们确定给定 locale 下允许的字符集。如果只指定[0-9]这样的 pattern就会漏掉阿拉伯数字。这一决策在源码中完整落地。状态层通过internationalized/number包packages/internationalized/number中的NumberParser和NumberFormatter完成解析与格式化// useNumberFieldState.ts let numberParser useMemo( () new NumberParser(locale, formatOptions), [locale, formatOptions] );NumberParser.parse(inputValue)会在用户每次输入后把文本解析为数字L210而NumberFormatter负责在提交时按 locale 重新格式化。解析器同时提供了isValidPartialNumber方法用于判定半输入状态比如只打了小数点是否合法——这正是状态对象上 validate 方法的实现let validate (value: string) numberParser.isValidPartialNumber(value, minValue, maxValue);规范中还提出可以在 stately 里做解析也许可以创建一个useNumberParserhook的设想。最终实现采用了等价的方案不单独导出解析 hook而是把NumberParser/NumberFormatter作为useNumberFieldState内部实现对外只暴露inputValue文本与numberValue数字两个视图符合文档中useNumberFieldState 可以返回 number 的value和字符串textValue的描述。一个值得注意的细节是数字系统numeral system的保留。规范文档提出不应把用户限制在某种数字系统内应保留用户的数字系统但按 locale 格式化并建议参照 CLDR 的numberingSystems.xml做字符映射检测。当前源码正是这样做的——useNumberFieldState 会从用户已输入的文本中检测其使用的数字系统再将其传入 formatterlet numberingSystem useMemo( () numberParser.getNumberingSystem(inputValue), [numberParser, inputValue] ); let formatter useMemo( () new NumberFormatter(locale, {...formatOptions, numberingSystem}), [locale, formatOptions, numberingSystem] );即用户用阿拉伯-印度数字输入输出仍保留该数字系统但千分位/小数分隔符等按 locale 规则处理。单位与货币分离处理的设计文档对单位/货币的结论是不要把单位/货币符号直接混入用户输入的字符串而是通过useNumberFormatter的 options即formatOptions传入由 formatter 根据 locale 自动把符号放到正确的位置货币符号前置还是后置、长度不同等都由 formatter 决定如果用户需要切换单位或币种应另提供一个Select之类的输入组件。原因是在字符串里处理会让解析变复杂因为符号位置不固定、长度不定也会让输入 pattern 变得更复杂。这一设计在 Aria 层得到了印证useNumberField 显式将输入框设为type: text注释写明不能使用 typenumber因为那样字段里就不能出现$之类的字符。输入框始终承载格式化后的文本含货币/百分号而真正的数值由commit时解析得出。关于数值输入输出文档的立场是NumberField 的输入与输出都应该是number这是最可能被存入数据库的值类型而不是格式化字符串。若消费方需要格式化后的字符串可以把onChange拿到的值用与NumberField相同的 options 传给useNumberFormatter即可得到与输入框一致的显示字符串。在源码中格式化能力同样以独立 hook 存在——useNumberField 内部就用useNumberFormatter(formatOptions)生成了供步进按钮朗读aria-valuetext语义使用的textValuelet numberFormatter useNumberFormatter(formatOptions); let textValueFormatter useNumberFormatter({...formatOptions, currencySign: undefined}); let textValue useMemo( () (isNaN(numberValue) ? : textValueFormatter.format(numberValue)), [textValueFormatter, numberValue] );这里还处理了一个细节currencySign: accounting时负数的格式化文本无法直接朗读所以单独用一个不带该配置的 formatter 生成可朗读的textValueL156-L162。行为约定聚焦全选、失焦提交、无效输入不触发 onChange规范文档的 Example Behavior 一节定义了一组关键交互以一个defaultValue为 50、formatOptions为 percent 的非受控 NumberField 为例用户聚焦时整个文本被选中方便直接键入新数字输入34.56时只在 blur 时才触发onChange只有合法数字字符或分隔符可以被输入字母等其他按键被忽略blur 时按formatOptions重新格式化%符号重新出现在数字后面用户也可以键入 formatter 会添加的任何合法符号如果用户在任何时刻输入了合法字符的非法组合导致解析失败不更新状态、不触发onChange从而避免不必要的重渲染用户仍有机会改成合法值若在非法状态下 blur由于状态从未被非法值污染输入框只会格式化并显示当前合法的值。对照 useNumberField.ts 的源码这些行为都能找到对应实现只在 blur 提交commit解析 钳制 触发 onChange被包在commitAndAnnounce中并挂在useFocus的onBlur回调上L134-L151。回车键也会触发flushSync(() commit())并执行commitValidation()L258-L271。无效输入被忽略输入框的onChange先调用state.validate(value)通过才更新inputValueL232-L236对应上面状态层的isValidPartialNumber判定。解析失败时回滚显示commit 的实现 中若parsedValue为 NaN就把输入框重置为当前numberValue的格式化文本后直接返回状态未被污染// if it failed to parse, then reset input to formatted version of current number if (isNaN(newParsedValue)) { setInputValue(format(numberValue)); return; }格式化只在必要时重渲染状态层用prevValue/prevLocale/prevFormatOptions三个比较状态L196-L208仅在数字值、locale 或 formatOptions 真正变化时才重写输入框文本避免打字过程中的重复格式化。此外源码还覆盖了文档未展开的两个输入场景整段粘贴onPaste 只在粘贴内容覆盖整个输入框时接管处理preventDefault 后直接commit(pastedText)让用户立即看到格式化结果部分粘贴因无法简单计算替换区间而被排除注释中明确说明了这一取舍。滚轮步进onWheel 在输入框聚焦时支持滚轮增/减并做了触控板判断|deltaY| |deltaX|时视为水平滚动而忽略可通过isWheelDisabled、isDisabled、isReadOnly关闭。commit 细节clamp、snap 与 percent 的默认 step状态层的 commit 是文档行为与状态机的交汇点其逻辑为输入为空 →setNumberValue(NaN)输入框清空非受控或恢复为当前值的格式化文本受控解析失败 → 输入框重置为当前numberValue的格式化文本见上文commitBehavior snap时值经snapValue无 step 则clamp到 min/max有 step 则snapValueToStep处理validate则不做钳制值原样保留交给校验流程报告 range/step 错误最后通过numberParser.parse(format(clampedValue))完成一次格式化再解析消除小数位超出maximumFractionDigits的残留再setNumberValue。step 的取值还有一处 locale 相关的默认值L188-L191当formatOptions.style percent且未显式给出 step 时默认 step 为0.01与文档示例中percent 模式下输入 34.56的场景吻合。步进按钮的方向与上下限由canIncrement/canDecrementL308-L330按当前解析值、min/max 与 step 共同推导Aria 层将其映射为按钮的isDisableduseNumberField.ts#L398、L412。步进运算本身也处理了浮点精度问题handleDecimalOperation 在操作数含小数时先按最大小数位放大为整数再运算避免0.1 0.2类精度漂移。移动端软键盘inputMode 的策略化选择规范文档在 More notes 中提出Input mode should go to numberpad in mobile如何在各数字系统、负号场景下做到最好当前源码给出的答案是按平台与取值范围动态选择inputMode见 useNumberField.ts#L206-L230let hasDecimals (intlOptions.maximumFractionDigits ?? 0) 0; let hasNegative state.minValue undefined || isNaN(state.minValue) || state.minValue 0; let inputMode: TextInputDOMProps[inputMode] numeric; if (isIPhone()) { // iPhone 的 numeric 和 decimal 键盘都没有负号 if (hasNegative) { inputMode text; } else if (hasDecimals) { inputMode decimal; } } else if (isAndroid()) { // Android 的 numeric 键盘同时有小数点和负号decimal 键盘没有负号 if (hasNegative) { inputMode numeric; } else if (hasDecimals) { inputMode decimal; } }判断依据来自formatter.resolvedOptions()的maximumFractionDigits是否允许小数与minValue是否允许负数。可以推断这是逐设备实测得出的经验策略iPhone 的 numeric/decimal 键盘均无负号因此允许负数时只能退化为text键盘Android 的numeric键盘恰好同时提供小数点与负号是含负数场景的首选。Spectrum 主题组件结构、步进按钮与表单提交Aadobe/react-spectrum 的 NumberField 将状态useNumberFieldStatelocale 取自 useLocale 的 Provider 上下文与行为useNumberField组装成 Spectrum 视觉组件结构上由Field包裹标签/描述/错误信息内部NumberFieldInput包含TextFieldBase复用 TextField 的输入框含校验图标两个StepButtondirectionup/down仅当!hideStepper时渲染L191-L196即规范中 v3 新增的hideStepper落地点一个隐藏的input typehidden当传入name时用于表单提交值为isNaN(state.numberValue) ? : state.numberValueL197-L204。这与 Aria 层把name/form从格式化输入框上剥离的做法useNumberField.ts#L283-L285配合保证提交给服务端的是纯数字。样式上组件通过classNames拼接spectrum-Stepper系列类名isQuiet映射到spectrum-Stepper--isQuietprovider.scale large移动端时加上spectrum-Stepper--isMobileL88-L108。无障碍方面Aria 层还有几个值得关注的处理输入框的 role 被清空spinbutton 的原生rolespinbutton会被覆盖为null注释说明VoiceOver 无法聚焦 spin buttonaria-valuemin/max/now/valuetext也一并移除改由两个真实按钮承载增/减能力L335-L345iOS 上还会移除aria-roledescription以让必填状态被正确朗读。按钮标签的四分法源码注释L369-L379详细说明了可见 label 为字符串/JSX、aria-label、aria-labelledby四种情况下increment/decrement 按钮的aria-label/aria-labelledby如何组合且优先把字段名放进aria-label字符串以便翻译时调整语序。按钮按下时的焦点策略onButtonPressStart 在输入框已聚焦时保持焦点不移动避免收起软键盘鼠标点击则把焦点移回输入框触屏/读屏器场景则聚焦按钮本身防止软键盘弹出。提交时朗读commitAndAnnounce在 commit 后若输入框文本发生变化会用announce(value, assertive)通过 live-announcer 朗读新值L134-L144。原生校验复用当commitBehavior为validate时useNativeValidation 会创建一个隐藏的typenumber输入框仅用它来计算 min/max/step 的ValidityState从而直接复用浏览器原生的校验消息文案免翻译并在validationBehavior native时通过setCustomValidity阻止表单提交。未来方向规范文档最后留了一个 Future considerations大数字缩写格式化——百万在英文缩写为M在法文是Mo。这指向Intl.NumberFormat的notation: compact能力在 locale 间的一致性维护属于formatOptions透传设计的自然延伸组件层不需要额外改动关键在于解析器NumberParser能否稳定还原 compact 表示。相关源码索引文件作用specs/api/NumberField.md本文核心依据接口定义、v2→v3 迁移表、行为约定packages/react-stately/src/numberfield/useNumberFieldState.ts状态管理解析、commit、step 钳制、数字系统保留packages/react-aria/src/numberfield/useNumberField.ts无障碍与行为blur/Enter 提交、inputMode、滚轮、原生校验、按钮焦点策略packages/adobe/react-spectrum/src/numberfield/NumberField.tsxSpectrum 主题组件isQuiet/hideStepper、StepButton、隐藏提交输入框packages/internationalized/numberNumberParser/NumberFormatter本地化解析与格式化库packages/react-stately/test/numberfield/useNumberFieldState.test.ts状态层测试packages/react-aria/test/numberfield/useNumberField.test.tsAria 层测试packages/adobe/react-spectrum/test/numberfield/NumberField.test.js组件层测试需要说明的是规范文档描述的是设计期的接口与行为约定例如hideStepper、formatOptions均标注为 v3 added而commitBehavior、isWheelDisabled等属性是规范之后在源码中新增的扩展阅读时建议以 useNumberFieldState 与 useNumberField 的当前类型定义为准本文所有行号均对应当前仓库代码。【免费下载链接】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),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。