Skip to content

fix(inbox): re-seat keyboard cursor on click#2308

Open
oliverb123 wants to merge 3 commits into
mainfrom
posthog-code/fix-inbox-keyboard-cursor-drift
Open

fix(inbox): re-seat keyboard cursor on click#2308
oliverb123 wants to merge 3 commits into
mainfrom
posthog-code/fix-inbox-keyboard-cursor-drift

Conversation

@oliverb123
Copy link
Copy Markdown
Contributor

Summary

  • Arrow-key navigation in the inbox kept a local keyboardCursorIdRef that was only written by the keyboard handler. After a click, the ref still pointed at the previous keyboard position, so the next arrow press navigated from there instead of from the just-clicked report.
  • Extracted the navigation into useInboxKeyboardNavigation, which subscribes to lastClickedId in the selection store and re-seats the cursor on every click. Shift+Arrow still walks the cursor independently (it deliberately doesn't touch lastClickedId, which stays the anchor for range extension).
  • 16 regression tests, including the exact scenario from the bug thread. Verified they fail (10/16) when the sync effect is removed.

Test plan

  • pnpm exec vitest run src/renderer/features/inbox/ — 81/81 pass
  • pnpm --filter code typecheck
  • pnpm exec biome check on changed files
  • Manual smoke: click a report, arrow-key, confirm neighbour is selected
  • Shift+Arrow extends a range and Shift+ArrowUp contracts it

Arrow-key navigation in the inbox kept a local `keyboardCursorIdRef` that was only written by the keyboard handler. Clicking a report changed the store selection but left the ref pointing at the previous keyboard position, so the next arrow press navigated from there instead of from the just-clicked report.

Extracted the navigation into `useInboxKeyboardNavigation`, which subscribes to `lastClickedId` in the selection store and re-seats the cursor whenever a click moves it. Shift+Arrow still walks the cursor independently (it deliberately doesn't touch `lastClickedId`, which stays the anchor for range extension).

Added 16 regression tests covering arrow-after-click for plain, cmd, shift clicks; shift+arrow range extension and contraction; bounds; and remount.

Generated-By: PostHog Code
Task-Id: 875c2ff4-ae68-49c8-9aed-46a832256901
@oliverb123 oliverb123 requested a review from a team May 22, 2026 18:58
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/code/src/renderer/features/inbox/hooks/useInboxKeyboardNavigation.test.tsx:64-86
The two "empty selection" tests exercise the same body with `1` and `-1`; prefer `it.each` per the team's parameterised-tests rule.

```suggestion
    it.each<[1 | -1, string]>([
      [1, "ArrowDown"],
      [-1, "ArrowUp"],
    ])("%s selects the first report when nothing is selected", (direction) => {
      const { result } = renderHook(() =>
        useInboxKeyboardNavigation({ reports: REPORTS }),
      );

      act(() => {
        result.current.navigateReport(direction, false);
      });

      expect(getSelection()).toEqual(["a"]);
    });
```

### Issue 2 of 2
apps/code/src/renderer/features/inbox/hooks/useInboxKeyboardNavigation.test.tsx:219-245
Same pattern in the bounds block — `ArrowDown at last` and `ArrowUp at first` are mirror images that collapse naturally into a single `it.each`.

```suggestion
    it.each<[1 | -1, string]>([
      [1, "e"],
      [-1, "a"],
    ])("direction %i at the boundary stays on the same report", (direction, reportId) => {
      const { result } = renderHook(() =>
        useInboxKeyboardNavigation({ reports: REPORTS }),
      );

      plainClick(reportId);

      act(() => {
        result.current.navigateReport(direction, false);
      });

      expect(getSelection()).toEqual([reportId]);
    });
```

Reviews (1): Last reviewed commit: "fix(inbox): re-seat keyboard cursor on c..." | Re-trigger Greptile

Comment on lines +64 to +86
it("ArrowDown selects the first report", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);

act(() => {
result.current.navigateReport(1, false);
});

expect(getSelection()).toEqual(["a"]);
});

it("ArrowUp also selects the first report when nothing is selected", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);

act(() => {
result.current.navigateReport(-1, false);
});

expect(getSelection()).toEqual(["a"]);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The two "empty selection" tests exercise the same body with 1 and -1; prefer it.each per the team's parameterised-tests rule.

Suggested change
it("ArrowDown selects the first report", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);
act(() => {
result.current.navigateReport(1, false);
});
expect(getSelection()).toEqual(["a"]);
});
it("ArrowUp also selects the first report when nothing is selected", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);
act(() => {
result.current.navigateReport(-1, false);
});
expect(getSelection()).toEqual(["a"]);
});
it.each<[1 | -1, string]>([
[1, "ArrowDown"],
[-1, "ArrowUp"],
])("%s selects the first report when nothing is selected", (direction) => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);
act(() => {
result.current.navigateReport(direction, false);
});
expect(getSelection()).toEqual(["a"]);
});

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/inbox/hooks/useInboxKeyboardNavigation.test.tsx
Line: 64-86

Comment:
The two "empty selection" tests exercise the same body with `1` and `-1`; prefer `it.each` per the team's parameterised-tests rule.

```suggestion
    it.each<[1 | -1, string]>([
      [1, "ArrowDown"],
      [-1, "ArrowUp"],
    ])("%s selects the first report when nothing is selected", (direction) => {
      const { result } = renderHook(() =>
        useInboxKeyboardNavigation({ reports: REPORTS }),
      );

      act(() => {
        result.current.navigateReport(direction, false);
      });

      expect(getSelection()).toEqual(["a"]);
    });
```

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +219 to +245
it("ArrowDown on the last report stays on the last report", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);

plainClick("e");

act(() => {
result.current.navigateReport(1, false);
});

expect(getSelection()).toEqual(["e"]);
});

it("ArrowUp on the first report stays on the first report", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);

plainClick("a");

act(() => {
result.current.navigateReport(-1, false);
});

expect(getSelection()).toEqual(["a"]);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Same pattern in the bounds block — ArrowDown at last and ArrowUp at first are mirror images that collapse naturally into a single it.each.

Suggested change
it("ArrowDown on the last report stays on the last report", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);
plainClick("e");
act(() => {
result.current.navigateReport(1, false);
});
expect(getSelection()).toEqual(["e"]);
});
it("ArrowUp on the first report stays on the first report", () => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);
plainClick("a");
act(() => {
result.current.navigateReport(-1, false);
});
expect(getSelection()).toEqual(["a"]);
});
it.each<[1 | -1, string]>([
[1, "e"],
[-1, "a"],
])("direction %i at the boundary stays on the same report", (direction, reportId) => {
const { result } = renderHook(() =>
useInboxKeyboardNavigation({ reports: REPORTS }),
);
plainClick(reportId);
act(() => {
result.current.navigateReport(direction, false);
});
expect(getSelection()).toEqual([reportId]);
});

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/inbox/hooks/useInboxKeyboardNavigation.test.tsx
Line: 219-245

Comment:
Same pattern in the bounds block — `ArrowDown at last` and `ArrowUp at first` are mirror images that collapse naturally into a single `it.each`.

```suggestion
    it.each<[1 | -1, string]>([
      [1, "e"],
      [-1, "a"],
    ])("direction %i at the boundary stays on the same report", (direction, reportId) => {
      const { result } = renderHook(() =>
        useInboxKeyboardNavigation({ reports: REPORTS }),
      );

      plainClick(reportId);

      act(() => {
        result.current.navigateReport(direction, false);
      });

      expect(getSelection()).toEqual([reportId]);
    });
```

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Greptile flagged the ArrowDown/ArrowUp empty-selection pair and the boundary pair as identical apart from the direction. Combine each pair into a single `it.each`.

Generated-By: PostHog Code
Task-Id: 875c2ff4-ae68-49c8-9aed-46a832256901
Paul's suggestion in the thread was a component-level test that passes keyboard input and asserts on DOM state. The original PR went one level lower with `renderHook`, which exercises the hook contract but skips the wiring between window keydown events, click handlers, and the selection store.

Adds a `TestInbox` harness that mirrors `InboxSignalsTab`'s window keydown handler and click dispatch, then drives it via `fireEvent.click` and `fireEvent.keyDown`. Covers the exact bug scenario (click → ArrowDown selects the report below the clicked one) plus shift/cmd-click and shift+arrow range behaviors.

Also drops the contrived multi-instance test — it exercised the seeding logic rather than any user-visible behavior.

Verified by temporarily disabling the cursor-sync effect: 16/22 tests fail, including all 6 new behavioral tests for the bug scenario.

Generated-By: PostHog Code
Task-Id: 875c2ff4-ae68-49c8-9aed-46a832256901
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant