Skip to content

Conversation

@codewizardTM
Copy link

@codewizardTM codewizardTM commented Oct 17, 2025

This PR fixes issue ant-design/ant-design#53495

Determine the focus key based on the selectable configuration:

If the item is not selectable, focus the default item.

If the item is selectable, decide whether to focus based on whether there is already a selected item.

Summary by CodeRabbit

  • Bug Fixes

    • 优化菜单组件焦点选择与聚焦行为:当支持可选项时优先聚焦已选项(若存在且有效),否则回退到活跃项或计算的默认焦点;当不支持可选项时始终使用计算的默认焦点,简化并统一聚焦优先级与回退逻辑。
  • Tests

    • 新增针对可选/不可选场景的焦点交互测试,覆盖键盘导航与点击、聚焦顺序与 focus 调用验证。

@coderabbitai
Copy link

coderabbitai bot commented Oct 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

修改 Menu 的聚焦与选中状态处理:引入并合并 select 键(internalSelectKeys / mergedSelectKeys),在 focus 流程中当 selectable 为 true 时优先聚焦首个选中键,否则尝试 mergedActiveKey,再回退到计算的默认焦点键;补充/调整相关单元测试覆盖行为。

Changes

Cohort / File(s) 变更摘要
菜单焦点与选中逻辑
src/Menu.tsx
引入内部 select 状态(internalSelectKeys)与合并键(mergedSelectKeys);重构 useImperativeHandle 中的 focus 流程:当 selectable === true 时优先使用第一个有效的 select 键,失败则尝试 mergedActiveKey,最终回退到 defaultFocusKey;当 selectable === false 时直接使用 defaultFocusKey。移除重复的 select 状态声明并合并处理位置。
焦点行为测试
tests/Focus.spec.tsx
新增键盘事件辅助 keyDown(含 SubMenu 动画推进)并引入 KeyCode 与本地 isActive,添加两组测试验证在 selectable 为 false/true 时的聚焦与交互(DOWN/ENTER/点击)行为,通过 spy 检查元素 focus() 调用。

Sequence Diagram(s)

sequenceDiagram
  participant User as 用户
  participant Menu as Menu 组件
  participant Keys as 键来源器
  participant Focus as DOM.focus

  User->>Menu: 触发 menuRef.focus()
  Menu->>Keys: 计算候选焦点键
  alt selectable == true
    Keys->>Keys: 读取 mergedSelectKeys
    alt mergedSelectKeys 首项 有效
      Keys-->>Menu: 返回 mergedSelectKeys[0] (已选项)
    else
      Keys->>Keys: 检查 mergedActiveKey
      alt mergedActiveKey 有效
        Keys-->>Menu: 返回 mergedActiveKey
      else
        Keys-->>Menu: 返回 defaultFocusKey
      end
    end
  else selectable == false
    Keys-->>Menu: 返回 defaultFocusKey
  end
  Menu->>Focus: 对应元素调用 focus()
  Focus-->>Menu: 聚焦完成 / 抛出异常
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 分钟

Possibly related PRs

Suggested reviewers

  • zombieJ
  • afc163

🐰 我是跳跃的小兔子,
先寻被选的那一只,
无果便望活跃位,
再无回首初始归,
菜单静好我来庆。

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR标题"fix: wrong focus behavior when configuring menu selectability"准确反映了本PR的主要变更。根据raw_summary,这个PR的核心是修复Menu组件的焦点行为,使其根据菜单的selectability配置进行条件判断:当selectable为false时焦点移到默认项,当selectable为true时焦点取决于是否有已选中的项。标题直接捕捉了这个核心改进,言简意赅且具体,避免了模糊措辞,使团队成员能够快速理解主要变更意图。
✨ Finishing touches
  • 📝 Generate docstrings
🧪 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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @codewizardTM, 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 addresses and resolves an issue where the menu's focus behavior was incorrect, particularly when the selectable configuration was in use. The changes refine the focus determination logic to ensure that the appropriate menu item is focused based on whether the menu allows selection and if any items are already selected, thereby enhancing the user experience and consistency of the component.

Highlights

  • Improved Focus Behavior: The logic for determining which menu item receives focus has been refactored to correctly account for the selectable property of the menu.
  • Conditional Focus Application: When a menu is selectable, the focus will now only be applied if there are no items already selected, preventing unintended focus shifts.
  • Default Focus Key: A defaultFocusKey variable has been introduced to consistently determine the fallback focus target, either the first focusable element or the first non-disabled child.
  • Helper Function: A new helper function, getMergedSelectKeys(), was added to provide access to the current selection state within the focus logic.
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.

Copy link

@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 fixes an issue with menu focus behavior when selectable is configured. The changes correctly determine which item to focus based on whether the menu is selectable and if any items are already selected. The logic seems correct and addresses the issue described. I've added one comment with a suggestion to simplify the new logic and improve the code structure for better maintainability.

Copy link

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

🧹 Nitpick comments (2)
src/Menu.tsx (2)

404-404: 移除不必要的可选链

由于 getMergedSelectKeys() 始终返回数组(在第 434-444 行确保),可选链 ?. 是不必要的。

应用此差异简化代码:

-        if (!getMergedSelectKeys()?.length) {
+        if (!getMergedSelectKeys().length) {

445-447: 考虑内联或重新放置辅助函数

getMergedSelectKeys 函数非常简单且仅在一处使用(第 404 行)。可以考虑:

  1. 内联调用(更简单):直接使用 mergedSelectKeys 而不是通过函数包装
  2. 移动位置(更好的组织):将函数定义移到 mergedSelectKeys 定义附近(第 434 行之后),提高代码可读性

选项 1:内联(推荐)

-        if (!getMergedSelectKeys()?.length) {
+        if (!mergedSelectKeys.length) {
           if (mergedActiveKey && keys.includes(mergedActiveKey)) {
             shouldFocusKey = mergedActiveKey;
           } else {
             shouldFocusKey = defaultFocusKey;
           }
         }
       } else {
         shouldFocusKey = defaultFocusKey;
       }
       const elementToFocus = key2element.get(shouldFocusKey);

       if (shouldFocusKey && elementToFocus) {
         elementToFocus?.focus?.(options);
       }
     },
     findItem: ({ key: itemKey }) => {
       const keys = getKeys();
       const { key2element } = refreshElements(keys, uuid);
       return key2element.get(itemKey) || null;
     },
   };
 });

 // ======================== Select ========================
 // >>>>> Select keys
 const [internalSelectKeys, setMergedSelectKeys] = useControlledState(
   defaultSelectedKeys || [],
   selectedKeys,
 );
 const mergedSelectKeys = React.useMemo(() => {
   if (Array.isArray(internalSelectKeys)) {
     return internalSelectKeys;
   }

   if (internalSelectKeys === null || internalSelectKeys === undefined) {
     return EMPTY_LIST;
   }

   return [internalSelectKeys];
 }, [internalSelectKeys]);
-function getMergedSelectKeys() {
-  return mergedSelectKeys;
-}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ba3f98 and 935b711.

📒 Files selected for processing (1)
  • src/Menu.tsx (2 hunks)
🔇 Additional comments (1)
src/Menu.tsx (1)

401-413: 该审查评论基于对代码设计的误解,应予以忽略

第 403 行的注释明确记录了设计意图:"if there is already a selected item, do not apply the focus"(如果已有选中项,则不应用焦点)。当 selectable=true 且存在已选中的键时,shouldFocusKey 保持 undefined有意为之,用于保留选中状态而不强制将焦点转移到其他元素。

这不是 bug。现有测试(Menu.spec.tsx 第 795-861 行)验证了焦点逻辑在其他场景下正确运行,Focus.spec.tsx 测试了多个焦点场景均通过。没有证据表明此行为会破坏键盘导航。

Likely an incorrect or invalid review comment.

@codecov
Copy link

codecov bot commented Oct 17, 2025

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.59%. Comparing base (3ba3f98) to head (62360b5).

Files with missing lines Patch % Lines
src/Menu.tsx 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #818      +/-   ##
==========================================
- Coverage   99.72%   99.59%   -0.14%     
==========================================
  Files          26       26              
  Lines         727      733       +6     
  Branches      199      202       +3     
==========================================
+ Hits          725      730       +5     
- Misses          2        3       +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.

Copy link

@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

🧹 Nitpick comments (2)
tests/Focus.spec.tsx (2)

210-255: 测试逻辑正确,建议保持一致性。

测试用例正确验证了 selectable={false} 时,无论之前的交互如何,调用 menuRef.current.focus() 都会聚焦第一个可用项。

建议:为了与文件中其他测试保持一致,建议在第 230 行使用 await act(async () => render(<TestApp />)) 包裹 render 调用。

-    const { getByTestId, container } = render(<TestApp />);
+    const { getByTestId, container } = await act(async () => render(<TestApp />));

256-276: 建议保持测试风格一致性。

与前一个测试用例类似,建议在第 276 行使用 await act(async () => render(<TestApp />)) 包裹 render 调用,以与文件中其他测试保持一致。

-    const { getByTestId, container } = render(<TestApp />);
+    const { getByTestId, container } = await act(async () => render(<TestApp />));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27b456e and 5a55a01.

📒 Files selected for processing (1)
  • tests/Focus.spec.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Focus.spec.tsx (2)
src/interface.ts (1)
  • MenuRef (124-132)
tests/util.ts (1)
  • isActive (1-9)
🔇 Additional comments (2)
tests/Focus.spec.tsx (2)

4-4: 导入语句合理。

引入 KeyCodeisActive 工具函数以支持新的焦点行为测试,符合测试需求。

Also applies to: 7-7


31-47: 辅助函数实现合理。

keyDown 函数正确模拟了键盘事件并处理了 SubMenu 动画的计时器推进。循环内多次调用 act() 虽然不太常见,但对于逐步推进计时器并允许 React 批量更新是合理的做法。

@zombieJ
Copy link
Member

zombieJ commented Oct 23, 2025

@gemini-code-assist 请再次 CR

Copy link

@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 addresses an issue with focus behavior in the Menu component when configuring menu selectability. The changes modify the logic for determining which item should be focused based on whether the menu is selectable and whether there are already selected items. Additionally, tests have been added to verify the new focus behavior in both selectable and non-selectable scenarios. I have added review comments to address a potential issue with the test cleanup.

Copy link

@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

🧹 Nitpick comments (1)
src/Menu.tsx (1)

392-407: 合并选择键计算可再简化,避免不必要的分支与潜在类型不一致

useControlledState 在本处的范型应为 string[];当前对 null/undefined/非数组的分支判断没有实际用处,也可能让 TS 推断变复杂。建议对齐上方 openKeys 的写法,直接用 state 或空数组即可。

-  const [internalSelectKeys, setMergedSelectKeys] = useControlledState(
-    defaultSelectedKeys || [],
-    selectedKeys,
-  );
-  const mergedSelectKeys = React.useMemo(() => {
-    if (Array.isArray(internalSelectKeys)) {
-      return internalSelectKeys;
-    }
-
-    if (internalSelectKeys === null || internalSelectKeys === undefined) {
-      return EMPTY_LIST;
-    }
-
-    return [internalSelectKeys];
-  }, [internalSelectKeys]);
+  const [internalSelectKeys, setMergedSelectKeys] = useControlledState(
+    defaultSelectedKeys,
+    selectedKeys,
+  );
+  const mergedSelectKeys = internalSelectKeys || EMPTY_LIST;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e64124 and 759581f.

📒 Files selected for processing (2)
  • src/Menu.tsx (1 hunks)
  • tests/Focus.spec.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Focus.spec.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/Menu.tsx (1)
src/hooks/useAccessibility.ts (2)
  • refreshElements (149-165)
  • getFocusableElements (110-113)

@zombieJ
Copy link
Member

zombieJ commented Oct 23, 2025

@gemini-code-assist 请完整的检测 PR 内容,并且对聚焦可能存在的 edge case 进行分析。

Copy link

@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 addresses an issue with menu focus behavior by differentiating between selectable and non-selectable items. The logic change in src/Menu.tsx is sound, and the accompanying tests in tests/Focus.spec.tsx provide good coverage for the new behavior. I've provided a suggestion to refactor the new logic for better readability and maintainability. Additionally, I've pointed out a couple of minor issues in the new test code, including a potential crash and some leftover commented code that should be cleaned up. Overall, this is a solid fix.

@codewizardTM
Copy link
Author

@zombieJ 再帮忙 cr 一下

mergedActiveKey && key2element.has(mergedActiveKey) ? mergedActiveKey : undefined;

const shouldFocusKey = selectable
? (selectedFocusKey ?? activeFocusKey ?? defaultFocusKey)
Copy link
Member

Choose a reason for hiding this comment

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

这里逻辑不对,mergedActiveKey 就是表示需要被聚焦的 key,应该是上游如果 activeKey 非受控,则在打开的时候 useLayoutEffect 更新一下 activeKey 到 selected 上

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