Add issue-detail g i inbox shortcut

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-04-08 08:27:34 -05:00
parent ede3206423
commit 69ff793c6a
3 changed files with 163 additions and 1 deletions
+46
View File
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import {
hasBlockingShortcutDialog,
isKeyboardShortcutTextInputTarget,
resolveGoToInboxKeyAction,
resolveInboxQuickArchiveKeyAction,
} from "./keyboardShortcuts";
@@ -109,4 +110,49 @@ describe("keyboardShortcuts helpers", () => {
hasOpenDialog: false,
})).toBe("disarm");
});
it("arms go-to-inbox on a clean g press", () => {
const button = document.createElement("button");
expect(resolveGoToInboxKeyAction({
armed: false,
defaultPrevented: false,
key: "g",
metaKey: false,
ctrlKey: false,
altKey: false,
target: button,
hasOpenDialog: false,
})).toBe("arm");
});
it("navigates to inbox on i after g", () => {
const button = document.createElement("button");
expect(resolveGoToInboxKeyAction({
armed: true,
defaultPrevented: false,
key: "i",
metaKey: false,
ctrlKey: false,
altKey: false,
target: button,
hasOpenDialog: false,
})).toBe("navigate");
});
it("disarms go-to-inbox instead of firing from an editor", () => {
const input = document.createElement("textarea");
expect(resolveGoToInboxKeyAction({
armed: true,
defaultPrevented: false,
key: "i",
metaKey: false,
ctrlKey: false,
altKey: false,
target: input,
hasOpenDialog: false,
})).toBe("disarm");
});
});
+33
View File
@@ -11,6 +11,7 @@ export const KEYBOARD_SHORTCUT_TEXT_INPUT_SELECTOR = [
const MODIFIER_ONLY_KEYS = new Set(["Shift", "Meta", "Control", "Alt"]);
export type InboxQuickArchiveKeyAction = "ignore" | "archive" | "disarm";
export type GoToInboxKeyAction = "ignore" | "arm" | "navigate" | "disarm";
export function isKeyboardShortcutTextInputTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
@@ -52,3 +53,35 @@ export function resolveInboxQuickArchiveKeyAction({
if (key === "y") return "archive";
return "disarm";
}
export function resolveGoToInboxKeyAction({
armed,
defaultPrevented,
key,
metaKey,
ctrlKey,
altKey,
target,
hasOpenDialog,
}: {
armed: boolean;
defaultPrevented: boolean;
key: string;
metaKey: boolean;
ctrlKey: boolean;
altKey: boolean;
target: EventTarget | null;
hasOpenDialog: boolean;
}): GoToInboxKeyAction {
if (defaultPrevented) return armed ? "disarm" : "ignore";
if (metaKey || ctrlKey || altKey || isModifierOnlyKey(key)) return "ignore";
if (hasOpenDialog || isKeyboardShortcutTextInputTarget(target)) {
return armed ? "disarm" : "ignore";
}
const normalizedKey = key.toLowerCase();
if (!armed) return normalizedKey === "g" ? "arm" : "ignore";
if (normalizedKey === "i") return "navigate";
if (normalizedKey === "g") return "arm";
return "disarm";
}
+84 -1
View File
@@ -26,7 +26,11 @@ import {
rememberIssueDetailLocationState,
shouldArmIssueDetailInboxQuickArchive,
} from "../lib/issueDetailBreadcrumb";
import { hasBlockingShortcutDialog, resolveInboxQuickArchiveKeyAction } from "../lib/keyboardShortcuts";
import {
hasBlockingShortcutDialog,
resolveGoToInboxKeyAction,
resolveInboxQuickArchiveKeyAction,
} from "../lib/keyboardShortcuts";
import {
applyOptimisticIssueFieldUpdate,
applyOptimisticIssueFieldUpdateToCollection,
@@ -1232,6 +1236,8 @@ export function IssueDetail() {
}, [closePanel, handleIssuePropertiesUpdate, issuePanelKey, openNewSubIssue, openPanel]);
const inboxQuickArchiveArmedRef = useRef(false);
const goToInboxShortcutArmedRef = useRef(false);
const goToInboxShortcutTimeoutRef = useRef<number | null>(null);
const canQuickArchiveFromInbox =
keyboardShortcutsEnabled &&
!issue?.hiddenAt &&
@@ -1301,6 +1307,83 @@ export function IssueDetail() {
};
}, [archiveFromInbox, canQuickArchiveFromInbox, issue?.id]);
useEffect(() => {
if (!keyboardShortcutsEnabled) {
goToInboxShortcutArmedRef.current = false;
if (goToInboxShortcutTimeoutRef.current !== null) {
window.clearTimeout(goToInboxShortcutTimeoutRef.current);
goToInboxShortcutTimeoutRef.current = null;
}
return;
}
const clearArmTimeout = () => {
if (goToInboxShortcutTimeoutRef.current !== null) {
window.clearTimeout(goToInboxShortcutTimeoutRef.current);
goToInboxShortcutTimeoutRef.current = null;
}
};
const disarm = () => {
goToInboxShortcutArmedRef.current = false;
clearArmTimeout();
};
const arm = () => {
goToInboxShortcutArmedRef.current = true;
clearArmTimeout();
goToInboxShortcutTimeoutRef.current = window.setTimeout(() => {
goToInboxShortcutArmedRef.current = false;
goToInboxShortcutTimeoutRef.current = null;
}, 1200);
};
const handlePointerDown = () => {
disarm();
};
const handleFocusIn = (event: FocusEvent) => {
if (event.target instanceof HTMLElement && event.target !== document.body) {
disarm();
}
};
const handleKeyDown = (event: KeyboardEvent) => {
const action = resolveGoToInboxKeyAction({
armed: goToInboxShortcutArmedRef.current,
defaultPrevented: event.defaultPrevented,
key: event.key,
metaKey: event.metaKey,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
target: event.target,
hasOpenDialog: hasBlockingShortcutDialog(document),
});
if (action === "ignore") return;
if (action === "arm") {
arm();
return;
}
disarm();
if (action !== "navigate") return;
event.preventDefault();
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox");
};
document.addEventListener("pointerdown", handlePointerDown, true);
document.addEventListener("focusin", handleFocusIn, true);
document.addEventListener("keydown", handleKeyDown, true);
return () => {
disarm();
document.removeEventListener("pointerdown", handlePointerDown, true);
document.removeEventListener("focusin", handleFocusIn, true);
document.removeEventListener("keydown", handleKeyDown, true);
};
}, [keyboardShortcutsEnabled, navigate, sourceBreadcrumb.href]);
const isImageAttachment = (attachment: IssueAttachment) => attachment.contentType.startsWith("image/");
const attachmentList = attachments ?? [];
const imageAttachments = attachmentList.filter(isImageAttachment);