From ad2e6e78c60db069deac145140731c8d17e05b00 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Tue, 5 May 2026 00:03:53 +0200 Subject: [PATCH 1/4] fix(search): keyboard navigation in global search results The Cmd+K palette had no keyboard navigation: users had to leave the keyboard and click to pick a result. Add Arrow Up/Down to move the selection and Enter to open it. The hovered row syncs with the keyboard cursor and the selected row scrolls into view as needed. --- apps/web/src/components/global-search.tsx | 52 ++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/global-search.tsx b/apps/web/src/components/global-search.tsx index 5cc11d4..b9737f3 100644 --- a/apps/web/src/components/global-search.tsx +++ b/apps/web/src/components/global-search.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback, useEffect, useRef } from "react"; +import { useState, useMemo, useCallback, useEffect, useRef, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { useLocation } from "wouter"; import { Search, X, BookOpen, Flag, CheckSquare, FileCheck } from "lucide-react"; import { format, parseISO } from "date-fns"; @@ -25,9 +25,11 @@ const TYPE_CONFIG = { export function GlobalSearch() { const [query, setQuery] = useState(""); const [isOpen, setIsOpen] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); const [, navigate] = useLocation(); const inputRef = useRef(null); const containerRef = useRef(null); + const resultRefs = useRef>([]); const { data: tasks = [] } = useListTasks(); const { data: journalEntries = [] } = useListJournalEntries(); @@ -100,11 +102,41 @@ export function GlobalSearch() { (result: SearchResult) => { setQuery(""); setIsOpen(false); + setSelectedIndex(0); navigate(result.href); }, [navigate] ); + const activeIndex = results.length > 0 ? Math.min(selectedIndex, results.length - 1) : 0; + + useEffect(() => { + resultRefs.current[activeIndex]?.scrollIntoView({ block: "nearest" }); + }, [activeIndex]); + + const handleQueryChange = useCallback((value: string) => { + setQuery(value); + setSelectedIndex(0); + }, []); + + const handleInputKeyDown = useCallback( + (e: ReactKeyboardEvent) => { + if (results.length === 0) return; + if (e.key === "ArrowDown") { + e.preventDefault(); + setSelectedIndex((i) => Math.min(i + 1, results.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setSelectedIndex((i) => Math.max(i - 1, 0)); + } else if (e.key === "Enter") { + e.preventDefault(); + const target = results[activeIndex]; + if (target) handleSelect(target); + } + }, + [results, activeIndex, handleSelect] + ); + // Keyboard shortcut Ctrl+K to open search useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -116,6 +148,7 @@ export function GlobalSearch() { if (e.key === "Escape") { setIsOpen(false); setQuery(""); + setSelectedIndex(0); } }; window.addEventListener("keydown", handler); @@ -142,14 +175,15 @@ export function GlobalSearch() { setQuery(e.target.value)} + onChange={(e) => handleQueryChange(e.target.value)} + onKeyDown={handleInputKeyDown} placeholder="Rechercher dans les tâches, journal, jalons..." className="pl-9 pr-9 h-10 rounded-xl border-primary" /> {query && (