From a6667f48b1e3cb1f1ec3142026992d95a1acbb96 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Sun, 28 Dec 2025 00:54:59 +0800 Subject: [PATCH 1/3] fix: handle StrictMode esc stack cleanup --- src/useEscKeyDown.ts | 10 +++++++++- tests/index.test.tsx | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/useEscKeyDown.ts b/src/useEscKeyDown.ts index 90e31c6..61d7cb8 100644 --- a/src/useEscKeyDown.ts +++ b/src/useEscKeyDown.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { type EscCallback } from './Portal'; import useId from '@rc-component/util/lib/hooks/useId'; import { useEvent } from '@rc-component/util'; @@ -7,6 +7,7 @@ export let stack: string[] = []; // export for testing export default function useEscKeyDown(open: boolean, onEsc?: EscCallback) { const id = useId(); + const indexRef = useRef(-1); const handleEscKeyDown = useEvent((event: KeyboardEvent) => { if (event.key === 'Escape' && !event.isComposing) { @@ -21,12 +22,19 @@ export default function useEscKeyDown(open: boolean, onEsc?: EscCallback) { } else if (!open) { stack = stack.filter(item => item !== id); } + indexRef.current = stack.indexOf(id); }, [open, id]); useEffect(() => { if (!open) { return; } + if (!stack.includes(id)) { + const index = indexRef.current; + const safeIndex = + index < 0 ? stack.length : Math.min(index, stack.length); + stack.splice(safeIndex, 0, id); + } window.addEventListener('keydown', handleEscKeyDown); diff --git a/tests/index.test.tsx b/tests/index.test.tsx index f95eeba..9e03733 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -381,5 +381,25 @@ describe('Portal', () => { unmount(); expect(stack).toHaveLength(0); }); + + it('onEsc should treat first mounted portal as top in StrictMode', () => { + const onEsc = jest.fn(); + + const Demo = ({ visible }: { visible: boolean }) => + visible ? ( + +
+ + ) : null; + + render(, { wrapper: React.StrictMode }); + + expect(stack).toHaveLength(1); + + fireEvent.keyDown(window, { key: 'Escape' }); + + expect(onEsc).toHaveBeenCalledWith(expect.objectContaining({ top: true })); + }); + }); }); From ed7fc6f5fbe17d21acbe15ef23f826dd4da5c737 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Mon, 29 Dec 2025 16:44:19 +0800 Subject: [PATCH 2/3] add test case --- tests/index.test.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 9e03733..db50b20 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -400,6 +400,30 @@ describe('Portal', () => { expect(onEsc).toHaveBeenCalledWith(expect.objectContaining({ top: true })); }); + + it('nested portals should trigger in correct order', () => { + const onEsc = jest.fn(); + const onEsc2 = jest.fn(); + const onEsc3 = jest.fn(); + + render( + +
+ +
+ +
+ + + + ); + + fireEvent.keyDown(window, { key: 'Escape' }); + + expect(onEsc).toHaveBeenCalledWith(expect.objectContaining({ top: false })); + expect(onEsc2).toHaveBeenCalledWith(expect.objectContaining({ top: false })); + expect(onEsc3).toHaveBeenCalledWith(expect.objectContaining({ top: true })); + }); }); }); From 831863bc157149a608dbf0309023f58ca3c35a84 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Mon, 29 Dec 2025 16:52:58 +0800 Subject: [PATCH 3/3] chore: adjust --- src/useEscKeyDown.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/useEscKeyDown.ts b/src/useEscKeyDown.ts index 61d7cb8..31b2cb9 100644 --- a/src/useEscKeyDown.ts +++ b/src/useEscKeyDown.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo } from 'react'; import { type EscCallback } from './Portal'; import useId from '@rc-component/util/lib/hooks/useId'; import { useEvent } from '@rc-component/util'; @@ -7,7 +7,6 @@ export let stack: string[] = []; // export for testing export default function useEscKeyDown(open: boolean, onEsc?: EscCallback) { const id = useId(); - const indexRef = useRef(-1); const handleEscKeyDown = useEvent((event: KeyboardEvent) => { if (event.key === 'Escape' && !event.isComposing) { @@ -22,19 +21,16 @@ export default function useEscKeyDown(open: boolean, onEsc?: EscCallback) { } else if (!open) { stack = stack.filter(item => item !== id); } - indexRef.current = stack.indexOf(id); }, [open, id]); useEffect(() => { + if (open && !stack.includes(id)) { + stack.push(id); + } + if (!open) { return; } - if (!stack.includes(id)) { - const index = indexRef.current; - const safeIndex = - index < 0 ? stack.length : Math.min(index, stack.length); - stack.splice(safeIndex, 0, id); - } window.addEventListener('keydown', handleEscKeyDown);