From b7015b1ede12e0e09229989cb91cf352b5a466ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 30 Jul 2026 02:06:20 +0000 Subject: [PATCH] file browser: type-ahead jump-to-item, flips to filter on longer bursts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the old "any key focuses the search box" with classic file-manager type-ahead in FileGrid: keys within 1s accumulate into a burst. 1–2 chars select and scroll to the first item (folder or file, in sort order) whose name starts with the burst; the 3rd char flips it into the search filter (dumps the typed text into the box and hands off focus, cursor at end). Idle >1s resets the burst. Removed the single-key focus-search branch from useFileBrowserApp. Co-Authored-By: Claude Opus 4.8 --- .../FileBrowserApp/components/FileGrid.tsx | 65 +++++++++++++++++++ .../FileBrowserApp/useFileBrowserApp.ts | 4 +- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx index f69023dd..16e6fc61 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx @@ -17,6 +17,10 @@ const LIST_ROW_HEIGHT = 42; const GRID_ROW_HEIGHT = 130; const GRID_GAP = 12; const HEADER_HEIGHT = 36; +// Type-ahead: keys pressed within this window accumulate into one burst. 1–2 chars jump to the first +// matching item; the 3rd char flips the burst into the search filter. Idle past the window resets it. +const TYPEAHEAD_RESET_MS = 1000; +const TYPEAHEAD_FILTER_AT = 3; const useColumnCount = (scrollRef: React.RefObject) => { const [cols, setCols] = useState(4); @@ -67,6 +71,8 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => { handleRunTask, handleCreateDashboard, fileScrollRef: scrollRef, + setSearchQuery, + searchInputRef, } = fileBrowserManager; const { defaultSort } = fileBrowserManager; const lastClickedIdx = useRef(-1); @@ -152,6 +158,65 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => { scrollMargin: HEADER_HEIGHT, }); + // Refs hold the latest values so the once-registered keydown listener always reads fresh state. + const sortedRef = useRef(sorted); + sortedRef.current = sorted; + const colsRef = useRef(cols); + colsRef.current = cols; + const viewModeRef = useRef(viewMode); + viewModeRef.current = viewMode; + const virtualizerRef = useRef(virtualizer); + virtualizerRef.current = virtualizer; + + // Type-ahead (see constants above): jump-to-item on a short burst, flip to the filter on the 3rd key. + useEffect(() => { + let buffer = ''; + let timer: ReturnType | undefined; + const reset = () => { + buffer = ''; + if (timer) clearTimeout(timer); + timer = undefined; + }; + const onKey = (ev: KeyboardEvent) => { + const t = ev.target as HTMLElement; + if (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.closest('.monaco-editor')) return; + if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.key.length !== 1) return; // printable, unmodified only + + ev.preventDefault(); + if (timer) clearTimeout(timer); + buffer += ev.key; + timer = setTimeout(reset, TYPEAHEAD_RESET_MS); + const typed = buffer; + + // Kept typing → treat it as a filter: dump the burst into the search box and hand off focus. + if (typed.length >= TYPEAHEAD_FILTER_AT) { + reset(); + setSearchQuery(typed); + requestAnimationFrame(() => { + const el = searchInputRef.current; + if (!el) return; + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); // cursor at the end, keep typing + }); + return; + } + + // Short burst → jump: select + scroll to the first item (folder or file) whose name starts with it. + const prefix = typed.toLowerCase(); + const list = sortedRef.current; + const idx = list.findIndex((e) => e.name.toLowerCase().startsWith(prefix)); + if (idx < 0) return; + setSelected(new Set([list[idx]!.name])); + const row = viewModeRef.current === 'list' ? idx : Math.floor(idx / Math.max(1, colsRef.current)); + virtualizerRef.current.scrollToIndex(row, { align: 'auto' }); + }; + window.addEventListener('keydown', onKey); + return () => { + window.removeEventListener('keydown', onKey); + if (timer) clearTimeout(timer); + }; + }, [setSearchQuery, searchInputRef, setSelected]); + if (entries.length === 0) { return
This folder is empty
; } diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts index 67d0ff50..56c56979 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts @@ -237,9 +237,7 @@ export const useFileBrowserApp = ( return; } - if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) { - searchInputRef.current?.focus(); - } + // (Printable keys are handled by FileGrid's type-ahead: jump-to-item, or flip to the filter.) }; window.addEventListener('keydown', handler);