Fix mine inbox keyboard selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
44f052f4c5
commit
7d81e4cb2a
5 changed files with 53 additions and 12 deletions
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue