Skip to content

Commit 96d48ef

Browse files
authored
Auto-scroll to active note (#352)
feat: add auto-scroll to active note in list
1 parent a6f5a6d commit 96d48ef

File tree

4 files changed

+47
-20
lines changed

4 files changed

+47
-20
lines changed

src/components/NoteManager/NoteManager.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function Notes() {
117117
[],
118118
);
119119

120-
const isActiveNotes = notes.some((note) => activeNotes.includes(note));
120+
const isActiveNotes = notes.some((note) => activeNotes.has(note));
121121

122122
useEffect(() => {
123123
if (notesReady) {

src/components/NoteManager/components/Note.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Button } from "@fluffylabs/shared-ui";
2-
import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
2+
import { type ChangeEvent, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import { validateMath } from "../../../utils/validateMath";
44
import { useVersionContext } from "../../LocationProvider/VersionProvider";
55
import { useGetLocationParamsToHash } from "../../LocationProvider/hooks/useGetLocationParamsToHash";
@@ -13,19 +13,20 @@ import { NoteLink } from "./NoteLink";
1313
import { NoteContainer } from "./SimpleComponents/NoteContainer";
1414

1515
export type NotesItem = {
16-
location: string; // serialized InDocSelection
16+
location: string;
1717
content: string;
1818
};
1919

2020
type NoteProps = {
21+
ref?: RefObject<HTMLDivElement | null>;
2122
note: IDecoratedNote;
2223
active: boolean;
2324
onEditNote: INotesContext["handleUpdateNote"];
2425
onDeleteNote: INotesContext["handleDeleteNote"];
2526
onSelectNote: (note: IDecoratedNote, opts: { type: "currentVersion" | "originalVersion" | "close" }) => void;
2627
};
2728

28-
export function Note({ note, active = false, onEditNote, onDeleteNote, onSelectNote }: NoteProps) {
29+
export function Note({ ref, note, active = false, onEditNote, onDeleteNote, onSelectNote }: NoteProps) {
2930
const [isEditing, setIsEditing] = useState(false);
3031

3132
const [noteDirty, setNoteDirty] = useState<IStorageNote>({
@@ -189,7 +190,8 @@ export function Note({ note, active = false, onEditNote, onDeleteNote, onSelectN
189190
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
190191
const mousePositionRef = useRef({ x: 0, y: 0 });
191192

192-
const noteRef = useRef<HTMLDivElement>(null);
193+
const internalNoteRef = useRef<HTMLDivElement>(null);
194+
const noteRef = ref ? ref : internalNoteRef;
193195

194196
useEffect(() => {
195197
if (!isDropdownOpen) return;
Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo } from "react";
1+
import { memo, useEffect, useRef } from "react";
22
import type { IDecoratedNote } from "../../NotesProvider/types/DecoratedNote";
33
import type { IStorageNote } from "../../NotesProvider/types/StorageNote";
44
import { Note } from "./Note";
@@ -13,23 +13,43 @@ export const NotesList = ({
1313
onSelectNote,
1414
}: {
1515
notes: IDecoratedNote[];
16-
activeNotes: IDecoratedNote[];
16+
activeNotes: Set<IDecoratedNote>;
1717
onEditNote: (noteToReplace: IDecoratedNote, newNote: IStorageNote) => void;
1818
onDeleteNote: (noteToDelete: IDecoratedNote) => void;
1919
onSelectNote: (note: IDecoratedNote, opts: { type: "currentVersion" | "originalVersion" | "close" }) => void;
2020
}) => {
21+
const noteToScrollToRef = useRef<HTMLDivElement>(null);
22+
23+
useEffect(() => {
24+
if (activeNotes.size > 0 && noteToScrollToRef.current && !isMostlyVisible(noteToScrollToRef.current)) {
25+
noteToScrollToRef.current.scrollIntoView({ behavior: "smooth" });
26+
}
27+
}, [activeNotes]);
28+
2129
return (
2230
<>
23-
{notes.map((note) => (
24-
<MemoizedNote
25-
key={note.key}
26-
active={activeNotes.includes(note)}
27-
note={note}
28-
onEditNote={onEditNote}
29-
onDeleteNote={onDeleteNote}
30-
onSelectNote={onSelectNote}
31-
/>
32-
))}
31+
{notes.map((note) => {
32+
const active = activeNotes.has(note);
33+
const isFirstActive = active && activeNotes.values().next().value === note;
34+
35+
return (
36+
<MemoizedNote
37+
ref={isFirstActive ? noteToScrollToRef : undefined}
38+
key={note.key}
39+
active={active}
40+
note={note}
41+
onEditNote={onEditNote}
42+
onDeleteNote={onDeleteNote}
43+
onSelectNote={onSelectNote}
44+
/>
45+
);
46+
})}
3347
</>
3448
);
3549
};
50+
51+
const isMostlyVisible = (el: HTMLElement) => {
52+
const rect = el.getBoundingClientRect();
53+
const visibleHeight = Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0);
54+
return visibleHeight >= rect.height * 0.6;
55+
};

src/components/NotesProvider/NotesProvider.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface INotesContext {
2222
setNotesPinned: (v: boolean) => void;
2323
notesReady: boolean;
2424
notes: IDecoratedNote[];
25-
activeNotes: IDecoratedNote[];
25+
activeNotes: Set<IDecoratedNote>;
2626
labels: ILabelTreeNode[];
2727
canUndo: boolean;
2828
canRedo: boolean;
@@ -43,6 +43,8 @@ interface INotesProviderProps {
4343
children: ReactNode;
4444
}
4545

46+
const emptyActiveNotes = new Set<IDecoratedNote>();
47+
4648
export function NotesProvider({ children }: INotesProviderProps) {
4749
const [remoteSources, setRemoteSources] = useState<IRemoteSource[]>([]);
4850

@@ -123,11 +125,14 @@ export function NotesProvider({ children }: INotesProviderProps) {
123125
}, []);
124126

125127
const activeNotes = useMemo(() => {
126-
if (!locationParams || !locationParams.selectionStart || !locationParams.selectionEnd) return [];
128+
if (!locationParams || !locationParams.selectionStart || !locationParams.selectionEnd) return emptyActiveNotes;
127129

128130
const { selectionStart, selectionEnd } = locationParams;
129131

130-
return filteredNotes.filter((note) => areSelectionsEqual(note.current, { selectionStart, selectionEnd }));
132+
const activeNotesArray = filteredNotes.filter((note) =>
133+
areSelectionsEqual(note.current, { selectionStart, selectionEnd }),
134+
);
135+
return new Set(activeNotesArray);
131136
}, [filteredNotes, locationParams]);
132137

133138
const context: INotesContext = {

0 commit comments

Comments
 (0)