fix: option position announcement for virtual mode#1226
Conversation
|
@Pareder is attempting to deploy a commit to the React Component Team on Vercel. A member of the Team first needs to authorize it. |
Walkthrough该 PR 调整了 Select 虚拟列表模式下的无障碍通报方式,新增选项位置序号计算与 Changes虚拟列表无障碍属性与分组渲染
🎯 3 (Moderate) | ⏱️ ~25 分钟 相关 PR
建议审查人
兔子之诗 🐰
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request improves accessibility in virtualized select lists by adding aria-setsize and aria-posinset attributes to options (skipping group headers) and nesting options inside role="group" wrappers to match native <select> announcements. Corresponding unit and snapshot tests have been added or updated. Feedback is provided regarding a performance issue in src/OptionList.tsx, where calculating option counts and positions on every render performs O(N) operations; it is recommended to precompute these values using React.useMemo to achieve O(1) lookups and prevent lag in large lists.
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.
| // Skip group headers to match native <select> announcements with <optgroup> | ||
| const optionCount = memoFlattenOptions.filter((option) => !option.group).length; | ||
|
|
||
| const getOptionPosition = (index: number) => | ||
| memoFlattenOptions.slice(0, index + 1).filter((option) => !option.group).length; |
There was a problem hiding this comment.
Calculating optionCount and getOptionPosition on every render performs array filtering and slicing operations ($O(N)$) over the entire memoFlattenOptions list. Since getOptionPosition is called up to three times per render (for activeIndex - 1, activeIndex, and activeIndex + 1), and renders occur on every keyboard navigation step (as activeIndex changes), this can cause noticeable lag/performance degradation in large virtualized lists.
Since memoFlattenOptions is already memoized, we can precompute the option count and positions map in a single React.useMemo. This reduces the lookup in getOptionPosition to
// Skip group headers to match native <select> announcements with <optgroup>
const [optionCount, optionPositions] = React.useMemo(() => {
let count = 0;
const positions = new Array(memoFlattenOptions.length);
for (let i = 0; i < memoFlattenOptions.length; i += 1) {
if (!memoFlattenOptions[i].group) {
count += 1;
}
positions[i] = count;
}
return [count, positions];
}, [memoFlattenOptions]);
const getOptionPosition = (index: number) => optionPositions[index] || 0;
There was a problem hiding this comment.
@afc163 Done. By the way it won't be a problem when using React Compiler.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1226 +/- ##
==========================================
+ Coverage 99.44% 99.45% +0.01%
==========================================
Files 31 31
Lines 1271 1295 +24
Branches 466 470 +4
==========================================
+ Hits 1264 1288 +24
Misses 7 7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/Accessibility.test.tsx (1)
266-303: ⚡ Quick win建议补一条“group wrapper 不应带
aria-posinset/aria-setsize”的显式断言。当前测试已验证
option上有位置属性,但还没直接锁定role="group"容器不应携带这些属性。补上后可更稳地覆盖“仅 option 有位置语义”的回归面。✅ 可追加断言示例
groupWrappers = getGroupWrappers(); expect(groupWrappers).toHaveLength(1); expect(groupWrappers[0]).toHaveAttribute('aria-label', 'Second group'); + groupWrappers.forEach((group) => { + expect(group).not.toHaveAttribute('aria-posinset'); + expect(group).not.toHaveAttribute('aria-setsize'); + });🤖 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 `@tests/Accessibility.test.tsx` around lines 266 - 303, Add explicit assertions to ensure the group wrapper elements returned by getGroupWrappers() do not carry positional attributes; after fetching groupWrappers (e.g., the variable groupWrappers used in the test) assert that groupWrappers[0] (and any group wrapper in the array) does not have aria-posinset and does not have aria-setsize, similar to how hiddenOptions (from getHiddenOptions()) are asserted for those attributes so the test guarantees only option elements expose position semantics.
🤖 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 `@src/OptionList.tsx`:
- Around line 304-306: The aria-label currently only sets when mergedLabel is a
string, which drops semantic labels for numeric titles; update the condition to
reuse the existing isTitleType check and coerce the title to string (e.g.,
aria-label={isTitleType(mergedLabel) ? String(mergedLabel) : null}) so numeric
labels are exposed to assistive tech; apply the same change to the other
occurrence that sets aria-label (the second block using
mergedLabel/getOptionPosition).
---
Nitpick comments:
In `@tests/Accessibility.test.tsx`:
- Around line 266-303: Add explicit assertions to ensure the group wrapper
elements returned by getGroupWrappers() do not carry positional attributes;
after fetching groupWrappers (e.g., the variable groupWrappers used in the test)
assert that groupWrappers[0] (and any group wrapper in the array) does not have
aria-posinset and does not have aria-setsize, similar to how hiddenOptions (from
getHiddenOptions()) are asserted for those attributes so the test guarantees
only option elements expose position semantics.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a0427256-7a22-48f6-b5ce-5248b088f57f
⛔ Files ignored due to path filters (3)
tests/__snapshots__/OptionList.test.tsx.snapis excluded by!**/*.snaptests/__snapshots__/Select.test.tsx.snapis excluded by!**/*.snaptests/__snapshots__/Tags.test.tsx.snapis excluded by!**/*.snap
📒 Files selected for processing (2)
src/OptionList.tsxtests/Accessibility.test.tsx
|
@afc163 @zombieJ @meet-student @QDyanbing Could you please review? |
|
@afc163 Could you please take a look? |
|
Thanks for the update. The current direction looks good to me: adding One small edge case before merging: Please also check the Codecov failures, or leave a note if they are expected/unrelated. The Vercel failure looks like the usual authorization issue and is not a code signal. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/Accessibility.test.tsx (1)
307-318: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win显式开启
virtual,避免用例依赖默认值。这里验证的是虚拟模式下隐藏 a11y 容器的分段渲染,但
virtual没有显式传入;如果默认值以后调整,这个用例就不再稳定地覆盖这条路径了。Line 307-318 建议把场景条件写死。♻️ 建议修改
const { container } = render( <Select id="virtual-select" open + virtual options={[🤖 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 `@tests/Accessibility.test.tsx` around lines 307 - 318, The Select accessibility test currently relies on the component’s default virtual behavior, which makes the scenario brittle if the default changes. Update the Select usage in the virtual a11y case to explicitly pass virtual so the test always exercises the hidden accessibility container path. Keep the change local to the test case around the Select and its grouped options setup.
🤖 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.
Nitpick comments:
In `@tests/Accessibility.test.tsx`:
- Around line 307-318: The Select accessibility test currently relies on the
component’s default virtual behavior, which makes the scenario brittle if the
default changes. Update the Select usage in the virtual a11y case to explicitly
pass virtual so the test always exercises the hidden accessibility container
path. Keep the change local to the test case around the Select and its grouped
options setup.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 47d4a3a6-f7ca-445e-b0fb-bbcc10862375
📒 Files selected for processing (2)
src/OptionList.tsxtests/Accessibility.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/OptionList.tsx
|
@afc163 Done. Fixed value for labels and adjusted code coverage. |
Problem
With
virtualmode enabled, the hidden accessibility container only renders 3 options around the active one (activeIndex ± 1). Screen readers computed option positions from the DOM, so users always heard "1 of 3" / "2 of 3" / "3 of 3" regardless of the actual option position and list size.Fix
aria-posinset/aria-setsizeto the hidden options so screen readers announce the real position and total (e.g. "4 of 6").<select>+<optgroup>announcements.role="presentation"group headers in the hidden container withrole="group"+aria-labelwrappers nesting their options, so screen readers also announce the group name when the active option enters a new group (MDN example).Tests
role="option"elements only.Related issues
Fixes ant-design/ant-design#58346
Summary by CodeRabbit
发布说明
Bug Fixes
aria通报逻辑,新增/完善aria-setsize与aria-posinset,并在分组场景中跳过组标题以保持连续的实际选项序号。Tests