Focus on search input when opening search popup

This commit is contained in:
DrMint 2023-05-11 00:41:36 +02:00
parent a8960d67ed
commit 5d2fe252ec
3 changed files with 58 additions and 43 deletions

View File

@ -11,6 +11,7 @@ import { Button } from "components/Inputs/Button";
*/ */
interface Props { interface Props {
onOpen?: () => void;
onCloseRequest?: () => void; onCloseRequest?: () => void;
isVisible: boolean; isVisible: boolean;
children: React.ReactNode; children: React.ReactNode;
@ -23,6 +24,7 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const Popup = ({ export const Popup = ({
onOpen,
onCloseRequest, onCloseRequest,
isVisible, isVisible,
children, children,
@ -47,13 +49,18 @@ export const Popup = ({
if (isVisible) { if (isVisible) {
setHidden(false); setHidden(false);
// We delay the visiblity of the element so that the opening animation is played // We delay the visiblity of the element so that the opening animation is played
timeouts.push(setTimeout(() => setActuallyVisible(true), 100)); timeouts.push(
setTimeout(() => {
setActuallyVisible(true);
onOpen?.();
}, 100)
);
} else { } else {
setActuallyVisible(false); setActuallyVisible(false);
timeouts.push(setTimeout(() => setHidden(true), 600)); timeouts.push(setTimeout(() => setHidden(true), 600));
} }
return () => timeouts.forEach(clearTimeout); return () => timeouts.forEach(clearTimeout);
}, [isVisible]); }, [isVisible, onOpen]);
return isHidden ? ( return isHidden ? (
<></> <></>

View File

@ -1,3 +1,4 @@
import { forwardRef } from "react";
import { Ico } from "components/Ico"; import { Ico } from "components/Ico";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/asserts"; import { isDefinedAndNotEmpty } from "helpers/asserts";
@ -18,34 +19,31 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TextInput = ({ export const TextInput = forwardRef<HTMLInputElement, Props>(
value, ({ value, onChange, className, name, placeholder, disabled = false }, ref) => (
onChange, <div className={cJoin("relative", className)}>
className, <input
name, ref={ref}
placeholder, className="w-full"
disabled = false, type="text"
}: Props): JSX.Element => ( name={name}
<div className={cJoin("relative", className)}> value={value}
<input disabled={disabled}
className="w-full" placeholder={placeholder ?? undefined}
type="text" onChange={(event) => {
name={name} onChange(event.target.value);
value={value} }}
disabled={disabled} />
placeholder={placeholder ?? undefined} {isDefinedAndNotEmpty(value) && (
onChange={(event) => { <div className="absolute bottom-0 right-4 top-0 grid place-items-center">
onChange(event.target.value); <Ico
}} className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))}
/> icon="close"
{isDefinedAndNotEmpty(value) && ( onClick={() => !disabled && onChange("")}
<div className="absolute bottom-0 right-4 top-0 grid place-items-center"> />
<Ico </div>
className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))} )}
icon="close" </div>
onClick={() => !disabled && onChange("")} )
/>
</div>
)}
</div>
); );
TextInput.displayName = "TextInput";

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { MaterialSymbol } from "material-symbols"; import { MaterialSymbol } from "material-symbols";
import { Popup } from "components/Containers/Popup"; import { Popup } from "components/Containers/Popup";
import { sendAnalytics } from "helpers/analytics"; import { sendAnalytics } from "helpers/analytics";
@ -55,13 +55,13 @@ export const SearchPopup = (): JSX.Element => {
const { format } = useFormat(); const { format } = useFormat();
const [multiResult, setMultiResult] = useState<MultiResult>({}); const [multiResult, setMultiResult] = useState<MultiResult>({});
useEffect(() => { const fetchSearchResults = useCallback((q: string) => {
const fetchMultiResult = async () => { const fetchMultiResult = async () => {
const searchResults = ( const searchResults = (
await meiliMultiSearch([ await meiliMultiSearch([
{ {
indexUid: MeiliIndices.LIBRARY_ITEM, indexUid: MeiliIndices.LIBRARY_ITEM,
q: query, q,
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
attributesToRetrieve: [ attributesToRetrieve: [
"title", "title",
@ -80,7 +80,7 @@ export const SearchPopup = (): JSX.Element => {
}, },
{ {
indexUid: MeiliIndices.CONTENT, indexUid: MeiliIndices.CONTENT,
q: query, q,
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"], attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
attributesToHighlight: ["translations"], attributesToHighlight: ["translations"],
@ -88,7 +88,7 @@ export const SearchPopup = (): JSX.Element => {
}, },
{ {
indexUid: MeiliIndices.VIDEOS, indexUid: MeiliIndices.VIDEOS,
q: query, q,
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
attributesToRetrieve: [ attributesToRetrieve: [
"title", "title",
@ -104,7 +104,7 @@ export const SearchPopup = (): JSX.Element => {
}, },
{ {
indexUid: MeiliIndices.POST, indexUid: MeiliIndices.POST,
q: query, q,
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"], attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"],
attributesToHighlight: [ attributesToHighlight: [
@ -117,7 +117,7 @@ export const SearchPopup = (): JSX.Element => {
}, },
{ {
indexUid: MeiliIndices.WEAPON, indexUid: MeiliIndices.WEAPON,
q: query, q,
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
attributesToHighlight: ["translations.description", "translations.names"], attributesToHighlight: ["translations.description", "translations.names"],
attributesToCrop: ["translations.description"], attributesToCrop: ["translations.description"],
@ -125,7 +125,7 @@ export const SearchPopup = (): JSX.Element => {
}, },
{ {
indexUid: MeiliIndices.WIKI_PAGE, indexUid: MeiliIndices.WIKI_PAGE,
q: query, q,
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
attributesToHighlight: [ attributesToHighlight: [
"translations.title", "translations.title",
@ -184,12 +184,16 @@ export const SearchPopup = (): JSX.Element => {
setMultiResult(result); setMultiResult(result);
}; };
if (query === "") { if (q === "") {
setMultiResult({}); setMultiResult({});
} else { } else {
fetchMultiResult(); fetchMultiResult();
} }
}, [query]);
setQuery(q);
}, []);
const searchInputRef = useRef<HTMLInputElement>(null);
return ( return (
<Popup <Popup
@ -198,12 +202,18 @@ export const SearchPopup = (): JSX.Element => {
setSearchOpened(false); setSearchOpened(false);
sendAnalytics("Search", "Close search"); sendAnalytics("Search", "Close search");
}} }}
onOpen={() => searchInputRef.current?.focus()}
fillViewport> fillViewport>
<h2 className="inline-flex place-items-center gap-2 text-2xl"> <h2 className="inline-flex place-items-center gap-2 text-2xl">
<Ico icon="search" isFilled /> <Ico icon="search" isFilled />
{format("search")} {format("search")}
</h2> </h2>
<TextInput onChange={setQuery} value={query} placeholder={format("search_title")} /> <TextInput
ref={searchInputRef}
onChange={fetchSearchResults}
value={query}
placeholder={format("search_title")}
/>
<div className="flex w-full flex-wrap gap-12 gap-x-16"> <div className="flex w-full flex-wrap gap-12 gap-x-16">
{isDefined(multiResult.libraryItems) && ( {isDefined(multiResult.libraryItems) && (