refactor: Unify document selection and drag interactions by replacing stack-specific callbacks and simplifying drag state.

This commit is contained in:
2025-11-25 20:54:53 +01:00
parent 13c87f4b07
commit e0fce40bc3
10 changed files with 279 additions and 267 deletions
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { createDocumentEntryKey, createFolderEntryKey, isFolderEntry } from '../../app/entryKey';
import type { DocumentId, FolderId } from '../../types/identifiers';
@@ -22,8 +23,8 @@ interface UseDocumentsSelectionOptions {
showingSearchResults?: boolean;
currentSubfolders?: FolderEntry[];
visibleDocuments?: DocumentEntry[];
configureSelectionEnvironment: (config: { visibleRowKeySet: Set<string>; navigableRowKeys: string[] }) => void;
visibleRowKeySet: Set<string>;
configureSelectionEnvironment: (config: { visibleEntryKeySet: Set<string>; navigableEntryKeys: string[] }) => void;
visibleEntryKeySet: Set<string>;
selectedEntries: string[];
selectionAnchorRef: { current: string | null };
promoteSelectionOrderRaw: (id: DocumentId) => void;
@@ -31,8 +32,8 @@ interface UseDocumentsSelectionOptions {
setActivePreviewId: (id: DocumentId | null) => void;
clearSelection: () => void;
focusedDocumentId: DocumentId | null;
setFocusedRowKey: (value: string | null | ((current: string | null) => string | null)) => void;
focusedRowKey: string | null;
setFocusedEntryKey: Dispatch<SetStateAction<string | null>>;
focusedEntryKey: string | null;
}
const useDocumentsSelection = ({
@@ -40,7 +41,7 @@ const useDocumentsSelection = ({
currentSubfolders = [],
visibleDocuments = [],
configureSelectionEnvironment,
visibleRowKeySet,
visibleEntryKeySet,
selectedEntries,
selectionAnchorRef,
promoteSelectionOrderRaw,
@@ -48,8 +49,8 @@ const useDocumentsSelection = ({
setActivePreviewId,
clearSelection,
focusedDocumentId,
setFocusedRowKey,
focusedRowKey,
setFocusedEntryKey,
focusedEntryKey,
}: UseDocumentsSelectionOptions) => {
const navigableRows = useMemo<NavigableRow[]>(() => {
const entries: NavigableRow[] = [];
@@ -77,10 +78,10 @@ const useDocumentsSelection = ({
useEffect(() => {
configureSelectionEnvironment({
visibleRowKeySet,
navigableRowKeys,
visibleEntryKeySet,
navigableEntryKeys: navigableRowKeys,
});
}, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]);
}, [configureSelectionEnvironment, visibleEntryKeySet, navigableRowKeys]);
const promoteSelectionOrder = useCallback(
(docId: DocumentId | null) => {
@@ -108,40 +109,40 @@ const useDocumentsSelection = ({
}
prevFocusedDocIdRef.current = focusedDocumentId;
if (focusedDocumentId) {
setFocusedRowKey(createDocumentEntryKey(focusedDocumentId));
setFocusedEntryKey(createDocumentEntryKey(focusedDocumentId));
} else {
setFocusedRowKey((current) => (current && isFolderEntry(current) ? current : null));
setFocusedEntryKey((current) => (current && isFolderEntry(current) ? current : null));
}
}, [focusedDocumentId, setFocusedRowKey]);
}, [focusedDocumentId, setFocusedEntryKey]);
useEffect(() => {
if (!navigableRowKeys.length) {
if (focusedRowKey) {
setFocusedRowKey(null);
if (focusedEntryKey) {
setFocusedEntryKey(null);
}
return;
}
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
if (focusedEntryKey && navigableRowKeys.includes(focusedEntryKey)) {
return;
}
const docKey = focusedDocumentId ? createDocumentEntryKey(focusedDocumentId) : null;
if (docKey && navigableRowKeys.includes(docKey)) {
setFocusedRowKey(docKey);
setFocusedEntryKey(docKey);
return;
}
const selectedKey = selectedEntries.find((key) => navigableRowKeys.includes(key));
if (selectedKey) {
setFocusedRowKey(selectedKey);
setFocusedEntryKey(selectedKey);
return;
}
if (focusedRowKey) {
setFocusedRowKey(null);
if (focusedEntryKey) {
setFocusedEntryKey(null);
}
}, [focusedRowKey, focusedDocumentId, navigableRowKeys, selectedEntries, setFocusedRowKey]);
}, [focusedEntryKey, focusedDocumentId, navigableRowKeys, selectedEntries, setFocusedEntryKey]);
return {
navigableRows,
+39 -39
View File
@@ -142,8 +142,8 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
}): ReactNode => {
const {
selectedEntries,
focusedRowKey,
setFocusedRowKey,
focusedEntryKey,
setFocusedEntryKey,
handleEntrySelection,
clearSelection,
selectionAnchorRef,
@@ -177,28 +177,28 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
return;
}
const rowKeys = docIds
const entryKeys = docIds
.map((id) => createDocumentEntryKey(id as Identifier))
.filter((value): value is string => typeof value === 'string');
if (!rowKeys.length) {
if (!entryKeys.length) {
return;
}
const nextKeys = [...selectedEntries];
rowKeys.forEach((key) => {
entryKeys.forEach((key) => {
if (!nextKeys.includes(key)) {
nextKeys.push(key);
}
});
const anchor = (rowKeys[0]
const anchor = (entryKeys[0]
|| selectionAnchorRef.current
|| nextKeys[nextKeys.length - 1]) as string | null;
applySelection(nextKeys, {
anchor,
interactedKeys: rowKeys,
interactedKeys: entryKeys,
});
},
[applySelection, selectedEntries, selectionAnchorRef],
@@ -482,24 +482,24 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
() => entries.map((entry) => ({ key: entry.key, type: entry.type, id: entry.id })),
[entries],
);
const navigableRowKeys = useMemo(() => navigableRows.map((row) => row.key), [navigableRows]);
const navigableEntryKeys = useMemo(() => navigableRows.map((row) => row.key), [navigableRows]);
const getEntryByKey = useCallback(
(rowKey) => entries.find((entry) => entry.key === rowKey) || null,
(entryKey) => entries.find((entry) => entry.key === entryKey) || null,
[entries],
);
const handlePanelFocus = useCallback(() => {
let resolvedKey = null;
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
resolvedKey = focusedRowKey;
if (focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey)) {
resolvedKey = focusedEntryKey;
}
if (!resolvedKey) {
for (let index = selectedEntries.length - 1; index >= 0; index -= 1) {
const candidate = selectedEntries[index];
if (navigableRowKeys.includes(candidate)) {
if (navigableEntryKeys.includes(candidate)) {
resolvedKey = candidate;
break;
}
@@ -519,12 +519,12 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
return;
}
setFocusedRowKey(resolvedKey);
setFocusedEntryKey(resolvedKey);
}, [
focusedRowKey,
navigableRowKeys,
focusedEntryKey,
navigableEntryKeys,
navigableRows,
setFocusedRowKey,
setFocusedEntryKey,
selectedEntries,
]);
@@ -543,15 +543,15 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
event.preventDefault();
let activeKey =
focusedRowKey && navigableRowKeys.includes(focusedRowKey)
? focusedRowKey
focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey)
? focusedEntryKey
: null;
if (!activeKey) {
if (selectedEntries.length) {
for (let index = selectedEntries.length - 1; index >= 0; index -= 1) {
const candidate = selectedEntries[index];
if (navigableRowKeys.includes(candidate)) {
if (navigableEntryKeys.includes(candidate)) {
activeKey = candidate;
break;
}
@@ -559,11 +559,11 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
}
if (!activeKey) {
activeKey = key === 'ArrowUp' ? navigableRowKeys[navigableRowKeys.length - 1] : navigableRowKeys[0];
activeKey = key === 'ArrowUp' ? navigableEntryKeys[navigableEntryKeys.length - 1] : navigableEntryKeys[0];
}
}
const currentIndex = navigableRowKeys.indexOf(activeKey);
const currentIndex = navigableEntryKeys.indexOf(activeKey);
const activeRow = currentIndex === -1 ? null : navigableRows[currentIndex];
if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') {
@@ -601,22 +601,22 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
return;
}
setFocusedRowKey(targetRow.key);
setFocusedEntryKey(targetRow.key);
handleEntrySelection(targetRow.key, {
shiftKey,
preventDefault: () => { },
});
},
[
focusedRowKey,
focusedEntryKey,
getEntryByKey,
navigableRowKeys,
navigableEntryKeys,
navigableRows,
onFolderSelect,
selectedEntries,
handleDocumentPreviewZoom,
handleEntrySelection,
setFocusedRowKey,
setFocusedEntryKey,
],
);
@@ -648,14 +648,14 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []);
const ensureFocusedRowVisible = useCallback(() => {
if (!focusedRowKey) return;
if (!focusedEntryKey) return;
const container = scrollRef.current;
if (!container) return;
let selector = null;
if (focusedRowKey.startsWith('document:')) {
selector = `#document-row-${focusedRowKey.slice('document:'.length)}`;
} else if (focusedRowKey.startsWith('folder:')) {
selector = `#folder-row-${focusedRowKey.slice('folder:'.length)}`;
if (focusedEntryKey.startsWith('document:')) {
selector = `#document-row-${focusedEntryKey.slice('document:'.length)}`;
} else if (focusedEntryKey.startsWith('folder:')) {
selector = `#folder-row-${focusedEntryKey.slice('folder:'.length)}`;
}
if (!selector) {
return;
@@ -681,22 +681,22 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
const nextScrollTop = rowBottom - container.clientHeight;
container.scrollTop = Math.max(nextScrollTop, 0);
}
}, [focusedRowKey]);
}, [focusedEntryKey]);
useEffect(() => {
ensureFocusedRowVisible();
}, [ensureFocusedRowVisible]);
const activeDescendantId = useMemo(() => {
if (!focusedRowKey) return undefined;
if (focusedRowKey.startsWith('document:')) {
return `document-row-${focusedRowKey.slice('document:'.length)}`;
if (!focusedEntryKey) return undefined;
if (focusedEntryKey.startsWith('document:')) {
return `document-row-${focusedEntryKey.slice('document:'.length)}`;
}
if (focusedRowKey.startsWith('folder:')) {
return `folder-row-${focusedRowKey.slice('folder:'.length)}`;
if (focusedEntryKey.startsWith('folder:')) {
return `folder-row-${focusedEntryKey.slice('folder:'.length)}`;
}
return undefined;
}, [focusedRowKey]);
}, [focusedEntryKey]);
const handleDocumentTagDragOver = useCallback(
(event) => {
@@ -756,10 +756,10 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
&& scrollRef.current
) {
scrollRef.current.focus({ preventScroll: true });
setFocusedRowKey(`folder:${folder.id}`);
setFocusedEntryKey(`folder:${folder.id}`);
}
},
[onEntryPointer, setFocusedRowKey],
[onEntryPointer, setFocusedEntryKey],
);
const handleDocumentDragStartLocal = useCallback(