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:
@@ -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<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);
|
||||
|
||||
Reference in New Issue
Block a user