Skip to content
Lenso UI

v3.2.0

基于 React Aria 1.18 的 Calendar 周/日视图与年份选择器,以及 Radio、Checkbox、Switch 的破坏性组合方式变更。

2026 年 6 月 15 日

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 改用 useFocusable hook 而非 <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.1v3.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。

链接

贡献者

感谢所有为本次发布做出贡献的人!

HeroUI contributors