From 111855f3ea17b5ef02fa3d5fac373b6f1dd24a2d Mon Sep 17 00:00:00 2001 From: Misode Date: Tue, 28 Jan 2025 00:40:28 +0100 Subject: [PATCH] Make generator switcher a generic component --- src/app/components/FancyMenu.tsx | 65 +++++++++++++++++++++++ src/app/components/Header.tsx | 89 +++++++++----------------------- src/styles/global.css | 16 +++--- 3 files changed, 96 insertions(+), 74 deletions(-) create mode 100644 src/app/components/FancyMenu.tsx diff --git a/src/app/components/FancyMenu.tsx b/src/app/components/FancyMenu.tsx new file mode 100644 index 00000000..16f144ee --- /dev/null +++ b/src/app/components/FancyMenu.tsx @@ -0,0 +1,65 @@ +import type { ComponentChildren } from 'preact' +import { useCallback, useMemo, useRef, useState } from 'preact/hooks' +import { useFocus } from '../hooks/index.js' + +interface Props { + placeholder?: string + getResults: (search: string, close: () => void) => ComponentChildren + children: ComponentChildren +} +export function FancyMenu({ placeholder, getResults, children }: Props) { + const [active, setActive] = useFocus() + const [search, setSearch] = useState('') + const inputRef = useRef(null) + const resultsRef = useRef(null) + + const results = useMemo(() => { + return getResults(search, () => setActive(false)) + }, [getResults, setActive, search]) + + const open = useCallback(() => { + setActive(true) + setTimeout(() => { + inputRef.current?.select() + }) + }, [setActive, inputRef]) + + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key == 'Enter') { + if (document.activeElement == inputRef.current) { + const firstResult = resultsRef.current?.firstElementChild + if (firstResult instanceof HTMLElement) { + firstResult.click() + } + } + } else if (e.key == 'ArrowDown') { + const nextElement = document.activeElement == inputRef.current + ? resultsRef.current?.firstElementChild + : document.activeElement?.nextElementSibling + if (nextElement instanceof HTMLElement) { + nextElement.focus() + } + e.preventDefault() + } else if (e.key == 'ArrowUp') { + const prevElement = document.activeElement?.previousElementSibling + if (prevElement instanceof HTMLElement) { + prevElement.focus() + } + e.preventDefault() + } else if (e.key == 'Escape') { + setActive(false) + } + }, [setActive, inputRef]) + + return
+
+ {children} +
+
+ setSearch((e.target as HTMLInputElement).value)} onClick={(e) => e.stopPropagation()} /> + {active &&
+ {results} +
} +
+
+} diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index b64a3670..2d62c88d 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -1,10 +1,10 @@ import { getCurrentUrl, Link } from 'preact-router' -import { useCallback, useMemo, useRef, useState } from 'preact/hooks' +import { useCallback } from 'preact/hooks' import type { ConfigGenerator } from '../Config.js' import config from '../Config.js' import { useLocale, useTheme, useTitle, useVersion } from '../contexts/index.js' -import { useFocus } from '../hooks/useFocus.js' import { cleanUrl, getGenerator, SOURCE_REPO_URL } from '../Utils.js' +import { FancyMenu } from './FancyMenu.jsx' import { Btn, BtnMenu, Icons, Octicon } from './index.js' const Themes: Record = { @@ -63,83 +63,40 @@ function GeneratorTitle({ title, gen }: GeneratorTitleProps) { const { locale } = useLocale() const { version } = useVersion() - const [active, setActive] = useFocus() - const [search, setSearch] = useState('') - const inputRef = useRef(null) - const resultsRef = useRef(null) - const icon = Object.keys(Icons).includes(gen.id) ? gen.id as keyof typeof Icons : undefined - const generators = useMemo(() => { - let result = config.generators + const getGenerators = useCallback((search: string, close: () => void) => { + let results = config.generators .filter(g => !g.dependency) .map(g => ({ ...g, name: locale(`generator.${g.id}`).toLowerCase() })) if (search) { const parts = search.split(' ') - result = result.filter(g => parts.some(p => g.name.includes(p)) + results = results.filter(g => parts.some(p => g.name.includes(p)) || parts.some(p => g.tags?.some(t => t.includes(p)) ?? false)) } - result.sort((a, b) => a.name.localeCompare(b.name)) + results.sort((a, b) => a.name.localeCompare(b.name)) if (search) { - result.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0)) + results.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0)) } - return result - }, [locale, version, search]) - - const open = useCallback(() => { - setActive(true) - setTimeout(() => { - inputRef.current?.select() - }) - }, [setActive, inputRef]) - - const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (e.key == 'Enter') { - if (document.activeElement == inputRef.current) { - const firstResult = resultsRef.current?.firstElementChild - if (firstResult instanceof HTMLElement) { - firstResult.click() - } - } - } else if (e.key == 'ArrowDown') { - const nextElement = document.activeElement == inputRef.current - ? resultsRef.current?.firstElementChild - : document.activeElement?.nextElementSibling - if (nextElement instanceof HTMLElement) { - nextElement.focus() - } - e.preventDefault() - } else if (e.key == 'ArrowUp') { - const prevElement = document.activeElement?.previousElementSibling - if (prevElement instanceof HTMLElement) { - prevElement.focus() - } - e.preventDefault() - } else if (e.key == 'Escape') { - setActive(false) + if (results.length === 0) { + return [{locale('generators.no_results')}] } - }, [setActive, inputRef]) + return results.map(g => + + {locale(`generator.${g.id}`)} + {Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined} +
+ {g.tags?.filter(t => t === 'assets').map(t => +
{t}
+ )} + + ) + }, [locale, version]) - return
-

+ return +

{title} {icon && Icons[icon]}

-
- setSearch((e.target as HTMLInputElement).value)} onClick={(e) => e.stopPropagation()} /> - {active &&
- {generators.length === 0 && {locale('generators.no_results')}} - {generators.map(g => - setActive(false)}> - {locale(`generator.${g.id}`)} - {Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined} -
- {g.tags?.filter(t => t === 'assets').map(t => -
{t}
- )} - - )} -
} -
-

+ } diff --git a/src/styles/global.css b/src/styles/global.css index 7408a2f0..aedf36b0 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -225,20 +225,20 @@ nav li .btn svg { height: 24px; } -.gen-menu { +.fancy-menu { background-color: var(--background-2); color: var(--text-2); } -.gen-menu input { +.fancy-menu input { background-color: var(--background-1); } -.gen-results > a { +.gen-result { outline-offset: -2px; } -.gen-results > a svg { +.gen-result svg { width: 16px; height: 16px; fill: var(--nav); @@ -247,13 +247,13 @@ nav li .btn svg { transition: margin 0.2s; } -.gen-results > a:focus-visible, -.gen-results > a:hover { +.gen-result:focus-visible, +.gen-result:hover { background-color: var(--background-3); } -.gen-results > a:focus-visible svg, -.gen-results > a:hover svg { +.gen-result:focus-visible svg, +.gen-result:hover svg { margin-left: 14px; margin-right: 0px; }