Skip to content

fix: prevent selecting disabled items with keyboard navigation#326

Merged
zombieJ merged 1 commit intoreact-component:masterfrom
QDyanbing:codex/fix-segmented-disabled-keyboard-navigation
Apr 8, 2026
Merged

fix: prevent selecting disabled items with keyboard navigation#326
zombieJ merged 1 commit intoreact-component:masterfrom
QDyanbing:codex/fix-segmented-disabled-keyboard-navigation

Conversation

@QDyanbing
Copy link
Copy Markdown
Contributor

@QDyanbing QDyanbing commented Apr 8, 2026

背景

修复 ant-design/ant-design#57567 提到的问题:当 Segmented 通过键盘方向键切换选项时,不应选中 disabled item。

变更说明

  • 在键盘切换时过滤掉 disabled 选项
  • 保留当前选中项,避免在仅剩当前项和 disabled 项时发生错误切换
  • 补充对应测试用例,覆盖只有下一个选项为 disabled 的场景

关联缺陷

fix ant-design/ant-design#57567

Summary by CodeRabbit

Bug Fixes

  • 优化键盘导航逻辑,在使用方向键导航时会自动跳过禁用选项,确保仅能选择启用的选项。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

现状总结

键盘导航逻辑改进,通过过滤禁用选项(保留当前选中项)来构建有效选项列表,基于该列表计算导航索引,并在状态更新前验证下一个选项与当前值不同。

更改

功能模块 / 文件 更改内容
键盘导航逻辑优化
src/index.tsx
改进偏移导航逻辑:构建过滤后的有效选项列表(排除禁用选项,但保留当前选中),从该列表计算当前索引和下一个索引,确保下一个选项值与当前值不同后才触发状态更新和回调。
导航禁用选项测试
tests/index.test.tsx
新增键盘导航测试用例:验证从已选中的启用选项向右移动时,若后续只有禁用选项,则不触发变更回调且不选中禁用选项。

估算代码审查工作量

🎯 3 (中等复杂) | ⏱️ ~20 分钟

可能相关的 PR

建议审查者

  • afc163

诗歌

🐰 按键舞动,选项闪烁,
禁用的路,巧妙跳过,
有效列表,精心甄别,
状态稳稳,不差一毫毛。✨

🚥 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 The pull request title "fix: prevent selecting disabled items with keyboard navigation" directly and accurately describes the main change: preventing disabled items from being selected during keyboard navigation.
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 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 and usage tips.

Copy link
Copy Markdown
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 updates the keyboard navigation logic in the Segmented component to ensure that disabled options are skipped during interaction. It filters the available options to exclude disabled items before calculating the next index and adds a unit test to verify this behavior. A review comment identified a potential edge case where an invalid initial value could result in a -1 index, leading to incorrect navigation calculations, and suggested a fix to handle this scenario.

Comment on lines +245 to +251
const currentIndex = validOptions.findIndex(
(option) => option.value === rawValue,
);

const total = segmentedOptions.length;
const total = validOptions.length;
const nextIndex = (currentIndex + offset + total) % total;
const nextOption = validOptions[nextIndex];
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

rawValue 不在 validOptions 中时(例如初始值不在选项列表中),currentIndex 会被设为 -1。在这种情况下,如果 offset-1(按下左/上方向键),nextIndex 的计算结果 (currentIndex + offset + total) % total 会指向错误的索引(例如 total 为 3 时,结果为 1 而不是 2)。建议对 currentIndex === -1 的情况进行特殊处理,以确保在初始值无效时也能正确导航到第一个或最后一个有效选项。

      const currentIndex = validOptions.findIndex(
        (option) => option.value === rawValue,
      );

      const total = validOptions.length;
      const nextIndex =
        currentIndex === -1
          ? (offset > 0 ? 0 : total - 1)
          : (currentIndex + offset + total) % total;
      const nextOption = validOptions[nextIndex];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

这里看起来和之前的逻辑是一致的;应该无需处理吧;

Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.tsx`:
- Around line 245-255: The navigation logic uses validOptions.findIndex to
compute currentIndex which can be -1 when the controlled value (rawValue) is no
longer in options; to avoid accidentally changing value with arrow keys, add a
short-circuit that returns early when currentIndex === -1 before computing
nextIndex/nextOption. Update the block around
validOptions.findIndex/currentIndex (and before nextIndex, nextOption,
setRawValue, onChange) to check currentIndex === -1 and exit without calling
setRawValue or onChange.
🪄 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: f30b3f17-191a-40f3-bffb-ab0783cad515

📥 Commits

Reviewing files that changed from the base of the PR and between 0a35404 and f12597b.

📒 Files selected for processing (2)
  • src/index.tsx
  • tests/index.test.tsx

Comment on lines +245 to 255
const currentIndex = validOptions.findIndex(
(option) => option.value === rawValue,
);

const total = segmentedOptions.length;
const total = validOptions.length;
const nextIndex = (currentIndex + offset + total) % total;
const nextOption = validOptions[nextIndex];

const nextOption = segmentedOptions[nextIndex];
if (nextOption) {
if (nextOption && nextOption.value !== rawValue) {
setRawValue(nextOption.value);
onChange?.(nextOption.value);
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

在当前值已失效时先短路,避免方向键偷偷改值。

value 是受控的且 rawValue 已不在 options 里时,Line 245 的 findIndex 会返回 -1。这样 Line 250 的取模会把 ArrowRight/ArrowDown 导到第一个有效项、ArrowLeft/ArrowUp 导到最后一个有效项,并触发一次非预期的 onChange,这和上面“值不存在时不自动切换”的约束相冲突。这里建议在 currentIndex === -1 时直接返回。

💡 建议修复
       const currentIndex = validOptions.findIndex(
         (option) => option.value === rawValue,
       );
+      if (currentIndex === -1) {
+        return;
+      }
 
       const total = validOptions.length;
       const nextIndex = (currentIndex + offset + total) % total;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.tsx` around lines 245 - 255, The navigation logic uses
validOptions.findIndex to compute currentIndex which can be -1 when the
controlled value (rawValue) is no longer in options; to avoid accidentally
changing value with arrow keys, add a short-circuit that returns early when
currentIndex === -1 before computing nextIndex/nextOption. Update the block
around validOptions.findIndex/currentIndex (and before nextIndex, nextOption,
setRawValue, onChange) to check currentIndex === -1 and exit without calling
setRawValue or onChange.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

同上,可以check一下这个边界是否需要处理;或者单独pr处理?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@QDyanbing,经过分析,我建议直接在本 PR 中处理这个边界情况,原因如下:

分析

validOptions 的过滤逻辑是 option.value === rawValue || !option.disabled,因此在正常情况下rawValue 存在于 options 中),当前值一定会出现在 validOptions 里,currentIndex 不会是 -1

但在受控模式下,如果上层传入的 value 不在 options 列表中,validOptions 里就找不到当前值,findIndex 返回 -1。此时:

offset 计算结果 效果
+1(ArrowRight) (-1 + 1 + N) % N = 0 跳到第一项
-1(ArrowLeft) (-1 - 1 + N) % N = N-2 跳到倒数第二项

由于 nextOption.value !== rawValue 成立,onChange 会被意外触发——这与代码中已有的注释"不应自动切换不存在于 options 中的值"直接冲突。

建议:在本 PR 中一并修复

修复量极小(3 行),且改动就在本次修改的同一函数内,建议直接加入:

      const currentIndex = validOptions.findIndex(
        (option) => option.value === rawValue,
      );
+     if (currentIndex === -1) {
+       return;
+     }

      const total = validOptions.length;
      const nextIndex = (currentIndex + offset + total) % total;

这样既保持本 PR 的改动集中,又能一次性封住"受控值不在 options 中时方向键仍触发 onChange"的漏洞,无需单独开 PR。

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.40%. Comparing base (0a35404) to head (f12597b).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #326   +/-   ##
=======================================
  Coverage   99.39%   99.40%           
=======================================
  Files           2        2           
  Lines         165      167    +2     
  Branches       41       40    -1     
=======================================
+ Hits          164      166    +2     
  Misses          1        1           

☔ 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.

@zombieJ zombieJ merged commit 533ee70 into react-component:master Apr 8, 2026
8 checks passed
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.

Segmented component can select disabled items when focusing

2 participants