Skip to content
Merged
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
121 changes: 121 additions & 0 deletions src/components/launch/LaunchWindow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ function resetLaunchMocks() {
i18nState.value.acceptSystemLocaleSuggestion.mockClear();
i18nState.value.dismissSystemLocaleSuggestion.mockClear();
i18nState.value.resolveSystemLocaleSuggestion.mockClear();
i18nState.value.setLocale.mockClear();
linuxHelperAvailable.value = true;
appInfoState.value = { version: "1.9.6", canCheckForUpdates: true };
updateCheckMock.mockReset();
Expand Down Expand Up @@ -829,6 +830,126 @@ describe("LaunchWindow language menu", () => {
});
});

describe("LaunchWindow popover dismissal", () => {
beforeEach(() => {
platformState.value = "darwin";
resetLaunchMocks();
});

afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});

/** Opens the language menu the way a user does, and hands back its panel. */
async function openLanguageMenu() {
fireEvent.click(await screen.findByRole("button", { name: "English" }));
return await screen.findByTestId("hud-language-menu");
}

/** Same for the device-settings panel. */
async function openDeviceSettings() {
fireEvent.click(await screen.findByTestId("launch-device-settings-button"));
return await screen.findByTestId("hud-device-settings");
}

it("closes the language menu on Escape without changing the locale", async () => {
renderLaunchWindow();
await openLanguageMenu();

fireEvent.keyDown(window, { key: "Escape" });

await waitFor(() => {
expect(screen.queryByTestId("hud-language-menu")).not.toBeInTheDocument();
});
// Escape dismisses; it must never pick whatever entry happened to be under
// the cursor or focused.
expect(i18nState.value.setLocale).not.toHaveBeenCalled();
expect(i18nState.value.resolveSystemLocaleSuggestion).not.toHaveBeenCalled();
});

it("closes the language menu on a pointerdown outside the trigger and the panel", async () => {
renderLaunchWindow();
await openLanguageMenu();

// The HUD window is mostly empty reserve above the bar; a press there is a
// real DOM pointerdown on the root, and it has to dismiss.
fireEvent.pointerDown(document.body);

await waitFor(() => {
expect(screen.queryByTestId("hud-language-menu")).not.toBeInTheDocument();
});
expect(i18nState.value.setLocale).not.toHaveBeenCalled();
});

it("keeps the language menu open for a pointerdown inside the panel", async () => {
renderLaunchWindow();
const menu = await openLanguageMenu();

fireEvent.pointerDown(menu);

expect(screen.getByTestId("hud-language-menu")).toBeInTheDocument();
});

it("closes the language menu when the HUD window loses focus", async () => {
renderLaunchWindow();
await openLanguageMenu();

// A click that lands beyond the HUD's native window produces no pointerdown
// in this renderer at all — the only signal it gets is the window blur. And
// once focus is gone, Escape can no longer be delivered here either, so this
// is the one listener that can unstick that state (issue #435).
fireEvent.blur(window);

await waitFor(() => {
expect(screen.queryByTestId("hud-language-menu")).not.toBeInTheDocument();
});
expect(i18nState.value.setLocale).not.toHaveBeenCalled();
});

it("closes the device-settings panel on Escape", async () => {
renderLaunchWindow();
await openDeviceSettings();

fireEvent.keyDown(window, { key: "Escape" });

await waitFor(() => {
expect(screen.queryByTestId("hud-device-settings")).not.toBeInTheDocument();
});
});

it("closes the device-settings panel on a pointerdown outside the trigger and the panel", async () => {
renderLaunchWindow();
await openDeviceSettings();

fireEvent.pointerDown(document.body);

await waitFor(() => {
expect(screen.queryByTestId("hud-device-settings")).not.toBeInTheDocument();
});
});

it("closes the device-settings panel when the HUD window loses focus", async () => {
renderLaunchWindow();
await openDeviceSettings();

fireEvent.blur(window);

await waitFor(() => {
expect(screen.queryByTestId("hud-device-settings")).not.toBeInTheDocument();
});
});

it("leaves a key that is not Escape alone", async () => {
renderLaunchWindow();
await openLanguageMenu();

fireEvent.keyDown(window, { key: "a" });

expect(screen.getByTestId("hud-language-menu")).toBeInTheDocument();
});
});

describe("LaunchWindow device buttons", () => {
beforeEach(() => {
platformState.value = "darwin";
Expand Down
13 changes: 12 additions & 1 deletion src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export function LaunchWindow() {
}, []);

// One dismiss handler for both floating surfaces — they're mutually exclusive,
// so a single pointerdown/Escape listener covers the pair instead of two.
// so a single pointerdown/Escape/blur listener covers the pair instead of two.
const closePopovers = useCallback(() => {
setIsDeviceSettingsOpen(false);
setIsLanguageMenuOpen(false);
Expand All @@ -311,10 +311,21 @@ export function LaunchWindow() {

window.addEventListener("pointerdown", handlePointerDown);
window.addEventListener("keydown", handleEscape);
// The third dismiss path, and the one that made issue #435: the HUD's native
// window is only 904x698, so a click anywhere else on screen reaches this
// renderer as nothing at all — no pointerdown to hit-test — while still taking
// keyboard focus away. Escape is then undeliverable here, and the popover was
// stuck open until the trigger was found again. `blur` is the one signal that
// crosses, so it closes the pair; after this the "popover open in a window
// that has no focus" state simply doesn't exist. Bubble phase on purpose:
// element blur doesn't bubble, so this only ever fires for the window itself
// and never when focus moves between the menu's own buttons.
window.addEventListener("blur", closePopovers);

return () => {
window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("keydown", handleEscape);
window.removeEventListener("blur", closePopovers);
};
}, [closePopovers, isPopoverOpen]);

Expand Down
Loading