feat: enhance keyboard navigation with grid support, refined selection, and arrow key navigation.

This commit is contained in:
2025-11-28 20:32:16 +01:00
parent 852ab511ab
commit 0b45eeb2d4
2 changed files with 71 additions and 11 deletions
+4
View File
@@ -32,6 +32,8 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
entries, entries,
onFolderSelect, onFolderSelect,
onPreview: openPreview, onPreview: openPreview,
viewMode: props.viewMode,
scrollRef: props.scrollRef,
}); });
const { clearSelection } = viewLogic; const { clearSelection } = viewLogic;
@@ -94,6 +96,7 @@ export const DocumentsList: React.FC<DocumentsViewProps> = (props) => {
<AbstractDocumentsView <AbstractDocumentsView
ContainerComponent={DocumentsListContainer} ContainerComponent={DocumentsListContainer}
ItemComponent={DocumentsListRow} ItemComponent={DocumentsListRow}
viewMode="list"
{...props} {...props}
/> />
); );
@@ -105,6 +108,7 @@ export const DocumentsGrid: React.FC<DocumentsViewProps & { gridIconSize?: numbe
ContainerComponent={DocumentsGridContainer} ContainerComponent={DocumentsGridContainer}
ItemComponent={DocumentsGridCard} ItemComponent={DocumentsGridCard}
containerProps={{ gridIconSize: props.gridIconSize }} containerProps={{ gridIconSize: props.gridIconSize }}
viewMode="grid"
{...props} {...props}
/> />
); );
@@ -6,18 +6,23 @@ interface UseDocumentsNavigationProps {
entries: DocumentsListEntry[]; entries: DocumentsListEntry[];
onFolderSelect?: (folderId: string) => void; onFolderSelect?: (folderId: string) => void;
onPreview?: (doc: any) => void; onPreview?: (doc: any) => void;
viewMode?: string;
scrollRef?: React.RefObject<HTMLElement | null>;
} }
export const useDocumentsNavigation = ({ export const useDocumentsNavigation = ({
entries, entries,
onFolderSelect, onFolderSelect,
onPreview, onPreview,
viewMode,
scrollRef,
}: UseDocumentsNavigationProps) => { }: UseDocumentsNavigationProps) => {
const { const {
selectedEntries, selectedEntries,
focusedEntryKey, focusedEntryKey,
setFocusedEntryKey, setFocusedEntryKey,
handleEntrySelection, handleEntrySelection,
applySelection,
} = useWorkspaceSelectionContext(); } = useWorkspaceSelectionContext();
const navigableRows = useMemo( const navigableRows = useMemo(
@@ -31,10 +36,20 @@ export const useDocumentsNavigation = ({
[entries], [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( const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => { (event: React.KeyboardEvent) => {
const { key, shiftKey } = event; 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)) { if (!triggers.includes(key)) {
return; return;
} }
@@ -43,7 +58,13 @@ export const useDocumentsNavigation = ({
return; return;
} }
// Allow default scrolling for Home/End if not preventing default
if (key !== 'Home' && key !== 'End') {
event.preventDefault(); event.preventDefault();
} else {
// Prevent default only if we are handling selection move, otherwise let browser scroll
event.preventDefault();
}
let activeKey = let activeKey =
focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey) focusedEntryKey && navigableEntryKeys.includes(focusedEntryKey)
@@ -62,7 +83,9 @@ export const useDocumentsNavigation = ({
} }
if (!activeKey) { 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 (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') {
if (activeRow) { 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') { if (activeRow.type === 'folder') {
onFolderSelect?.(activeRow.id as string); onFolderSelect?.(activeRow.id as string);
} else { } else {
@@ -87,17 +114,36 @@ export const useDocumentsNavigation = ({
} }
let nextIndex = currentIndex; let nextIndex = currentIndex;
const isGrid = viewMode === 'grid';
const columns = isGrid ? getGridColumns() : 1;
if (key === 'ArrowDown') { if (key === 'ArrowDown') {
if (isGrid) {
if (currentIndex + columns < navigableRows.length) {
nextIndex = currentIndex + columns;
}
} else {
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1); nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1);
}
} else if (key === 'ArrowUp') { } else if (key === 'ArrowUp') {
if (isGrid) {
if (currentIndex - columns >= 0) {
nextIndex = currentIndex - columns;
}
} else {
nextIndex = currentIndex === -1 ? navigableRows.length - 1 : Math.max(currentIndex - 1, 0); 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') { } else if (key === 'Home') {
nextIndex = 0; nextIndex = 0;
} else if (key === 'End') { } else if (key === 'End') {
nextIndex = navigableRows.length - 1; nextIndex = navigableRows.length - 1;
} }
if (nextIndex === -1 || nextIndex >= navigableRows.length) { if (nextIndex === -1 || nextIndex >= navigableRows.length || nextIndex === currentIndex) {
return; return;
} }
@@ -107,10 +153,17 @@ export const useDocumentsNavigation = ({
} }
setFocusedEntryKey(targetRow.key); setFocusedEntryKey(targetRow.key);
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, { handleEntrySelection(targetRow.key, {
shiftKey, shiftKey,
preventDefault: () => { }, preventDefault: () => { },
}); });
}
}, },
[ [
focusedEntryKey, focusedEntryKey,
@@ -122,6 +175,9 @@ export const useDocumentsNavigation = ({
onPreview, onPreview,
handleEntrySelection, handleEntrySelection,
setFocusedEntryKey, setFocusedEntryKey,
viewMode,
getGridColumns,
applySelection,
], ],
); );