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
+66 -47
View File
@@ -35,23 +35,23 @@ export const useDocumentSelection = ({
const selectionAnchorRef = useRef<string | null>(null);
const selectionInitializedRef = useRef(false);
const [focusedDocumentId, setFocusedDocumentId] = useState<DocumentId | null>(null);
const [focusedRowKey, setFocusedRowKey] = useState<string | null>(null);
const [focusedEntryKey, setFocusedEntryKey] = useState<string | null>(null);
const visibleRowKeySetRef = useRef<Set<string>>(new Set());
const navigableRowKeysRef = useRef<string[]>([]);
const visibleEntryKeySetRef = useRef<Set<string>>(new Set());
const navigableEntryKeysRef = useRef<string[]>([]);
const configureSelectionEnvironment = useCallback(({
visibleRowKeySet,
navigableRowKeys,
visibleEntryKeySet,
navigableEntryKeys,
}: {
visibleRowKeySet?: Set<string>;
navigableRowKeys?: string[];
visibleEntryKeySet?: Set<string>;
navigableEntryKeys?: string[];
}) => {
if (visibleRowKeySet) {
visibleRowKeySetRef.current = visibleRowKeySet;
if (visibleEntryKeySet) {
visibleEntryKeySetRef.current = visibleEntryKeySet;
}
if (Array.isArray(navigableRowKeys)) {
navigableRowKeysRef.current = navigableRowKeys;
if (Array.isArray(navigableEntryKeys)) {
navigableEntryKeysRef.current = navigableEntryKeys;
}
}, []);
@@ -88,16 +88,16 @@ export const useDocumentSelection = ({
const applySelection = useCallback(
(
rowKeys: Array<string | null>,
entryKeys: Array<string | null>,
{ anchor = null, interactedKeys = [] }: ApplySelectionOptions = { anchor: null, interactedKeys: [] },
) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const visibleEntryKeySet = visibleEntryKeySetRef.current;
const unique: string[] = [];
(rowKeys || []).forEach((key) => {
(entryKeys || []).forEach((key) => {
if (!key) return;
let canonicalKey: string | null = null;
if (visibleRowKeySet.has(key)) {
if (visibleEntryKeySet.has(key)) {
canonicalKey = key;
} else if (isDocumentEntry(key)) {
const id = getEntryId(key);
@@ -107,7 +107,7 @@ export const useDocumentSelection = ({
canonicalKey = id ? createFolderEntryKey(id) : null;
}
if (!canonicalKey || !visibleRowKeySet.has(canonicalKey)) {
if (!canonicalKey || !visibleEntryKeySet.has(canonicalKey)) {
return;
}
@@ -159,19 +159,25 @@ export const useDocumentSelection = ({
);
const clearSelection = useCallback(() => {
setFocusedRowKey(null);
setFocusedEntryKey(null);
applySelection([], { anchor: null, interactedKeys: [] });
}, [applySelection]);
const handleEntrySelection = useCallback(
(rowKey: string, event?: SelectionEventLike) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const navigableRowKeys = navigableRowKeysRef.current;
if (!rowKey || !visibleRowKeySet.has(rowKey)) {
(entryKeyOrKeys: string | string[], event?: SelectionEventLike) => {
const visibleEntryKeySet = visibleEntryKeySetRef.current;
const navigableEntryKeys = navigableEntryKeysRef.current;
const entryKeys = Array.isArray(entryKeyOrKeys) ? entryKeyOrKeys : [entryKeyOrKeys];
const validKeys = entryKeys.filter((key) => key && visibleEntryKeySet.has(key));
if (validKeys.length === 0) {
return;
}
setFocusedRowKey(rowKey);
// Focus the last valid key
const lastKey = validKeys[validKeys.length - 1];
setFocusedEntryKey(lastKey);
const shiftKey = Boolean(event?.shiftKey);
const metaKey = Boolean(event?.metaKey);
@@ -187,44 +193,57 @@ export const useDocumentSelection = ({
anchorKey = selectedEntries[selectedEntries.length - 1];
}
if (!anchorKey) {
anchorKey = rowKey;
anchorKey = lastKey;
}
let nextKeys: string[] = [];
let interactedKeys: string[] = [];
if (shiftKey && anchorKey) {
const anchorIndex = navigableRowKeys.indexOf(anchorKey);
const targetIndex = navigableRowKeys.indexOf(rowKey);
// Shift selection logic (range) - primarily for single click + shift
if (shiftKey && anchorKey && validKeys.length === 1) {
const entryKey = validKeys[0];
const anchorIndex = navigableEntryKeys.indexOf(anchorKey);
const targetIndex = navigableEntryKeys.indexOf(entryKey);
if (anchorIndex !== -1 && targetIndex !== -1) {
const [start, end] = anchorIndex <= targetIndex
? [anchorIndex, targetIndex]
: [targetIndex, anchorIndex];
const range = navigableRowKeys.slice(start, end + 1);
const range = navigableEntryKeys.slice(start, end + 1);
nextKeys = range;
const previousSet = new Set(selectedEntries);
interactedKeys = range.filter((key) => key === rowKey || !previousSet.has(key));
if (!interactedKeys.includes(rowKey)) {
interactedKeys.push(rowKey);
interactedKeys = range.filter((key) => key === entryKey || !previousSet.has(key));
if (!interactedKeys.includes(entryKey)) {
interactedKeys.push(entryKey);
}
} else {
nextKeys = [rowKey];
interactedKeys = [rowKey];
nextKeys = [entryKey];
interactedKeys = [entryKey];
}
} else if (additive) {
if (selectedEntries.includes(rowKey)) {
nextKeys = selectedEntries.filter((key) => key !== rowKey);
interactedKeys = [];
// Additive batch
const previousSet = new Set(selectedEntries);
if (validKeys.length === 1) {
const entryKey = validKeys[0];
if (previousSet.has(entryKey)) {
nextKeys = selectedEntries.filter((key) => key !== entryKey);
interactedKeys = [];
} else {
nextKeys = [...selectedEntries, entryKey];
interactedKeys = [entryKey];
}
} else {
nextKeys = [...selectedEntries, rowKey];
interactedKeys = [rowKey];
// Batch add
validKeys.forEach(key => previousSet.add(key));
nextKeys = Array.from(previousSet) as string[];
interactedKeys = validKeys;
}
anchorKey = rowKey;
anchorKey = lastKey;
} else {
nextKeys = [rowKey];
interactedKeys = [rowKey];
anchorKey = rowKey;
// Replace with batch
nextKeys = validKeys;
interactedKeys = validKeys;
anchorKey = lastKey;
}
applySelection(nextKeys, { anchor: anchorKey, interactedKeys });
@@ -235,14 +254,14 @@ export const useDocumentSelection = ({
const promoteSelectionOrder = useCallback(
(docId?: DocumentId | null) => {
if (!docId) return;
const rowKey = createDocumentEntryKey(docId);
if (!rowKey) return;
const entryKey = createDocumentEntryKey(docId);
if (!entryKey) return;
if (!selectedEntries.includes(rowKey)) {
if (!selectedEntries.includes(entryKey)) {
return;
}
updateSelectionOrder(selectedEntries, [rowKey]);
updateSelectionOrder(selectedEntries, [entryKey]);
},
[selectedEntries, updateSelectionOrder],
);
@@ -257,8 +276,8 @@ export const useDocumentSelection = ({
selectionInitializedRef,
focusedDocumentId,
setFocusedDocumentId,
focusedRowKey,
setFocusedRowKey,
focusedEntryKey,
setFocusedEntryKey,
applySelection,
clearSelection,
handleEntrySelection,
+17 -11
View File
@@ -5,7 +5,7 @@ import { isDocumentEntry, isFolderEntry, getEntryId } from './entryKey';
interface SelectionEntry {
entryKey?: string;
// Legacy field for compatibility
rowKey?: string;
// rowKey?: string; // Removed as part of refactor
[key: string]: unknown;
}
@@ -32,8 +32,8 @@ export const useWorkspaceSelection = ({
selectionInitializedRef,
focusedDocumentId,
setFocusedDocumentId,
focusedRowKey,
setFocusedRowKey,
focusedEntryKey,
setFocusedEntryKey,
applySelection,
clearSelection,
handleEntrySelection,
@@ -60,12 +60,18 @@ export const useWorkspaceSelection = ({
);
const selectEntry = useCallback(
(entry: SelectionEntry | string, event?: unknown) => {
const rowKey = entry && Object(entry) === entry
? (entry as SelectionEntry).rowKey ?? undefined
: (entry as string);
if (!rowKey) return;
handleEntrySelection(rowKey, event);
(entryOrEntries: SelectionEntry | string | Array<SelectionEntry | string>, event?: unknown) => {
const entries = Array.isArray(entryOrEntries) ? entryOrEntries : [entryOrEntries];
const entryKeys = entries
.map((entry) => {
return entry && Object(entry) === entry
? (entry as SelectionEntry).entryKey ?? undefined
: (entry as string);
})
.filter((key): key is string => Boolean(key));
if (entryKeys.length === 0) return;
handleEntrySelection(entryKeys, event);
},
[handleEntrySelection],
);
@@ -96,8 +102,8 @@ export const useWorkspaceSelection = ({
selectionInitializedRef,
focusedDocumentId,
setFocusedDocumentId,
focusedRowKey,
setFocusedRowKey,
focusedEntryKey,
setFocusedEntryKey,
applySelection,
clearSelection,
handleEntrySelection: selectEntry,