Skip to content

feat(Choice): 新增 Choice 选择交互组件 - #1978

Open
Somtry wants to merge 3 commits into
ant-design:mainfrom
Somtry:feat/choice-1957
Open

feat(Choice): 新增 Choice 选择交互组件#1978
Somtry wants to merge 3 commits into
ant-design:mainfrom
Somtry:feat/choice-1957

Conversation

@Somtry

@Somtry Somtry commented Jul 16, 2026

Copy link
Copy Markdown

🎯 关联 Issue

Close #1957

📋 变更内容

新增 Choice 选择交互组件,用于 AI 对话场景中呈现一组结构化选项供用户选择。

核心功能

  • 单选/多选模式(mode: single | multiple
  • 三种布局模式(layout: list | grid | card
  • 四种指示器类型(indicator: check | radio | number | none
  • 受控/非受控模式
  • maxCount 多选上限控制
  • confirmable 确认按钮 + onConfirm 回调

AI 场景特性

  • recommended 推荐标记(primary / secondary
  • disabledReason 禁用原因提示(Tooltip + 行内文本)
  • loading 骨架屏加载态
  • meta 元数据透传
  • 嵌套子选项分组

工程实现

  • 完整的 TypeScript 类型定义
  • CSS-in-JS 样式方案 + 24 个 Design Token
  • 语义化 DOM 结构(SemanticType)
  • fadeIn / fadeInLeft 渐入动画
  • 国际化支持(locale zh_CN / en_US)
  • 键盘导航 + 无障碍支持
  • XProvider 组件配置注册
  • 11 个 Demo + 中英文文档
  • 71 个单元测试全部通过

✅ 自检

  • TypeScript 类型检查通过
  • 单元测试全部通过(71 个)
  • Biome lint + Prettier 通过
  • 中英文文档完整
  • Demo 可正常渲染

Summary by CodeRabbit

  • 新功能

    • 新增 Choice 选项选择组件,支持单选、多选、受控/非受控模式及最大选择数量限制。
    • 支持列表、网格、卡片布局,以及单选、勾选、数字和无指示器样式。
    • 支持推荐标记、禁用原因提示、加载状态、确认按钮、键盘操作和嵌套选项。
    • 提供滚动到指定选项、获取当前选中值等能力,并支持中英文文案与主题配置。
  • 文档

    • 新增基础、多选、布局、推荐、禁用、确认、加载及受控模式等示例和双语 API 文档。
  • 测试

    • 补充渲染、选择交互、布局、键盘操作、受控状态及边界场景测试。

liaojinhao.ljh added 2 commits July 16, 2026 15:27
实现 AI 对话场景中的结构化选择交互组件,包含:

核心功能:
- 单选/多选模式 (mode: single/multiple)
- 三种布局模式 (layout: list/grid/card)
- 四种指示器类型 (indicator: check/radio/number/none)
- 受控/非受控模式
- maxCount 多选上限控制
- confirmable 确认按钮 + onConfirm 回调

AI 场景特性:
- recommended 推荐标记 (primary/secondary)
- disabledReason 禁用原因提示
- loading 骨架屏加载态
- meta 元数据透传
- 嵌套子选项分组

工程实现:
- 完整的 TypeScript 类型定义
- CSS-in-JS 样式方案 + Design Token
- 语义化 DOM 结构 (SemanticType)
- fadeIn/fadeInLeft 渐入动画
- 键盘导航支持
- XProvider 组件配置注册
- 11 个 Demo + 中英文文档
- 28 个单元测试全部通过
对照 issue ant-design#1957 验收标准补充以下缺失项:

1. Tooltip for disabledReason:禁用选项 hover 时通过 antd Tooltip 展示原因
2. locale 国际化:注册 Choice 到 xLocale,支持 confirmText/recommended/maxSelected/disabledHint
3. Design Token 完整化:补充 itemActiveBg/itemRecommendedBorderColor/itemDisabledBorderColor/
   titleFontSize/titleLineHeight/labelFontSize/labelLineHeight/descFontSize/descLineHeight/
   headerPadding/listPadding/footerPadding/borderRadius/gridColumns/motionDuration 等 token
4. select.test.tsx:新增 19 个选择逻辑专项测试(单选/多选/maxCount/受控/嵌套/ChangeInfo 等)
5. items 默认值处理,避免无 props 渲染时报错

全部 71 个测试通过,TypeScript 类型检查通过
@dosubot dosubot Bot added documentation Improvements or additions to documentation enhancement New feature or request javascript Pull requests that update Javascript code labels Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

新增完整的 Choice 选择组件,支持单选、多选、受控状态、布局、指示器、禁用提示、推荐标记、确认操作、加载态、动画、主题样式、无障碍交互及嵌套选项,并补充测试、演示、文档、本地化和组件导出。

Changes

Choice 组件

Layer / File(s) Summary
选择契约与状态管理
packages/x/components/choice/interface.ts, packages/x/components/choice/context.ts, packages/x/components/choice/hooks/use-select.ts
定义 Choice 类型、上下文和 ref 契约,实现单选、多选、受控/非受控值、maxCountChangeInfo
组件渲染与条目交互
packages/x/components/choice/index.tsx, packages/x/components/choice/Item.tsx
实现 Choice 主组件与条目渲染,支持指示器、嵌套项、确认页脚、键盘交互、禁用原因和 Tooltip。
主题样式与动效
packages/x/components/choice/style/index.ts
新增 Choice token、list/grid/card 布局、条目状态、推荐标记、页脚、加载态和淡入动效样式。
组件行为验证
packages/x/components/choice/__tests__/*
覆盖渲染、选择行为、受控状态、禁用、多选限制、布局、指示器、确认、键盘和语义属性。
组件接入与示例文档
packages/x/components/index.ts, packages/x/components/locale/*, packages/x/components/theme/interface/components.ts, packages/x/components/x-provider/context.ts, packages/x/components/choice/demo/*, packages/x/components/choice/index.*.md
接入公共导出、Provider 配置、主题 token 和中英文文案,并新增多个功能演示及 API 文档。

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: github-actions

Poem

小兔敲键盘,选项排成行,
单选多选都亮堂。
推荐、禁用有提示,
回车空格也能忙。
Choice 新芽,跳进对话光。

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 多数核心能力已覆盖,但 useXChat/A2UI 协议集成与 90% 覆盖率无法从摘要中确认。 补充 useXChat/A2UI 协议集成和覆盖率、集成测试的实现或摘要证据。
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题简洁且准确概括了新增 Choice 选择交互组件。
Out of Scope Changes check ✅ Passed 变更主要集中在 Choice 组件、示例、文档、类型和测试,没有明显无关改动。
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Somtry

Somtry commented Jul 16, 2026

Copy link
Copy Markdown
Author

#1957

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the new Choice component, designed to present structured options in AI conversation scenarios with support for single/multiple selection, various layouts, loading states, and recommended badges. The review feedback highlights several areas for improvement: the ChangeInfo payload in onConfirm contains misleading empty or hardcoded values; the onChange and related value types should include undefined to correctly type deselection in single mode; default texts for recommended badges and the maxCount footer text are hardcoded and should be internationalized using the locale; and the opacity value 0.6 in the styles should utilize a design token instead of a magic number.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +170 to +175
onConfirm(val, {
value: val,
items,
changedItems: [],
type: 'select',
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

传递给 onConfirmChangeInfo 对象似乎包含不正确或有误导性的值。

  1. changedItems 是一个空数组。它应该包含正在被确认的选项,这可以从 mergedValueitems 中派生出来。
  2. type 硬编码为 'select'。对于确认操作来说,这是有误导性的。最好引入一个 'confirm' 类型。

为了解决这个问题,我建议:

  • interface.ts 中扩展 ChangeInfo['type'] 以包含 'confirm'
  • 在调用 onConfirm 时,使用 type: 'confirm' 并填充 changedItems 为当前选中的选项。
                const confirmedItems = items.filter((item) => mergedValue.includes(item.key));
                onConfirm(val, {
                  value: val,
                  items,
                  changedItems: confirmedItems,
                  type: 'select', // TODO: 更新 interface.ts 后应改为 'confirm'
                });

* @desc 选中变化回调
* @descEN Callback when selection changes
*/
onChange?: (value: ChoiceValueType | ChoiceValueType[], info: ChangeInfo) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

onChangevalue 参数的类型不正确。在单选模式下取消选择某个项目时,onChange 会以 undefined 作为参数被调用,但类型 ChoiceValueType | ChoiceValueType[] 不包含 undefined。这可能会给组件的 TypeScript 用户导致类型错误。

应将其更改为 ChoiceValueType | ChoiceValueType[] | undefined

这也适用于:

  • ChoiceProps['value']
  • ChoiceProps['defaultValue']
  • ChoiceProps['onConfirm'] 的 value 参数
  • ChangeInfo['value']
Suggested change
onChange?: (value: ChoiceValueType | ChoiceValueType[], info: ChangeInfo) => void;
onChange?: (value: ChoiceValueType | ChoiceValueType[] | undefined, info: ChangeInfo) => void;

Comment on lines +113 to +124
{(item.recommended === true || item.recommended === 'primary') && (
<div className={`${prefixCls}-item-recommend-badge`}>
{item.recommendedReason || 'AI Recommended'}
</div>
)}
{item.recommended === 'secondary' && (
<div
className={`${prefixCls}-item-recommend-badge ${prefixCls}-item-recommend-badge-secondary`}
>
{item.recommendedReason || 'Recommended'}
</div>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

推荐徽章的默认文案 'AI Recommended' 和 'Recommended' 是硬编码的。该组件支持 i18n,因此这些字符串应该来自语言包文件。

为了解决这个问题,你可以将 locale 对象从 Choice 主组件通过 ChoiceContext 向下传递。

  1. packages/x/components/choice/context.ts 中,将 locale 添加到 ChoiceContextProps
  2. packages/x/components/choice/index.tsx 中,将 locale 对象传递到 ChoiceContext.Provider 的 value 中。
  3. Item.tsx 文件中,从 context 中消费 locale 并使用相应的键,例如:item.recommendedReason || locale.aiRecommendeditem.recommendedReason || locale.recommended

目前的语言包文件已经包含了 recommended 键,你可能需要为 'AI Recommended' 添加一个新的键。

if (!footer && !confirmable) return null;

const selectedCount = mergedValue.length;
const maxText = mode === 'multiple' && maxCount ? `${selectedCount}/${maxCount}` : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

maxCount 的文本格式是硬编码的。它应该使用语言包中的 maxSelected 键进行国际化,该键支持 {count}{max} 占位符。

Suggested change
const maxText = mode === 'multiple' && maxCount ? `${selectedCount}/${maxCount}` : null;
const maxText = mode === 'multiple' && maxCount ? locale.maxSelected.replace('{count}', String(selectedCount)).replace('{max}', String(maxCount)) : null;

cursor: 'not-allowed',
background: token.itemDisabledBg,
borderColor: token.itemDisabledBorderColor,
opacity: 0.6,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

透明度值 0.6 是一个魔法数字。最好使用设计令牌(例如 token.opacityDisabled),如果可用的话。或者,为了更好的可维护性和一致性,应在组件的令牌集内将其定义为变量。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (2)
packages/x/components/choice/demo/basic.tsx (1)

54-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

为默认导出的组件添加名称。

在 React 中,默认导出匿名箭头函数会导致组件在 React DevTools 中显示为 "Unknown",降低了可调试性。建议为其提供一个明确的名称。

♻️ 建议的重构
-export default () => (
+const BasicDemo: React.FC = () => (
   <App>
     <Demo />
   </App>
 );
+
+export default BasicDemo;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/demo/basic.tsx` around lines 54 - 58, 为
basic.tsx 中的默认导出组件提供明确名称,将匿名箭头函数改为命名组件并保持现有 App 与 Demo 渲染结构不变,以便 React DevTools
正确显示组件名称。
packages/x/components/choice/context.ts (1)

1-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

优化类型引入与声明。

建议将 ChoiceItemType 提升至文件顶部的 import 中以提升可读性。另外,由于 useSelect 内部已将选中值严格归一化为数组(mergedValue),传递给 Context 的 selectedKeys 类型一定是数组,可以将其类型安全地收窄为 ChoiceValueType[]

♻️ 建议修复
 import React from 'react';
-import type { ChoiceProps } from './interface';
+import type { ChoiceProps, ChoiceItemType, ChoiceValueType } from './interface';

 export interface ChoiceContextProps {
   prefixCls: string;
   mode: 'single' | 'multiple';
   disabled: boolean;
   indicator: 'check' | 'radio' | 'number' | 'none';
-  selectedKeys: ChoiceProps['value'];
+  selectedKeys: ChoiceValueType[];
   isSelected: (key: string | number) => boolean;
-  onItemClick: (item: import('./interface').ChoiceItemType, index: number) => void;
+  onItemClick: (item: ChoiceItemType, index: number) => void;
   classNames?: ChoiceProps['classNames'];
   styles?: ChoiceProps['styles'];
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/context.ts` around lines 1 - 14, 优化
ChoiceContextProps 的类型声明:将 ChoiceItemType 和 ChoiceValueType 提升为文件顶部的类型导入,并将
selectedKeys 从 ChoiceProps['value'] 收窄为 ChoiceValueType[],保持 onItemClick 使用已导入的
ChoiceItemType。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/x/components/choice/index.tsx`:
- Around line 227-241: 在组件渲染逻辑中为 item.children.map 返回的每个子选项添加与顶层选项一致的 data-key
容器,并使用 child.key 作为该属性值,使 ChoiceRef.scrollTo 能通过选择器定位子选项;保持现有 ChoiceItem 的 item
和 childIndex 传递不变。
- Around line 190-195: 调整根容器的 style 合并顺序,定位到根节点的 style 属性:先合并
contextConfig.style,再合并用户传入的 style,最后保留 styles.root,以确保用户显式样式不会被
contextConfig.style 覆盖,并与组件其它区域的优先级约定一致。
- Around line 163-181: Update the confirm Button in the confirmable Choice block
to include the component-level disabled prop in its disabled condition, so it
remains disabled whenever Choice is disabled or selectedCount is zero. Preserve
the existing onConfirm behavior for enabled choices.
- Around line 66-74: 在组件中更新 useSelect 的解构以取得 isMaxReached,并将其加入
ChoiceContext.Provider 的 value;确保 ChoiceItem 可依据 isMaxReached && !selected 为达到
maxCount 后的未选中项应用禁用样式。
- Around line 200-205: 补充 Choice 的真实 ARIA 语义:在
packages/x/components/choice/index.tsx 的列表容器保留现有 data-role,并根据 mode 添加
role="radiogroup" 或 role="group";在 packages/x/components/choice/Item.tsx 的条目根据
mode 添加 role="radio" 或 "checkbox",同时设置反映选中状态的 aria-checked 和禁用状态的 aria-disabled。

In `@packages/x/components/choice/interface.ts`:
- Around line 148-151: Update the onChange callback type in the choice interface
to allow undefined as a possible value, including the single-selection
deselection case, while preserving the existing single- and multi-selection
value types.
- Around line 84-90: Update the ChangeInfo.value type to include undefined,
preserving the existing ChoiceValueType and array variants so single-selection
cancellation is represented correctly.

In `@packages/x/components/choice/Item.tsx`:
- Around line 126-140: Remove the always-visible disabled-reason block from the
item content rendered by the component, while preserving the Tooltip wrapper
around itemNode when disabled and item.disabledReason are present. Keep
item.disabledReason as the Tooltip title so the reason is shown only on hover.

In `@packages/x/components/choice/style/index.ts`:
- Around line 205-217: 合并 `${componentCls}-layout-card` 的重复样式定义:保留前面用于容器网格布局的
display、gridTemplateColumns 和 gap,并将末尾重复块中的必要 `-item`
样式合并到同一个对象中;删除文件末尾重复声明,确保该计算属性键只定义一次。
- Around line 113-117: 在 genChoiceStyle 的根选择器中应用 token.borderRadius 作为容器的
border-radius,使 ConfigProvider 自定义的整体圆角生效;保留 ComponentToken、ChoiceToken 的声明及
prepareComponentToken 中默认值,并确保引用与现有 token 样式生成方式一致。
- Around line 350-357: Update the `${componentCls}-nested` style block to
replace the physical `marginLeft` property with the appropriate logical margin
property, preserving the existing indentation amount and ensuring it mirrors
correctly under the `${componentCls}-rtl` direction.
- Around line 183-184: Remove the unit() wrapper from line-height assignments
for titleLineHeight, labelLineHeight, and descLineHeight, using each token’s raw
unitless value directly. Keep unit() for font-size values unchanged.

---

Nitpick comments:
In `@packages/x/components/choice/context.ts`:
- Around line 1-14: 优化 ChoiceContextProps 的类型声明:将 ChoiceItemType 和
ChoiceValueType 提升为文件顶部的类型导入,并将 selectedKeys 从 ChoiceProps['value'] 收窄为
ChoiceValueType[],保持 onItemClick 使用已导入的 ChoiceItemType。

In `@packages/x/components/choice/demo/basic.tsx`:
- Around line 54-58: 为 basic.tsx 中的默认导出组件提供明确名称,将匿名箭头函数改为命名组件并保持现有 App 与 Demo
渲染结构不变,以便 React DevTools 正确显示组件名称。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2a9e8e21-20c6-47eb-9680-9b095300cdc2

📥 Commits

Reviewing files that changed from the base of the PR and between 17db20b and 437c745.

⛔ Files ignored due to path filters (3)
  • packages/x/components/choice/__tests__/__snapshots__/demo-extend.test.ts.snap is excluded by !**/*.snap
  • packages/x/components/choice/__tests__/__snapshots__/demo.test.ts.snap is excluded by !**/*.snap
  • packages/x/components/choice/__tests__/__snapshots__/index.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (41)
  • packages/x/components/choice/Item.tsx
  • packages/x/components/choice/__tests__/demo-extend.test.ts
  • packages/x/components/choice/__tests__/demo.test.ts
  • packages/x/components/choice/__tests__/index.test.tsx
  • packages/x/components/choice/__tests__/select.test.tsx
  • packages/x/components/choice/context.ts
  • packages/x/components/choice/demo/_semantic.tsx
  • packages/x/components/choice/demo/basic.md
  • packages/x/components/choice/demo/basic.tsx
  • packages/x/components/choice/demo/card-layout.md
  • packages/x/components/choice/demo/card-layout.tsx
  • packages/x/components/choice/demo/confirmable.md
  • packages/x/components/choice/demo/confirmable.tsx
  • packages/x/components/choice/demo/controlled.md
  • packages/x/components/choice/demo/controlled.tsx
  • packages/x/components/choice/demo/disabled.md
  • packages/x/components/choice/demo/disabled.tsx
  • packages/x/components/choice/demo/grid-layout.md
  • packages/x/components/choice/demo/grid-layout.tsx
  • packages/x/components/choice/demo/in-bubble.md
  • packages/x/components/choice/demo/in-bubble.tsx
  • packages/x/components/choice/demo/indicator.md
  • packages/x/components/choice/demo/indicator.tsx
  • packages/x/components/choice/demo/loading.md
  • packages/x/components/choice/demo/loading.tsx
  • packages/x/components/choice/demo/multiple.md
  • packages/x/components/choice/demo/multiple.tsx
  • packages/x/components/choice/demo/recommended.md
  • packages/x/components/choice/demo/recommended.tsx
  • packages/x/components/choice/hooks/use-select.ts
  • packages/x/components/choice/index.en-US.md
  • packages/x/components/choice/index.tsx
  • packages/x/components/choice/index.zh-CN.md
  • packages/x/components/choice/interface.ts
  • packages/x/components/choice/style/index.ts
  • packages/x/components/index.ts
  • packages/x/components/locale/en_US.ts
  • packages/x/components/locale/index.tsx
  • packages/x/components/locale/zh_CN.ts
  • packages/x/components/theme/interface/components.ts
  • packages/x/components/x-provider/context.ts

Comment on lines +66 to +74
const { mergedValue, handleItemClick, isSelected } = useSelect({
mode,
value,
defaultValue,
onChange,
maxCount,
disabled,
items,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

isMaxReached 被计算但从未使用,达到 maxCount 后未选中项缺少禁用反馈

useSelect 返回了 isMaxReached(见 hooks/use-select.tsreturn { mergedValue, handleItemClick, isSelected, isMaxReached };),但此处未解构、也未通过 ChoiceContext 传给 ChoiceItem。结果是:一旦达到 maxCount,未被选中的选项在 UI 上仍呈现为可点击/可用状态(无禁用样式),用户点击后无任何视觉反馈,仅表现为“点击无效”。

🐛 建议修复
-  const { mergedValue, handleItemClick, isSelected } = useSelect({
+  const { mergedValue, handleItemClick, isSelected, isMaxReached } = useSelect({
     mode,
     value,
     defaultValue,
     onChange,
     maxCount,
     disabled,
     items,
   });

并将 isMaxReached 加入 ChoiceContext.Provider 的 value,供 ChoiceItem 用于给未选中项追加禁用样式(isMaxReached && !selected)。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/index.tsx` around lines 66 - 74, 在组件中更新
useSelect 的解构以取得 isMaxReached,并将其加入 ChoiceContext.Provider 的 value;确保 ChoiceItem
可依据 isMaxReached && !selected 为达到 maxCount 后的未选中项应用禁用样式。

Comment on lines +163 to +181
{confirmable && (
<Button
type="primary"
disabled={selectedCount === 0}
onClick={() => {
if (onConfirm) {
const val = mode === 'single' ? mergedValue[0] : mergedValue;
onConfirm(val, {
value: val,
items,
changedItems: [],
type: 'select',
});
}
}}
>
{confirmText || locale.confirmText}
</Button>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

确认按钮未考虑组级 disabled,禁用整个 Choice 时仍可点击“确认”

disabled={selectedCount === 0} 只判断了是否有选中项,未结合组件级 disabled prop。当 disabled=true(例如结合受控 valuedefaultValue 预置了选中项)时,确认按钮依然可点击并触发 onConfirm,与组件整体禁用语义相悖。

🐛 建议修复
-            disabled={selectedCount === 0}
+            disabled={disabled || selectedCount === 0}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{confirmable && (
<Button
type="primary"
disabled={selectedCount === 0}
onClick={() => {
if (onConfirm) {
const val = mode === 'single' ? mergedValue[0] : mergedValue;
onConfirm(val, {
value: val,
items,
changedItems: [],
type: 'select',
});
}
}}
>
{confirmText || locale.confirmText}
</Button>
)}
{confirmable && (
<Button
type="primary"
disabled={disabled || selectedCount === 0}
onClick={() => {
if (onConfirm) {
const val = mode === 'single' ? mergedValue[0] : mergedValue;
onConfirm(val, {
value: val,
items,
changedItems: [],
type: 'select',
});
}
}}
>
{confirmText || locale.confirmText}
</Button>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/index.tsx` around lines 163 - 181, Update the
confirm Button in the confirmable Choice block to include the component-level
disabled prop in its disabled condition, so it remains disabled whenever Choice
is disabled or selectedCount is zero. Preserve the existing onConfirm behavior
for enabled choices.

Comment on lines +190 to +195
<div
{...htmlProps}
ref={composeRef(containerRef, motionRef)}
className={clsx(mergedCls, motionClassName)}
style={{ ...style, ...contextConfig.style, ...styles.root }}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

根容器 style 合并顺序与组件内其它位置不一致,contextConfig.style 会覆盖用户显式传入的 style

对比同文件中 header(Line 123)、description(Line 141)、list(Line 203)、footer(Line 159)的合并顺序均为 {...contextConfig.styles.x, ...styles.x}(组件级配置最终生效),但根节点却是 {...style, ...contextConfig.style, ...styles.root},即 contextConfig.style 排在用户显式 style 之后,会反过来覆盖用户传入的行内样式,与其它位置的优先级约定相反。

🐛 建议修复
-            style={{ ...style, ...contextConfig.style, ...styles.root }}
+            style={{ ...contextConfig.style, ...style, ...styles.root }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
{...htmlProps}
ref={composeRef(containerRef, motionRef)}
className={clsx(mergedCls, motionClassName)}
style={{ ...style, ...contextConfig.style, ...styles.root }}
>
<div
{...htmlProps}
ref={composeRef(containerRef, motionRef)}
className={clsx(mergedCls, motionClassName)}
style={{ ...contextConfig.style, ...style, ...styles.root }}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/index.tsx` around lines 190 - 195, 调整根容器的 style
合并顺序,定位到根节点的 style 属性:先合并 contextConfig.style,再合并用户传入的 style,最后保留
styles.root,以确保用户显式样式不会被 contextConfig.style 覆盖,并与组件其它区域的优先级约定一致。

Comment on lines +200 to +205
<div
ref={listRef}
className={mergedListCls}
style={{ ...contextConfig.styles.list, ...styles.list }}
data-role={mode === 'single' ? 'radiogroup' : 'group'}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== outline: packages/x/components/choice/index.tsx =="
ast-grep outline packages/x/components/choice/index.tsx --view expanded || true

echo
echo "== outline: packages/x/components/choice/Item.tsx =="
ast-grep outline packages/x/components/choice/Item.tsx --view expanded || true

echo
echo "== relevant snippets =="
sed -n '1,260p' packages/x/components/choice/index.tsx | nl -ba | sed -n '160,230p'
echo
sed -n '1,220p' packages/x/components/choice/Item.tsx | nl -ba | sed -n '1,180p'

echo
echo "== search for ARIA / role usage in choice components =="
rg -n --no-heading "aria-|role=|data-role|data-selected|data-disabled" packages/x/components/choice

Repository: ant-design/x

Length of output: 733


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/x/components/choice/index.tsx (around list container) =="
sed -n '170,220p' packages/x/components/choice/index.tsx | awk '{printf "%4d  %s\n", NR+169, $0}'

echo
echo "== packages/x/components/choice/Item.tsx (entire file) =="
sed -n '1,220p' packages/x/components/choice/Item.tsx | awk '{printf "%4d  %s\n", NR, $0}'

echo
echo "== search for aria/role usage in related choice files =="
rg -n --no-heading "aria-|role=|data-role|data-selected|data-disabled" packages/x/components/choice

Repository: ant-design/x

Length of output: 41407


补上 Choice 的真实 ARIA 语义

data-role 只适合测试/样式,当前两个交互节点都没有真实 role/aria-*,辅助技术无法识别分组、单选/复选状态和禁用状态。保留 data-* 的同时,在 packages/x/components/choice/index.tsx 的列表容器加 role="radiogroup"/group",在 packages/x/components/choice/Item.tsx 的条目加 role="radio"/checkbox"aria-checkedaria-disabled

📍 Affects 2 files
  • packages/x/components/choice/index.tsx#L200-L205 (this comment)
  • packages/x/components/choice/Item.tsx#L74-L93
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/index.tsx` around lines 200 - 205, 补充 Choice 的真实
ARIA 语义:在 packages/x/components/choice/index.tsx 的列表容器保留现有 data-role,并根据 mode 添加
role="radiogroup" 或 role="group";在 packages/x/components/choice/Item.tsx 的条目根据
mode 添加 role="radio" 或 "checkbox",同时设置反映选中状态的 aria-checked 和禁用状态的 aria-disabled。

Comment on lines +227 to +241
{items.map((item, index) => {
return (
<div key={item.key} data-key={item.key}>
<ChoiceItem item={item} index={index} />
{/* Nested children */}
{item.children && item.children.length > 0 && (
<div className={`${prefixCls}-nested`}>
{item.children.map((child, childIndex) => (
<ChoiceItem key={child.key} item={child} index={childIndex} />
))}
</div>
)}
</div>
);
})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

嵌套子选项缺少 data-key 容器,导致 ChoiceRef.scrollTo 对子选项静默失效

顶层选项用 <div key={item.key} data-key={item.key}> 包裹,scrollTo(Line 82-85)通过 listRef.current?.querySelector('[data-key="${key}"]') 定位。但 item.children 的渲染(Line 234-236)直接返回 <ChoiceItem key={child.key} item={child} index={childIndex} />,没有任何带 data-key 的包裹元素。调用 choiceRef.current?.scrollTo(childKey)querySelector 找不到对应节点,itemElundefined,因可选链而静默无效,与 Issue 中“ChoiceRef,用于滚动到指定选项”的验收标准不符。

🐛 建议修复
                         {item.children.map((child, childIndex) => (
-                          <ChoiceItem key={child.key} item={child} index={childIndex} />
+                          <div key={child.key} data-key={child.key}>
+                            <ChoiceItem item={child} index={childIndex} />
+                          </div>
                         ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{items.map((item, index) => {
return (
<div key={item.key} data-key={item.key}>
<ChoiceItem item={item} index={index} />
{/* Nested children */}
{item.children && item.children.length > 0 && (
<div className={`${prefixCls}-nested`}>
{item.children.map((child, childIndex) => (
<ChoiceItem key={child.key} item={child} index={childIndex} />
))}
</div>
)}
</div>
);
})}
{items.map((item, index) => {
return (
<div key={item.key} data-key={item.key}>
<ChoiceItem item={item} index={index} />
{/* Nested children */}
{item.children && item.children.length > 0 && (
<div className={`${prefixCls}-nested`}>
{item.children.map((child, childIndex) => (
<div key={child.key} data-key={child.key}>
<ChoiceItem item={child} index={childIndex} />
</div>
))}
</div>
)}
</div>
);
})}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/index.tsx` around lines 227 - 241, 在组件渲染逻辑中为
item.children.map 返回的每个子选项添加与顶层选项一致的 data-key 容器,并使用 child.key 作为该属性值,使
ChoiceRef.scrollTo 能通过选择器定位子选项;保持现有 ChoiceItem 的 item 和 childIndex 传递不变。

Comment on lines +126 to +140
{/* Disabled reason (inline) */}
{disabled && item.disabledReason && (
<div className={`${prefixCls}-item-disabled-reason`}>{item.disabledReason}</div>
)}
</div>
);

// Wrap with Tooltip for disabled reason on hover
if (disabled && item.disabledReason) {
return (
<Tooltip title={item.disabledReason} placement="top">
{itemNode}
</Tooltip>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

禁用原因被重复渲染:卡片内常驻文案 + Tooltip 悬浮文案内容完全相同

Line 127-129 在 disabled && item.disabledReason 时始终内联渲染原因文本;Line 134-140 又用 Tooltip 包裹整个 itemNodetitle 同样是 item.disabledReason。按提交历史描述,Tooltip 是后续为满足验收标准补充的能力,但原有的常驻内联文案没有被替换/移除,导致同一段文案在卡片上永久可见,鼠标悬浮时又在浮层里再次出现,属于重复展示,与“禁用原因提示”应为 hover 提示的设计意图冲突。

🐛 建议修复(二选一,示例保留 Tooltip 方案)
-      {/* Disabled reason (inline) */}
-      {disabled && item.disabledReason && (
-        <div className={`${prefixCls}-item-disabled-reason`}>{item.disabledReason}</div>
-      )}
     </div>
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Disabled reason (inline) */}
{disabled && item.disabledReason && (
<div className={`${prefixCls}-item-disabled-reason`}>{item.disabledReason}</div>
)}
</div>
);
// Wrap with Tooltip for disabled reason on hover
if (disabled && item.disabledReason) {
return (
<Tooltip title={item.disabledReason} placement="top">
{itemNode}
</Tooltip>
);
}
</div>
);
// Wrap with Tooltip for disabled reason on hover
if (disabled && item.disabledReason) {
return (
<Tooltip title={item.disabledReason} placement="top">
{itemNode}
</Tooltip>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/Item.tsx` around lines 126 - 140, Remove the
always-visible disabled-reason block from the item content rendered by the
component, while preserving the Tooltip wrapper around itemNode when disabled
and item.disabledReason are present. Keep item.disabledReason as the Tooltip
title so the reason is shown only on hover.

Comment on lines +113 to +117
/**
* @desc 整体圆角
* @descEN Container border radius
*/
borderRadius?: number;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

borderRadius(整体圆角)token 定义了但从未在样式中使用。

ComponentToken.borderRadius(113-117 行)、ChoiceToken.borderRadius(150-152 行)以及 prepareComponentToken 中的默认值(426-429 行)都声明了该 token,但在 genChoiceStyle 的根选择器(161-170 行)及全文中都没有任何地方消费 token.borderRadius。作为对外暴露的可配置 Design Token,用户通过 ConfigProvider 自定义该值不会产生任何视觉效果,属于一个"空转"的公开配置契约。

♻️ 建议修复:在根容器应用该圆角
     [componentCls]: {
       '&, & *': {
         boxSizing: 'border-box',
       },

       maxWidth: '100%',
+      borderRadius: unit(token.borderRadius),

Also applies to: 150-152, 161-170, 426-429

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/style/index.ts` around lines 113 - 117, 在
genChoiceStyle 的根选择器中应用 token.borderRadius 作为容器的 border-radius,使 ConfigProvider
自定义的整体圆角生效;保留 ComponentToken、ChoiceToken 的声明及 prepareComponentToken
中默认值,并确保引用与现有 token 样式生成方式一致。

Comment on lines +183 to +184
fontSize: unit(token.titleFontSize),
lineHeight: unit(token.titleLineHeight),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@ant-design/cssinjs unit function source code implementation number to px string

💡 Result:

The unit utility function in @ant-design/cssinjs is a helper designed to ensure that numeric values are correctly formatted as CSS pixel strings while allowing non-numeric values to pass through unchanged [1]. Implementation Details: The unit function specifically appends 'px' to input values that are numbers, with the notable exception of 0, which is returned as is [1]. For inputs that are already strings (such as CSS variables like var(--token) or existing units like 1.5rem), the function returns the value without modification [1]. This utility is primarily used within style functions to handle design tokens that might be passed as either raw numbers or CSS variable strings [1][2]. Example Usage: import { unit } from '@ant-design/cssinjs'; unit(16); // Returns '16px' unit(0); // Returns 0 unit('var(--my-spacing)'); // Returns 'var(--my-spacing)' unit('1.5rem'); // Returns '1.5rem' This approach ensures that style definitions remain flexible, accommodating both static numeric values and dynamic CSS variables injected via the design token system [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline packages/x/components/choice/style/index.ts --view expanded || true

echo
echo "== relevant lines =="
sed -n '160,340p' packages/x/components/choice/style/index.ts | cat -n

echo
echo "== search for unit() in this file =="
rg -n "unit\\(" packages/x/components/choice/style/index.ts

echo
echo "== search for lineHeight token definitions/usages =="
rg -n "titleLineHeight|labelLineHeight|descLineHeight|lineHeightLG|lineHeight[^A-Za-z]" packages/x/components -g '!**/dist/**' -g '!**/build/**'

Repository: ant-design/x

Length of output: 17276


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== open package file if present =="
git ls-files | rg '^packages/x/.*/token|^packages/x/.*/style|theme|token' || true

echo
echo "== locate unit import source usage =="
rg -n "from '`@ant-design/cssinjs`'|unit" packages/x -g '*.ts' -g '*.tsx' | head -n 200

Repository: ant-design/x

Length of output: 20883


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== choice prepareComponentToken =="
sed -n '400,430p' packages/x/components/choice/style/index.ts | cat -n

echo
echo "== font token map =="
sed -n '1,130p' packages/x/components/theme/interface/maps/font.ts | cat -n

echo
echo "== unitless token config =="
sed -n '1,60p' packages/x/components/theme/useToken.ts | cat -n

Repository: ant-design/x

Length of output: 6776


行高不要用 unit() 包裹

titleLineHeight / labelLineHeight / descLineHeight 是无单位的行高倍数(token.lineHeighttoken.lineHeightLG),这里用了 unit() 之后会变成 1.5715px 这类值,导致标题/标签/描述的行高异常偏小,文本容易重叠或裁切。
把这几处都改回直接使用 token 值即可。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/style/index.ts` around lines 183 - 184, Remove
the unit() wrapper from line-height assignments for titleLineHeight,
labelLineHeight, and descLineHeight, using each token’s raw unitless value
directly. Keep unit() for font-size values unchanged.

Comment on lines +205 to +217
[`${componentCls}-layout-grid`]: {
display: 'grid',
gridTemplateColumns: `repeat(${token.gridColumns}, 1fr)`,
gap: unit(token.itemGap),
},

// Card layout
[`${componentCls}-layout-card`]: {
display: 'grid',
gridTemplateColumns: `repeat(${token.gridColumns}, 1fr)`,
gap: unit(token.itemGap),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

关键 bug:${componentCls}-layout-card 键重复声明,导致网格布局样式被整体覆盖。

第 212-216 行与第 381-400 行在同一个对象字面量中两次定义了计算属性键 ${componentCls}-layout-card。JavaScript 对象字面量中同名 key 不会合并,后声明的值会完全替换前一个,因此第 381-400 行的定义会把第 212-216 行的 display: 'grid'gridTemplateColumnsgap 全部抹除,最终 -layout-card 只保留内部 -item 相关样式,容器本身根本不会以 grid 方式排列。card 布局将无法正常渲染。

🐛 建议修复:合并成一个块
       // Card layout
       [`${componentCls}-layout-card`]: {
         display: 'grid',
         gridTemplateColumns: `repeat(${token.gridColumns}, 1fr)`,
         gap: unit(token.itemGap),
+
+        [`${componentCls}-item`]: {
+          flexDirection: 'column',
+          alignItems: 'stretch',
+          minHeight: 'auto',
+          padding: token.paddingMD,
+
+          [`${componentCls}-indicator`]: {
+            position: 'absolute',
+            top: token.paddingSM,
+            insetInlineStart: token.paddingSM,
+          },
+
+          [`${componentCls}-item-content`]: {
+            alignItems: 'center',
+            textAlign: 'center',
+            marginTop: token.paddingSM,
+          },
+        },
       },

并删除文件末尾第 380-401 行的重复块。

Also applies to: 380-401

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/style/index.ts` around lines 205 - 217, 合并
`${componentCls}-layout-card` 的重复样式定义:保留前面用于容器网格布局的 display、gridTemplateColumns
和 gap,并将末尾重复块中的必要 `-item` 样式合并到同一个对象中;删除文件末尾重复声明,确保该计算属性键只定义一次。

Comment on lines +350 to +357
// ======================== Nested ========================
[`${componentCls}-nested`]: {
marginLeft: token.controlHeightSM,
marginTop: token.paddingXS,
display: 'flex',
flexDirection: 'column',
gap: token.paddingXS,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

RTL 缺陷:嵌套子选项使用物理属性 marginLeft

文件其余位置(如 328、372、391 行)都统一使用逻辑属性(insetInlineEnd/insetInlineStart/marginInlineEnd)以配合第 168-170 行的 &${componentCls}-rtl { direction: 'rtl' },但 -nested 区块用的是物理属性 marginLeft,在 RTL 场景下不会镜像翻转,导致嵌套子选项的缩进方向在 RTL 布局下出错。PR/Issue 明确要求 RTL 支持,这里与既有约定不一致。

🐛 建议修复
       [`${componentCls}-nested`]: {
-        marginLeft: token.controlHeightSM,
+        marginInlineStart: token.controlHeightSM,
         marginTop: token.paddingXS,
         display: 'flex',
         flexDirection: 'column',
         gap: token.paddingXS,
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ======================== Nested ========================
[`${componentCls}-nested`]: {
marginLeft: token.controlHeightSM,
marginTop: token.paddingXS,
display: 'flex',
flexDirection: 'column',
gap: token.paddingXS,
},
// ======================== Nested ========================
[`${componentCls}-nested`]: {
marginInlineStart: token.controlHeightSM,
marginTop: token.paddingXS,
display: 'flex',
flexDirection: 'column',
gap: token.paddingXS,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x/components/choice/style/index.ts` around lines 350 - 357, Update
the `${componentCls}-nested` style block to replace the physical `marginLeft`
property with the appropriate logical margin property, preserving the existing
indentation amount and ensuring it mirrors correctly under the
`${componentCls}-rtl` direction.

@kimteayon kimteayon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

主要问题

  1. [P1] 默认行高被错误转换为像素值
    [style/index.ts:184](

    lineHeight: unit(token.titleLineHeight),
    )
    lineHeightLG 默认是无单位的 1.5,经 unit() 后变为 1.5px;正文同样变为约 1.57px。标题、标签和描述的多行内容会严重重叠。应直接使用 token,不要调用 unit()。同类问题还在 190、311、317 行。

  2. [P1] 选择组件没有实际的可访问性语义
    [Item.tsx:78](

    data-role={mode === 'single' ? 'radio' : 'checkbox'}
    )
    data-roledata-selecteddata-disabled 对辅助技术没有作用;列表也使用了 data-role="radiogroup"。屏幕阅读器无法识别选项、选中状态或禁用状态。需要真正的 rolearia-checkedaria-disabled,并处理 radio group 的焦点和方向键交互。

  3. [P2] 单选模式可以同时显示多个选中项
    [use-select.ts:27](

    )
    类型和文档允许 value={['1', '2']},但单选模式没有截断数组,includes() 会让两个 radio 同时选中;getValue()onConfirm 却只返回第一个值。应使用模式判别类型,或在 single 模式只保留一个值。

  4. [P2] 禁用原因 Tooltip 无法触发
    [style/index.ts:252](

    pointerEvents: 'none',
    )
    Tooltip 直接克隆禁用项作为触发节点,但该节点设置了 pointer-events: none,无法接收 hover/focus 事件。应增加可交互包装节点,或调整禁用样式的位置。

  5. [P2] scrollTo 不支持嵌套选项
    [index.tsx:235](

    <ChoiceItem key={child.key} item={child} index={childIndex} />
    )
    只有顶层包装元素包含 data-key,嵌套 ChoiceItem 没有,因此 scrollTo(childKey) 永远静默失败。

  6. [P2] 合法字符串 key 可能使 scrollTo 抛异常
    [index.tsx:83](

    const itemEl = listRef.current?.querySelector(`[${'data-key'}="${key}"]`);
    )
    key 被直接拼入 CSS selector。包含 "、反斜杠等字符时,querySelector 会抛 DOMException。建议维护 key 到元素的 ref map,避免构造选择器。

  7. [P2] 推荐文案绕过了新增的 locale
    [Item.tsx:115](

    {item.recommendedReason || 'AI Recommended'}
    )
    默认文案硬编码为 AI Recommended / Recommended,切换中文 locale 后仍显示英文。PR 新增的 locale.Choice.recommendedmaxSelecteddisabledHint 均未实际使用。

  8. [P2] 合法的 falsy ReactNode 不会渲染
    [Item.tsx:105](

    {item.label && <div className={`${prefixCls}-item-label`}>{item.label}</div>}
    )
    label 类型是 ReactNode,但使用 truthy 判断,因此 label={0} 会消失。descriptionextra、标题和 footer 也存在相同问题,应使用 node != null

验证结果:Choice 全量测试 71/71 通过、34 个快照通过,TypeScript、Biome 和 git diff --check 均通过。现有测试没有覆盖上述状态和真实 ARIA 属性。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OSS26-Team] 新组件:Choice 选择交互

2 participants