Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/Dom/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,20 @@ export function triggerFocus(
// ======================================================
let lastFocusElement: HTMLElement | null = null;
let focusElements: HTMLElement[] = [];
const ignoredElementMap = new Map<HTMLElement, HTMLElement | null>();

function getLastElement() {
return focusElements[focusElements.length - 1];
}

function isIgnoredElement(element: HTMLElement | null): boolean {
if (!element) return false;
const ignoredEle = ignoredElementMap.get(getLastElement());
return (
!!ignoredEle && (ignoredEle === element || ignoredEle.contains(element))
);
}
Comment thread
zombieJ marked this conversation as resolved.
Outdated

function hasFocus(element: HTMLElement) {
const { activeElement } = document;
return element === activeElement || element.contains(activeElement);
Expand All @@ -116,6 +125,11 @@ function syncFocus() {
const lastElement = getLastElement();
const { activeElement } = document;

// If current focus is on an ignored element, don't force it back
if (isIgnoredElement(activeElement as HTMLElement)) {
return;
}

if (lastElement && !hasFocus(lastElement)) {
const focusableList = getFocusNodeList(lastElement);

Expand Down Expand Up @@ -166,6 +180,7 @@ export function lockFocus(element: HTMLElement): VoidFunction {
return () => {
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
ignoredElementMap.delete(element);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
Expand All @@ -177,11 +192,12 @@ export function lockFocus(element: HTMLElement): VoidFunction {
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
export function useLockFocus(
lock: boolean,
getElement: () => HTMLElement | null,
) {
): [ignoreElement: (ele: HTMLElement) => void] {
useEffect(() => {
if (lock) {
const element = getElement();
Expand All @@ -190,4 +206,15 @@ export function useLockFocus(
}
}
}, [lock]);

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);
}
};
Comment thread
zombieJ marked this conversation as resolved.

return [ignoreElement];
Comment on lines +235 to +242
Copy link
Copy Markdown

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.

}
Loading