v3.2.0
基于 React Aria 1.18 的 Calendar 周/日视图与年份选择器,以及 Radio、Checkbox、Switch 的破坏性组合方式变更。
Calendar 新增周视图与日视图、重做的年份选择器,以及基于 React Aria 1.18 的范围日历演示。Autocomplete 新增用于大型选项列表的 Virtualizer 示例。Tooltip 新增用于全局显示与隐藏延迟的主题变量。Radio、Checkbox、Switch 迁移到 React Aria 的 *Field + *Button 组合方式。本次发布同时纳入了虚拟化列表、分组字段自动填充、Toast 与 Fieldset 行为,以及滚动与 RTL 样式方面的补丁修复。
⚠️ 破坏性变更:Radio、Checkbox、Switch 改为显式的 *.Content 组合 —— *.Control 嵌套进 *.Content,标签变为 *.Content 内的纯文本(不嵌套 <Label>),Description/FieldError 变为 *.Content 的兄弟节点。详见破坏性变更。
安装
升级到最新版本:
npm i @lenso/tokens@latest @lenso/ui@latest正在使用 AI 助手? 对它说「Hey Cursor,把 HeroUI 升级到最新版本」。它会对比版本并应用必要变更。了解更多请参阅 HeroUI MCP 服务器。
新增内容
Calendar
Calendar 与 RangeCalendar 新增周视图与日视图,以及来自 React Aria 1.18 的新日历属性。
- 周视图 / 日视图:通过
visibleDuration渲染多周或单日布局 - 多选:在单个
Calendar中选择多个日期 - React Aria 1.18 属性:
weeksInMonth与用于范围选择的isDateUnavailable(date, anchorDate) - 内部实现:月份标题使用 React Aria 的
CalendarHeading,年份选择器基于 React Aria calendar hooks 重建
周视图:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Select. */import { Calendar } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarHeader, CalendarGrid, DurationSelect, layout } from "./demo-parts";export function WeekView() { const [weeks, setWeeks] = useState(1); return ( <div {...stylex.props(layout.controlsStack)}> <DurationSelect unit="weeks" value={weeks} onChange={setWeeks} /> <Calendar aria-label="Week view" visibleDuration={{ weeks }}> <CalendarHeader /> <CalendarGrid /> </Calendar> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
日视图:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Select. */import { Calendar } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarHeader, CalendarGrid, DurationSelect, layout } from "./demo-parts";export function DayView() { const [days, setDays] = useState(5); return ( <div {...stylex.props(layout.controlsStack)}> <DurationSelect unit="days" value={days} onChange={setDays} /> <Calendar aria-label="Day view" visibleDuration={{ days }}> <CalendarHeader /> <CalendarGrid /> </Calendar> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
多选:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { Calendar } from "@lenso/ui";import type { DateValue } from "@internationalized/date";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarNote } from "./demo-parts";
const styles = stylex.create({ root: { display: "flex", flexDirection: "column", alignItems: "center", gap: 16 }, description: { textAlign: "center" },});
export function MultipleSelection() { const [value, setValue] = useState<readonly DateValue[]>([]); return ( <div {...stylex.props(styles.root)}> <Calendar aria-label="Event dates" selectionMode="multiple" value={value} onChange={setValue}> <Calendar.Header> <Calendar.Heading /> <Calendar.NavButton slot="previous" /> <Calendar.NavButton slot="next" /> </Calendar.Header> <Calendar.Grid> <Calendar.GridHeader> {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>} </Calendar.GridHeader> <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody> </Calendar.Grid> </Calendar> <CalendarNote> {value.length ? `${value.length} date(s) selected` : "Select multiple dates"} </CalendarNote> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Autocomplete
支持通过 Virtualizer 渲染大型选项列表,并补充文档、Storybook 示例,以及针对 popover 尺寸和 listbox 滚动高度的样式修复。
- 虚拟化:在
Autocomplete.Popover内用 React Aria 的<Virtualizer>包裹ListBox(#6642) - Popover 尺寸:listbox 高度上限为
320px并支持内部滚动;搜索框固定在列表上方
虚拟化:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** HeroUI v3.2.6 adaptation. SPDX-License-Identifier: Apache-2.0 */import { Autocomplete } from "@lenso/ui";import { useRef, useState } from "react";import * as stylex from "@stylexjs/stylex";import { NativeAutocomplete, OptionContent, type Option } from "./_native";
const firstNames = [ "Emma", "Liam", "Olivia", "Noah", "Ava", "James", "Sophia", "Oliver", "Isabella", "Lucas", "Mia", "Ethan", "Charlotte", "Mason", "Amelia", "Logan", "Harper", "Alexander", "Ella", "Benjamin",];const lastNames = [ "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Anderson", "Taylor", "Thomas", "Jackson", "White", "Harris", "Clark", "Lewis", "Robinson", "Walker",];const allUsers: Option[] = Array.from({ length: 1000 }, (_, index) => { const first = firstNames[index % firstNames.length] ?? ""; const last = lastNames[Math.floor(index / firstNames.length) % lastNames.length] ?? ""; return { id: String(index + 1), name: `${first} ${last}`, email: `${first.toLowerCase()}.${last.toLowerCase()}@acme.com`, };});const virtualStyles = stylex.create({ field: { width: 300 }, list: { height: 300, overflowY: "auto", position: "relative", padding: 0 }, row: { height: 50, boxSizing: "border-box" }, spacer: (height: number) => ({ height }),});export function Virtualization() { const { contains } = Autocomplete.useFilter({ sensitivity: "base" }); const [query, setQuery] = useState(""); const [scrollTop, setScrollTop] = useState(0); const listRef = useRef<HTMLDivElement>(null); const filtered = allUsers.filter( (user) => contains(user.name, query) || contains(user.email ?? "", query), ); const start = Math.max(0, Math.floor(scrollTop / 50) - 3); const end = Math.min(filtered.length, start + 14); const onHighlight = (item: Option | undefined) => { const index = filtered.findIndex((user) => user.id === item?.id); const element = listRef.current; if (index < 0 || !element) return; const top = index * 50; if (top < element.scrollTop) element.scrollTop = top; else if (top + 50 > element.scrollTop + 300) element.scrollTop = top - 250; setScrollTop(element.scrollTop); }; return ( <NativeAutocomplete items={allUsers} xstyle={virtualStyles.field} filteredItems={filtered} virtualized onItemHighlighted={onHighlight} label="User" placeholder="Select a user" searchLabel="Search users" searchPlaceholder="Search users..." inputValue={query} onInputValueChange={(next) => { setQuery(next); setScrollTop(0); if (listRef.current) listRef.current.scrollTop = 0; }} list={ <Autocomplete.List ref={listRef} xstyle={virtualStyles.list} onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)} > <div aria-hidden="true" {...stylex.props(virtualStyles.spacer(start * 50))} /> {filtered.slice(start, end).map((item, offset) => ( <Autocomplete.Item key={item.id} value={item} index={start + offset} aria-setsize={filtered.length} aria-posinset={start + offset + 1} xstyle={virtualStyles.row} > <OptionContent item={item} /> <Autocomplete.ItemIndicator /> </Autocomplete.Item> ))} <div aria-hidden="true" {...stylex.props(virtualStyles.spacer((filtered.length - end) * 50))} /> </Autocomplete.List> } /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Table.SortableColumnHeader
Table.SortableColumnHeader 用于渲染 sortable 列标题和可选的升降序指示器。放在 Table.Column render prop 中,并传入 sortDirection (#6588)。
<Table.Column allowsSorting> {({sortDirection}) => ( <Table.SortableColumnHeader sortDirection={sortDirection}> Name </Table.SortableColumnHeader> )}</Table.Column>- 默认指示器:存在排序方向时显示 chevron
- 自定义指示器:传入
indicator,或用showIndicator={false}隐藏 - 样式插槽:
.table__sortable-column-header+.table__sortable-column-indicator
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6 sorting adaptation (Apache-2.0).import { useState } from "react";import { Table, type SortDescriptor } from "@lenso/ui";import { Headers, Rows, styles, users, type User } from "./data";export function Sorting() { const [sort, setSort] = useState<SortDescriptor>({ column: "name", direction: "ascending" }); const sorted = users .slice(0, 5) .sort( (a, b) => String(a[sort.column as keyof User]).localeCompare(String(b[sort.column as keyof User])) * (sort.direction === "ascending" ? 1 : -1), ); return ( <Table> <Table.ScrollContainer> <Table.Content aria-label="Sortable table" xstyle={styles.content} sortDescriptor={sort} onSortChange={setSort} > <Headers sorting /> <Table.Body> <Rows items={sorted} /> </Table.Body> </Table.Content> </Table.ScrollContainer> </Table> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Tooltip 延迟主题变量
Tooltip 现在会从主题 CSS 变量读取默认的显示与隐藏延迟 (#6617):
--tooltip-delay— 显示 Tooltip 前的延迟(默认:1500ms)--tooltip-close-delay— 隐藏 Tooltip 前的延迟(默认:500ms)
全局覆盖示例:
:root { --tooltip-delay: 700ms; --tooltip-close-delay: 0ms;}单个 Tooltip 上的 delay 和 closeDelay 属性仍会覆盖这些值。
行为变更: 使用默认 HeroUI 主题时,Tooltip 延迟现在默认为 1500ms / 500ms,而不是之前的 React Aria 默认值 700ms / 0ms。如需保留旧行为,请显式设置 CSS 变量或 props。
组件修复
- Toast:Toast 队列会串行化 ViewTransition 更新,避免在
toast.promise()关闭 loading toast 并打开成功/失败反馈时出现被跳过的过渡和 AbortError (#6511)。 - Fieldset:
disabled会传递到 React Aria 的 Button、CheckboxGroup、Link、RadioGroup、Slider、ToggleButton 和 ToggleButtonGroup 上下文 (#6596)。 - Autocomplete:Popover 内容包裹在 React Aria 的
Dialog中,打开 popover 时 listbox 不再出现多余的焦点环 (#6627)。 - Tooltip:Trigger 改用
useFocusablehook 而非<Focusable>包装组件,避免 Tooltip 挂载在inert子树中(例如位于已打开的 Drawer/Modal 之后)时出现误报的 “child must be focusable” 警告 (#6628)。
样式修复
- Modal / AlertDialog:
scroll-inside对话框通过max-h-full min-h-0限制高度,使内容区域滚动而非溢出 (#6597)。 - ScrollShadow:渐隐遮罩通过
--scroll-shadow-scrollbar-size为可见的原生滚动条预留空间 (#6598)。 - Table RTL:列分隔线与拖拽手柄在 RTL 下使用逻辑属性
end-0定位 (#6606)。 - Link:移除硬编码的
text-sm,让链接从父元素继承字号;.link__icon改为相对单位size-[0.75em],随文本大小缩放,而不再使用固定的size-2(#6621)。 - DatePicker / DateRangePicker:日历 popover 由
max-w-(--trigger-width)改为min-w-(--trigger-width),确保 popover 至少与触发器一样宽,避免被水平裁剪 (#6622)。 - Table:当 Table 被 React Aria 的
<Virtualizer>包裹时,secondary 表头的边框与圆角能够正确渲染 —— 列选择器不再把每个虚拟化列都同时视为 first 和 last child (#6624)。 - Autocomplete:Popover 限制为触发器宽度,listbox 高度上限为
320px并支持内部滚动,避免虚拟化列表溢出 popover (#6642)。 - ListBox:将
flex flex-col gap改为普通块级流 + 兄弟节点间距,避免 React Aria Virtualizer 的内容高度被 flex-shrink 压缩 —— 修复虚拟化滚动时滚动条滑块尺寸变化的问题 (#6636)。 - InputGroup / NumberField / SearchField:浏览器自动填充高亮提升到分组容器上,使 prefix、suffix 和增减按钮槽位共享圆角高亮 (#6625)。
- Spinner:改用
inline-flex搭配shrink-0(替代relative),使旋转动画在非 flex 布局中也能正确渲染,并支持motion-reduce(#6644)。 - Toast:关闭按钮在所有断点下统一使用
-top-1 -right-1,与容器内边距对齐 (#6574)。
依赖
- React Aria Components:
1.17.0→1.18.0(#6586)。1.18 引入了 toggles 所采用的*Field+*Button组合方式、CalendarHeading,以及isDateUnavailable(date, anchorDate)。 - @internationalized/date:
3.12.1→3.12.2 - React Aria / Stately 辅助包:
@react-aria/*、@react-stately/*与@react-types/shared补丁更新
⚠️ Breaking Changes
Radio、Checkbox 与 Switch:显式 *.Content 组合
这些组件现在在底层使用 React Aria 的 *Field + *Button 组合方式。X.Content 现在是可点击的 label(React Aria 的 *Button)。共有三点变化:
X.Control移进X.Content—— 它们以前是兄弟节点。- 标签是
X.Content内部的纯文本 ——X.Content渲染的是<label>元素,所以不要嵌套<Label>组件(嵌套的<label>是无效 HTML)。若要使用独立的Label,请把它放在外部,并用htmlFor+ 组件id关联。 Description/FieldError移到外部,作为X.Content的兄弟节点,这样它们会通过aria-describedby朗读,而不会被并入无障碍名称。
Checkbox
// v3.1<Checkbox> <Checkbox.Control> <Checkbox.Indicator /> </Checkbox.Control> <Checkbox.Content> <Label>Accept terms</Label> <Description>You agree to our terms</Description> </Checkbox.Content></Checkbox>
// v3.2<Checkbox> <Checkbox.Content> <Checkbox.Control> <Checkbox.Indicator /> </Checkbox.Control> Accept terms </Checkbox.Content> <Description>You agree to our terms</Description></Checkbox>Radio
// v3.1<Radio value="a"> <Radio.Control> <Radio.Indicator /> </Radio.Control> <Radio.Content> <Label>Option A</Label> </Radio.Content></Radio>
// v3.2<Radio value="a"> <Radio.Content> <Radio.Control> <Radio.Indicator /> </Radio.Control> Option A </Radio.Content></Radio>Switch
// v3.1<Switch> <Switch.Control> <Switch.Thumb /> </Switch.Control> <Switch.Content> <Label>Enable notifications</Label> </Switch.Content></Switch>
// v3.2<Switch> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content></Switch>迁移对照
| v3.1 | v3.2 |
|---|---|
X.Control 与 X.Content 为兄弟节点 | X.Control 嵌套进 X.Content |
X.Content 是包裹 Label + 帮助文本的布局 <div> | X.Content 是包裹 X.Control + 标签文本的可点击 <label> |
通过 X.Content 内的 <Label> 提供标签 | 标签是 X.Content 内的纯文本(不嵌套 <Label>) |
Description / FieldError 位于 X.Content 内 | Description / FieldError 作为 X.Content 的兄弟节点 |
外部标签 —— 若要使用独立的 Label,请把它放在组件外部,并用 htmlFor + 组件 id 关联:
<div className="flex items-center gap-3"> <Checkbox id="terms"> <Checkbox.Content> <Checkbox.Control> <Checkbox.Indicator /> </Checkbox.Control> </Checkbox.Content> </Checkbox> <Label htmlFor="terms">Accept terms</Label></div>仅含控件的 Checkbox 和 Switch(没有标签,例如表格行选择或图标开关)仍需用 X.Content 作为可点击包装。请把 Checkbox.Control / Switch.Control 放进 X.Content,省略标签,并在根组件上传入 aria-label。
各组件完整迁移指南:Checkbox、Checkbox Group、Radio、Radio Group、Switch。
链接
- Calendar 文档
- Autocomplete 文档
- Tooltip 文档
- Checkbox 文档
- Radio Group 文档
- Switch 文档
- 组件文档
- GitHub 仓库
- GitHub PR #6616
贡献者
感谢所有为本次发布做出贡献的人!
HeroUI contributors