Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Suspense } from "react";
import { describe, expect, it, vi } from "vite-plus/test";
import { userEvent } from "vite-plus/test/browser";

import { render } from "../../../test/render";
import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog";
import { ThemeEditorPanel } from "./ThemeEditorPanel";

// The modal Settings dialog marks everything outside it inert in this harness, so the
// test drives the editor through the DOM instead of pointer actions.
async function editorElement<T extends HTMLElement>(selector: string): Promise<T> {
await expect
.poll(() => document.querySelector(`[data-theme-editor-panel] ${selector}`))
.toBeTruthy();
return document.querySelector<T>(`[data-theme-editor-panel] ${selector}`)!;
}

// The editor floats beside the Settings dialog, whose Esc handler sits on the document.
function SettingsWithEditor(props: {
onSettingsOpenChange: (open: boolean) => void;
onEditorOpenChange: (open: boolean) => void;
}) {
return (
<>
<Dialog onOpenChange={props.onSettingsOpenChange} open>
<DialogPopup>
<DialogTitle>Settings</DialogTitle>
</DialogPopup>
</Dialog>
<Suspense fallback={null}>
<ThemeEditorPanel
editingTheme={null}
initialAppearance="light"
onOpenChange={props.onEditorOpenChange}
onSaved={() => true}
open
restoreTheme={() => undefined}
/>
</Suspense>
</>
);
}

describe("Esc in the theme editor", () => {
it("closes the editor, not Settings", async () => {
const onSettingsOpenChange = vi.fn();
const onEditorOpenChange = vi.fn();
await using _screen = await render(
<SettingsWithEditor
onEditorOpenChange={onEditorOpenChange}
onSettingsOpenChange={onSettingsOpenChange}
/>,
);

(await editorElement<HTMLInputElement>("input")).focus();
await userEvent.keyboard("{Escape}");

expect(onEditorOpenChange).toHaveBeenCalledWith(false);
expect(onSettingsOpenChange).not.toHaveBeenCalled();
});

it("cancels inspecting only", async () => {
const onSettingsOpenChange = vi.fn();
const onEditorOpenChange = vi.fn();
await using _screen = await render(
<SettingsWithEditor
onEditorOpenChange={onEditorOpenChange}
onSettingsOpenChange={onSettingsOpenChange}
/>,
);

(await editorElement('[aria-label="Inspect app colors"]')).click();
await editorElement('[aria-label="Cancel inspecting app colors"]');
await userEvent.keyboard("{Escape}");

await editorElement('[aria-label="Inspect app colors"]');
expect(onEditorOpenChange).not.toHaveBeenCalled();
expect(onSettingsOpenChange).not.toHaveBeenCalled();
});
});
9 changes: 9 additions & 0 deletions apps/web/src/components/settings/ThemeEditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,9 @@ export function ThemeEditorPanel({
};
const cancelInspection = (event: KeyboardEvent) => {
if (event.key !== "Escape") return;
// Esc leaves inspection only; Settings must not see it and close too.
event.preventDefault();
event.stopImmediatePropagation();
clearHover();
clearInspectorSelection();
};
Expand Down Expand Up @@ -1077,6 +1079,13 @@ export function ThemeEditorPanel({
isMinimized && "max-h-none",
)}
data-theme-editor-panel
onKeyDown={(event) => {
// Popups opened from the editor are portaled outside it and handle their own Esc.
if (event.key !== "Escape" || event.defaultPrevented) return;
if (!event.currentTarget.contains(event.target as Node)) return;
event.stopPropagation();
onOpenChange(false);
}}
ref={panelRef}
role="dialog"
style={{
Expand Down
Loading