Refine mine inbox shortcut behavior
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
7d81e4cb2a
commit
403aeff7f6
5 changed files with 39 additions and 19 deletions
|
|
@ -136,8 +136,8 @@ describe("SwipeToArchive", () => {
|
||||||
|
|
||||||
const surface = container.querySelector("[data-inbox-row-surface]") as HTMLDivElement | null;
|
const surface = container.querySelector("[data-inbox-row-surface]") as HTMLDivElement | null;
|
||||||
expect(surface).not.toBeNull();
|
expect(surface).not.toBeNull();
|
||||||
expect(surface?.style.backgroundColor).toBe("hsl(var(--primary) / 0.06)");
|
expect(surface?.style.backgroundColor).toBe("hsl(var(--muted))");
|
||||||
expect(surface?.style.boxShadow).toBe("inset 3px 0 0 hsl(var(--primary))");
|
expect(surface?.style.boxShadow).toBe("");
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,7 @@ export function SwipeToArchive({
|
||||||
style={{
|
style={{
|
||||||
transform: `translate3d(${offsetX}px, 0, 0)`,
|
transform: `translate3d(${offsetX}px, 0, 0)`,
|
||||||
transition: isDragging ? "none" : "transform 180ms ease-out",
|
transition: isDragging ? "none" : "transform 180ms ease-out",
|
||||||
backgroundColor: selected ? "hsl(var(--primary) / 0.06)" : undefined,
|
backgroundColor: selected ? "hsl(var(--muted))" : undefined,
|
||||||
boxShadow: selected ? "inset 3px 0 0 hsl(var(--primary))" : undefined,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
computeInboxBadgeData,
|
computeInboxBadgeData,
|
||||||
getApprovalsForTab,
|
getApprovalsForTab,
|
||||||
getInboxWorkItems,
|
getInboxWorkItems,
|
||||||
|
getInboxKeyboardSelectionIndex,
|
||||||
getRecentTouchedIssues,
|
getRecentTouchedIssues,
|
||||||
getUnreadTouchedIssues,
|
getUnreadTouchedIssues,
|
||||||
isMineInboxTab,
|
isMineInboxTab,
|
||||||
|
|
@ -411,9 +412,15 @@ describe("inbox helpers", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("anchors Mine selection to the first available inbox row", () => {
|
it("anchors Mine selection to the first available inbox row", () => {
|
||||||
expect(resolveInboxSelectionIndex(-1, 3, true)).toBe(0);
|
expect(resolveInboxSelectionIndex(-1, 3)).toBe(-1);
|
||||||
expect(resolveInboxSelectionIndex(-1, 3, false)).toBe(-1);
|
expect(resolveInboxSelectionIndex(5, 3)).toBe(2);
|
||||||
expect(resolveInboxSelectionIndex(5, 3, true)).toBe(2);
|
expect(resolveInboxSelectionIndex(1, 0)).toBe(-1);
|
||||||
expect(resolveInboxSelectionIndex(1, 0, true)).toBe(-1);
|
});
|
||||||
|
|
||||||
|
it("selects the first row only after keyboard navigation starts", () => {
|
||||||
|
expect(getInboxKeyboardSelectionIndex(-1, 3, "next")).toBe(0);
|
||||||
|
expect(getInboxKeyboardSelectionIndex(-1, 3, "previous")).toBe(0);
|
||||||
|
expect(getInboxKeyboardSelectionIndex(0, 3, "next")).toBe(1);
|
||||||
|
expect(getInboxKeyboardSelectionIndex(0, 3, "previous")).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -105,13 +105,24 @@ export function isMineInboxTab(tab: InboxTab): boolean {
|
||||||
export function resolveInboxSelectionIndex(
|
export function resolveInboxSelectionIndex(
|
||||||
previousIndex: number,
|
previousIndex: number,
|
||||||
itemCount: number,
|
itemCount: number,
|
||||||
canSelectItems: boolean,
|
|
||||||
): number {
|
): number {
|
||||||
if (itemCount === 0) return -1;
|
if (itemCount === 0) return -1;
|
||||||
if (previousIndex < 0) return canSelectItems ? 0 : -1;
|
if (previousIndex < 0) return -1;
|
||||||
return Math.min(previousIndex, itemCount - 1);
|
return Math.min(previousIndex, itemCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInboxKeyboardSelectionIndex(
|
||||||
|
previousIndex: number,
|
||||||
|
itemCount: number,
|
||||||
|
direction: "next" | "previous",
|
||||||
|
): number {
|
||||||
|
if (itemCount === 0) return -1;
|
||||||
|
if (previousIndex < 0) return 0;
|
||||||
|
return direction === "next"
|
||||||
|
? Math.min(previousIndex + 1, itemCount - 1)
|
||||||
|
: Math.max(previousIndex - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
export function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] {
|
export function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] {
|
||||||
const sorted = [...runs].sort(
|
const sorted = [...runs].sort(
|
||||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import {
|
||||||
ACTIONABLE_APPROVAL_STATUSES,
|
ACTIONABLE_APPROVAL_STATUSES,
|
||||||
getApprovalsForTab,
|
getApprovalsForTab,
|
||||||
getInboxWorkItems,
|
getInboxWorkItems,
|
||||||
|
getInboxKeyboardSelectionIndex,
|
||||||
getLatestFailedRunsByAgent,
|
getLatestFailedRunsByAgent,
|
||||||
getRecentTouchedIssues,
|
getRecentTouchedIssues,
|
||||||
isMineInboxTab,
|
isMineInboxTab,
|
||||||
|
|
@ -929,10 +930,10 @@ export function Inbox() {
|
||||||
return `join:${item.joinRequest.id}`;
|
return `join:${item.joinRequest.id}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Keep Mine anchored to a real row so keyboard navigation always lands on an item.
|
// Keep selection valid when the list shape changes, but do not auto-select on initial load.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedIndex((prev) => resolveInboxSelectionIndex(prev, workItemsToRender.length, canArchiveFromTab));
|
setSelectedIndex((prev) => resolveInboxSelectionIndex(prev, workItemsToRender.length));
|
||||||
}, [canArchiveFromTab, workItemsToRender.length]);
|
}, [workItemsToRender.length]);
|
||||||
|
|
||||||
// Use refs for keyboard handler to avoid stale closures
|
// Use refs for keyboard handler to avoid stale closures
|
||||||
const kbStateRef = useRef({
|
const kbStateRef = useRef({
|
||||||
|
|
@ -976,13 +977,15 @@ export function Inbox() {
|
||||||
// Keyboard shortcuts (mail-client style) — single stable listener using refs
|
// Keyboard shortcuts (mail-client style) — single stable listener using refs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
|
||||||
// Don't capture when typing in inputs/textareas or with modifier keys
|
// Don't capture when typing in inputs/textareas or with modifier keys
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target;
|
||||||
if (
|
if (
|
||||||
target.tagName === "INPUT" ||
|
!(target instanceof HTMLElement) ||
|
||||||
target.tagName === "TEXTAREA" ||
|
target.closest("input, textarea, select, [contenteditable='true'], [role='textbox'], [role='combobox']") ||
|
||||||
target.tagName === "SELECT" ||
|
|
||||||
target.isContentEditable ||
|
target.isContentEditable ||
|
||||||
|
document.querySelector("[role='dialog'], [aria-modal='true']") ||
|
||||||
e.metaKey ||
|
e.metaKey ||
|
||||||
e.ctrlKey ||
|
e.ctrlKey ||
|
||||||
e.altKey
|
e.altKey
|
||||||
|
|
@ -1002,12 +1005,12 @@ export function Inbox() {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "j": {
|
case "j": {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSelectedIndex((prev) => Math.min(prev + 1, itemCount - 1));
|
setSelectedIndex((prev) => getInboxKeyboardSelectionIndex(prev, itemCount, "next"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "k": {
|
case "k": {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
setSelectedIndex((prev) => getInboxKeyboardSelectionIndex(prev, itemCount, "previous"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "a":
|
case "a":
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue