Skip to content

Conversation

@zombieJ
Copy link
Member

@zombieJ zombieJ commented Dec 3, 2025

统一将 blur 逻辑上移,在 Select 层收口。

fix ant-design/ant-design#56033

Summary by CodeRabbit

发布说明

  • 重构
    • 集中并优化了选择器的焦点/模糊与外部点击处理,弹出层现在可响应根级模糊事件以更可靠地控制关闭时机。
  • Bug Fixes
    • 调整了下拉关闭行为:输入组件不再通过内部 blur 自动关闭,模糊/关闭逻辑改由弹出层处理,避免部分焦点切换场景误关。
  • 测试
    • 增强了焦点/模糊交互测试,加入自定义输入场景覆盖。

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Dec 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
select Ready Ready Preview Comment Dec 3, 2025 3:44am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 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

本PR调整 Select 的焦点/模糊与外部点击处理:新增 isInside 辅助、将触发器控制集中到 getSelectElements,用宏任务延迟处理 root-level blur,并在弹出层增加 onPopupBlur 支持;同时移除 SelectInput 的根 onBlur 绑定并微调宏任务与测试定时。

Changes

同类变更 / 文件 变更摘要
焦点/模糊处理核心变更
src/BaseSelect/index.tsx
添加 getSelectElements 用于收集相关元素;集成 useSelectTriggerControl(基于元素集判断外部点击);新增 onRootBlur,通过 macroTask 延迟与 isInside 检查控制关闭;将其接入内部 blur 路径并作为 popup 的 blur 转发。
选择触发器 API 扩展
src/SelectTrigger.tsx
SelectTriggerProps 中新增可选 onPopupBlur?: React.FocusEventHandler<HTMLDivElement>,并将其绑定到 popup 容器的 onBlur
输入框模糊处理移除
src/SelectInput/index.tsx
删除 macroTask 导入及内部 onInternalBlur 回调,移除根 divonBlur 绑定,去除 SelectInput 的 blur 自动关闭路径。
钩子函数增强
src/hooks/useSelectTriggerControl.ts
新增并导出 isInside(elements, target) 帮助函数;用其替换全局 mousedown 内联包含检查以判断是否应触发关闭。
计时微调
src/hooks/useOpen.ts
macroTask 延迟值从 2 调整为 3(影响 ignoreNext 路径的锁释放时机)。
测试基础与用例
tests/setup.ts, tests/focus.test.tsx
tests/setup.ts 中 postMessage 超时从 0 改为 10ms;tests/focus.test.tsx 添加 beforeEach/afterEach 管理假定时器并新增用例:当 popupRender 含自定义 input 时,聚焦/失焦不应立即关闭弹窗,仅在点击 body 后关闭。

Sequence Diagram(s)

sequenceDiagram
    participant User as 用户
    participant Popup as 弹出层(自定义)
    participant BaseSelect as BaseSelect
    participant Trigger as SelectTrigger
    participant GlobalMouse as 全局鼠标事件

    User->>Popup: 在自定义区域点击或聚焦输入
    Popup->>Trigger: 焦点停留于 popup 内

    GlobalMouse->>BaseSelect: mousedown 事件
    BaseSelect->>BaseSelect: 收集 elements -> 调用 isInside(elements, target)

    alt 目标在元素集合内
        BaseSelect-->>GlobalMouse: isInside=true(跳过关闭)
        Note right of BaseSelect: 弹窗保持打开
    else 目标在外部
        BaseSelect-->>GlobalMouse: isInside=false
        BaseSelect->>Trigger: 触发 onPopupBlur -> 转为 onRootBlur
        Trigger->>BaseSelect: onRootBlur(通过 macroTask 延迟) 
        BaseSelect->>Popup: 执行关闭逻辑
    end
Loading

Estimated code review effort

🎯 3 (中等) | ⏱️ ~20-25 分钟

需要重点检查:

  • src/BaseSelect/index.tsxgetSelectElements 的元素收集与 onRootBlur 的 macroTask 时序与边界条件。
  • src/hooks/useSelectTriggerControl.tsisInside 对 undefined、SVG 元素及边缘 DOM 情况的处理。
  • SelectInput 移除 onBlur 后,焦点流在 BaseSelect / SelectTrigger / Popup 的一致性与回退路径。
  • 测试中定时调整(postMessage 10ms 与 macroTask 3)的稳定性对 CI 的影响。

Possibly related PRs

Suggested reviewers

  • afc163

Poem

小兔子跳,耳朵竖着笑,
点击弹窗里,菜单稳当抱,
isInside 护栏,焦点不乱跑,
宏任务微调,关闭慢些到,
代码又暖,用户更欢畅 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive 标题「Not close」过于简洁且缺乏具体信息,未能清晰传达主要变更内容,虽然与修复问题相关但表述模糊不清。 建议使用更具体的标题,如「Consolidate blur logic at Select layer to prevent popup closure on dropdownRender click」或类似的描述,以便清晰传达主要变更内容。
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed 所有代码变更都直接指向解决#56033问题的核心目标:将blur逻辑上移到Select层,实现外部点击感知和自定义popup blur处理,从而防止点击dropdownRender扩展区域时自动收起Select。
Out of Scope Changes check ✅ Passed 所有变更都与问题修复直接相关;timing调整、blur处理重构和外部点击检测都是实现核心修复所必需的,无明显超出范围的改动。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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 @zombieJ, 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!

此拉取请求旨在重构 Select 组件的失焦处理机制,将原本分散在子组件中的 blur 逻辑统一收敛到 BaseSelect 组件中。通过引入 isInside 辅助函数和调整事件处理流程,解决了特定场景下(如弹窗内有自定义输入框)焦点切换导致弹窗意外关闭的问题,并修复了相关 issue。这一改动提升了组件的健壮性和可维护性,确保了 Select 组件在复杂交互场景下的正确行为。

Highlights

  • 统一失焦处理逻辑: 将 Select 组件的失焦(blur)处理逻辑统一上移至 BaseSelect 层进行集中管理,解决了之前分散在子组件中可能导致的问题。
  • 引入 isInside 工具函数: 新增并导出了 isInside 工具函数,用于判断一个元素是否在指定元素集合内部,从而简化了外部点击关闭弹窗的逻辑。
  • 优化 SelectInput 失焦行为: 移除了 SelectInput 组件中原有的失焦处理逻辑,使其不再独立管理自身的 blur 行为,而是由 BaseSelect 统一协调。
  • 调整定时任务延迟: 将 macroTask 的执行延迟从 2ms 调整为 3ms,以优化定时任务的调度和事件处理时序。
  • 新增焦点测试用例: 增加了新的测试用例,以验证在 Select 弹窗内存在自定义输入框时,焦点切换不会意外关闭弹窗,同时确保点击外部区域能正确关闭弹窗。
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.

@codecov
Copy link

codecov bot commented Dec 3, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.47%. Comparing base (18f176f) to head (e659ff1).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1180      +/-   ##
==========================================
+ Coverage   99.41%   99.47%   +0.05%     
==========================================
  Files          31       31              
  Lines        1202     1331     +129     
  Branches      407      468      +61     
==========================================
+ Hits         1195     1324     +129     
  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

@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

本次 PR 统一了 blur 事件的处理逻辑,将其上移到 BaseSelect 组件中,以解决 popupRender 中包含自定义 input 时下拉框错误关闭的问题。这个重构方向是合理的,代码结构清晰。核心改动在于新增的 onRootBlur 处理函数,它能正确判断焦点是否移出 Select 组件及其弹层,从而决定是否关闭弹层。同时,新增的测试用例也很好地覆盖了该场景,有助于防止未来出现回归。

我有几点关于类型安全和代码一致性的建议,请看具体的评论。

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

🧹 Nitpick comments (1)
tests/focus.test.tsx (1)

6-13: 优秀的测试基础设施改进!

集中管理 fake timers 的设置和清理,避免了在每个测试中重复编写 timer 管理代码,提高了测试代码的可维护性。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f32cdd8 and 1c7a405.

📒 Files selected for processing (7)
  • src/BaseSelect/index.tsx (6 hunks)
  • src/SelectInput/index.tsx (0 hunks)
  • src/SelectTrigger.tsx (3 hunks)
  • src/hooks/useOpen.ts (1 hunks)
  • src/hooks/useSelectTriggerControl.ts (2 hunks)
  • tests/focus.test.tsx (2 hunks)
  • tests/setup.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/SelectInput/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/BaseSelect/index.tsx (2)
src/hooks/useSelectTriggerControl.ts (2)
  • useSelectTriggerControl (10-43)
  • isInside (4-8)
src/hooks/useOpen.ts (1)
  • macroTask (10-19)
🪛 GitHub Check: CodeQL
src/BaseSelect/index.tsx

[warning] 589-589: Superfluous trailing arguments
Superfluous argument passed to function onRootBlur.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (9)
src/SelectTrigger.tsx (1)

80-80: 代码实现正确!

新增的 onPopupBlur 属性已正确定义类型、解构并绑定到弹出层包装 div 的 onBlur 事件上。这使得 BaseSelect 能够统一处理弹出层的失焦逻辑,与 PR 目标一致。

Also applies to: 108-108, 170-170

tests/focus.test.tsx (1)

70-120: 新增测试用例准确覆盖了 issue #56033!

此测试验证了当 popupRender 包含自定义输入框时,聚焦该输入框或在弹出层内部失焦不会关闭 Select,只有点击外部元素(如 body)才会触发关闭。这直接解决了 PR 目标中提到的问题:在 antd 6.0 中点击 dropdownRender 扩展区域会自动收起的 bug。

src/hooks/useSelectTriggerControl.ts (1)

32-32: 简化了外部点击检测逻辑!

使用新提取的 isInside 辅助函数替代内联的包含性检查,提高了代码可读性。

src/BaseSelect/index.tsx (4)

10-10: 正确引入了必要的工具函数!

从相关 hooks 中导入 isInsidemacroTask,为实现集中式失焦处理提供支持。

Also applies to: 24-24


541-547: 集中式外部点击处理实现得当!

新增的 getSelectElements 辅助函数获取所有需要检查的元素(容器和弹出层),并通过 useSelectTriggerControl 统一处理外部点击关闭逻辑。这种方式将控制逻辑上移到 Select 层,与 PR 目标一致。


566-572: 失焦处理逻辑设计合理!

onRootBlur 使用 macroTask 延迟检查焦点是否真正离开了 Select 组件(包括弹出层),只有在焦点移到外部元素时才关闭。这正是修复 issue #56033 的核心逻辑:点击 dropdownRender 中的自定义内容时,焦点仍在 Select 元素内部,因此不会关闭。

注意onRootBlur 的类型签名是 React.FocusEventHandler<HTMLElement>,但实现中并未使用 event 参数。考虑将签名改为 () => void 或在需要时使用 event 参数。


778-778: 正确连接了弹出层失焦处理!

onRootBlur 传递给 SelectTriggeronPopupBlur 属性,使得弹出层的失焦事件能够触发统一的根级失焦处理逻辑,完成了失焦逻辑向 Select 层的迁移。

tests/setup.ts (1)

14-14: Timing adjustment in MessageChannel polyfill is consistent with test patterns and should be stable.

The 10ms delay for postMessage in the MessageChannel polyfill (tests/setup.ts:14) aligns with other test timeout patterns in the codebase (e.g., dynamicChildrenTest.tsx) and matches the macroTask scheduling mechanism used throughout the component (src/hooks/useOpen.ts). The change from 0ms to 10ms provides better stability for async test operations and should not cause timing-related failures. This adjustment is part of broader macroTask timing normalization in the PR and follows established testing patterns.

src/hooks/useOpen.ts (1)

89-89: The concern about race conditions is theoretically inverted—increasing the delay from 2 to 3 cycles should reduce risk, not increase it.

The code uses MessageChannel (macroTasks) for deterministic timing, which is the correct approach for this scenario. The web search confirms that proper timing separation prevents React update/effect race conditions. A longer macroTask chain (3 cycles vs 2) provides a larger time window for the current toggle operation to complete before accepting new events, making interleaving less likely.

Verification Limitation:

  • No test suite exists in this repository to run the proposed verification script
  • The timing change (1 additional async cycle) is minimal and directionally safer
  • Without actual test failures, concrete race condition reports, or edge case demonstrations, this remains theoretical

Assessment:
The code change appears sound from both theoretical timing principles and React event loop ordering. If there are specific timing-dependent edge cases or test failures, those should be explicitly documented and reproduced to warrant further investigation.

Likely an incorrect or invalid review comment.

@zombieJ zombieJ merged commit 6a22b76 into master Dec 3, 2025
11 of 12 checks passed
@zombieJ zombieJ deleted the not-close branch December 3, 2025 03:48
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: 0

♻️ Duplicate comments (1)
src/BaseSelect/index.tsx (1)

24-24: onRootBlur 使用 document.activeElement 存在潜在空值/类型隐患,建议配合 isInside 做更健壮的封装

这里把 document.activeElement 强制断言为 HTMLElement,如果在极端情况下为 null 或非 HTMLElement,会在 isInside 内部调用 .contains 时产生运行时风险;建议按之前机器人评论的思路,把 isInside 的第二个参数扩展为 Element | null 并在函数内部做空值判断,然后本处直接传入 document.activeElement,既避免不安全断言,又保留对 SVG 等元素的支持。

src/hooks/useSelectTriggerControl.ts 中示例改动:

-export function isInside(elements: (HTMLElement | SVGElement | undefined)[], target: HTMLElement) {
-  return elements
-    .filter((element) => element)
-    .some((element) => element.contains(target) || element === target);
-}
+export function isInside(
+  elements: (HTMLElement | SVGElement | undefined)[],
+  target: Element | null,
+) {
+  if (!target) {
+    return false;
+  }
+
+  return elements
+    .filter((element) => element)
+    .some((element) => element.contains(target) || element === target);
+}

然后本文件中可简化为:

-  const onRootBlur = () => {
-    macroTask(() => {
-      if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) {
-        triggerOpen(false);
-      }
-    });
-  };
+  const onRootBlur = () => {
+    macroTask(() => {
+      if (!isInside(getSelectElements(), document.activeElement)) {
+        triggerOpen(false);
+      }
+    });
+  };

这样 onInternalBluronPopupBlur 统一走 onRootBlur 的同时,也避免了潜在的空指针问题。

Also applies to: 566-572, 589-589, 778-778

🧹 Nitpick comments (1)
src/BaseSelect/index.tsx (1)

10-10: 集中化全局点击控制(useSelectTriggerControl + getSelectElements)的方向是对的

通过 getSelectElements 把容器 DOM 和弹层 DOM 一起交给 useSelectTriggerControl,可以避免 dropdownRender 区域被误判为“组件外”,整体策略和 issue 里的预期是一致的。可以顺带确认一下 TS 类型:如果 getDOM(containerRef.current) 在类型上可能为 null 或更宽的 Element,可考虑在这里显式收窄为 (HTMLElement | SVGElement | undefined)(例如加一层 as 或本地变量并过滤),以和 useSelectTriggerControl 的签名完全对齐,避免将来需要在别处做重复断言。

-  const getSelectElements = () => [
-    getDOM(containerRef.current),
-    triggerRef.current?.getPopupElement(),
-  ];
+  const getSelectElements = () => [
+    getDOM(containerRef.current) as HTMLElement | undefined,
+    triggerRef.current?.getPopupElement(),
+  ];

Also applies to: 540-548

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c7a405 and e659ff1.

📒 Files selected for processing (1)
  • src/BaseSelect/index.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/BaseSelect/index.tsx (2)
src/hooks/useSelectTriggerControl.ts (2)
  • useSelectTriggerControl (10-43)
  • isInside (4-8)
src/hooks/useOpen.ts (1)
  • macroTask (10-19)

zombieJ added a commit that referenced this pull request Dec 3, 2025
* chore: extract func

* fix: blur logic

* test: add test case

* chore: clean up
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.

Select组件 dropdownRender 的自由扩展,点击会自动收起

2 participants