Skip to content

Conversation

@aojunhao123
Copy link
Contributor

@aojunhao123 aojunhao123 commented Dec 30, 2025

Summary by CodeRabbit

  • Bug Fixes

    • 改进了在使用输入法编辑器(IME)时对 Escape 键的处理:加入组合结束后的短暂防抖,避免在输入组成期误触发或漏触发 Escape,且在无实例时移除全局监听器。
  • Tests

    • 添加并调整测试以覆盖 IME 场景和全局 Escape 行为,包含测试隔离与计时控制。
  • Refactor

    • 重构了 Escape 处理为集中化的全局监听与堆栈管理,简化实例注册与清理逻辑。
  • New Features

    • 新增测试专用接口用于检查与重置内部状态以便更可靠的测试。

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

@coderabbitai
Copy link

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

useEscKeyDown 重构为私有栈(由对象组成),新增 IME_LOCK_DURATION(200ms)与 compositionend 防抖(通过 lastCompositionEndTime),用全局事件处理器替代直接 window keydown 监听,导出测试用 _test 接口并更新相关测试以覆盖 IME 锁定行为和清理逻辑。

Changes

内聚组别 / 文件 变更摘要
核心 Hook 与栈管理
src/useEscKeyDown.ts
将导出的 stack: string[] 改为私有 stack: { id: string; onEsc?: EscCallback }[];引入 IME_LOCK_DURATION = 200lastCompositionEndTime;增加全局处理器 onGlobalKeyDownonGlobalCompositionEndattachGlobalEventListenersdetachGlobalEventListeners;使用 ensure/clear helper 管理栈条目并在无实例时卸载全局监听器;移除以前的直接 window keydown 绑定。
测试接口与测试更新
src/useEscKeyDown.ts, tests/index.test.tsx
新增导出测试接口 export const _test(用于读取内部 stackreset());测试中替换对旧 stack 的直接访问为 _test().stack,在 beforeEach/afterEach 使用 _test().reset() 与假定时钟;新增用例验证 compositionend 后的 IME 锁定期内忽略 Escape、锁定期后触发 onEsc;更新 StrictMode/嵌套 Portal 相关断言以配合新栈结构与清理行为。

Sequence Diagram(s)

(无序列图:变更为局部事件与内部栈/监听器重构,不满足生成序列图的条件)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 我在键盘旁嗅着风的味道,轻轻记下 compositionend 的脚步,
两百毫秒的守候是我给输入的护符,Escape 在那之后才获准跳舞。
测试绿灯闪闪,我绕着栈一圈又一圈,安心把事件交给夜色与窗口。

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标题准确概括了主要变更——为了防止意外关闭而添加IME锁定机制,与更改内容高度相关。
✨ 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
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 useEscKeyDown hook by implementing an Input Method Editor (IME) lock. The core purpose is to prevent accidental component closures, such as modals or popovers, that can occur when the Escape key is pressed inadvertently shortly after completing text input using an IME. This change improves the overall user experience by making the Escape key handling more robust and less prone to misinterpretation during IME usage.

Highlights

  • IME Lock Implementation: A new mechanism has been introduced to prevent the onEsc callback from being triggered immediately after an Input Method Editor (IME) composition ends. This lock lasts for 200 milliseconds, mitigating unintended closures.
  • Composition End Event Handling: The useEscKeyDown hook now listens for the compositionend event, recording the timestamp to enforce the IME lock duration effectively.
  • Enhanced Test Coverage: A dedicated test case has been added to tests/index.test.tsx to thoroughly verify the behavior of the new IME lock, ensuring onEsc is not called prematurely after IME activity.

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

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 30, 2025

Codecov Report

❌ Patch coverage is 97.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.57%. Comparing base (52bdacf) to head (b09a597).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/useEscKeyDown.ts 97.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #34      +/-   ##
==========================================
+ Coverage   94.36%   94.57%   +0.21%     
==========================================
  Files           7        7              
  Lines         142      166      +24     
  Branches       49       53       +4     
==========================================
+ Hits          134      157      +23     
- Misses          8        9       +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
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 an IME lock to prevent the onEsc callback from firing unintentionally when the Escape key is used to interact with an IME. The implementation is clean and includes a new test case that validates the behavior correctly. I have a couple of suggestions to improve the reliability of the timing mechanism by using performance.now() instead of Date.now(). Additionally, I noticed a potential pre-existing issue in useEscKeyDown.ts where useMemo is used for side effects and logic is duplicated with useEffect, which could lead to bugs. I recommend addressing this in a follow-up to improve the hook's correctness.

Comment on lines 17 to 18
const now = Date.now();
if (now - compositionEndTimeRef.current < IME_LOCK_DURATION) {
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 measuring time intervals, performance.now() is generally preferred over Date.now(). performance.now() provides a monotonically increasing timestamp that is not affected by system time changes, making it more reliable for calculating durations.

Suggested change
const now = Date.now();
if (now - compositionEndTimeRef.current < IME_LOCK_DURATION) {
const now = performance.now();
if (now - compositionEndTimeRef.current < IME_LOCK_DURATION) {

});

const handleCompositionEnd = useEvent(() => {
compositionEndTimeRef.current = Date.now();
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 and reliability in measuring time intervals, it's better to use performance.now() here as well, to match the usage in handleEscKeyDown.

Suggested change
compositionEndTimeRef.current = Date.now();
compositionEndTimeRef.current = performance.now();

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 (1)
tests/index.test.tsx (1)

373-397: 测试逻辑正确,建议考虑导入常量以提高可维护性。

测试覆盖了 IME 锁定窗口的行为:在 compositionEnd 后的 200ms 内按下 Escape 不会触发 onEsc,超过 200ms 后才会触发。逻辑清晰正确。

不过,测试中的时间值(100ms、150ms)与实现中的 IME_LOCK_DURATION(200ms)紧密耦合。如果将来调整锁定时长,测试也需要同步修改。可以考虑从实现文件导出该常量以提高可维护性。

🔎 可选优化方案
- import { stack } from '../src/useEscKeyDown';
+ import { stack, IME_LOCK_DURATION } from '../src/useEscKeyDown';

然后在测试中使用:

jest.advanceTimersByTime(IME_LOCK_DURATION - 100);
// ...
jest.advanceTimersByTime(100 + 50); // 超过 IME_LOCK_DURATION
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52bdacf and 1bb9ceb.

📒 Files selected for processing (2)
  • src/useEscKeyDown.ts
  • tests/index.test.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/useEscKeyDown.ts (1)
src/Portal.tsx (1)
  • EscCallback (24-30)
🔇 Additional comments (4)
src/useEscKeyDown.ts (4)

8-9: 常量定义合理。

200ms 的锁定时长对于处理 IME 组合结束后的误触场景是合适的。如果需要在测试中使用该值,可以考虑导出此常量。


13-29: IME 锁定逻辑实现正确。

  • compositionEndTimeRef 初始值为 0 确保首次 Escape 不会被误阻止
  • 使用 useEvent 包装 handleCompositionEnd 保持回调引用稳定,避免重复注册监听器
  • 时间差判断逻辑清晰,Date.now() - compositionEndTimeRef.current < IME_LOCK_DURATION 能有效过滤 IME 结束后的短时间内的 Escape 按键

48-55: 事件监听器管理正确。

compositionend 监听器在 Portal 打开时注册,关闭时清理,与 keydown 监听器保持一致的模式。确保了不会产生内存泄漏。


10-57: 整体实现审查通过。

该实现有效防止了 IME 组合结束后立即按下 Escape 键导致的意外关闭行为:

  1. 每个 Portal 实例独立维护 compositionEndTimeRef
  2. compositionend 事件在 window 上监听,能捕获所有输入框的 IME 结束事件
  3. 锁定窗口(200ms)足够覆盖用户完成 IME 输入后按 Escape 的常见场景

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

♻️ Duplicate comments (1)
src/useEscKeyDown.ts (1)

26-27: 建议使用 performance.now() 替代 Date.now() 进行时间间隔测量。

performance.now() 提供单调递增的时间戳,不受系统时间调整的影响,对于计算时间间隔更加可靠。此外,这也与之前的评审建议一致。

🔎 建议的修复方案
 const onGlobalKeyDown = (event: KeyboardEvent) => {
   if (event.key === 'Escape' && !event.isComposing) {
-    const now = Date.now();
+    const now = performance.now();
     if (now - lastCompositionEndTime < IME_LOCK_DURATION) {
       return;
     }
 const onGlobalCompositionEnd = () => {
-  lastCompositionEndTime = Date.now();
+  lastCompositionEndTime = performance.now();
 };

Also applies to: 41-43

🧹 Nitpick comments (1)
src/useEscKeyDown.ts (1)

72-78: useMemo 用于执行副作用不符合 React 最佳实践。

useMemo 的设计目的是缓存计算值,而非执行副作用。React 文档明确指出 useMemo 的回调可能在某些情况下被跳过或重复执行。当前代码已经在 useEffect 中有相同的逻辑,这里的 useMemo 用于在渲染期间同步更新 stack,但这种模式不够清晰。

建议考虑使用 useLayoutEffect 或重构逻辑,使 stack 管理完全在 effect 中完成。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd9081a and b09a597.

📒 Files selected for processing (2)
  • src/useEscKeyDown.ts
  • tests/index.test.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
tests/index.test.tsx (1)
src/useEscKeyDown.ts (1)
  • _test (12-21)
src/useEscKeyDown.ts (1)
src/Portal.tsx (1)
  • EscCallback (24-30)
🔇 Additional comments (5)
src/useEscKeyDown.ts (2)

45-55: 全局事件监听器的附加逻辑是正确的。

addEventListener 在使用相同函数引用时不会重复添加,所以多次调用 attachGlobalEventListeners 是安全的。detachGlobalEventListeners 在 stack 为空时正确移除监听器。这种实现避免了为每个 Portal 实例添加独立监听器的问题,符合之前评审中 zombieJ 提出的建议。


62-66: 此 concern 是不必要的 - 代码已通过 useEvent 正确处理了回调更新。

useEvent@rc-component/util 实现了"稳定身份+最新回调"模式:它返回一个保持恒定引用的包装函数,该函数在内部通过 ref 跟踪最新的回调,并在每次调用时都执行最新的回调。因此,即使 stack 中的条目未被更新,当事件触发时(第 33 行),调用的仍然是最新的 onEsc 回调,而非旧的回调。依赖数组 [open] 是正确的,因为 onEventEsc 本身是稳定的引用。

tests/index.test.tsx (3)

301-309: 测试设置正确,使用 _test().reset() 和 fake timers。

beforeEach 中重置 lastCompositionEndTime 并启用 fake timers,在 afterEach 中正确清理。这确保了测试之间的隔离性。

如果采纳 performance.now() 的建议,Jest 27+ 的 modern fake timers 默认会 mock performance.now(),所以无需额外配置。


383-406: IME 锁定时长测试逻辑正确。

测试验证了:

  1. compositionEnd 后立即按 Escape 被阻止 (t=0)
  2. 100ms 后按 Escape 仍被阻止 (t=100 < 200ms)
  3. 250ms 后按 Escape 成功触发 (t=250 >= 200ms)

时间计算正确,覆盖了 IME 锁定功能的核心行为。


415-418: 使用 _test().stack 验证 unmount 后 stack 清理是正确的测试方式。

确保了组件卸载时正确清理 stack 条目,避免内存泄漏和状态污染。

@zombieJ zombieJ merged commit 9f5fcc6 into react-component:master Dec 31, 2025
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.

2 participants