Skip to content

Commit c4a9bc0

Browse files
committed
Add keyboard navigation to generator switcher
1 parent 953425b commit c4a9bc0

File tree

2 files changed

+37
-3
lines changed

2 files changed

+37
-3
lines changed

src/app/components/Header.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function GeneratorTitle({ title, gen }: GeneratorTitleProps) {
6666
const [active, setActive] = useFocus()
6767
const [search, setSearch] = useState('')
6868
const inputRef = useRef<HTMLInputElement>(null)
69+
const resultsRef = useRef<HTMLDivElement>(null)
6970

7071
const icon = Object.keys(Icons).includes(gen.id) ? gen.id as keyof typeof Icons : undefined
7172

@@ -92,14 +93,41 @@ function GeneratorTitle({ title, gen }: GeneratorTitleProps) {
9293
})
9394
}, [setActive, inputRef])
9495

96+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
97+
if (e.key == 'Enter') {
98+
if (document.activeElement == inputRef.current) {
99+
const firstResult = resultsRef.current?.firstElementChild
100+
if (firstResult instanceof HTMLElement) {
101+
firstResult.click()
102+
}
103+
}
104+
} else if (e.key == 'ArrowDown') {
105+
const nextElement = document.activeElement == inputRef.current
106+
? resultsRef.current?.firstElementChild
107+
: document.activeElement?.nextElementSibling
108+
if (nextElement instanceof HTMLElement) {
109+
nextElement.focus()
110+
}
111+
e.preventDefault()
112+
} else if (e.key == 'ArrowUp') {
113+
const prevElement = document.activeElement?.previousElementSibling
114+
if (prevElement instanceof HTMLElement) {
115+
prevElement.focus()
116+
}
117+
e.preventDefault()
118+
} else if (e.key == 'Escape') {
119+
setActive(false)
120+
}
121+
}, [setActive, inputRef])
122+
95123
return <div class="px-1 relative">
96124
<h1 class="font-bold flex items-center cursor-pointer text-lg sm:text-2xl" onClick={open}>
97125
{title}
98126
{icon && Icons[icon]}
99127
</h1>
100-
<div class={`gen-menu absolute flex flex-col gap-2 p-2 rounded-lg drop-shadow-xl ${active ? '' : 'hidden'}`}>
101-
<input ref={inputRef} type="text" class="py-1 px-2 w-full rounded" value={search} placeholder={locale('generators.search')} onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onClick={e => e.stopPropagation()} />
102-
{active && <div class="gen-results overflow-y-auto overscroll-none flex flex-col pr-2 h-96 max-h-max min-w-max">
128+
<div class={`gen-menu absolute flex flex-col gap-2 p-2 rounded-lg drop-shadow-xl ${active ? '' : 'hidden'}`} onKeyDown={handleKeyDown}>
129+
<input ref={inputRef} type="text" class="py-1 px-2 w-full rounded" value={search} placeholder={locale('generators.search')} onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onClick={(e) => e.stopPropagation()} />
130+
{active && <div ref={resultsRef} class="gen-results overflow-y-auto overscroll-none flex flex-col pr-2 h-96 max-h-max min-w-max">
103131
{generators.length === 0 && <span class="note">{locale('generators.no_results')}</span>}
104132
{generators.map(g =>
105133
<Link class="flex items-center cursor-pointer no-underline rounded p-1" href={cleanUrl(g.url)} onClick={() => setActive(false)}>

src/styles/global.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ nav li .btn svg {
234234
background-color: var(--background-1);
235235
}
236236

237+
.gen-results > a {
238+
outline-offset: -2px;
239+
}
240+
237241
.gen-results > a svg {
238242
width: 16px;
239243
height: 16px;
@@ -243,10 +247,12 @@ nav li .btn svg {
243247
transition: margin 0.2s;
244248
}
245249

250+
.gen-results > a:focus-visible,
246251
.gen-results > a:hover {
247252
background-color: var(--background-3);
248253
}
249254

255+
.gen-results > a:focus-visible svg,
250256
.gen-results > a:hover svg {
251257
margin-left: 14px;
252258
margin-right: 0px;

0 commit comments

Comments
 (0)