refactor: Update debounce logic to use SolidJS signals
This commit is contained in:
parent
b8a8213f27
commit
f42e2b5788
@ -31,10 +31,10 @@ const SearchContext = createContext<{
|
|||||||
results: Accessor<SearchResult[]>;
|
results: Accessor<SearchResult[]>;
|
||||||
isLoading: Accessor<boolean>;
|
isLoading: Accessor<boolean>;
|
||||||
isOpened: Accessor<boolean>;
|
isOpened: Accessor<boolean>;
|
||||||
setQuery: (query: string) => void;
|
setQuery: Setter<string>;
|
||||||
setIsOpened: (isOpened: boolean) => void;
|
setIsOpened: Setter<boolean>;
|
||||||
setIsLoading: (isLoading: boolean) => void;
|
setIsLoading: Setter<boolean>;
|
||||||
setResults: (results: SearchResult[]) => void;
|
setResults: Setter<SearchResult[]>;
|
||||||
}>({
|
}>({
|
||||||
query: () => "",
|
query: () => "",
|
||||||
close: () => {},
|
close: () => {},
|
||||||
@ -89,11 +89,11 @@ function SearchInput() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="group relative flex h-12">
|
<div class="group relative flex h-12">
|
||||||
<SearchIcon class="pointer-events-none absolute top-0 left-4 h-full w-5 fill-slate-400 dark:fill-slate-500" />
|
<SearchIcon class="pointer-events-none absolute top-0 left-4 h-full w-5 fill-slate-400" />
|
||||||
<input
|
<input
|
||||||
data-autofocus
|
data-autofocus
|
||||||
class={clsx(
|
class={clsx(
|
||||||
"flex-auto appearance-none bg-transparent pl-12 text-slate-900 outline-hidden placeholder:text-slate-400 focus:w-full focus:flex-none sm:text-sm dark:text-white [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
|
"flex-auto appearance-none bg-transparent pl-12 text-slate-900 outline-hidden placeholder:text-slate-400 focus:w-full focus:flex-none sm:text-sm [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
|
||||||
isLoading() ? "pr-11" : "pr-4",
|
isLoading() ? "pr-11" : "pr-4",
|
||||||
)}
|
)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@ -108,11 +108,14 @@ function SearchInput() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={query()}
|
value={query()}
|
||||||
onChange={(event) => setQuery(event.currentTarget.value)}
|
onInput={(event) => {
|
||||||
|
const { value } = event.currentTarget;
|
||||||
|
setQuery(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{isLoading() && (
|
{isLoading() && (
|
||||||
<div class="absolute inset-y-0 right-3 flex items-center">
|
<div class="absolute inset-y-0 right-3 flex items-center">
|
||||||
<LoadingIcon class="h-6 w-6 animate-spin stroke-slate-200 text-slate-400 dark:stroke-slate-700 dark:text-slate-500" />
|
<LoadingIcon class="h-6 w-6 animate-spin stroke-slate-200 text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +149,7 @@ function SearchResultItem(props: { result: SearchResult; query: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
class="group block cursor-default rounded-lg px-3 py-2 aria-selected:bg-slate-100 dark:aria-selected:bg-slate-700/30 hover:bg-slate-100 dark:hover:bg-slate-700/30"
|
class="group block cursor-default rounded-lg px-3 py-2 aria-selected:bg-slate-100 hover:bg-slate-100"
|
||||||
aria-labelledby={`${id}-hierarchy ${id}-title`}
|
aria-labelledby={`${id}-hierarchy ${id}-title`}
|
||||||
tab-index={0}
|
tab-index={0}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@ -163,7 +166,7 @@ function SearchResultItem(props: { result: SearchResult; query: string }) {
|
|||||||
<div
|
<div
|
||||||
id={`${id}-title`}
|
id={`${id}-title`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="text-sm text-slate-700 group-aria-selected:text-violet-600 dark:text-slate-300 dark:group-aria-selected:text-violet-400"
|
class="text-sm text-slate-700 group-aria-selected:text-violet-600"
|
||||||
>
|
>
|
||||||
<HighlightQuery text={props.result.title} query={props.query} />
|
<HighlightQuery text={props.result.title} query={props.query} />
|
||||||
</div>
|
</div>
|
||||||
@ -196,13 +199,11 @@ function SearchResultItem(props: { result: SearchResult; query: string }) {
|
|||||||
function SearchResults() {
|
function SearchResults() {
|
||||||
const { results, query } = useContext(SearchContext);
|
const { results, query } = useContext(SearchContext);
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results().length === 0) {
|
||||||
return (
|
return (
|
||||||
<p class="px-4 py-8 text-center text-sm text-slate-700 dark:text-slate-400">
|
<p class="px-4 py-8 text-center text-sm text-slate-700">
|
||||||
Aucun résultat pour “
|
Aucun résultat pour “
|
||||||
<span class="break-words text-slate-900 dark:text-white">
|
<span class="break-words text-slate-900">{query()}</span>
|
||||||
{query()}
|
|
||||||
</span>
|
|
||||||
”
|
”
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@ -241,6 +242,14 @@ function SearchDialog(props: { class?: string }) {
|
|||||||
};
|
};
|
||||||
}, [isOpened, setIsOpened]);
|
}, [isOpened, setIsOpened]);
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const { target, currentTarget } = event;
|
||||||
|
|
||||||
|
if (target instanceof Node && currentTarget instanceof Node) {
|
||||||
|
if (target === currentTarget) close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -250,12 +259,18 @@ function SearchDialog(props: { class?: string }) {
|
|||||||
>
|
>
|
||||||
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" />
|
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" />
|
||||||
|
|
||||||
<div class="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]">
|
<div
|
||||||
<DialogPanel class="mx-auto transform-gpu overflow-hidden rounded-xl bg-white shadow-xl sm:max-w-xl dark:bg-slate-800 dark:ring-1 dark:ring-slate-700">
|
class="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]"
|
||||||
|
onClick={handleClickOutside}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Escape") close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogPanel class="mx-auto transform-gpu overflow-hidden rounded-xl bg-white shadow-xl sm:max-w-xl">
|
||||||
<form onSubmit={(event) => event.preventDefault()}>
|
<form onSubmit={(event) => event.preventDefault()}>
|
||||||
<SearchInput />
|
<SearchInput />
|
||||||
<div class="border-t border-slate-200 bg-white px-2 py-3 empty:hidden dark:border-slate-400/10 dark:bg-slate-800">
|
<div class="border-t border-slate-200 bg-white px-2 py-3 empty:hidden">
|
||||||
{results.length > 0 && <SearchResults />}
|
{results().length > 0 && <SearchResults />}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
@ -267,23 +282,22 @@ function SearchDialog(props: { class?: string }) {
|
|||||||
|
|
||||||
export function Search() {
|
export function Search() {
|
||||||
const [results, setResults] = createSignal<SearchResult[]>([]);
|
const [results, setResults] = createSignal<SearchResult[]>([]);
|
||||||
const [debouncedQuery, setDebouncedQuery] = useDebounce("");
|
|
||||||
const [modifierKey, setModifierKey] = createSignal<string>();
|
const [modifierKey, setModifierKey] = createSignal<string>();
|
||||||
const [isLoading, setIsLoading] = createSignal(false);
|
const [isLoading, setIsLoading] = createSignal(false);
|
||||||
const [isOpened, setIsOpened] = createSignal(false);
|
const [isOpened, setIsOpened] = createSignal(false);
|
||||||
const [query, setQuery] = createSignal("");
|
const [query, setQuery] = createSignal("");
|
||||||
|
|
||||||
|
const debouncedQuery = useDebounce(query, 300);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const platform = navigator.userAgentData?.platform || navigator.platform;
|
const platform = navigator.userAgentData?.platform || navigator.platform;
|
||||||
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(platform) ? "⌘" : "Ctrl ");
|
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(platform) ? "⌘" : "Ctrl ");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setDebouncedQuery(query());
|
const query = debouncedQuery();
|
||||||
}, [query()]);
|
|
||||||
|
|
||||||
createEffect(() => {
|
if (query.length === 0) {
|
||||||
if (debouncedQuery.length === 0) {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setResults([]);
|
setResults([]);
|
||||||
return;
|
return;
|
||||||
@ -291,12 +305,12 @@ export function Search() {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
onSearch(debouncedQuery())
|
onSearch(query)
|
||||||
.then(setResults)
|
.then(setResults)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, [debouncedQuery()]);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchContext.Provider
|
<SearchContext.Provider
|
||||||
@ -314,15 +328,15 @@ export function Search() {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pr-3.5 md:pl-4 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 lg:w-96 dark:md:bg-slate-800/75 dark:md:ring-white/5 dark:md:ring-inset dark:md:hover:bg-slate-700/40 dark:md:hover:ring-slate-500"
|
class="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pr-3.5 md:pl-4 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 lg:w-96"
|
||||||
onClick={() => setIsOpened(true)}
|
onClick={() => setIsOpened(true)}
|
||||||
>
|
>
|
||||||
<SearchIcon class="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400 dark:fill-slate-500" />
|
<SearchIcon class="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400" />
|
||||||
<span class="sr-only md:not-sr-only md:ml-2 md:text-slate-500 md:dark:text-slate-400">
|
<span class="sr-only md:not-sr-only md:ml-2 md:text-slate-500">
|
||||||
Rechercher...
|
Rechercher...
|
||||||
</span>
|
</span>
|
||||||
{modifierKey && (
|
{modifierKey && (
|
||||||
<kbd class="ml-auto hidden font-medium text-slate-400 md:block dark:text-slate-500">
|
<kbd class="ml-auto hidden font-medium text-slate-400 md:block">
|
||||||
<kbd class="font-sans">{modifierKey()}</kbd>
|
<kbd class="font-sans">{modifierKey()}</kbd>
|
||||||
<kbd class="font-sans">K</kbd>
|
<kbd class="font-sans">K</kbd>
|
||||||
</kbd>
|
</kbd>
|
||||||
|
|||||||
@ -1,23 +1,17 @@
|
|||||||
import type { Accessor, Setter } from "solid-js";
|
import { createSignal, createEffect, onCleanup } from "solid-js";
|
||||||
|
import type { Accessor } from "solid-js";
|
||||||
|
|
||||||
import { createEffect, createSignal } from "solid-js";
|
export function useDebounce<T>(value: Accessor<T>, delay = 1000): Accessor<T> {
|
||||||
|
const [debouncedValue, setDebouncedValue] = createSignal(value());
|
||||||
export function useDebounce<T>(
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
defaultValue: T,
|
|
||||||
debounceTime = 300,
|
|
||||||
): [Accessor<T>, Setter<T>] {
|
|
||||||
const [debouncedValue, setDebouncedValue] = createSignal<T>(defaultValue);
|
|
||||||
const [value, setValue] = createSignal<T>(defaultValue);
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
value();
|
||||||
setDebouncedValue(value());
|
clearTimeout(timeoutId);
|
||||||
}, debounceTime);
|
timeoutId = setTimeout(() => setDebouncedValue(() => value()), delay);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
onCleanup(() => clearTimeout(timeoutId));
|
||||||
clearTimeout(handler);
|
|
||||||
};
|
|
||||||
}, [value()]);
|
|
||||||
|
|
||||||
return [debouncedValue, setValue];
|
return debouncedValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,6 @@ function Header() {
|
|||||||
// <div class="h-6 w-6 animate-pulse rounded-full bg-slate-200" />
|
// <div class="h-6 w-6 animate-pulse rounded-full bg-slate-200" />
|
||||||
// }
|
// }
|
||||||
/>
|
/>
|
||||||
<div class="h-6 w-6 animate-pulse rounded-full bg-slate-200" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex basis-0 justify-end gap-6 sm:gap-8 md:grow">
|
<div class="relative flex basis-0 justify-end gap-6 sm:gap-8 md:grow">
|
||||||
|
|||||||
@ -37,8 +37,10 @@ export function buildFlexSearch(data: FlexSearchData) {
|
|||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
const { content, hash, subsections } = section;
|
const { content, hash, subsections } = section;
|
||||||
|
|
||||||
|
const url = key.replace("index", "").replace(/(\+Page)?.md(x)?$/, "");
|
||||||
|
|
||||||
sectionIndex.add({
|
sectionIndex.add({
|
||||||
url: key + (hash ? `#${hash}` : ""),
|
url: url + (hash ? `#${hash}` : ""),
|
||||||
title: content,
|
title: content,
|
||||||
content: [content, ...subsections].join("\n"),
|
content: [content, ...subsections].join("\n"),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user