mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(panel): let Escape close only the topmost full-screen panel
Each FullScreenPanel adds its own keydown listener on window, and stopPropagation does not stop other listeners on the same target, so stacked panels (pricing -> pricing editor, edit provider -> endpoint speed test) all closed on a single Escape. Track open panels in a module-level stack and only let the top one react.
This commit is contained in:
1 parent
7cc3141cae
commit
f4e9211a11
2 files changed
+86
-2
No files matched your search
@@ -58,6 +58,13 @@ const unlockBodyScroll = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开着的面板,后打开的在最后。二级页里再开二级页(定价 → 编辑定价、编辑供应商 → 测速)时
|
||||
* 每层都在 window 上挂了 ESC 监听,stopPropagation 拦不住同一目标上的其他监听,
|
||||
* 所以只让栈顶那层响应,一次 ESC 只退一层。
|
||||
*/
|
||||
const openPanelStack: object[] = [];
|
||||
|
||||
/**
|
||||
* Reusable full-screen panel component
|
||||
* Handles portal rendering, header with back button, and footer
|
||||
@@ -103,8 +110,16 @@ export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const entry = {};
|
||||
openPanelStack.push(entry);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
// 上面还叠着别的面板:交给最上面那层
|
||||
if (openPanelStack[openPanelStack.length - 1] !== entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 子组件(例如 Radix 的 Select/Dialog/Dropdown)如果已经消费了 ESC,就不要再关闭整个面板
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
@@ -123,6 +138,8 @@ export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
|
||||
window.addEventListener("keydown", handleKeyDown, false);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown, false);
|
||||
const index = openPanelStack.indexOf(entry);
|
||||
if (index >= 0) openPanelStack.splice(index, 1);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { DRAG_REGION_ENABLED } from "@/lib/platform";
|
||||
|
||||
@@ -75,6 +75,73 @@ describe("FullScreenPanel body scroll locking", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("FullScreenPanel Escape", () => {
|
||||
const Stacked = ({
|
||||
innerOpen,
|
||||
onOuterClose,
|
||||
onInnerClose,
|
||||
}: {
|
||||
innerOpen: boolean;
|
||||
onOuterClose: () => void;
|
||||
onInnerClose: () => void;
|
||||
}) => (
|
||||
<>
|
||||
<FullScreenPanel isOpen title="Pricing" onClose={onOuterClose}>
|
||||
pricing
|
||||
</FullScreenPanel>
|
||||
<FullScreenPanel
|
||||
isOpen={innerOpen}
|
||||
title="Edit pricing"
|
||||
onClose={onInnerClose}
|
||||
>
|
||||
<input aria-label="price" />
|
||||
</FullScreenPanel>
|
||||
</>
|
||||
);
|
||||
|
||||
it("closes only the topmost of two stacked panels", async () => {
|
||||
const onOuterClose = vi.fn();
|
||||
const onInnerClose = vi.fn();
|
||||
const view = render(
|
||||
<Stacked
|
||||
innerOpen={false}
|
||||
onOuterClose={onOuterClose}
|
||||
onInnerClose={onInnerClose}
|
||||
/>,
|
||||
);
|
||||
// 子页后打开,叠在上面
|
||||
view.rerender(
|
||||
<Stacked
|
||||
innerOpen
|
||||
onOuterClose={onOuterClose}
|
||||
onInnerClose={onInnerClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.keyDown(document.body, { key: "Escape" });
|
||||
expect(onInnerClose).toHaveBeenCalledTimes(1);
|
||||
expect(onOuterClose).not.toHaveBeenCalled();
|
||||
|
||||
// 输入框里的 ESC 留给输入框,哪层都不关
|
||||
fireEvent.keyDown(screen.getByLabelText("price"), { key: "Escape" });
|
||||
expect(onInnerClose).toHaveBeenCalledTimes(1);
|
||||
expect(onOuterClose).not.toHaveBeenCalled();
|
||||
|
||||
// 子页关掉后,下一次 ESC 才轮到下面那层
|
||||
view.rerender(
|
||||
<Stacked
|
||||
innerOpen={false}
|
||||
onOuterClose={onOuterClose}
|
||||
onInnerClose={onInnerClose}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByLabelText("price")).toBeNull());
|
||||
fireEvent.keyDown(document.body, { key: "Escape" });
|
||||
expect(onOuterClose).toHaveBeenCalledTimes(1);
|
||||
expect(onInnerClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FullScreenPanel header", () => {
|
||||
it("lets a long title shrink and truncate instead of pushing the window controls away", () => {
|
||||
const title = "Edit " + "a-very-long-user-provided-name-".repeat(8);
|
||||
|
||||
Reference in new issue
Block a user