Skip to content

Conversation

@aojunhao123
Copy link
Contributor

@aojunhao123 aojunhao123 commented Dec 4, 2025

Summary by CodeRabbit

新功能

  • 用户现在可按 Escape 键快速关闭弹窗
  • 支持嵌套弹窗的 Escape 键处理,内层弹窗优先关闭

测试

  • 新增 Escape 键功能的测试覆盖

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

@coderabbitai
Copy link

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

走读

PR 引入了一个新的 useEscKeyDown 钩子,用于管理弹窗的 Escape 键处理逻辑。该钩子使用每窗口堆栈机制跟踪多个弹窗,并将其集成到 Trigger 组件和 UniqueProvider 中。同时新增了相应的测试用例和 TypeScript 配置更新。

变更

内聚组件 / 文件 变更摘要
Escape 键处理钩子
src/hooks/useEscKeyDown.ts
新增 React 钩子,通过每窗口堆栈管理多个弹窗的 Escape 键处理,包含 registerEscEntryunregisterEscEntry 等内部函数用于维护堆栈状态;导出 useEscKeyDown 函数供组件集成
Trigger 组件集成
src/index.tsx
导入并调用 useEscKeyDown 钩子,在触发器的打开/关闭流中集成 Escape 键取消处理逻辑
UniqueProvider 集成
src/UniqueProvider/index.tsx
导入并调用 useEscKeyDown 钩子,将 Escape 键处理与 UniqueProvider 的状态管理联动
单元测试
tests/basic.test.jsx, tests/unique.test.tsx
新增三个测试用例验证基础 Trigger 的 Escape 键关闭功能和嵌套弹窗的内向外关闭逻辑;新增 UniqueProvider 场景的 Escape 键测试
TypeScript 配置
tsconfig.json
compilerOptions.types 中添加 @testing-library/jest-domnode 类型声明

预估代码审查工作量

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

需要重点关注的区域:

  • src/hooks/useEscKeyDown.ts 中的堆栈管理逻辑,特别是 registerEscEntryunregisterEscEntry 函数如何确保嵌套弹窗的正确打开/关闭顺序
  • 事件监听器的添加和移除机制(addEscListener / removeEscListener),需验证其在多窗口场景下的正确性
  • 嵌套弹窗的三个测试用例是否完整覆盖了边界情况

可能相关的 PR

建议审查者

  • zombieJ
  • MadCcc

🐰 按下 Escape 键,弹窗悄悄关闭,
堆栈管理有序,嵌套不混乱,
键盘交互测试全覆盖,
小兔祝贺,代码更优雅! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request title accurately summarizes the main change: adding Escape key support for closing popups.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57a27f7 and 881eacc.

📒 Files selected for processing (6)
  • src/UniqueProvider/index.tsx (2 hunks)
  • src/hooks/useEscKeyDown.ts (1 hunks)
  • src/index.tsx (2 hunks)
  • tests/basic.test.jsx (1 hunks)
  • tests/unique.test.tsx (1 hunks)
  • tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/basic.test.jsx
  • src/index.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/UniqueProvider/index.tsx (1)
src/hooks/useEscKeyDown.ts (1)
  • useEscKeyDown (65-89)
tests/unique.test.tsx (2)
src/index.tsx (1)
  • UniqueProvider (39-39)
tests/util.tsx (1)
  • awaitFakeTimer (97-104)
src/hooks/useEscKeyDown.ts (1)
src/util.ts (1)
  • getWin (39-41)
🔇 Additional comments (7)
src/hooks/useEscKeyDown.ts (3)

56-63: registerEscEntry 逻辑正确

此函数正确处理了同一 popup ID 的重复注册(通过过滤移除旧条目),并确保在添加监听器之前将条目推入栈中。这避免了 addEscListener 中的处理器在栈为空时被触发的问题。


71-73: 使用 useEvent 确保回调稳定性

通过 useEvent 包装 triggerOpen 回调是一个很好的做法,这确保了即使 triggerOpen 引用改变,注册到栈中的回调也保持稳定,避免不必要的重新注册。


75-88: useEffect 依赖项管理得当

Hook 正确地在 open 变为 false 或其他依赖项变化时清理注册的条目。deps 数组 [popupId, open, popupEle, memoTriggerOpen] 是合适的。

tsconfig.json (1)

11-11: 测试类型声明配置正确

添加 @testing-library/jest-domnode 类型声明是测试环境的标准配置,可以支持 jest-dom 匹配器和 Node.js API 的类型检查。

src/UniqueProvider/index.tsx (2)

18-18: 正确导入 Escape 键处理 Hook

导入语句遵循现有的代码结构,与文件顶部的其他 hook 导入保持一致。


95-95: UniqueProvider 中 Escape 键处理集成正确

调用 useEscKeyDown 时传递的参数合理:

  • mergedOptions?.id 作为 popup 标识符
  • open 状态控制注册时机
  • popupEle 用于获取所属 window
  • 回调 () => trigger(false) 正确关闭 popup

Hook 内部的 guard 检查(line 76)能够安全处理参数为 undefinednull 的情况。

tests/unique.test.tsx (1)

378-393: Escape 键关闭 unique popup 的测试实现正确

测试用例结构合理,正确验证了以下流程:

  1. 渲染 UniqueProvider 包裹的 Trigger
  2. 点击触发器打开 popup
  3. 验证 popup 可见(无 hidden 类)
  4. 触发 Escape 键事件
  5. 验证 popup 隐藏(有 hidden 类)

测试使用 fireEvent.keyDown(window, { key: 'Escape' })window 对象上触发事件,这与实现中通过 popupEle.ownerDocument.defaultView 获取的 window 在 JSDOM 测试环境中是一致的。


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
Contributor

Summary of Changes

Hello @aojunhao123, 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 enhances the user experience by implementing the ability to close popups using the Escape key. It introduces a dedicated React hook to manage this behavior, ensuring proper handling of nested popups by closing them in a sequential, inside-out manner. The change is seamlessly integrated into the existing popup triggering mechanism and is accompanied by robust test coverage.

Highlights

  • New useEscCancel Hook: A new React hook, useEscCancel, has been introduced to manage the closing of popups when the 'Escape' key is pressed. This hook implements a stack-based mechanism to ensure that nested popups close in the correct last-in, first-out order.
  • Integration with generateTrigger: The useEscCancel hook has been integrated into the core generateTrigger function, automatically enabling Escape key functionality for all popups managed by this component.
  • Comprehensive Test Coverage: New test cases have been added to tests/basic.test.jsx to thoroughly verify the Escape key functionality, covering both single and nested popup closing scenarios.
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 4, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.07%. Comparing base (338a80f) to head (881eacc).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #594      +/-   ##
==========================================
+ Coverage   96.40%   97.07%   +0.66%     
==========================================
  Files          17       18       +1     
  Lines         947      992      +45     
  Branches      279      277       -2     
==========================================
+ Hits          913      963      +50     
+ Misses         34       29       -5     

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

🧹 Nitpick comments (3)
src/hooks/useEscCancel.ts (2)

19-38: Esc 处理函数可以加上更稳健的防御性判断

当前 handler 中直接从 stackMap 取值并访问 stack[stack.length - 1],如果以后有改动导致存在监听器但对应窗口没有栈或栈为空,按 Esc 会直接抛异常。removeEscListener 也会在 handler 不存在时多做一次无意义的 removeEventListener。建议轻量加一层判空,提升健壮性:

  const handler = (event: KeyboardEvent) => {
    if (event.key !== 'Escape') {
      return;
    }

-    const stack = stackMap.get(win);
-
-    const top = stack[stack.length - 1];
-    top.triggerOpen(false);
+    const stack = stackMap.get(win);
+    if (!stack?.length) {
+      return;
+    }
+
+    const top = stack[stack.length - 1];
+    top.triggerOpen(false);
  };

以及:

 function removeEscListener(win: Window) {
-  const handler = handlerMap.get(win);
-  win.removeEventListener('keydown', handler);
-  handlerMap.delete(win);
+  const handler = handlerMap.get(win);
+  if (!handler) {
+    return;
+  }
+  win.removeEventListener('keydown', handler);
+  handlerMap.delete(win);
 }

这不会改变现有行为,只是避免未来改动时出现难排查的运行时错误。


65-89: useEscCancelpopupEle 类型建议允许 null 以匹配调用方

Hook 签名中 popupEle 声明为 HTMLElement,但在 Trigger 里传入的是可能为 null 的 state(初始值就是 null),同时 effect 已经通过 if (!open || !popupEle) 做了运行时防御。为让类型与实际使用一致,可以小改一下:

-export default function useEscCancel(
-  popupId: string,
-  open: boolean,
-  popupEle: HTMLElement,
-  triggerOpen: (open: boolean) => void,
-) {
+export default function useEscCancel(
+  popupId: string,
+  open: boolean,
+  popupEle: HTMLElement | null,
+  triggerOpen: (open: boolean) => void,
+) {

这样可以避免调用处/后续重构引入多余的类型断言。

tests/basic.test.jsx (1)

1220-1269: 嵌套 Esc 测试建议显式刷一次计时器以避免潜在时序问题

esc should close nested popup from inside out 中,两次 fireEvent.keyDown(window, { key: 'Escape' }) 后直接做同步断言。如果 triggerOpen(false)useDelay 走的是 setTimeout(..., 0) 之类实现,这里可能会产生依赖实现细节的时序问题或偶现失败。

建议在两次 Esc 之后分别调用 awaitFakeTimer(),与前一个 Esc 用例及其它依赖延时的测试保持一致,例如:

        fireEvent.keyDown(window, { key: 'Escape' });
-        expect(isPopupClassHidden('.inner-popup')).toBeTruthy();
-        expect(isPopupClassHidden('.outer-popup')).toBeFalsy();
+        await awaitFakeTimer();
+        expect(isPopupClassHidden('.inner-popup')).toBeTruthy();
+        expect(isPopupClassHidden('.outer-popup')).toBeFalsy();
 
        fireEvent.keyDown(window, { key: 'Escape' });
-        expect(isPopupClassHidden('.outer-popup')).toBeTruthy();
+        await awaitFakeTimer();
+        expect(isPopupClassHidden('.outer-popup')).toBeTruthy();

另外,使用

const useIdModule = require('@rc-component/util/lib/hooks/useId');
const useIdSpy = jest.spyOn(useIdModule, 'default').mockImplementation(...);

的方式来稳定 id 顺序在当前 CJS/interop 流程下是可行的;如果后续构建切到纯 ESM,可以留意一下是否需要改成 jest.mock 形式来继续保证这一用例的稳定性。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 338a80f and 57a27f7.

📒 Files selected for processing (3)
  • src/hooks/useEscCancel.ts (1 hunks)
  • src/index.tsx (2 hunks)
  • tests/basic.test.jsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/basic.test.jsx (1)
tests/util.tsx (1)
  • awaitFakeTimer (97-104)
src/index.tsx (1)
src/hooks/useEscCancel.ts (1)
  • useEscCancel (65-89)
src/hooks/useEscCancel.ts (1)
src/util.ts (1)
  • getWin (39-41)
🔇 Additional comments (2)
src/index.tsx (1)

19-19: Esc 关闭逻辑在 Trigger 中的集成是合理的

Trigger 内引入并调用 useEscCancel(id, mergedOpen, popupEle, triggerOpen)

  • 使用的是合并后的可见状态 mergedOpen,与现有逻辑保持一致;
  • 通过传入统一的 triggerOpen,Esc 关闭会走同一套受控/非受控、UniqueProvider 分支和回调链;
  • 调用位置固定且无条件,不会破坏 Hooks 调用顺序。

整体接入看起来没有明显问题。

Also applies to: 651-652

tests/basic.test.jsx (1)

1204-1218: 基础 Esc 关闭用例覆盖合理

这里通过点击触发弹层,再触发 keyDown(Escape) 并用 awaitFakeTimer() 刷新定时器后断言隐藏,验证了 Esc 走的是与其它关闭路径一致的异步流程,写法和文件里现有依赖计时器的用例风格一致,看起来没有问题。

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 introduces a new feature to close popups by pressing the Escape key. A new hook useEscCancel is created to manage a stack of open popups and handle the keydown events. The implementation is clean and includes good test coverage for both single and nested popups. I've added a couple of suggestions in useEscCancel.ts to improve robustness by adding defensive checks against potential edge cases.


const stack = stackMap.get(win);

const top = stack[stack.length - 1];
Copy link
Member

Choose a reason for hiding this comment

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

stack 其实不靠谱,因为用户是可以通过 zIndex 来调整展示先后的。所以很有可能先出来的弹层反而在最前面

Copy link
Contributor Author

Choose a reason for hiding this comment

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

这个倒是确实没考虑到这种情况。组件库层面要处理这种因为UI交互设计不规范造成的corner case吗

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