mirror of
https://github.com/laurent22/joplin.git
synced 2026-03-13 08:09:59 +08:00
This commit is contained in:
@@ -341,9 +341,11 @@ packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useActiveDescendantId.js
|
||||
packages/app-desktop/gui/NoteList/utils/useAutoScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusVisible.js
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -314,9 +314,11 @@ packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useActiveDescendantId.js
|
||||
packages/app-desktop/gui/NoteList/utils/useAutoScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusVisible.js
|
||||
|
||||
@@ -30,6 +30,7 @@ import useFocusVisible from './utils/useFocusVisible';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import useOnNoteDoubleClick from './utils/useOnNoteDoubleClick';
|
||||
import useAutoScroll from './utils/useAutoScroll';
|
||||
|
||||
const commands = {
|
||||
focusElementNoteList,
|
||||
@@ -131,6 +132,10 @@ const NoteList = (props: Props) => {
|
||||
};
|
||||
}, [focusNote]);
|
||||
|
||||
const selectedNoteId = props.selectedNoteIds.length === 1 ? props.selectedNoteIds[0] : '';
|
||||
const targetIndex = props.notes.findIndex(note => note.id === selectedNoteId);
|
||||
useAutoScroll(selectedNoteId, props.selectedFolderId, targetIndex, makeItemIndexVisible);
|
||||
|
||||
const onItemContextMenu = useOnContextMenu(
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
|
||||
106
packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.ts
Normal file
106
packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import useAutoScroll from './useAutoScroll';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
type Props = {
|
||||
selectedNoteId: string;
|
||||
selectedFolderId: string;
|
||||
targetIndex: number;
|
||||
makeItemIndexVisible: (index: number)=> void;
|
||||
};
|
||||
|
||||
describe('useAutoScroll', () => {
|
||||
|
||||
test('scrolls to the note when a new note is selected', () => {
|
||||
const makeItemIndexVisible = jest.fn();
|
||||
|
||||
renderHook(() => useAutoScroll('note-1', 'folder-1', 5, makeItemIndexVisible));
|
||||
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledTimes(1);
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledWith(5);
|
||||
});
|
||||
|
||||
test('does not scroll when the same note is already selected', () => {
|
||||
const makeItemIndexVisible = jest.fn();
|
||||
|
||||
const { rerender } = renderHook(() =>
|
||||
useAutoScroll('note-1', 'folder-1', 5, makeItemIndexVisible),
|
||||
);
|
||||
|
||||
makeItemIndexVisible.mockClear();
|
||||
rerender();
|
||||
|
||||
expect(makeItemIndexVisible).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not scroll for multi-selection or no selection', () => {
|
||||
const makeItemIndexVisible = jest.fn();
|
||||
|
||||
renderHook(() => useAutoScroll('', 'folder-1', -1, makeItemIndexVisible));
|
||||
|
||||
expect(makeItemIndexVisible).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('defers scroll until notes load after folder change', () => {
|
||||
const makeItemIndexVisible = jest.fn();
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(props: Props) => useAutoScroll(
|
||||
props.selectedNoteId,
|
||||
props.selectedFolderId,
|
||||
props.targetIndex,
|
||||
props.makeItemIndexVisible,
|
||||
),
|
||||
{ initialProps: { selectedNoteId: 'note-1', selectedFolderId: 'folder-2', targetIndex: -1, makeItemIndexVisible } },
|
||||
);
|
||||
|
||||
expect(makeItemIndexVisible).not.toHaveBeenCalled();
|
||||
|
||||
rerender({ selectedNoteId: 'note-1', selectedFolderId: 'folder-2', targetIndex: 3, makeItemIndexVisible });
|
||||
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledTimes(1);
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
test('scrolls again when the folder changes even if note ID is the same', () => {
|
||||
const makeItemIndexVisible = jest.fn();
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(props: Props) => useAutoScroll(
|
||||
props.selectedNoteId,
|
||||
props.selectedFolderId,
|
||||
props.targetIndex,
|
||||
props.makeItemIndexVisible,
|
||||
),
|
||||
{ initialProps: { selectedNoteId: 'note-1', selectedFolderId: 'folder-1', targetIndex: 2, makeItemIndexVisible } },
|
||||
);
|
||||
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender({ selectedNoteId: 'note-1', selectedFolderId: 'folder-2', targetIndex: 2, makeItemIndexVisible });
|
||||
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('does not scroll again when targetIndex changes after the pending flag is cleared', () => {
|
||||
// Covers the case where a sort or filter changes targetIndex without a new selection.
|
||||
// Without this guard, arrow-key navigation would trigger a spurious second scroll.
|
||||
const makeItemIndexVisible = jest.fn();
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(props: Props) => useAutoScroll(
|
||||
props.selectedNoteId,
|
||||
props.selectedFolderId,
|
||||
props.targetIndex,
|
||||
props.makeItemIndexVisible,
|
||||
),
|
||||
{ initialProps: { selectedNoteId: 'note-1', selectedFolderId: 'folder-1', targetIndex: 5, makeItemIndexVisible } },
|
||||
);
|
||||
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender({ selectedNoteId: 'note-1', selectedFolderId: 'folder-1', targetIndex: 7, makeItemIndexVisible });
|
||||
|
||||
expect(makeItemIndexVisible).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
});
|
||||
43
packages/app-desktop/gui/NoteList/utils/useAutoScroll.ts
Normal file
43
packages/app-desktop/gui/NoteList/utils/useAutoScroll.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
// Auto-scrolls the note list to the selected note when selection changes. Uses a pending flag
|
||||
// to handle cross-folder navigation where notes may not be loaded on the first render.
|
||||
const useAutoScroll = (
|
||||
selectedNoteId: string,
|
||||
selectedFolderId: string,
|
||||
targetIndex: number,
|
||||
makeItemIndexVisible: (index: number)=> void,
|
||||
) => {
|
||||
const lastNoteIdRef = useRef('');
|
||||
const lastFolderIdRef = useRef('');
|
||||
const scrollPendingRef = useRef(false); // true when scroll requested but notes not yet loaded
|
||||
|
||||
useEffect(() => {
|
||||
// No selection or multi-selection — reset tracking state.
|
||||
if (!selectedNoteId) {
|
||||
lastNoteIdRef.current = '';
|
||||
lastFolderIdRef.current = selectedFolderId;
|
||||
scrollPendingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const isNewNote = selectedNoteId !== lastNoteIdRef.current;
|
||||
const isFolderChange = selectedFolderId !== lastFolderIdRef.current;
|
||||
|
||||
if (isNewNote || isFolderChange) {
|
||||
lastNoteIdRef.current = selectedNoteId;
|
||||
lastFolderIdRef.current = selectedFolderId;
|
||||
scrollPendingRef.current = true;
|
||||
}
|
||||
|
||||
// targetIndex is -1 until the new folder's notes load — re-runs automatically when they do.
|
||||
if (!scrollPendingRef.current || targetIndex === -1) return;
|
||||
|
||||
// makeItemIndexVisible has its own visibility guard and is a no-op when the note is
|
||||
// already visible — this covers arrow-key and click navigation without double-scrolling.
|
||||
makeItemIndexVisible(targetIndex);
|
||||
scrollPendingRef.current = false;
|
||||
}, [selectedNoteId, selectedFolderId, targetIndex, makeItemIndexVisible]);
|
||||
};
|
||||
|
||||
export default useAutoScroll;
|
||||
Reference in New Issue
Block a user