Skip to content

Commit 7dbd67d

Browse files
Add note pane keyboard navigation
1 parent 7462afe commit 7dbd67d

4 files changed

Lines changed: 97 additions & 0 deletions

File tree

app/src/features/editor/note-editor.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,14 @@ export const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
521521
return true;
522522
},
523523
},
524+
{
525+
key: "Escape",
526+
run(view) {
527+
useShellStore.getState().setFocusedPane("notes");
528+
blurEditorView(view);
529+
return true;
530+
},
531+
},
524532
{
525533
key: "Mod-a",
526534
run(view) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { getAdjacentNoteId } from "@/features/notes/lib/note-list-navigation";
4+
5+
const notes = [{ id: "note-1" }, { id: "note-2" }, { id: "note-3" }];
6+
7+
describe("getAdjacentNoteId", () => {
8+
it("returns the next note id when moving forward", () => {
9+
expect(getAdjacentNoteId(notes, "note-2", "next")).toBe("note-3");
10+
});
11+
12+
it("returns the previous note id when moving backward", () => {
13+
expect(getAdjacentNoteId(notes, "note-2", "previous")).toBe("note-1");
14+
});
15+
16+
it("returns null when moving past the end of the list", () => {
17+
expect(getAdjacentNoteId(notes, "note-3", "next")).toBeNull();
18+
});
19+
20+
it("returns null when moving before the start of the list", () => {
21+
expect(getAdjacentNoteId(notes, "note-1", "previous")).toBeNull();
22+
});
23+
24+
it("returns null when the current note is not in the list", () => {
25+
expect(getAdjacentNoteId(notes, "missing-note", "next")).toBeNull();
26+
});
27+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export type NoteListNavigationDirection = "next" | "previous";
2+
3+
type NoteListItem = {
4+
id: string;
5+
};
6+
7+
export function getAdjacentNoteId<T extends NoteListItem>(
8+
notes: T[],
9+
currentNoteId: string,
10+
direction: NoteListNavigationDirection,
11+
) {
12+
const currentIndex = notes.findIndex((note) => note.id === currentNoteId);
13+
if (currentIndex === -1) {
14+
return null;
15+
}
16+
17+
const nextIndex = direction === "next" ? currentIndex + 1 : currentIndex - 1;
18+
19+
return notes[nextIndex]?.id ?? null;
20+
}

app/src/features/notes/ui/notes-pane.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ import { useInView } from "react-intersection-observer";
3232

3333
import { Button } from "@/shared/ui/button";
3434
import { searchWordsFromQuery } from "@/shared/lib/search";
35+
import {
36+
type NoteListNavigationDirection,
37+
getAdjacentNoteId,
38+
} from "@/features/notes/lib/note-list-navigation";
3539
import { useShellStore } from "@/features/shell/store/use-shell-store";
3640

3741
import {
@@ -378,6 +382,7 @@ type NoteRowProps = {
378382
isSearchFocused: boolean;
379383
note: NoteSummary;
380384
onContextMenu(event: MouseEvent<HTMLButtonElement>, note: NoteSummary): void;
385+
onMoveSelection(direction: NoteListNavigationDirection): void;
381386
onSelectNote(noteId: string): void;
382387
searchWords: string[];
383388
selectedNoteId: string | null;
@@ -437,6 +442,7 @@ const NoteRow = memo(function NoteRow({
437442
isSearchFocused,
438443
note,
439444
onContextMenu,
445+
onMoveSelection,
440446
onSelectNote,
441447
searchWords,
442448
selectedNoteId,
@@ -474,6 +480,18 @@ const NoteRow = memo(function NoteRow({
474480
onFocus={() => {
475481
useShellStore.getState().setFocusedPane("notes");
476482
}}
483+
onKeyDown={(event) => {
484+
if (event.key === "ArrowDown") {
485+
event.preventDefault();
486+
onMoveSelection("next");
487+
return;
488+
}
489+
490+
if (event.key === "ArrowUp") {
491+
event.preventDefault();
492+
onMoveSelection("previous");
493+
}
494+
}}
477495
onPointerDown={(event) => {
478496
handleNoteRowPointerDown(event);
479497
setShouldRestoreSelectedRowFocus();
@@ -619,6 +637,27 @@ export function NotesPane({
619637
[onChangeSearch],
620638
);
621639

640+
const handleMoveSelection = useCallback(
641+
(currentNoteId: string, direction: NoteListNavigationDirection) => {
642+
if (isMutatingNote) {
643+
return;
644+
}
645+
646+
const nextNoteId = getAdjacentNoteId(
647+
filteredNotes,
648+
currentNoteId,
649+
direction,
650+
);
651+
if (!nextNoteId) {
652+
return;
653+
}
654+
655+
setIsSearchFocused(false);
656+
onSelectNote(nextNoteId);
657+
},
658+
[filteredNotes, isMutatingNote, onSelectNote],
659+
);
660+
622661
useEffect(() => {
623662
if (searchQuery) {
624663
setIsSearchOpen(true);
@@ -817,6 +856,9 @@ export function NotesPane({
817856
key={note.id}
818857
note={note}
819858
onContextMenu={handleNoteContextMenu}
859+
onMoveSelection={(direction) => {
860+
handleMoveSelection(note.id, direction);
861+
}}
820862
onSelectNote={(noteId) => {
821863
setIsSearchFocused(false);
822864
onSelectNote(noteId);

0 commit comments

Comments
 (0)