Skip to content

feat: add ignored element support for focus lock#727

Merged
zombieJ merged 6 commits intomasterfrom
feat/focus-lock-ignored-element
Feb 6, 2026
Merged

feat: add ignored element support for focus lock#727
zombieJ merged 6 commits intomasterfrom
feat/focus-lock-ignored-element

Conversation

@zombieJ
Copy link
Member

@zombieJ zombieJ commented Feb 6, 2026

Add ability to mark elements as ignored during focus lock, allowing temporary focus on elements outside the locked area.

ref ant-design/ant-design#56782

Summary by CodeRabbit

  • 新功能

    • 为每个焦点锁上下文引入基于稳定 ID 的映射,并支持为特定锁标记“被忽略”的元素,使其在锁定时仍可获焦点。
  • 变更

    • 锁定 API 扩展:lockFocus 接收 ID,useLockFocus 返回用于标记忽略元素的回调,映射在设置/拆除时初始化与清理。
  • 测试

    • 添加测试验证被标记为忽略的元素在锁定存在时可正常获取焦点。

Add ability to mark elements as ignored during focus lock, allowing temporary focus on elements outside the locked area.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 6, 2026

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

Project Deployment Actions Updated (UTC)
util Ready Ready Preview, Comment Feb 6, 2026 7:46am

@gemini-code-assist
Copy link
Contributor

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 significant enhancement to the focus lock mechanism by providing a way to temporarily bypass the focus restriction for designated elements. This feature addresses scenarios where certain interactive components, such as tooltips or modals, need to receive focus even when a primary focus lock is active, thereby improving the flexibility and user experience of applications utilizing focus management.

Highlights

  • Ignored Element Support: Introduced the ability to mark specific elements as 'ignored' within a focus lock, allowing temporary focus on these elements even if they are outside the locked area.
  • New ignoredElementMap: A new Map called ignoredElementMap has been added to src/Dom/focus.ts to store references to elements that should be ignored for a given focus lock.
  • Focus Sync Logic Update: The syncFocus function now checks if the currently active element is an ignored element (or contained within one) and, if so, prevents the focus from being forced back into the locked area.
  • useLockFocus Hook Enhancement: The useLockFocus hook now returns an ignoreElement function, allowing consumers to programmatically designate an element to be ignored for the current focus lock.
  • Cleanup for Ignored Elements: When a focus lock is released, the associated ignored element is now properly removed from the ignoredElementMap to prevent memory leaks and ensure correct behavior.

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

Changelog
  • src/Dom/focus.ts
    • Added ignoredElementMap to manage elements that should be excluded from focus lock enforcement.
    • Implemented isIgnoredElement function to check if an element is currently marked as ignored.
    • Modified syncFocus to prevent focus from being reset if the active element is an ignored element.
    • Updated the cleanup logic in lockFocus to remove ignored elements from the map when the lock is released.
    • Enhanced useLockFocus hook to return a new ignoreElement function for dynamic ignored element assignment.
Activity
  • The pull request introduces a new feature (feat) to add support for ignored elements in focus lock.
  • The change aims to allow temporary focus on elements outside a locked area, addressing a specific issue referenced as ant-design/ant-design/issues/56782.
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.

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

在焦点锁定逻辑中新增按锁(id)跟踪的“被忽略元素”映射与检测:引入 ignoreElement API 以在当前锁定上下文中标记元素为被忽略;syncFocus 在检测到被忽略元素时提前返回;在注销/teardown 时清理映射;useLockFocus 返回元组形式以暴露 ignoreElement 回调。

Changes

Cohort / File(s) Summary
焦点锁定实现
src/Dom/focus.ts
新增 ignoredElementMapidByElementMap,实现 isIgnoredElementignoreElementsyncFocus 在检测到被忽略元素时早期返回;lockFocus 新增 id 参数并保存元素↔id 关联,teardown 时清理映射;useLockFocus 改为返回 [ignoreElement]
测试
tests/focus.test.tsx
新增测试 "ignoreElement should allow focus on ignored elements",验证通过 useLockFocus 返回的 ignoreElement 回调标记元素后,该元素可获得焦点。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

庆祝诗

🐇 我是小兔在字段间,轻轻画下一个圈,
忽略的小按钮,今夜也能安心跳舞;
映射来去有规矩,焦点不再乱跑旋,
胡萝卜当鼓敲,月光里我跳跃欢颜。

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title "feat: add ignored element support for focus lock" accurately reflects the main change: introducing the ability to ignore specific elements during focus lock operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/focus-lock-ignored-element

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.

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 86.19%. Comparing base (595856a) to head (6cd187e).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/Dom/focus.ts 96.15% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #727      +/-   ##
==========================================
+ Coverage   85.98%   86.19%   +0.21%     
==========================================
  Files          38       38              
  Lines        1020     1043      +23     
  Branches      380      370      -10     
==========================================
+ Hits          877      899      +22     
- Misses        141      142       +1     
  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
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 support for ignoring specific elements within the focus lock mechanism, which is a valuable addition. The implementation is generally sound, introducing an ignoredElementMap to track elements that should be exempt from the focus trap and exposing a function via the useLockFocus hook to manage this. My review includes one suggestion to enhance type safety when dealing with document.activeElement, which will make the new logic more robust.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
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

🤖 Fix all issues with AI agents
In `@src/Dom/focus.ts`:
- Around line 210-217: 当前实现中如果目标元素先获得焦点,syncFocus(触发于 focusin)会在 ignoreElement
被调用前将焦点拉回锁定区域,导致 ignoreElement 时序问题;请在 ignoreElement 的
JSDoc(或模块顶层使用文档)明确说明必须在触发焦点转移(例如在调用 element.focus() 之前)先调用
ignoreElement(ele),并在文档中引用相关符号 ignoreElement、syncFocus、ignoredElementMap 和
getElement,以便调用方按正确顺序使用这些 API;如需额外保障,可在 ignoreElement 或 syncFocus
中加入运行时断言或警告(可选),但主要要求是更新注释/文档以声明调用时序约束。
🧹 Nitpick comments (3)
src/Dom/focus.ts (3)

105-105: 值类型 HTMLElement | null 中的 | null 是多余的。

ignoreElement(第 212 行)在 ele 为真时才会调用 set,因此 map 中永远不会存储 null 值。建议将类型简化为 Map<HTMLElement, HTMLElement>,使意图更清晰。

♻️ 建议修改
-const ignoredElementMap = new Map<HTMLElement, HTMLElement | null>();
+const ignoredElementMap = new Map<HTMLElement, HTMLElement>();

210-217: ignoreElement 每次渲染都会生成新的引用,建议使用 useCallback 包裹。

当前 ignoreElement 在每次渲染时都会创建一个新的函数引用。如果调用方将其作为 prop 传递或放入 useEffect 的依赖数组中,会导致不必要的重新渲染或 effect 重新执行。

♻️ 建议使用 useCallback
+import { useCallback, useEffect } from 'react';

-  const ignoreElement = (ele: HTMLElement) => {
-    const element = getElement();
-    if (element && ele) {
-      // Set the ignored element for current lock element
-      // Only one element can be ignored at a time for this lock
-      ignoredElementMap.set(element, ele);
-    }
-  };
+  const ignoreElement = useCallback((ele: HTMLElement) => {
+    const element = getElement();
+    if (element && ele) {
+      ignoredElementMap.set(element, ele);
+    }
+  }, [getElement]);

195-219: 缺少取消忽略(un-ignore)的 API。

一旦调用 ignoreElement(ele),该元素将一直处于被忽略状态,直到焦点锁释放。如果被忽略的元素(如弹出层)关闭但仍保留在 DOM 中(如通过 display: none 以外的方式隐藏),焦点锁对该元素的豁免会持续生效,可能导致焦点意外逸出锁定区域。

建议考虑以下方案之一:

  1. ignoreElement 返回一个清除函数,用于取消忽略状态。
  2. 提供 ignoreElement(null) 的语义来主动清除当前被忽略的元素。
♻️ 方案 2 示例
-  const ignoreElement = (ele: HTMLElement) => {
-    const element = getElement();
-    if (element && ele) {
-      // Set the ignored element for current lock element
-      // Only one element can be ignored at a time for this lock
-      ignoredElementMap.set(element, ele);
-    }
-  };
+  const ignoreElement = (ele: HTMLElement | null) => {
+    const element = getElement();
+    if (element) {
+      if (ele) {
+        ignoredElementMap.set(element, ele);
+      } else {
+        ignoredElementMap.delete(element);
+      }
+    }
+  };

Add a simple test to verify that ignoreElement allows focus on ignored elements outside the locked area.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use useId from React hooks as a stable key instead of element reference for the ignoredElementMap. This prevents issues where element reference changes (e.g., during component re-renders) cause the ignore functionality to break.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change from elementToIdMap to idToElementMap for clearer logic: stable ID as key maps to element.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simplify the isIgnoredElement function for better readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

🤖 Fix all issues with AI agents
In `@src/Dom/focus.ts`:
- Around line 235-242: The ignoreElement function is recreated on every render
which causes unstable references; wrap it in React's useCallback to memoize it
(e.g. const ignoreElement = useCallback((ele: HTMLElement) => { if (ele) {
ignoredElementMap.set(id, ele); } }, [id])) and ensure useCallback is imported;
this will keep the returned [ignoreElement] stable for consumers and effects
while still using the stable id from useId.
🧹 Nitpick comments (3)
src/Dom/focus.ts (3)

106-109: 每个锁 ID 只能存储一个被忽略元素,多次调用会静默覆盖。

ignoredElementMapMap<string, HTMLElement | null> 结构,每个锁 ID 只能关联一个被忽略的元素。如果消费者对同一个锁调用多次 ignoreElement(eleA)ignoreElement(eleB)eleA 会被静默覆盖。如果业务场景需要同时忽略多个不相关的元素(如多个 popup),当前设计将无法满足。

如果确认只需要支持单个元素,建议在 JSDoc 中注明;否则可考虑改用 Map<string, Set<HTMLElement>>

另外,HTMLElement | null 中的 | null 是多余的——ignoreElementif (ele) 已过滤掉 null,且 Map.get() 对不存在的 key 返回 undefined

♻️ 建议简化类型
-const ignoredElementMap = new Map<string, HTMLElement | null>();
+const ignoredElementMap = new Map<string, HTMLElement>();

115-135: 反向查找抵消了 idToElementMap 重构的优势。

isIgnoredElement 遍历 idToElementMap 的所有条目,通过元素引用比较找到对应的 lockId。这本质上是一次 element → id 的反向查找,而 commit message 提到重构的目的正是"避免依赖元素引用"。

建议将 focusElementsHTMLElement[] 改为存储 { element, id } 的结构,或者直接维护一个 elementToIdMap 反向映射,这样 isIgnoredElement 可以 O(1) 查找,逻辑也更清晰。

♻️ 方案示例:使用带 id 的栈结构
-let focusElements: HTMLElement[] = [];
+let focusElements: { element: HTMLElement; id: string }[] = [];

 function getLastElement() {
-  return focusElements[focusElements.length - 1];
+  return focusElements[focusElements.length - 1]?.element;
+}
+
+function getLastLockId() {
+  return focusElements[focusElements.length - 1]?.id;
 }

 function isIgnoredElement(element: Element | null): boolean {
   if (!element) return false;
-  const lastElement = getLastElement();
-  if (!lastElement) return false;
-
-  // Find the ID that maps to the last element
-  let lockId: string | undefined;
-  for (const [id, ele] of idToElementMap.entries()) {
-    if (ele === lastElement) {
-      lockId = id;
-      break;
-    }
-  }
-
-  if (!lockId) return false;
+  const lockId = getLastLockId();
+  if (!lockId) return false;

   const ignoredEle = ignoredElementMap.get(lockId);
   return (
     !!ignoredEle && (ignoredEle === element || ignoredEle.contains(element))
   );
 }

226-233: getElement 未纳入 useEffect 依赖数组。

如果 getElement 返回的元素在 lockid 未变的情况下发生变化,useEffect 不会重新执行,焦点锁定仍作用于旧元素。

这可能是延续既有模式(将 getElement 视为稳定的 ref 回调),但如果有动态切换容器元素的场景,需注意此限制。

Comment on lines +235 to +242
const ignoreElement = (ele: HTMLElement) => {
if (ele) {
// Set the ignored element using stable ID
ignoredElementMap.set(id, ele);
}
};

return [ignoreElement];
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

ignoreElement 未使用 useCallback 包裹,每次渲染生成新引用。

ignoreElement 在每次渲染时都会创建新的函数实例。如果消费者将其传递给子组件的 props 或用于 useEffect 的依赖数组,会导致不必要的重渲染或副作用重新执行。

由于 id(来自 useId)是稳定的,用 useCallback 包裹后引用也将保持稳定。

♻️ 建议使用 useCallback
+import { useEffect, useCallback } from 'react';
-import { useEffect } from 'react';
-  const ignoreElement = (ele: HTMLElement) => {
-    if (ele) {
-      // Set the ignored element using stable ID
-      ignoredElementMap.set(id, ele);
-    }
-  };
+  const ignoreElement = useCallback((ele: HTMLElement) => {
+    if (ele) {
+      ignoredElementMap.set(id, ele);
+    }
+  }, [id]);
🤖 Prompt for AI Agents
In `@src/Dom/focus.ts` around lines 235 - 242, The ignoreElement function is
recreated on every render which causes unstable references; wrap it in React's
useCallback to memoize it (e.g. const ignoreElement = useCallback((ele:
HTMLElement) => { if (ele) { ignoredElementMap.set(id, ele); } }, [id])) and
ensure useCallback is imported; this will keep the returned [ignoreElement]
stable for consumers and effects while still using the stable id from useId.

@zombieJ zombieJ merged commit c1eaa75 into master Feb 6, 2026
9 checks passed
@zombieJ zombieJ deleted the feat/focus-lock-ignored-element branch February 6, 2026 07:47
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.

1 participant