Skip to content

Comments

fix: add aria-disabled attribute for disabled options in Select component#1212

Merged
afc163 merged 2 commits intoreact-component:masterfrom
meet-student:patch
Feb 24, 2026
Merged

fix: add aria-disabled attribute for disabled options in Select component#1212
afc163 merged 2 commits intoreact-component:masterfrom
meet-student:patch

Conversation

@meet-student
Copy link
Contributor

@meet-student meet-student commented Feb 24, 2026

ref ant-design/ant-design#57036

Summary by CodeRabbit

发版说明

  • 辅助功能改进

    • 为禁用的选项添加了 ARIA 标签支持,使屏幕阅读器等辅助技术能够正确识别禁用状态,改善了无障碍用户体验。
  • 测试

    • 新增辅助功能测试,验证禁用选项的 ARIA 标签行为。

@vercel
Copy link

vercel bot commented Feb 24, 2026

@meet-student is attempting to deploy a commit to the React Component Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @meet-student, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the accessibility of the Select component by ensuring that disabled options are properly identified with the aria-disabled attribute. This change improves the experience for users relying on assistive technologies, allowing them to accurately perceive the interactive state of select options.

Highlights

  • Accessibility Enhancement: The aria-disabled attribute is now correctly applied to disabled options within the Select component, improving compatibility with assistive technologies.
  • New Test Case: A dedicated accessibility test was introduced to validate the presence and correctness of the aria-disabled attribute on select options.
  • Snapshot Updates: Existing snapshot tests were updated to reflect the addition of the aria-disabled attribute to option elements.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/OptionList.tsx
    • Extracted the disabled property from item data to be used for ARIA attributes.
    • Applied the aria-disabled attribute to option elements based on their disabled state.
  • tests/Accessibility.test.tsx
    • Added a new test case to verify that the aria-disabled attribute is correctly set for disabled options and not set for enabled options.
  • tests/snapshots/OptionList.test.tsx.snap
    • Updated snapshots to include the aria-disabled attribute for option elements.
  • tests/snapshots/Select.test.tsx.snap
    • Updated snapshots to include the aria-disabled attribute for option elements.
  • tests/snapshots/Tags.test.tsx.snap
    • Updated snapshots to include the aria-disabled attribute for option elements.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

总体说明

在OptionList组件中添加了aria-disabled属性以增强无障碍支持,对禁用的选项项进行了正确的ARIA标记,并新增了相应的可访问性测试用例。

变更一览表

分组/文件 总结
无障碍属性增强
src/OptionList.tsx
在选项项渲染路径中提取禁用状态并应用aria-disabled属性;在列表项渲染路径中传递mergedDisabled状态的aria-disabled属性,确保禁用状态对辅助技术正确暴露。
可访问性测试
tests/Accessibility.test.tsx
新增测试用例验证禁用选项的ARIA行为,确保仅禁用的选项项设置aria-disabled="true"属性,而启用的选项项不包含该属性。

代码审查工作量估计

🎯 2 (简单) | ⏱️ ~10 分钟

建议审查者

  • zombieJ

庆祝诗

🐰 无障碍的路途更畅通,
禁用的状态有了言语,
aria标记闪闪发亮,
屏幕阅读器也能唱歌,
每一个用户都被看见!✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确地反映了此拉取请求的主要变更,即为Select组件中的禁用选项添加aria-disabled属性。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.43%. Comparing base (2cc7ab3) to head (46d5078).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1212   +/-   ##
=======================================
  Coverage   99.43%   99.43%           
=======================================
  Files          31       31           
  Lines        1236     1236           
  Branches      446      446           
=======================================
  Hits         1229     1229           
  Misses          7        7           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

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

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 correctly adds the aria-disabled attribute to disabled options in the Select component, improving accessibility. However, a high-severity Cross-Site Scripting (XSS) vulnerability was identified in the OptionList component, allowing injection of arbitrary HTML attributes like dangerouslySetInnerHTML via the options prop, which could lead to malicious script execution. While this PR does not cause or mitigate this issue, a remediation recommendation is provided in the review comments. Additionally, my review includes suggestions to improve code consistency and the robustness of the new test.

key={index}
{...getItemAriaProps(item, index)}
aria-selected={isAriaSelected(value)}
aria-disabled={disabled}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For consistency with how aria-disabled is handled elsewhere in this component (e.g., on line 405 with mergedDisabled), it would be better to ensure a boolean is always passed here. When disabled is undefined, the attribute is currently omitted, whereas in other cases it becomes aria-disabled="false". Using !!disabled would make the behavior consistent across the component for enabled options.

        aria-disabled={!!disabled}

expect(input).toHaveAttribute('aria-activedescendant', 'select_list_0');
});

// https://github.com/ant-design/ant-design/issues/xxxxx
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Please replace xxxxx with the actual GitHub issue number that this change is fixing. This helps with code maintenance and tracking the context of this fix.

Comment on lines +339 to +342
const optionA = Array.from(optionItems).find((el) => el.textContent === 'a');
const optionB = Array.from(optionItems).find((el) => el.textContent === 'b');
expect(optionA).not.toHaveAttribute('aria-disabled', 'true');
expect(optionB).toHaveAttribute('aria-disabled', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

These assertions are good, but could be more robust. Consider these improvements:

  • Assert that optionA and optionB are found before checking their attributes to avoid potential test failures if the elements aren't found (e.g., expect(optionA).not.toBeNull()).
  • Make the assertion for optionA more specific. Instead of checking that it's not true, you could assert that the attribute is not present at all: expect(optionA).not.toHaveAttribute('aria-disabled');.
  • Add assertions for optionC as well to ensure all non-disabled options in your test data are handled correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/OptionList.tsx (1)

298-309: ⚠️ Potential issue | 🟠 Major

renderItem 使用原始 disabled 而非 mergedDisabled,导致 overMaxCount 场景下无障碍状态不一致

virtual={true}(默认)时,屏幕阅读器通过隐藏 ARIA div 中的 renderItem 输出感知选项状态。当前 aria-disabled={disabled} 仅反映数据层面的 disabled 字段,而可视列表(第 405 行)使用 mergedDisabled = disabled || (!selected && overMaxCount)

当用户在多选模式下达到 maxCount 上限时,未被选中的选项会因 overMaxCount 而被视觉禁用(mergedDisabled = true),但隐藏 ARIA div 中的对应节点 aria-disabled 仍为 undefined/false,导致辅助技术无法正确感知选项的禁用状态,屏幕阅读器用户将获得与视觉/交互行为相矛盾的信息。

🛡️ 建议修复:在 renderItem 中计算 mergedDisabled
 const renderItem = (index: number) => {
   const item = memoFlattenOptions[index];
   if (!item) {
     return null;
   }
   const itemData = item.data || {};
   const { value, disabled } = itemData;
   const { group } = item;
   const attrs = pickAttrs(itemData, true);
   const mergedLabel = getLabel(item);
+  const selected = isSelected(value);
+  const mergedDisabled = disabled || (!selected && overMaxCount);
   return item ? (
     <div
       aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
       {...attrs}
       key={index}
       {...getItemAriaProps(item, index)}
       aria-selected={isAriaSelected(value)}
-      aria-disabled={disabled}
+      aria-disabled={mergedDisabled}
     >
       {value}
     </div>
   ) : null;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OptionList.tsx` around lines 298 - 309, The hidden ARIA output in
renderItem uses the raw disabled value instead of the visual mergedDisabled,
causing mismatch when overMaxCount disables unselected items; update renderItem
to compute mergedDisabled = disabled || (!selected && overMaxCount) (same logic
used in the visual list) and use mergedDisabled for aria-disabled (replace
aria-disabled={disabled} with aria-disabled={mergedDisabled}) so screen readers
reflect the actual disabled state for the option; ensure you reference
itemData.disabled, item.selected (or selected variable) and overMaxCount when
computing mergedDisabled and keep isAriaSelected(value) unchanged.
🧹 Nitpick comments (1)
tests/Accessibility.test.tsx (1)

326-343: 缺少 virtual={false} 路径的覆盖

当前测试仅覆盖 virtual={true}(默认值)下隐藏 ARIA div 的 renderItem 路径(对应 src/OptionList.tsx 第 309 行的变更)。src/OptionList.tsx 第 405 行新增的 aria-disabled={mergedDisabled} 作用于 virtual={false} 时的可见列表项(此时列表项具有 role="option"),但此路径缺少对应的测试用例。

♻️ 建议补充 virtual=false 路径的测试
+    it('aria-disabled should be set on disabled options (virtual=false)', () => {
+      const { container } = render(
+        <Select
+          id="non-virtual-select"
+          open
+          virtual={false}
+          options={[
+            { label: 'Option A', value: 'a' },
+            { label: 'Option B', value: 'b', disabled: true },
+            { label: 'Option C', value: 'c' },
+          ]}
+        />,
+      );
+
+      const dropdown = document.querySelector('#non-virtual-select_list');
+      const optionItems = dropdown!.querySelectorAll('[role="option"]');
+      const optionA = Array.from(optionItems).find(
+        (el) => el.querySelector('.rc-select-item-option-content')?.textContent === 'Option A',
+      );
+      const optionB = Array.from(optionItems).find(
+        (el) => el.querySelector('.rc-select-item-option-content')?.textContent === 'Option B',
+      );
+      expect(optionA).toBeDefined();
+      expect(optionB).toBeDefined();
+      expect(optionA).not.toHaveAttribute('aria-disabled', 'true');
+      expect(optionB).toHaveAttribute('aria-disabled', 'true');
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Accessibility.test.tsx` around lines 326 - 343, Add a test that covers
the virtual={false} rendering path so the visible option elements receive the
new aria-disabled attribute: render <Select open virtual={false}
options=[{label:'Option A',value:'a'},{label:'Option
B',value:'b',disabled:true},{label:'Option C',value:'c'}] />, query for elements
with role="option" (same approach as the existing test), locate the option whose
text matches 'Option B' and assert it has aria-disabled="true" while 'Option A'
does not; this ensures the OptionList.tsx mergedDisabled behavior for the
non-virtual branch is tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/Accessibility.test.tsx`:
- Line 325: Replace the placeholder comment "//
https://github.com/ant-design/ant-design/issues/xxxxx" in Accessibility.test.tsx
with the actual GitHub issue URL referenced by other tests (or remove the
comment entirely); locate the placeholder string in the file and either update
it to the correct issue link used by the related test case or delete the line so
no placeholder remains.
- Around line 338-342: The test is querying options from the test container but
the Select renders its popup via a Portal with an auto-generated id, so
container.querySelectorAll('[role="option"]') returns undefined; update the test
to pass a stable id prop to the Select (e.g., id="virtual-select"), then query
the portal using document.querySelector(`#${'virtual-select'}_list`) (or
document.querySelector('#virtual-select_list')) to get optionItems, and then
find optionA/optionB from that NodeList before asserting aria-disabled on
optionA and optionB.

---

Outside diff comments:
In `@src/OptionList.tsx`:
- Around line 298-309: The hidden ARIA output in renderItem uses the raw
disabled value instead of the visual mergedDisabled, causing mismatch when
overMaxCount disables unselected items; update renderItem to compute
mergedDisabled = disabled || (!selected && overMaxCount) (same logic used in the
visual list) and use mergedDisabled for aria-disabled (replace
aria-disabled={disabled} with aria-disabled={mergedDisabled}) so screen readers
reflect the actual disabled state for the option; ensure you reference
itemData.disabled, item.selected (or selected variable) and overMaxCount when
computing mergedDisabled and keep isAriaSelected(value) unchanged.

---

Nitpick comments:
In `@tests/Accessibility.test.tsx`:
- Around line 326-343: Add a test that covers the virtual={false} rendering path
so the visible option elements receive the new aria-disabled attribute: render
<Select open virtual={false} options=[{label:'Option
A',value:'a'},{label:'Option B',value:'b',disabled:true},{label:'Option
C',value:'c'}] />, query for elements with role="option" (same approach as the
existing test), locate the option whose text matches 'Option B' and assert it
has aria-disabled="true" while 'Option A' does not; this ensures the
OptionList.tsx mergedDisabled behavior for the non-virtual branch is tested.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cc7ab3 and 46d5078.

⛔ Files ignored due to path filters (3)
  • tests/__snapshots__/OptionList.test.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Select.test.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Tags.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • src/OptionList.tsx
  • tests/Accessibility.test.tsx

expect(input).toHaveAttribute('aria-activedescendant', 'select_list_0');
});

// https://github.com/ant-design/ant-design/issues/xxxxx
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

占位符 Issue URL 应替换为真实链接或删除

同文件中其他测试均引用了真实的 GitHub Issue(如第 24、46、127、162 行),此处 xxxxx 仍为占位符,应在合并前替换为对应 issue 链接或将该注释删除。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Accessibility.test.tsx` at line 325, Replace the placeholder comment
"// https://github.com/ant-design/ant-design/issues/xxxxx" in
Accessibility.test.tsx with the actual GitHub issue URL referenced by other
tests (or remove the comment entirely); locate the placeholder string in the
file and either update it to the correct issue link used by the related test
case or delete the line so no placeholder remains.

Comment on lines +338 to +342
const optionItems = container.querySelectorAll('[role="option"]');
const optionA = Array.from(optionItems).find((el) => el.textContent === 'a');
const optionB = Array.from(optionItems).find((el) => el.textContent === 'b');
expect(optionA).not.toHaveAttribute('aria-disabled', 'true');
expect(optionB).toHaveAttribute('aria-disabled', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the Accessibility.test.tsx file around the mentioned lines
# to understand the context and check lines 183, 226 to verify the query pattern
fd Accessibility.test.tsx

Repository: react-component/select

Length of output: 94


🏁 Script executed:

# Search for the test file and read the relevant sections
rg -n "querySelectorAll|querySelector.*_list" tests/Accessibility.test.tsx -B2 -A2

Repository: react-component/select

Length of output: 1335


🏁 Script executed:

# Check the Select component implementation for portal/container configuration
fd -e tsx -e ts -e jsx -e js | grep -i select | head -20

Repository: react-component/select

Length of output: 623


🏁 Script executed:

# Let's look at the full test case around line 338 to understand context
sed -n '320,345p' tests/Accessibility.test.tsx

Repository: react-component/select

Length of output: 1086


🏁 Script executed:

# Check the Select component implementation to understand portal/getContainer configuration
head -100 src/Select.tsx

Repository: react-component/select

Length of output: 3293


🏁 Script executed:

# Search for getContainer or portal-related configuration in SelectTrigger
rg -n "getContainer\|getPopupContainer\|portal" src/SelectTrigger.tsx -B2 -A2

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Search more broadly for getContainer, getPopupContainer, or portal in all source files
rg "getContainer\|getPopupContainer\|portal" src/ -n

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Let's look at SelectTrigger.tsx to understand how it renders
head -150 src/SelectTrigger.tsx

Repository: react-component/select

Length of output: 3862


🏁 Script executed:

# Check if rc-trigger is used and what its default behavior is
rg -n "rc-trigger" src/ -A3

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Continue reading SelectTrigger.tsx to see Trigger component usage
sed -n '150,250p' src/SelectTrigger.tsx

Repository: react-component/select

Length of output: 1454


🏁 Script executed:

# Look at the render call in SelectTrigger to see if getPopupContainer is passed to Trigger
sed -n '200,300p' src/SelectTrigger.tsx

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Check the test for hints about how Select renders elements
grep -n "render(" tests/Accessibility.test.tsx | head -5

Repository: react-component/select

Length of output: 299


🏁 Script executed:

# Check BaseSelect.tsx for default getPopupContainer configuration
rg -n "getPopupContainer" src/BaseSelect.tsx -B3 -A3

Repository: react-component/select

Length of output: 124


🏁 Script executed:

# Look for where the id prop gets generated/used
rg -n "id.*_list\|selectId\|auto.*id" src/ -t tsx -t ts | head -20

Repository: react-component/select

Length of output: 93


🏁 Script executed:

# Check the OptionList component to see how it gets its id
fd -e tsx -e ts | xargs grep -l "role=\"listbox\"" | head -5

Repository: react-component/select

Length of output: 84


🏁 Script executed:

# Find BaseSelect file
fd BaseSelect

Repository: react-component/select

Length of output: 107


🏁 Script executed:

# Check OptionList for how listbox id is generated
head -80 src/OptionList.tsx

Repository: react-component/select

Length of output: 2467


🏁 Script executed:

# Search for getPopupContainer in the entire src directory
rg "getPopupContainer" src/ -B2 -A2

Repository: react-component/select

Length of output: 1680


🏁 Script executed:

# Look at the full test function to see all render parameters
sed -n '326,345p' tests/Accessibility.test.tsx

Repository: react-component/select

Length of output: 802


🏁 Script executed:

# Check OptionList for the id prop and how it renders the listbox
rg -n "role=\"listbox\"" src/ -B5 -A5

Repository: react-component/select

Length of output: 616


🏁 Script executed:

# Check if rc-trigger has default getPopupContainer behavior
rg -n "Trigger" src/SelectTrigger.tsx | head -10

Repository: react-component/select

Length of output: 596


🏁 Script executed:

# Let's run the test to see if it actually passes or fails
grep -n "aria-disabled should be set on disabled options" tests/Accessibility.test.tsx

Repository: react-component/select

Length of output: 135


🏁 Script executed:

# Check how id is generated in BaseSelect
rg -n "useId\|const id" src/BaseSelect/ -A2

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Look for where id comes from in OptionList
sed -n '35,65p' src/OptionList.tsx

Repository: react-component/select

Length of output: 664


🏁 Script executed:

# Check the test setup to see if there's any id configuration
sed -n '1,30p' tests/Accessibility.test.tsx

Repository: react-component/select

Length of output: 1057


🏁 Script executed:

# Search for rc-trigger default behavior with getPopupContainer in package.json
cat package.json | grep -A5 -B5 "rc-component/trigger"

Repository: react-component/select

Length of output: 337


🏁 Script executed:

# Check how useBaseProps gets the id value
rg -n "useBaseProps\|selectId\|useId" src/BaseSelect/ -B2 -A5

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Let's look at the full OptionList render to see where listbox is rendered
sed -n '260,290p' src/OptionList.tsx

Repository: react-component/select

Length of output: 824


🏁 Script executed:

# Check if SelectTrigger passes getPopupContainer from BaseSelect
sed -n '400,500p' src/BaseSelect/index.tsx

Repository: react-component/select

Length of output: 3373


🏁 Script executed:

# Find useBaseProps implementation
fd useBaseProps

Repository: react-component/select

Length of output: 91


🏁 Script executed:

# Search for id generation in BaseSelect/index.tsx
rg -n "id.*=" src/BaseSelect/index.tsx | head -20

Repository: react-component/select

Length of output: 270


🏁 Script executed:

# Check if there's any default getPopupContainer configuration
rg -n "getPopupContainer\s*=" src/ -B2 -A2

Repository: react-component/select

Length of output: 761


🏁 Script executed:

# Let's look at where Trigger is rendered with getPopupContainer in BaseSelect
sed -n '550,650p' src/BaseSelect/index.tsx

Repository: react-component/select

Length of output: 3143


🏁 Script executed:

# Look at useBaseProps to see how id is generated
cat src/hooks/useBaseProps.ts

Repository: react-component/select

Length of output: 625


🏁 Script executed:

# Check if there's a default value for getPopupContainer in BaseSelect props
sed -n '1,150p' src/BaseSelect/index.tsx | grep -A20 "interface.*Props"

Repository: react-component/select

Length of output: 1678


🏁 Script executed:

# Let's check the test render function to see if RTL setup affects portal rendering
grep -n "render\|setupTest\|beforeEach" tests/Accessibility.test.tsx | head -20

Repository: react-component/select

Length of output: 568


🏁 Script executed:

# Search for where id is defined/initialized in BaseSelect/index.tsx
rg -n "const.*id\s*=" src/BaseSelect/index.tsx -A2

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Let's look at the start of BaseSelect/index.tsx to see the full component signature
head -400 src/BaseSelect/index.tsx | tail -200

Repository: react-component/select

Length of output: 5500


🏁 Script executed:

# Let's check if there's a default id or useId hook in the component
rg -n "useId\|getId\|generateId" src/ -B2 -A2

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Check if id is required or has a default value in the BaseSelectPrivateProps
sed -n '1,200p' src/BaseSelect/index.tsx | grep -A5 "BaseSelectPrivateProps"

Repository: react-component/select

Length of output: 500


🏁 Script executed:

# Let's check the Select.tsx component which wraps BaseSelect
head -100 src/Select.tsx

Repository: react-component/select

Length of output: 3293


🏁 Script executed:

# Check how Select component uses useId
sed -n '40,150p' src/Select.tsx

Repository: react-component/select

Length of output: 3670


🏁 Script executed:

# Let's also check if rc-trigger has default getPopupContainer behavior
rg -n "export.*Trigger" src/SelectTrigger.tsx -A20

Repository: react-component/select

Length of output: 869


🏁 Script executed:

# Check if the test might actually be querying the document instead of just container
grep -n "document\|portal\|Trigger" tests/Accessibility.test.tsx | head -30

Repository: react-component/select

Length of output: 494


🏁 Script executed:

# Let's see how Select.tsx generates id and passes it to BaseSelect
sed -n '100,250p' src/Select.tsx

Repository: react-component/select

Length of output: 4584


🏁 Script executed:

# Check if there are any other tests in Accessibility.test.tsx that also use container.querySelectorAll
rg -n "container\.querySelectorAll\|container\.querySelector" tests/Accessibility.test.tsx

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Let's verify if the portal is actually rendered by checking all references to `#virtual-select_list`
rg -n "virtual-select_list\|_list" tests/Accessibility.test.tsx -B3 -A3

Repository: react-component/select

Length of output: 48


🏁 Script executed:

# Continue reading Select.tsx to see where useId is called
sed -n '180,350p' src/Select.tsx

Repository: react-component/select

Length of output: 5326


🏁 Script executed:

# Let's check if the test is actually working or failing by looking for test runner configs
cat package.json | grep -A10 "jest\|test"

Repository: react-component/select

Length of output: 990


🏁 Script executed:

# More specifically, let's see if there are any Trigger import or usage patterns in tests
rg -n "Trigger\|getPopupElement\|document\.querySelector" tests/Accessibility.test.tsx

Repository: react-component/select

Length of output: 48


需要使用 document.querySelector 访问 Portal 中的下拉列表元素

同文件其他测试(第 183、226 行)均通过 document.querySelector('#virtual-select_list') 访问下拉内容。这表明 Select 组件的下拉列表通过 Portal 渲染到 document.body,而非 container 内部。

当前测试未传递 id 属性,Select 会通过 useId 自动生成一个随机 id(如 rc-select_12345),因此 container.querySelectorAll('[role="option"]') 无法访问到 Portal 中的选项元素,optionAoptionB 都会是 undefined。后续的 toHaveAttribute 断言可能返回不可靠的结果。

应参照其他测试的做法,显式传递 id 并通过 document.querySelector 查询:

🛡️ 建议的修复方案
+      const id = 'test-select';
       const { container } = render(
         <Select
+          id={id}
           open
           options={[
             { label: 'Option A', value: 'a' },
             { label: 'Option B', value: 'b', disabled: true },
             { label: 'Option C', value: 'c' },
           ]}
         />,
       );

-      const optionItems = container.querySelectorAll('[role="option"]');
+      const dropdown = document.querySelector(`#${id}_list`);
+      const optionItems = dropdown!.querySelectorAll('[role="option"]');
       const optionA = Array.from(optionItems).find((el) => el.textContent === 'a');
       const optionB = Array.from(optionItems).find((el) => el.textContent === 'b');
+      expect(optionA).toBeDefined();
+      expect(optionB).toBeDefined();
       expect(optionA).not.toHaveAttribute('aria-disabled', 'true');
       expect(optionB).toHaveAttribute('aria-disabled', 'true');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Accessibility.test.tsx` around lines 338 - 342, The test is querying
options from the test container but the Select renders its popup via a Portal
with an auto-generated id, so container.querySelectorAll('[role="option"]')
returns undefined; update the test to pass a stable id prop to the Select (e.g.,
id="virtual-select"), then query the portal using
document.querySelector(`#${'virtual-select'}_list`) (or
document.querySelector('#virtual-select_list')) to get optionItems, and then
find optionA/optionB from that NodeList before asserting aria-disabled on
optionA and optionB.

@afc163 afc163 merged commit d8b63c0 into react-component:master Feb 24, 2026
9 of 10 checks passed
@meet-student meet-student deleted the patch branch February 24, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants