Skip to content

Commit ba194a5

Browse files
zombieJclaude
andcommitted
refactor: use stable ID as key for ignoredElementMap
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>
1 parent d73787b commit ba194a5

File tree

1 file changed

+24
-8
lines changed

1 file changed

+24
-8
lines changed

src/Dom/focus.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect } from 'react';
22
import isVisible from './isVisible';
3+
import useId from '../hooks/useId';
34

45
type DisabledElement =
56
| HTMLLinkElement
@@ -102,15 +103,22 @@ export function triggerFocus(
102103
// ======================================================
103104
let lastFocusElement: HTMLElement | null = null;
104105
let focusElements: HTMLElement[] = [];
105-
const ignoredElementMap = new Map<HTMLElement, HTMLElement | null>();
106+
// Map lock element to its stable ID
107+
const elementToIdMap = new Map<HTMLElement, string>();
108+
// Map stable ID to ignored element
109+
const ignoredElementMap = new Map<string, HTMLElement | null>();
106110

107111
function getLastElement() {
108112
return focusElements[focusElements.length - 1];
109113
}
110114

111115
function isIgnoredElement(element: Element | null): boolean {
112116
if (!element) return false;
113-
const ignoredEle = ignoredElementMap.get(getLastElement());
117+
const lastElement = getLastElement();
118+
if (!lastElement) return false;
119+
const lockId = elementToIdMap.get(lastElement);
120+
if (!lockId) return false;
121+
const ignoredEle = ignoredElementMap.get(lockId);
114122
return (
115123
!!ignoredEle && (ignoredEle === element || ignoredEle.contains(element))
116124
);
@@ -163,9 +171,13 @@ function onWindowKeyDown(e: KeyboardEvent) {
163171
/**
164172
* Lock focus in the element.
165173
* It will force back to the first focusable element when focus leaves the element.
174+
* @param id - A stable ID for this lock instance
166175
*/
167-
export function lockFocus(element: HTMLElement): VoidFunction {
176+
export function lockFocus(element: HTMLElement, id: string): VoidFunction {
168177
if (element) {
178+
// Store the mapping between element and its stable ID
179+
elementToIdMap.set(element, id);
180+
169181
// Refresh focus elements
170182
focusElements = focusElements.filter(ele => ele !== element);
171183
focusElements.push(element);
@@ -180,7 +192,8 @@ export function lockFocus(element: HTMLElement): VoidFunction {
180192
return () => {
181193
lastFocusElement = null;
182194
focusElements = focusElements.filter(ele => ele !== element);
183-
ignoredElementMap.delete(element);
195+
elementToIdMap.delete(element);
196+
ignoredElementMap.delete(id);
184197
if (focusElements.length === 0) {
185198
window.removeEventListener('focusin', syncFocus);
186199
window.removeEventListener('keydown', onWindowKeyDown, true);
@@ -198,21 +211,24 @@ export function useLockFocus(
198211
lock: boolean,
199212
getElement: () => HTMLElement | null,
200213
): [ignoreElement: (ele: HTMLElement) => void] {
214+
const id = useId();
215+
201216
useEffect(() => {
202217
if (lock) {
203218
const element = getElement();
204219
if (element) {
205-
return lockFocus(element);
220+
return lockFocus(element, id);
206221
}
207222
}
208-
}, [lock]);
223+
}, [lock, id]);
209224

210225
const ignoreElement = (ele: HTMLElement) => {
211226
const element = getElement();
212227
if (element && ele) {
213-
// Set the ignored element for current lock element
228+
// Set the ignored element for current lock using stable ID
214229
// Only one element can be ignored at a time for this lock
215-
ignoredElementMap.set(element, ele);
230+
const lockId = elementToIdMap.get(element) || id;
231+
ignoredElementMap.set(lockId, ele);
216232
}
217233
};
218234

0 commit comments

Comments
 (0)