feat: add keyboard navigation for document views, externalize document preview handling, and include physics model documentation.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { DocumentsListEntry } from '../../types/documents';
|
||||
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
||||
|
||||
interface UseDocumentsNavigationProps {
|
||||
entries: DocumentsListEntry[];
|
||||
onFolderSelect?: (folderId: string) => void;
|
||||
onPreview?: (doc: any) => void;
|
||||
}
|
||||
|
||||
export const useDocumentsNavigation = ({
|
||||
entries,
|
||||
onFolderSelect,
|
||||
onPreview,
|
||||
}: UseDocumentsNavigationProps) => {
|
||||
const {
|
||||
selectedEntries,
|
||||
focusedEntryKey,
|
||||
setFocusedEntryKey,
|
||||
handleEntrySelection,
|
||||
} = useWorkspaceSelectionContext();
|
||||
|
||||
const navigableRows = useMemo(
|
||||
() => entries.map((entry) => ({ key: entry.key, type: entry.type, id: entry.id })),
|
||||
[entries],
|
||||
);
|
||||
const navigableEntryKeys = useMemo(() => navigableRows.map((row) => row.key), [navigableRows]);
|
||||
|
||||
const getEntryByKey = useCallback(
|
||||
(entryKey: string) => entries.find((entry) => entry.key === entryKey) || null,
|
||||
[entries],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
const { key, shiftKey } = event;
|
||||
const triggers = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar'];
|
||||
if (!triggers.includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigableRows.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
let activeKey =
|
||||
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 (navigableEntryKeys.includes(candidate)) {
|
||||
activeKey = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeKey) {
|
||||
activeKey = key === 'ArrowUp' ? navigableEntryKeys[navigableEntryKeys.length - 1] : navigableEntryKeys[0];
|
||||
}
|
||||
}
|
||||
|
||||
const currentIndex = navigableEntryKeys.indexOf(activeKey);
|
||||
const activeRow = currentIndex === -1 ? null : navigableRows[currentIndex];
|
||||
|
||||
if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') {
|
||||
if (activeRow) {
|
||||
handleEntrySelection(activeRow.key, event);
|
||||
if (activeRow.type === 'folder') {
|
||||
onFolderSelect?.(activeRow.id as string);
|
||||
} else {
|
||||
const entry = getEntryByKey(activeRow.key);
|
||||
// @ts-ignore
|
||||
if (entry?.document) {
|
||||
// @ts-ignore
|
||||
onPreview?.(entry.document);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let nextIndex = currentIndex;
|
||||
if (key === 'ArrowDown') {
|
||||
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1);
|
||||
} else if (key === 'ArrowUp') {
|
||||
nextIndex = currentIndex === -1 ? navigableRows.length - 1 : Math.max(currentIndex - 1, 0);
|
||||
} else if (key === 'Home') {
|
||||
nextIndex = 0;
|
||||
} else if (key === 'End') {
|
||||
nextIndex = navigableRows.length - 1;
|
||||
}
|
||||
|
||||
if (nextIndex === -1 || nextIndex >= navigableRows.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRow = navigableRows[nextIndex];
|
||||
if (!targetRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocusedEntryKey(targetRow.key);
|
||||
handleEntrySelection(targetRow.key, {
|
||||
shiftKey,
|
||||
preventDefault: () => { },
|
||||
});
|
||||
},
|
||||
[
|
||||
focusedEntryKey,
|
||||
getEntryByKey,
|
||||
navigableEntryKeys,
|
||||
navigableRows,
|
||||
onFolderSelect,
|
||||
selectedEntries,
|
||||
onPreview,
|
||||
handleEntrySelection,
|
||||
setFocusedEntryKey,
|
||||
],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
let resolvedKey = null;
|
||||
|
||||
if (focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey)) {
|
||||
resolvedKey = focusedEntryKey;
|
||||
}
|
||||
|
||||
if (!resolvedKey) {
|
||||
for (let index = selectedEntries.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = selectedEntries[index];
|
||||
if (navigableEntryKeys.includes(candidate)) {
|
||||
resolvedKey = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedKey) {
|
||||
if (!selectedEntries.length) {
|
||||
return;
|
||||
}
|
||||
if (navigableRows.length) {
|
||||
resolvedKey = navigableRows[0].key;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocusedEntryKey(resolvedKey);
|
||||
}, [
|
||||
focusedEntryKey,
|
||||
navigableEntryKeys,
|
||||
navigableRows,
|
||||
setFocusedEntryKey,
|
||||
selectedEntries,
|
||||
]);
|
||||
|
||||
return {
|
||||
handleKeyDown,
|
||||
handleFocus,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user