From 0b45eeb2d4584b4680334f76385c81a89c725bea Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 28 Nov 2025 20:32:16 +0100 Subject: [PATCH] feat: enhance keyboard navigation with grid support, refined selection, and arrow key navigation. --- frontend/src/documents/DocumentsView.tsx | 4 + .../documents/hooks/useDocumentsNavigation.ts | 78 ++++++++++++++++--- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/frontend/src/documents/DocumentsView.tsx b/frontend/src/documents/DocumentsView.tsx index bd86c19..178272b 100644 --- a/frontend/src/documents/DocumentsView.tsx +++ b/frontend/src/documents/DocumentsView.tsx @@ -32,6 +32,8 @@ const AbstractDocumentsView = void; chil entries, onFolderSelect, onPreview: openPreview, + viewMode: props.viewMode, + scrollRef: props.scrollRef, }); const { clearSelection } = viewLogic; @@ -94,6 +96,7 @@ export const DocumentsList: React.FC = (props) => { ); @@ -105,6 +108,7 @@ export const DocumentsGrid: React.FC ); diff --git a/frontend/src/documents/hooks/useDocumentsNavigation.ts b/frontend/src/documents/hooks/useDocumentsNavigation.ts index 07362e7..ec28476 100644 --- a/frontend/src/documents/hooks/useDocumentsNavigation.ts +++ b/frontend/src/documents/hooks/useDocumentsNavigation.ts @@ -6,18 +6,23 @@ interface UseDocumentsNavigationProps { entries: DocumentsListEntry[]; onFolderSelect?: (folderId: string) => void; onPreview?: (doc: any) => void; + viewMode?: string; + scrollRef?: React.RefObject; } export const useDocumentsNavigation = ({ entries, onFolderSelect, onPreview, + viewMode, + scrollRef, }: UseDocumentsNavigationProps) => { const { selectedEntries, focusedEntryKey, setFocusedEntryKey, handleEntrySelection, + applySelection, } = useWorkspaceSelectionContext(); const navigableRows = useMemo( @@ -31,10 +36,20 @@ export const useDocumentsNavigation = ({ [entries], ); + const getGridColumns = useCallback(() => { + if (!scrollRef?.current) return 1; + const grid = scrollRef.current.querySelector('.documents-grid'); + if (!grid) return 1; + const style = window.getComputedStyle(grid); + const templateColumns = style.gridTemplateColumns; + if (!templateColumns) return 1; + return templateColumns.split(' ').length; + }, [scrollRef]); + const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { const { key, shiftKey } = event; - const triggers = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar']; + const triggers = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar']; if (!triggers.includes(key)) { return; } @@ -43,7 +58,13 @@ export const useDocumentsNavigation = ({ return; } - event.preventDefault(); + // Allow default scrolling for Home/End if not preventing default + if (key !== 'Home' && key !== 'End') { + event.preventDefault(); + } else { + // Prevent default only if we are handling selection move, otherwise let browser scroll + event.preventDefault(); + } let activeKey = focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey) @@ -62,7 +83,9 @@ export const useDocumentsNavigation = ({ } if (!activeKey) { - activeKey = key === 'ArrowUp' ? navigableEntryKeys[navigableEntryKeys.length - 1] : navigableEntryKeys[0]; + activeKey = (key === 'ArrowUp' || key === 'ArrowLeft') + ? navigableEntryKeys[navigableEntryKeys.length - 1] + : navigableEntryKeys[0]; } } @@ -71,7 +94,11 @@ export const useDocumentsNavigation = ({ if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') { if (activeRow) { - handleEntrySelection(activeRow.key, event); + event.preventDefault(); + // Do not call handleEntrySelection here, as it resets selection if multiple items are selected. + // Space/Enter should just trigger the action (Preview/Open) on the focused item + // without modifying the selection state. + if (activeRow.type === 'folder') { onFolderSelect?.(activeRow.id as string); } else { @@ -87,17 +114,36 @@ export const useDocumentsNavigation = ({ } let nextIndex = currentIndex; + const isGrid = viewMode === 'grid'; + const columns = isGrid ? getGridColumns() : 1; + if (key === 'ArrowDown') { - nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1); + if (isGrid) { + if (currentIndex + columns < navigableRows.length) { + nextIndex = currentIndex + columns; + } + } else { + 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); + if (isGrid) { + if (currentIndex - columns >= 0) { + nextIndex = currentIndex - columns; + } + } else { + nextIndex = currentIndex === -1 ? navigableRows.length - 1 : Math.max(currentIndex - 1, 0); + } + } else if (key === 'ArrowLeft') { + nextIndex = Math.max(currentIndex - 1, 0); + } else if (key === 'ArrowRight') { + nextIndex = Math.min(currentIndex + 1, navigableRows.length - 1); } else if (key === 'Home') { nextIndex = 0; } else if (key === 'End') { nextIndex = navigableRows.length - 1; } - if (nextIndex === -1 || nextIndex >= navigableRows.length) { + if (nextIndex === -1 || nextIndex >= navigableRows.length || nextIndex === currentIndex) { return; } @@ -107,10 +153,17 @@ export const useDocumentsNavigation = ({ } setFocusedEntryKey(targetRow.key); - handleEntrySelection(targetRow.key, { - shiftKey, - preventDefault: () => { }, - }); + + if (shiftKey) { + // Additive selection for Shift+Arrow (Finder style) + const newSelection = Array.from(new Set([...selectedEntries, targetRow.key])); + applySelection(newSelection, { anchor: targetRow.key, interactedKeys: [targetRow.key] }); + } else { + handleEntrySelection(targetRow.key, { + shiftKey, + preventDefault: () => { }, + }); + } }, [ focusedEntryKey, @@ -122,6 +175,9 @@ export const useDocumentsNavigation = ({ onPreview, handleEntrySelection, setFocusedEntryKey, + viewMode, + getGridColumns, + applySelection, ], );