fix(web): ignore title shortcuts while a dialog or menu is open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-06 12:23:04 -04:00
1 parent f36820321c
commit 4eadce286b
3 files changed
+80 -10

No files matched your search

@@ -2,10 +2,16 @@ import { useHotkey } from "@tanstack/react-hotkeys";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { commandPaletteOpenAtom } from "@/lib/atoms/command-palette"; import { commandPaletteOpenAtom } from "@/lib/atoms/command-palette";
import { isOverlayOpen } from "@/lib/overlays";
import { useTitleContext, useTitleUserInfo } from "./title-context"; import { useTitleContext, useTitleUserInfo } from "./title-context";
import { useTitleActions } from "./use-title-actions"; import { useTitleActions } from "./use-title-actions";
const whenNoOverlay = (fn: () => void) => () => {
if (isOverlayOpen()) return;
fn();
};
export function TitleKeyboardShortcuts() { export function TitleKeyboardShortcuts() {
const { titleType } = useTitleContext(); const { titleType } = useTitleContext();
const { userStatus } = useTitleUserInfo(); const { userStatus } = useTitleUserInfo();
@@ -16,12 +22,16 @@ export function TitleKeyboardShortcuts() {
const enabled = !commandPaletteOpen; const enabled = !commandPaletteOpen;
// W: toggle watchlist (add if not in library, remove if in library) // W: toggle watchlist (add if not in library, remove if in library)
useHotkey("W", () => handleStatusChange(userStatus ? null : "watchlist"), { useHotkey(
"W",
whenNoOverlay(() => handleStatusChange(userStatus ? null : "watchlist")),
{
enabled, enabled,
}); },
);
useHotkey( useHotkey(
"M", "M",
() => { whenNoOverlay(() => {
if (titleType === "movie") { if (titleType === "movie") {
if (userStatus === "completed") { if (userStatus === "completed") {
handleUnwatchMovie(); handleUnwatchMovie();
@@ -29,16 +39,40 @@ export function TitleKeyboardShortcuts() {
handleWatchMovie(); handleWatchMovie();
} }
} }
}, }),
{ enabled },
);
useHotkey(
"Escape",
whenNoOverlay(() => window.history.back()),
{ enabled }, { enabled },
); );
useHotkey("Escape", () => window.history.back(), { enabled });
useHotkey("1", () => handleRating(1), { enabled }); useHotkey(
useHotkey("2", () => handleRating(2), { enabled }); "1",
useHotkey("3", () => handleRating(3), { enabled }); whenNoOverlay(() => handleRating(1)),
useHotkey("4", () => handleRating(4), { enabled }); { enabled },
useHotkey("5", () => handleRating(5), { enabled }); );
useHotkey(
"2",
whenNoOverlay(() => handleRating(2)),
{ enabled },
);
useHotkey(
"3",
whenNoOverlay(() => handleRating(3)),
{ enabled },
);
useHotkey(
"4",
whenNoOverlay(() => handleRating(4)),
{ enabled },
);
useHotkey(
"5",
whenNoOverlay(() => handleRating(5)),
{ enabled },
);
return null; return null;
} }
+30
View File
@@ -0,0 +1,30 @@
import { afterEach, describe, expect, test } from "vitest";
import { isOverlayOpen } from "./overlays";
describe("isOverlayOpen", () => {
const added: HTMLElement[] = [];
function add(role: string) {
const el = document.createElement("div");
el.setAttribute("role", role);
document.body.appendChild(el);
added.push(el);
return el;
}
afterEach(() => {
for (const el of added.splice(0)) el.remove();
});
test("false when nothing is open", () => {
expect(isOverlayOpen()).toBe(false);
});
test.each(["dialog", "alertdialog", "menu"])("true with role=%s", (role) => {
const el = add(role);
expect(isOverlayOpen()).toBe(true);
el.remove();
expect(isOverlayOpen()).toBe(false);
});
});
+6
View File
@@ -0,0 +1,6 @@
const OVERLAY_SELECTOR = '[role="dialog"], [role="alertdialog"], [role="menu"]';
/** True while any dialog, alert dialog, popover or menu is open on the page. */
export function isOverlayOpen(root: ParentNode = document): boolean {
return root.querySelector(OVERLAY_SELECTOR) !== null;
}