Fix mine inbox keyboard selection

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-28 16:24:23 -05:00
parent 44f052f4c5
commit 7d81e4cb2a
5 changed files with 53 additions and 12 deletions

View file

@ -122,4 +122,25 @@ describe("SwipeToArchive", () => {
root.unmount(); root.unmount();
}); });
}); });
it("renders the selected inbox treatment on the swipe surface", () => {
const root = createRoot(container);
act(() => {
root.render(
<SwipeToArchive onArchive={() => {}} selected>
<button type="button">Open issue</button>
</SwipeToArchive>,
);
});
const surface = container.querySelector("[data-inbox-row-surface]") as HTMLDivElement | null;
expect(surface).not.toBeNull();
expect(surface?.style.backgroundColor).toBe("hsl(var(--primary) / 0.06)");
expect(surface?.style.boxShadow).toBe("inset 3px 0 0 hsl(var(--primary))");
act(() => {
root.unmount();
});
});
}); });

View file

@ -6,6 +6,7 @@ interface SwipeToArchiveProps {
children: ReactNode; children: ReactNode;
onArchive: () => void; onArchive: () => void;
disabled?: boolean; disabled?: boolean;
selected?: boolean;
className?: string; className?: string;
} }
@ -17,6 +18,7 @@ export function SwipeToArchive({
children, children,
onArchive, onArchive,
disabled = false, disabled = false,
selected = false,
className, className,
}: SwipeToArchiveProps) { }: SwipeToArchiveProps) {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@ -148,10 +150,13 @@ export function SwipeToArchive({
</span> </span>
</div> </div>
<div <div
data-inbox-row-surface
className="relative bg-card will-change-transform" className="relative bg-card will-change-transform"
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,
boxShadow: selected ? "inset 3px 0 0 hsl(var(--primary))" : undefined,
}} }}
> >
{children} {children}

View file

@ -11,6 +11,7 @@ import {
isMineInboxTab, isMineInboxTab,
loadLastInboxTab, loadLastInboxTab,
RECENT_ISSUES_LIMIT, RECENT_ISSUES_LIMIT,
resolveInboxSelectionIndex,
saveLastInboxTab, saveLastInboxTab,
shouldShowInboxSection, shouldShowInboxSection,
} from "./inbox"; } from "./inbox";
@ -408,4 +409,11 @@ describe("inbox helpers", () => {
expect(isMineInboxTab("unread")).toBe(false); expect(isMineInboxTab("unread")).toBe(false);
expect(isMineInboxTab("all")).toBe(false); expect(isMineInboxTab("all")).toBe(false);
}); });
it("anchors Mine selection to the first available inbox row", () => {
expect(resolveInboxSelectionIndex(-1, 3, true)).toBe(0);
expect(resolveInboxSelectionIndex(-1, 3, false)).toBe(-1);
expect(resolveInboxSelectionIndex(5, 3, true)).toBe(2);
expect(resolveInboxSelectionIndex(1, 0, true)).toBe(-1);
});
}); });

View file

@ -102,6 +102,16 @@ export function isMineInboxTab(tab: InboxTab): boolean {
return tab === "mine"; return tab === "mine";
} }
export function resolveInboxSelectionIndex(
previousIndex: number,
itemCount: number,
canSelectItems: boolean,
): number {
if (itemCount === 0) return -1;
if (previousIndex < 0) return canSelectItems ? 0 : -1;
return Math.min(previousIndex, itemCount - 1);
}
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(),

View file

@ -50,6 +50,7 @@ import {
getLatestFailedRunsByAgent, getLatestFailedRunsByAgent,
getRecentTouchedIssues, getRecentTouchedIssues,
isMineInboxTab, isMineInboxTab,
resolveInboxSelectionIndex,
InboxApprovalFilter, InboxApprovalFilter,
saveLastInboxTab, saveLastInboxTab,
shouldShowInboxSection, shouldShowInboxSection,
@ -928,12 +929,10 @@ export function Inbox() {
return `join:${item.joinRequest.id}`; return `join:${item.joinRequest.id}`;
}, []); }, []);
// Reset selection when the list changes // Keep Mine anchored to a real row so keyboard navigation always lands on an item.
useEffect(() => { useEffect(() => {
setSelectedIndex((prev) => setSelectedIndex((prev) => resolveInboxSelectionIndex(prev, workItemsToRender.length, canArchiveFromTab));
prev >= workItemsToRender.length ? workItemsToRender.length - 1 : prev, }, [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({
@ -1233,15 +1232,9 @@ export function Inbox() {
<div <div
key={`sel-${key}`} key={`sel-${key}`}
data-inbox-item data-inbox-item
className={cn( className="relative"
"relative",
isSelected && "bg-primary/[0.06] [&>*]:bg-transparent",
)}
onClick={() => setSelectedIndex(index)} onClick={() => setSelectedIndex(index)}
> >
{isSelected && (
<div className="absolute inset-y-0 left-0 w-[3px] bg-primary rounded-r-sm" />
)}
{child} {child}
</div> </div>
); );
@ -1289,6 +1282,7 @@ export function Inbox() {
elements.push(wrapItem(approvalKey, isSelected, canArchiveFromTab ? ( elements.push(wrapItem(approvalKey, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={approvalKey} key={approvalKey}
selected={isSelected}
disabled={isArchiving} disabled={isArchiving}
onArchive={() => handleArchiveNonIssue(approvalKey)} onArchive={() => handleArchiveNonIssue(approvalKey)}
> >
@ -1325,6 +1319,7 @@ export function Inbox() {
elements.push(wrapItem(runKey, isSelected, canArchiveFromTab ? ( elements.push(wrapItem(runKey, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={runKey} key={runKey}
selected={isSelected}
disabled={isArchiving} disabled={isArchiving}
onArchive={() => handleArchiveNonIssue(runKey)} onArchive={() => handleArchiveNonIssue(runKey)}
> >
@ -1358,6 +1353,7 @@ export function Inbox() {
elements.push(wrapItem(joinKey, isSelected, canArchiveFromTab ? ( elements.push(wrapItem(joinKey, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={joinKey} key={joinKey}
selected={isSelected}
disabled={isArchiving} disabled={isArchiving}
onArchive={() => handleArchiveNonIssue(joinKey)} onArchive={() => handleArchiveNonIssue(joinKey)}
> >
@ -1428,6 +1424,7 @@ export function Inbox() {
elements.push(wrapItem(`issue:${issue.id}`, isSelected, canArchiveFromTab ? ( elements.push(wrapItem(`issue:${issue.id}`, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={`issue:${issue.id}`} key={`issue:${issue.id}`}
selected={isSelected}
disabled={isArchiving || archiveIssueMutation.isPending} disabled={isArchiving || archiveIssueMutation.isPending}
onArchive={() => archiveIssueMutation.mutate(issue.id)} onArchive={() => archiveIssueMutation.mutate(issue.id)}
> >