"use client" import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import algoliasearch, { SearchClient } from "algoliasearch/lite" import { InstantSearch, SearchBox } from "react-instantsearch" import Modal from "../../Modal" import clsx from "clsx" import IconMagnifyingGlass from "../../Icons/MagnifyingGlass" import IconXMark from "../../Icons/XMark" import SearchEmptyQueryBoundary from "../EmptyQueryBoundary" import SearchSuggestions from "../Suggestions" import { useSearch } from "../../../providers/search" import checkArraySameElms from "../../../utils/array-same-elms" import SearchHitsWrapper from "../Hits" import Button from "../../Button" import Kbd from "../../MDXComponents/Kbd" import { OptionType } from "../../../hooks/use-select" import SelectBadge from "../../Select/Badge" import useKeyboardShortcut from "../../../hooks/use-keyboard-shortcut" import { findNextSibling, findPrevSibling } from "../../../utils/dom-utils" const algoliaClient = algoliasearch( process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "temp", process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp" ) const searchClient: SearchClient = { ...algoliaClient, async search(requests) { if (requests.every(({ params }) => !params?.query)) { return Promise.resolve({ results: requests.map(() => ({ hits: [], nbHits: 0, nbPages: 0, page: 0, processingTimeMS: 0, hitsPerPage: 0, exhaustiveNbHits: false, query: "", params: "", })), }) } return algoliaClient.search(requests) }, } const SearchModal = () => { const modalRef = useRef(null) const options: OptionType[] = useMemo(() => { return [ { value: "admin", label: "Admin API", }, { value: "store", label: "Store API", }, { value: "docs", label: "Docs", }, { value: "user-guide", label: "User Guide", }, { value: "plugins", label: "Plugins", }, { value: "reference", label: "References", }, { value: "ui", label: "UI", }, ] }, []) const { isOpen, setIsOpen, defaultFilters } = useSearch() const [filters, setFilters] = useState(defaultFilters) const formattedFilters: string = useMemo(() => { let formatted = "" filters.forEach((filter) => { const split = filter.split("_") split.forEach((f) => { if (formatted.length) { formatted += " OR " } formatted += `_tags:${f}` }) }) return formatted }, [filters]) const searchBoxRef = useRef(null) const focusSearchInput = () => searchBoxRef.current?.querySelector("input")?.focus() useEffect(() => { if (!checkArraySameElms(defaultFilters, filters)) { setFilters(defaultFilters) } }, [defaultFilters]) useEffect(() => { if (isOpen && searchBoxRef.current) { focusSearchInput() } else if (!isOpen) { const focusedItem = modalRef.current?.querySelector( ":focus" ) as HTMLElement if ( focusedItem && focusedItem === searchBoxRef.current?.querySelector("input") ) { // remove focus focusedItem.blur() } } }, [isOpen]) const handleKeyAction = (e: KeyboardEvent) => { if (!isOpen) { return } e.preventDefault() const focusedItem = modalRef.current?.querySelector(":focus") as HTMLElement if (!focusedItem) { // focus the first data-hit const nextItem = modalRef.current?.querySelector( "[data-hit]" ) as HTMLElement nextItem?.focus() return } const isHit = focusedItem.hasAttribute("data-hit") const isInput = focusedItem.tagName.toLowerCase() === "input" if (!isHit && !isInput) { // ignore if focused items aren't input/data-hit return } const lowerPressedKey = e.key.toLowerCase() if (lowerPressedKey === "enter") { if (isHit) { // trigger click event of the focused element focusedItem.click() } return } if (lowerPressedKey === "arrowup") { // only hit items has action on arrow up if (isHit) { // find if there's a data-hit item before this one const beforeItem = findPrevSibling(focusedItem, "[data-hit]") if (!beforeItem) { // focus the input focusSearchInput() } else { // focus the previous item beforeItem.focus() } } } else if (lowerPressedKey === "arrowdown") { // check if item is input or hit if (isInput) { // go to the first data-hit item const nextItem = modalRef.current?.querySelector( "[data-hit]" ) as HTMLElement nextItem?.focus() } else { // handle go down for hit items // find if there's a data-hit item after this one const afterItem = findNextSibling(focusedItem, "[data-hit]") if (afterItem) { // focus the next item afterItem.focus() } } } } const shortcutKeys = useMemo(() => ["ArrowUp", "ArrowDown", "Enter"], []) useKeyboardShortcut({ metakey: false, shortcutKeys, action: handleKeyAction, checkEditing: false, preventDefault: false, }) const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (!isOpen) { return } // check if shortcut keys were pressed const lowerPressedKey = e.key.toLowerCase() const pressedShortcut = [...shortcutKeys, "Escape"].some( (s) => s.toLowerCase() === lowerPressedKey ) if (pressedShortcut) { return } const focusedItem = modalRef.current?.querySelector( ":focus" ) as HTMLElement const searchInput = searchBoxRef.current?.querySelector( "input" ) as HTMLInputElement if (searchInput && focusedItem !== searchInput) { searchInput.focus() } }, [shortcutKeys, isOpen] ) useEffect(() => { window.addEventListener("keydown", handleKeyDown) return () => { window.removeEventListener("keydown", handleKeyDown) } }, [handleKeyDown]) return ( setIsOpen(false)} ref={modalRef} >
( )} resetIconComponent={() => ( )} placeholder="Find something..." autoFocus formRef={searchBoxRef} />
}>
setFilters(Array.isArray(value) ? [...value] : [value]) } addSelected={(value) => setFilters((prev) => [...prev, value])} removeSelected={(value) => setFilters((prev) => prev.filter((v) => v !== value)) } showClearButton={false} placeholder="Filters" handleAddAll={(isAllSelected: boolean) => { if (isAllSelected) { setFilters(defaultFilters) } else { setFilters(options.map((option) => option.value)) } }} />
Navigation
Open Result
) } export default SearchModal