file browser: type-ahead jump-to-item, flips to filter on longer bursts

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 02:06:20 +00:00
co-authored by Claude Opus 4.8
parent 184adc0e8e
commit b7015b1ede
2 changed files with 66 additions and 3 deletions
@@ -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. 12 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<HTMLDivElement | null>) => {
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<number>(-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<typeof setTimeout> | 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 <div className="flex items-center justify-center py-20 text-duck-dark/40 text-sm">This folder is empty</div>;
}
@@ -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);