Skip to content

Conversation

@zombieJ
Copy link
Member

@zombieJ zombieJ commented Dec 9, 2025

Summary by CodeRabbit

  • 新功能

    • 添加焦点锁定功能,可在指定容器内约束并管理键盘焦点导航与恢复。
  • 文档

    • 新增焦点工具演示文档及交互示例,包含可运行的演示组件。
  • 重构

    • 移除已弃用的焦点恢复与制表范围工具,改为更可靠的锁定实现。
  • 测试

    • 新增/更新测试以覆盖焦点锁定与恢复行为。

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

@vercel
Copy link

vercel bot commented Dec 9, 2025

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

Project Deployment Preview Comments Updated (UTC)
util Error Error Dec 9, 2025 8:37am

@coderabbitai
Copy link

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

新增焦点锁定实现:引入 lockFocus 与 React 钩子 useLockFocus,移除旧的焦点恢复工具,新增演示文档与示例组件,并添加测试覆盖焦点锁定与恢复行为。

Changes

内聚组/文件 变更摘要
焦点锁定核心功能
src/Dom/focus.ts
移除旧的焦点恢复工具(如 saveLastFocusNode 等);新增 lockFocus(element: HTMLElement): VoidFunction 实现焦点堆栈、focusin/keydown 监听与焦点约束逻辑;新增导出钩子 `useLockFocus(lock: boolean, getElement: () => HTMLElement
文档与演示
docs/demo/focus.md, docs/examples/focus.tsx
添加文档页面 docs/demo/focus.md(Front matter + 简短说明)与交互示例 docs/examples/focus.tsx(FocusDemo),示例包含开关焦点锁、容器内可聚焦元素与外部按钮。
测试套件
tests/focus.test.tsx
新增/更新测试以覆盖 useLockFocus 行为:验证启用锁定时焦点保留在容器内、内部元素聚焦允许、外部聚焦尝试被重定向或恢复的场景。

序列图

sequenceDiagram
    participant User as 用户
    participant Component as React 组件 (useLockFocus)
    participant FocusMgr as 焦点管理 (lockFocus)
    participant DOM as 浏览器 DOM

    User->>Component: 设置 lock = true
    activate Component
    Component->>FocusMgr: 调用 lockFocus(element)
    activate FocusMgr
    FocusMgr->>DOM: 注册 focusin 和 keydown 监听器

    User->>DOM: 按 Tab 或 点击元素
    DOM->>FocusMgr: 触发 focusin / keydown 事件

    alt 焦点落在锁定容器内
        FocusMgr->>DOM: 允许焦点移动(不干预)
    else 尝试移到容器外
        FocusMgr->>DOM: 阻止/重置焦点到已定义元素
    end

    User->>Component: 设置 lock = false
    Component->>FocusMgr: 注销监听器并移除锁定
    deactivate FocusMgr
    deactivate Component
Loading

代码审查工作量评估

🎯 4 (复杂) | ⏱️ ~30–90 分钟

需要额外关注的区域:

  • src/Dom/focus.ts — 事件监听注册/注销、焦点堆栈管理、Tab 边界与键盘事件处理、内存泄漏与竞态条件。
  • tests/focus.test.tsx — 异步焦点切换、嵌套/重复锁定场景与恢复顺序的覆盖是否充分。
  • docs/examples/focus.tsx — 示例中 useRefuseLockFocus 的集成、交互是否能复现真实场景及可访问性(ARIA/键盘导航)。

🐰 我是小兔守焦点,
栈中弹簧紧又稳,
Tab 间穿梭不出圈,
监听轻唱夜不惊,
新章落笔旧章迎 ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 拉取请求标题清晰准确地概括了主要变更:添加了useLockFocus钩子功能,这正是代码变更的核心内容。
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch focus

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!

This pull request introduces a robust and modern solution for managing focus within specific UI elements, preventing focus from inadvertently leaving a designated area. This enhancement significantly improves keyboard accessibility for interactive components by ensuring that tab navigation and direct focus attempts are contained, providing a more predictable and user-friendly experience.

Highlights

  • New Feature: Focus Lock: Introduced a new lockFocus utility function and a useLockFocus React hook to enable focus trapping within a specified DOM element. This is crucial for accessibility in components like modals, ensuring keyboard navigation remains contained.
  • Deprecated Function Removal: Removed several deprecated focus-related functions (saveLastFocusNode, clearLastFocusNode, backLastFocusNode, limitTabRange) from src/Dom/focus.ts, streamlining the focus management API.
  • Documentation and Examples: Added new documentation (docs/demo/focus.md) and an example component (docs/examples/focus.tsx) to demonstrate the usage and behavior of the new focus lock functionality.
  • Testing Improvements: The test file tests/focus.test.ts was renamed to tests/focus.test.tsx and updated to include comprehensive tests for the useLockFocus hook, verifying its ability to restore focus to the locked container when focus attempts to escape.
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 introduces a new useLockFocus hook and its underlying lockFocus utility to manage focus within a specific DOM element, preventing users from tabbing out of it. It also includes a demo and tests for this new functionality. The changes involve removing deprecated focus-related functions and adding the new focus lock logic. While the core functionality seems to work as intended, there are some concerns regarding global state management and potential runtime errors that should be addressed for robustness and maintainability.

lock: boolean,
getElement: () => HTMLElement | null,
) {
useEffect(() => {

Choose a reason for hiding this comment

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

high

The lockFocus function expects an HTMLElement as its argument, but getElement() can return null. If getElement() returns null, this will lead to a runtime error when lockFocus is called. A null check should be performed before attempting to call lockFocus.

      if (lock) {
        const element = getElement();
        if (element) {
          return lockFocus(element);
        }
      }

Comment on lines +103 to +104
let lastFocusElement: HTMLElement | null = null;
let focusElements: HTMLElement[] = [];

Choose a reason for hiding this comment

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

high

The lastFocusElement and focusElements variables are global mutable states. The current implementation of getLastElement() always returns the most recently added element to focusElements, and syncFocus and onWindowKeyDown exclusively operate on this single "last" element. This design implies that only one focus lock can be truly active at any given time, with newer locks overriding older ones. If the intention is to support multiple independent focus locks simultaneously (e.g., for nested modals or multiple active components), this global state management will lead to incorrect and unpredictable behavior. Consider encapsulating the state for each lockFocus instance or redesigning the global state to properly manage multiple active locks, perhaps by iterating through focusElements or using a more robust stack/queue approach that respects the order and context of active locks.

src/Dom/focus.ts Outdated
Comment on lines 159 to 160
window.addEventListener('focusin', syncFocus);
window.addEventListener('keydown', onWindowKeyDown, true);

Choose a reason for hiding this comment

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

medium

The window.addEventListener calls are executed every time lockFocus is invoked. While the cleanup logic correctly removes listeners when no elements are locked, repeatedly calling addEventListener for the same function reference on the same target (window) will register the listener multiple times. This can lead to syncFocus and onWindowKeyDown being executed redundantly for a single event, potentially impacting performance or causing unexpected behavior. These listeners should ideally only be added once when the first element is locked and removed when the last element is unlocked.

@zombieJ zombieJ changed the title feat: Support focus lock feat: Support useLockFocus Dec 9, 2025
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: 4

🧹 Nitpick comments (2)
docs/examples/focus.tsx (1)

1-8: 建议统一 React hooks 的导入风格。

useRef 使用了解构导入,但 useState 使用的是 React.useState。建议保持一致的导入风格。

-import React, { useRef } from 'react';
+import React, { useRef, useState } from 'react';

然后将第 8 行改为:

-  const [locking, setLocking] = React.useState(true);
+  const [locking, setLocking] = useState(true);
tests/focus.test.tsx (1)

62-98: 测试覆盖了基本场景,建议补充更多测试用例。

当前测试验证了锁定状态下焦点会被限制在容器内,这是核心功能。建议后续补充以下测试场景:

  1. 解锁后焦点可以正常移出容器
  2. Tab 键循环导航行为
  3. Shift+Tab 反向循环导航
  4. 多个嵌套 focus lock 的行为
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea0ed1 and c441ad1.

📒 Files selected for processing (4)
  • docs/demo/focus.md (1 hunks)
  • docs/examples/focus.tsx (1 hunks)
  • src/Dom/focus.ts (2 hunks)
  • tests/focus.test.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/Dom/focus.ts (2)
src/Dom/isVisible.ts (1)
  • element (1-27)
src/hooks/useEffect.ts (1)
  • useEffect (4-18)
tests/focus.test.tsx (1)
src/Dom/focus.ts (1)
  • useLockFocus (173-182)
docs/examples/focus.tsx (1)
src/Dom/focus.ts (1)
  • useLockFocus (173-182)
🪛 GitHub Actions: ✅ test
src/Dom/focus.ts

[warning] 181-181: React Hook useEffect has a missing dependency: 'getElement'. Either include it or remove the dependency array.

tests/focus.test.tsx

[error] 2-2: JSX is defined but never used. (@typescript-eslint/no-unused-vars)


[error] 3-3: 'cleanup' is defined but never used. (no-unused-vars)

🪛 GitHub Check: CodeQL
tests/focus.test.tsx

[notice] 2-2: Unused variable, import, function or class
Unused import JSX.


[notice] 3-3: Unused variable, import, function or class
Unused import cleanup.

🔇 Additional comments (2)
docs/demo/focus.md (1)

1-9: 文档结构清晰。

文档格式正确,正确引用了示例文件。

src/Dom/focus.ts (1)

103-104: 模块级可变状态可能在 SSR 或多实例场景下产生问题。

lastFocusElementfocusElements 作为模块级变量,在服务端渲染(SSR)环境或同时存在多个 focus lock 实例时可能产生状态污染。

当前实现适用于客户端单实例场景,但如果后续需要支持 SSR 或更复杂的场景,建议考虑使用 Context 或其他隔离方案管理状态。

Comment on lines 173 to 181
export function useLockFocus(
lock: boolean,
getElement: () => HTMLElement | null,
) {
useEffect(() => {
if (lock) {
return lockFocus(getElement());
}
}, [lock]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

修复 useEffect 缺少依赖项的问题。

Pipeline 报告 useEffect 缺少 getElement 依赖。虽然 getElement 通常是稳定的回调函数,但为了符合 React hooks 规范并消除警告,建议添加到依赖数组中。

 export function useLockFocus(
   lock: boolean,
   getElement: () => HTMLElement | null,
 ) {
   useEffect(() => {
     if (lock) {
       return lockFocus(getElement());
     }
-  }, [lock]);
+  }, [lock, getElement]);
 }

如果担心 getElement 引用不稳定导致不必要的重新执行,可以考虑使用 useCallback 包装调用方的 getter,或在此处使用 ref 存储 getter。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useLockFocus(
lock: boolean,
getElement: () => HTMLElement | null,
) {
useEffect(() => {
if (lock) {
return lockFocus(getElement());
}
}, [lock]);
export function useLockFocus(
lock: boolean,
getElement: () => HTMLElement | null,
) {
useEffect(() => {
if (lock) {
return lockFocus(getElement());
}
}, [lock, getElement]);
}
🧰 Tools
🪛 GitHub Actions: ✅ test

[warning] 181-181: React Hook useEffect has a missing dependency: 'getElement'. Either include it or remove the dependency array.

🤖 Prompt for AI Agents
In src/Dom/focus.ts around lines 173 to 181, the useEffect hook is missing
getElement in its dependency array which triggers the React hooks warning;
update the dependencies to include getElement (i.e. [lock, getElement]) so the
effect responds correctly to changes, and if caller-side instability is a
concern, advise wrapping the getter with useCallback or storing it in a ref to
stabilize its identity.

@codecov
Copy link

codecov bot commented Dec 9, 2025

Codecov Report

❌ Patch coverage is 78.72340% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.57%. Comparing base (8ea0ed1) to head (8b1d93a).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/Dom/focus.ts 78.72% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #712      +/-   ##
==========================================
+ Coverage   90.53%   91.57%   +1.03%     
==========================================
  Files          37       37              
  Lines         909      938      +29     
  Branches      306      306              
==========================================
+ Hits          823      859      +36     
+ Misses         84       77       -7     
  Partials        2        2              

☔ 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

♻️ Duplicate comments (1)
src/Dom/focus.ts (1)

176-188: useEffect 缺少 getElement 依赖项。

此问题在之前的审查中已被指出但尚未修复。虽然 getElement 通常是稳定的回调,但为符合 React Hooks 规范并消除 lint 警告,应将其添加到依赖数组中。

   useEffect(() => {
     if (lock) {
       const element = getElement();
       if (element) {
         return lockFocus(element);
       }
     }
-  }, [lock]);
+  }, [lock, getElement]);
 }

如果担心调用方传入的 getElement 引用不稳定导致不必要的重新执行,可以在调用方使用 useCallback 包装,或在此处使用 useRef 存储 getter。

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

62-98: 测试覆盖了基本场景,建议补充边界情况。

当前测试验证了焦点锁定的核心行为,但建议补充以下测试用例:

  1. lock=false 时焦点不应被锁定
  2. 组件卸载时清理函数是否正确执行
  3. locktrue 切换到 false 后焦点行为

示例测试用例:

it('should not lock focus when lock is false', () => {
  const { getByTestId } = render(<TestComponent lock={false} />);
  
  const outerButton = getByTestId('outer-button') as HTMLButtonElement;
  outerButton.focus();
  expect(document.activeElement).toBe(outerButton);
});
src/Dom/focus.ts (2)

103-108: 全局状态设计支持嵌套场景,但需注意限制。

当前设计使用栈结构管理多个焦点锁定元素,但 syncFocusonWindowKeyDown 仅操作栈顶元素。这意味着嵌套的焦点锁定(如嵌套模态框)会正确工作,但同级的多个焦点锁定会相互覆盖。

如果这是预期行为,建议在 JSDoc 中说明此限制。


132-147: 建议添加防御性检查。

虽然事件监听器仅在 focusElements 非空时注册,但存在边界情况:如果在事件触发过程中执行了清理,getLastElement() 可能返回 undefined,导致 getFocusNodeList 接收到无效参数。

 function onWindowKeyDown(e: KeyboardEvent) {
   if (e.key === 'Tab') {
     const { activeElement } = document;
     const lastElement = getLastElement();
+    if (!lastElement) {
+      return;
+    }
     const focusableList = getFocusNodeList(lastElement);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c441ad1 and 59eeebc.

📒 Files selected for processing (3)
  • docs/examples/focus.tsx (1 hunks)
  • src/Dom/focus.ts (2 hunks)
  • tests/focus.test.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/examples/focus.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/Dom/focus.ts (2)
src/Dom/isVisible.ts (1)
  • element (1-27)
src/hooks/useEffect.ts (1)
  • useEffect (4-18)
tests/focus.test.tsx (1)
src/Dom/focus.ts (1)
  • useLockFocus (176-188)
🔇 Additional comments (3)
tests/focus.test.tsx (1)

2-5: 导入修复看起来正确。

之前的审查中标记的未使用导入 (JSXcleanup) 已被移除,新增了 useLockFocus 的导入。

src/Dom/focus.ts (2)

115-130: syncFocus 实现逻辑正确。

焦点同步逻辑会在焦点离开锁定区域时,优先恢复到上次聚焦的元素,否则聚焦第一个可聚焦元素。可选链操作符的使用也处理了空列表的边界情况。


153-174: lockFocus 实现已正确处理 null 参数。

Line 154 的 if (element) 检查解决了之前提出的 null 参数问题。事件监听器的处理也是正确的——addEventListener 对相同的函数引用、事件类型和捕获标志组合会自动去重。

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

♻️ Duplicate comments (2)
src/Dom/focus.ts (2)

160-161: 重复注册事件监听器导致性能问题。

之前的审查已经指出,每次调用 lockFocus 时都会执行 window.addEventListener。虽然清理逻辑在没有元素锁定时会正确移除监听器,但对同一目标(window)重复调用 addEventListener 会多次注册同一个监听器。这会导致 syncFocusonWindowKeyDown 在单个事件中被冗余执行多次,可能影响性能或导致意外行为。

理想情况下,这些监听器应该只在第一个元素被锁定时添加一次,在最后一个元素解锁时移除。

+let listenersAttached = false;
+
 export function lockFocus(element: HTMLElement): VoidFunction {
   if (element) {
     // Refresh focus elements
     focusElements = focusElements.filter(ele => ele !== element);
     focusElements.push(element);
 
-    // Just add event since it will de-duplicate
-    window.addEventListener('focusin', syncFocus);
-    window.addEventListener('keydown', onWindowKeyDown, true);
+    // Only add listeners once
+    if (!listenersAttached) {
+      window.addEventListener('focusin', syncFocus);
+      window.addEventListener('keydown', onWindowKeyDown, true);
+      listenersAttached = true;
+    }
     syncFocus();
   }
 
   // Always return unregister function
   return () => {
     lastFocusElement = null;
     focusElements = focusElements.filter(ele => ele !== element);
     if (focusElements.length === 0) {
       window.removeEventListener('focusin', syncFocus);
       window.removeEventListener('keydown', onWindowKeyDown, true);
+      listenersAttached = false;
     }
   };
 }

基于之前的审查意见。


181-192: useEffect 仍缺少 getElement 依赖项。

之前的审查和 Pipeline 都已指出,useEffect 缺少 getElement 依赖。虽然 getElement 通常是稳定的回调函数,但为了符合 React Hooks 规范并消除警告,应将其添加到依赖数组中。

应用以下差异修复:

 export function useLockFocus(
   lock: boolean,
   getElement: () => HTMLElement | null,
 ) {
   useEffect(() => {
     if (lock) {
       const element = getElement();
       if (element) {
         return lockFocus(element);
       }
     }
-  }, [lock]);
+  }, [lock, getElement]);
 }

如果担心 getElement 引用不稳定导致不必要的重新执行,可以考虑使用 useCallback 包装调用方的 getter,或在此处使用 ref 存储 getter。

基于之前的审查意见和 Pipeline 警告。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59eeebc and 8b1d93a.

📒 Files selected for processing (1)
  • src/Dom/focus.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Dom/focus.ts (1)
src/Dom/isVisible.ts (1)
  • element (1-27)
⏰ 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: test / react component workflow
  • GitHub Check: test / react component workflow

Comment on lines +103 to +104
let lastFocusElement: HTMLElement | null = null;
let focusElements: HTMLElement[] = [];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

全局状态设计仍然存在多锁冲突问题。

之前的审查已经指出,lastFocusElementfocusElements 作为全局可变状态,getLastElement() 总是返回最新添加的元素,syncFocusonWindowKeyDown 只对这个"最后"的元素进行操作。这意味着同一时间只有一个焦点锁真正生效,新的锁会覆盖旧的锁。

如果需要支持多个独立的焦点锁同时存在(例如嵌套的模态框),当前的全局状态管理会导致不正确和不可预测的行为。建议为每个 lockFocus 实例封装状态,或重新设计全局状态以正确管理多个活动锁(例如使用栈/队列方式来尊重活动锁的顺序和上下文)。

基于之前的审查意见。

🤖 Prompt for AI Agents
In src/Dom/focus.ts around lines 103-104, the globals lastFocusElement and
focusElements create multi-lock conflicts because
getLastElement()/syncFocus/onWindowKeyDown only operate on the most recently
added element, allowing newer locks to override older ones; refactor to
encapsulate focus lock state per lock (e.g., a FocusLock class or factory) and
maintain a central stack/registry of active locks so only the top (active) lock
handles keyboard/window events, and ensure locks register/unregister themselves
(push on lock creation, remove on release) and perform cleanup; update syncFocus
and onWindowKeyDown to consult the central stack for the current active lock
rather than global arrays so nested modals/focus regions behave predictably.

Comment on lines +167 to +172
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

清理函数过早重置 lastFocusElement 导致焦点追踪失效。

在第 167 行,清理函数无条件地将 lastFocusElement 设置为 null,即使还存在其他被锁定的元素(focusElements.length > 0)。这会破坏焦点追踪机制。

当多个元素依次被锁定和解锁时,如果某个中间元素解锁,lastFocusElement 会被重置,导致剩余锁定元素的焦点恢复逻辑失效。

应用以下差异修复:

   // Always return unregister function
   return () => {
-    lastFocusElement = null;
     focusElements = focusElements.filter(ele => ele !== element);
     if (focusElements.length === 0) {
+      lastFocusElement = null;
       window.removeEventListener('focusin', syncFocus);
       window.removeEventListener('keydown', onWindowKeyDown, true);
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
if (focusElements.length === 0) {
lastFocusElement = null;
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
Suggested change
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
focusElements = focusElements.filter(ele => ele !== element);
if (focusElements.length === 0) {
lastFocusElement = null;
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
🤖 Prompt for AI Agents
In src/Dom/focus.ts around lines 167 to 172, the cleanup unconditionally sets
lastFocusElement = null which breaks focus tracking when other locked elements
remain; only clear lastFocusElement and remove the window listeners when
focusElements.length === 0. Move the lastFocusElement = null assignment inside
the existing if block (so it only runs when focusElements becomes empty) and
keep the event listener removals there as well.

@zombieJ zombieJ merged commit b134564 into master Dec 9, 2025
10 of 12 checks passed
@zombieJ zombieJ deleted the focus branch December 9, 2025 08:42
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