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);