feat: add keyboard navigation for document views, externalize document preview handling, and include physics model documentation.

This commit is contained in:
2025-11-27 01:06:31 +01:00
parent 3588fde3a2
commit 9ba581f64e
6 changed files with 306 additions and 392 deletions
@@ -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,
};
};