Skip to content

Commit 111855f

Browse files
committed
Make generator switcher a generic component
1 parent c4a9bc0 commit 111855f

File tree

3 files changed

+96
-74
lines changed

3 files changed

+96
-74
lines changed

src/app/components/FancyMenu.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { ComponentChildren } from 'preact'
2+
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
3+
import { useFocus } from '../hooks/index.js'
4+
5+
interface Props {
6+
placeholder?: string
7+
getResults: (search: string, close: () => void) => ComponentChildren
8+
children: ComponentChildren
9+
}
10+
export function FancyMenu({ placeholder, getResults, children }: Props) {
11+
const [active, setActive] = useFocus()
12+
const [search, setSearch] = useState('')
13+
const inputRef = useRef<HTMLInputElement>(null)
14+
const resultsRef = useRef<HTMLDivElement>(null)
15+
16+
const results = useMemo(() => {
17+
return getResults(search, () => setActive(false))
18+
}, [getResults, setActive, search])
19+
20+
const open = useCallback(() => {
21+
setActive(true)
22+
setTimeout(() => {
23+
inputRef.current?.select()
24+
})
25+
}, [setActive, inputRef])
26+
27+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
28+
if (e.key == 'Enter') {
29+
if (document.activeElement == inputRef.current) {
30+
const firstResult = resultsRef.current?.firstElementChild
31+
if (firstResult instanceof HTMLElement) {
32+
firstResult.click()
33+
}
34+
}
35+
} else if (e.key == 'ArrowDown') {
36+
const nextElement = document.activeElement == inputRef.current
37+
? resultsRef.current?.firstElementChild
38+
: document.activeElement?.nextElementSibling
39+
if (nextElement instanceof HTMLElement) {
40+
nextElement.focus()
41+
}
42+
e.preventDefault()
43+
} else if (e.key == 'ArrowUp') {
44+
const prevElement = document.activeElement?.previousElementSibling
45+
if (prevElement instanceof HTMLElement) {
46+
prevElement.focus()
47+
}
48+
e.preventDefault()
49+
} else if (e.key == 'Escape') {
50+
setActive(false)
51+
}
52+
}, [setActive, inputRef])
53+
54+
return <div class="px-1 relative">
55+
<div onClick={open}>
56+
{children}
57+
</div>
58+
<div class={`fancy-menu absolute flex flex-col gap-2 p-2 rounded-lg drop-shadow-xl ${active ? '' : 'hidden'}`} onKeyDown={handleKeyDown}>
59+
<input ref={inputRef} type="text" class="py-1 px-2 w-full rounded" value={search} placeholder={placeholder} onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onClick={(e) => e.stopPropagation()} />
60+
{active && <div ref={resultsRef} class="overflow-y-auto overscroll-none flex flex-col pr-2 h-96 max-h-max min-w-max">
61+
{results}
62+
</div>}
63+
</div>
64+
</div>
65+
}

src/app/components/Header.tsx

Lines changed: 23 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { getCurrentUrl, Link } from 'preact-router'
2-
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
2+
import { useCallback } from 'preact/hooks'
33
import type { ConfigGenerator } from '../Config.js'
44
import config from '../Config.js'
55
import { useLocale, useTheme, useTitle, useVersion } from '../contexts/index.js'
6-
import { useFocus } from '../hooks/useFocus.js'
76
import { cleanUrl, getGenerator, SOURCE_REPO_URL } from '../Utils.js'
7+
import { FancyMenu } from './FancyMenu.jsx'
88
import { Btn, BtnMenu, Icons, Octicon } from './index.js'
99

1010
const Themes: Record<string, keyof typeof Octicon> = {
@@ -63,83 +63,40 @@ function GeneratorTitle({ title, gen }: GeneratorTitleProps) {
6363
const { locale } = useLocale()
6464
const { version } = useVersion()
6565

66-
const [active, setActive] = useFocus()
67-
const [search, setSearch] = useState('')
68-
const inputRef = useRef<HTMLInputElement>(null)
69-
const resultsRef = useRef<HTMLDivElement>(null)
70-
7166
const icon = Object.keys(Icons).includes(gen.id) ? gen.id as keyof typeof Icons : undefined
7267

73-
const generators = useMemo(() => {
74-
let result = config.generators
68+
const getGenerators = useCallback((search: string, close: () => void) => {
69+
let results = config.generators
7570
.filter(g => !g.dependency)
7671
.map(g => ({ ...g, name: locale(`generator.${g.id}`).toLowerCase() }))
7772
if (search) {
7873
const parts = search.split(' ')
79-
result = result.filter(g => parts.some(p => g.name.includes(p))
74+
results = results.filter(g => parts.some(p => g.name.includes(p))
8075
|| parts.some(p => g.tags?.some(t => t.includes(p)) ?? false))
8176
}
82-
result.sort((a, b) => a.name.localeCompare(b.name))
77+
results.sort((a, b) => a.name.localeCompare(b.name))
8378
if (search) {
84-
result.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0))
79+
results.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0))
8580
}
86-
return result
87-
}, [locale, version, search])
88-
89-
const open = useCallback(() => {
90-
setActive(true)
91-
setTimeout(() => {
92-
inputRef.current?.select()
93-
})
94-
}, [setActive, inputRef])
95-
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)
81+
if (results.length === 0) {
82+
return [<span class="note">{locale('generators.no_results')}</span>]
12083
}
121-
}, [setActive, inputRef])
84+
return results.map(g =>
85+
<Link class="gen-result flex items-center cursor-pointer no-underline rounded p-1" href={cleanUrl(g.url)} onClick={close}>
86+
{locale(`generator.${g.id}`)}
87+
{Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined}
88+
<div class="m-auto"></div>
89+
{g.tags?.filter(t => t === 'assets').map(t =>
90+
<div class="badge ml-2 mr-0 text-sm" style="--color: #555;">{t}</div>
91+
)}
92+
</Link>
93+
)
94+
}, [locale, version])
12295

123-
return <div class="px-1 relative">
124-
<h1 class="font-bold flex items-center cursor-pointer text-lg sm:text-2xl" onClick={open}>
96+
return <FancyMenu getResults={getGenerators} placeholder={locale('generators.search')}>
97+
<h1 class="font-bold flex items-center cursor-pointer text-lg sm:text-2xl">
12598
{title}
12699
{icon && Icons[icon]}
127100
</h1>
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">
131-
{generators.length === 0 && <span class="note">{locale('generators.no_results')}</span>}
132-
{generators.map(g =>
133-
<Link class="flex items-center cursor-pointer no-underline rounded p-1" href={cleanUrl(g.url)} onClick={() => setActive(false)}>
134-
{locale(`generator.${g.id}`)}
135-
{Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined}
136-
<div class="m-auto"></div>
137-
{g.tags?.filter(t => t === 'assets').map(t =>
138-
<div class="badge ml-2 mr-0 text-sm" style="--color: #555;">{t}</div>
139-
)}
140-
</Link>
141-
)}
142-
</div>}
143-
</div>
144-
</div>
101+
</FancyMenu>
145102
}

src/styles/global.css

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,20 +225,20 @@ nav li .btn svg {
225225
height: 24px;
226226
}
227227

228-
.gen-menu {
228+
.fancy-menu {
229229
background-color: var(--background-2);
230230
color: var(--text-2);
231231
}
232232

233-
.gen-menu input {
233+
.fancy-menu input {
234234
background-color: var(--background-1);
235235
}
236236

237-
.gen-results > a {
237+
.gen-result {
238238
outline-offset: -2px;
239239
}
240240

241-
.gen-results > a svg {
241+
.gen-result svg {
242242
width: 16px;
243243
height: 16px;
244244
fill: var(--nav);
@@ -247,13 +247,13 @@ nav li .btn svg {
247247
transition: margin 0.2s;
248248
}
249249

250-
.gen-results > a:focus-visible,
251-
.gen-results > a:hover {
250+
.gen-result:focus-visible,
251+
.gen-result:hover {
252252
background-color: var(--background-3);
253253
}
254254

255-
.gen-results > a:focus-visible svg,
256-
.gen-results > a:hover svg {
255+
.gen-result:focus-visible svg,
256+
.gen-result:hover svg {
257257
margin-left: 14px;
258258
margin-right: 0px;
259259
}

0 commit comments

Comments
 (0)