Public Access
feat(ui): global keyboard shortcuts + shortcut help banner (Sprint 5 F2)
F2 — Vim-style keyboard shortcuts (the audit's F2 / H7 finding).
New files:
- frontend/src/hooks/useKeyboardShortcuts.ts: lightweight global
handler. Supports both single keys ('/', '?', 'Escape') and
vim-style 2-key sequences ('g d', 'g r', 'g p', 'g s' for nav).
Sequence timeout is 1500ms; pending prefix is cleared on any
unrecognised key so typing 'g' alone is safe. Suppressed when
the user is typing in an input/textarea/select/contenteditable,
or when any modifier key (Ctrl/Cmd/Alt) is held — those chords
belong to the browser or other handlers. Uses a ref so the
listener is registered once and always sees the latest callbacks.
- frontend/src/hooks/useFocusSearch.ts: tiny CustomEvent bus.
requestFocusSearch() dispatches a 'mealplanner:focus-search'
event; useFocusSearchOnShortcut(ref) subscribes and focuses the
supplied input. The decoupling lets any page opt in without the
global handler needing to know the page's DOM.
- frontend/src/components/ShortcutHelpBanner.tsx: dismissible help
dialog that slides down under the nav when '?' is pressed.
Auto-dismisses after 6s; Escape also dismisses. role=dialog +
aria-label for screen readers; the kbd elements use the
<kbd> semantic for assistive tech.
Wired in App.tsx:
- New <GlobalShortcuts /> child of <BrowserRouter> calls
useKeyboardShortcuts with the 4 nav sequences, '/' →
requestFocusSearch(), and '?' → dispatch SHOW_SHORTCUT_HELP_EVENT.
- <ShortcutHelpBanner /> mounted inside the page wrapper (after
<main>).
Pantry and Recipes now call useFocusSearchOnShortcut with a
forwardRef attached to their top search inputs. Recipes's search
already debounced via handleSearch so focusing just selects the
existing text for the user to replace. Pantry's search is a plain
controlled input, same treatment.
Behaviour summary:
- g d / g r / g p / g s → navigate to the 4 main pages
- / → focus the search input on the current page (Pantry + Recipes
only — other pages have no search)
- ? → show the help banner
- All shortcuts are no-ops inside text-entry controls, so a user
typing 'p' into the pantry search box will not trigger navigation.
Build: tsc 0 errors, vite 0 errors. 5 files, +185/-3.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Search, Trash2, Package, AlertTriangle } from 'lucide-react'
|
||||
import { mealPlannerApi } from '../api'
|
||||
@@ -10,6 +10,7 @@ import { Select } from '../components/ui/Select'
|
||||
import { Skeleton, SkeletonText } from '../components/ui/Skeleton'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { showToast, showApiError } from '../lib/toast'
|
||||
import { useFocusSearchOnShortcut } from '../hooks/useFocusSearch'
|
||||
|
||||
const AISLE_OPTIONS = [
|
||||
{ value: '', label: 'Select aisle…' },
|
||||
@@ -21,6 +22,8 @@ export default function Pantry() {
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const [removeId, setRemoveId] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
useFocusSearchOnShortcut(searchInputRef)
|
||||
|
||||
/* ingredient name typed by user */
|
||||
const [ingredientName, setIngredientName] = useState('')
|
||||
@@ -272,6 +275,7 @@ export default function Pantry() {
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-surface-400" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search pantry items..."
|
||||
|
||||
Reference in New Issue
Block a user