Sync inbox hover with j/k selection

Hovering an inbox row now sets it as the selected index used by the
mail-client j/k keyboard shortcuts, so the keyboard cursor follows the
mouse instead of disagreeing with it.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-19 14:26:51 -05:00
1 parent af7c2bdff5
commit 575311283d
2 files changed
+68 -1

No files matched your search

+58
View File
@@ -138,6 +138,11 @@ vi.mock("@/lib/router", () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
// jsdom doesn't implement scrollIntoView; the inbox calls it from a passive effect.
if (typeof Element !== "undefined" && !Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {};
}
function createIssue(overrides: Partial<Issue> = {}): Issue {
return {
id: "issue-1",
@@ -289,6 +294,59 @@ describe("Inbox toolbar", () => {
root.unmount();
});
});
it("syncs hover with j/k selection on inbox rows", async () => {
routerMock.location.pathname = "/inbox/mine";
const issueA = createIssue({ id: "issue-a", identifier: "PAP-1001", title: "First inbox row" });
const issueB = createIssue({ id: "issue-b", identifier: "PAP-1002", title: "Second inbox row" });
apiMocks.issuesList.mockResolvedValue([issueA, issueB]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await act(async () => {
await Promise.resolve();
});
const rows = container.querySelectorAll("[data-inbox-item]");
expect(rows.length).toBeGreaterThanOrEqual(2);
const linkOf = (row: Element): HTMLAnchorElement | null =>
row.querySelector("a[data-inbox-issue-link]");
// Nothing selected before hover — both rows show the hover-accent class.
expect(linkOf(rows[0]!)?.className).toContain("hover:bg-accent/50");
expect(linkOf(rows[1]!)?.className).toContain("hover:bg-accent/50");
await act(async () => {
rows[1]!.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
});
// After hovering row 1, that row is "selected" — same visual state as j/k selection.
expect(linkOf(rows[1]!)?.className).toContain("hover:bg-transparent");
expect(linkOf(rows[0]!)?.className).toContain("hover:bg-accent/50");
await act(async () => {
rows[0]!.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
});
// Hovering a different row moves the selection to follow the mouse.
expect(linkOf(rows[0]!)?.className).toContain("hover:bg-transparent");
expect(linkOf(rows[1]!)?.className).toContain("hover:bg-accent/50");
act(() => {
root.unmount();
});
});
});
describe("FailedRunInboxRow", () => {
+10 -1
View File
@@ -2440,6 +2440,9 @@ export function Inbox() {
onClick={() => {
if (groupNavIdx >= 0) setSelectedIndex(groupNavIdx);
}}
onMouseEnter={() => {
if (groupNavIdx >= 0) setSelectedIndex(groupNavIdx);
}}
>
<IssueGroupHeader
label={group.label}
@@ -2476,6 +2479,7 @@ export function Inbox() {
data-inbox-item
className="relative"
onClick={() => setSelectedIndex(navIdx)}
onMouseEnter={() => setSelectedIndex(navIdx)}
>
{child}
</div>
@@ -2643,7 +2647,12 @@ export function Inbox() {
key={`sel-issue:${child.id}`}
data-inbox-item
className="relative"
onClick={() => setSelectedIndex(childNavIdx)}
onClick={() => {
if (childNavIdx >= 0) setSelectedIndex(childNavIdx);
}}
onMouseEnter={() => {
if (childNavIdx >= 0) setSelectedIndex(childNavIdx);
}}
>
{canArchiveIssue ? (
<SwipeToArchive