1+ import {
2+ Suspense ,
3+ createContext ,
4+ lazy ,
5+ useCallback ,
6+ useContext ,
7+ useMemo ,
8+ useRef ,
9+ useState ,
10+ } from "react" ;
11+ import { createPortal } from "react-dom" ;
12+ import { useDocSearchKeyboardEvents } from "@docsearch/react/dist/esm" ;
113import type { DocSearchProps } from "@docsearch/react" ;
2- import { useHydrated } from "~/ui/utils" ;
3- import { Suspense , lazy } from "react" ;
414
5- const OriginalDocSearch = lazy ( ( ) =>
15+ let OriginalDocSearchModal = lazy ( ( ) =>
616 import ( "@docsearch/react" ) . then ( ( module ) => ( {
7- default : module . DocSearch ,
17+ default : module . DocSearchModal ,
18+ } ) )
19+ ) ;
20+
21+ let OriginalDocSearchButton = lazy ( ( ) =>
22+ import ( "@docsearch/react" ) . then ( ( module ) => ( {
23+ default : module . DocSearchButton ,
824 } ) )
925) ;
1026
@@ -14,24 +30,81 @@ let docSearchProps = {
1430 apiKey : "b50c5d7d9f4610c9785fa945fdc97476" ,
1531} satisfies DocSearchProps ;
1632
17- // TODO: Refactor a bit when we add Vite with css imports per component
18- // This will allow us to have two versions of the component, one that has
19- // the button with display: none, and the other with button styles
20- export function DocSearch ( ) {
21- let hydrated = useHydrated ( ) ;
22-
23- if ( ! hydrated ) {
24- // The Algolia doc search container is hard-coded at 40px. It doesn't
25- // render anything on the server, so we get a mis-match after hydration.
26- // This placeholder prevents layout shift when the search appears.
27- return < div className = "h-10" /> ;
33+ const DocSearchContext = createContext < {
34+ onOpen : ( ) => void ;
35+ searchButtonRef : React . RefObject < HTMLButtonElement > ;
36+ } | null > ( null ) ;
37+
38+ /**
39+ * DocSearch but only the modal accessible by keyboard command
40+ * Intended for people instinctively pressing cmd+k on a non-doc page
41+ *
42+ * If you need a DocSearch button to appear, use the DocSearch component
43+ * Modified from https://github.com/algolia/docsearch/blob/main/packages/docsearch-react/src/DocSearch.tsx
44+ */
45+ export function DocSearch ( { children } : { children : React . ReactNode } ) {
46+ const searchButtonRef = useRef < HTMLButtonElement > ( null ) ;
47+ const [ isOpen , setIsOpen ] = useState ( false ) ;
48+
49+ const onOpen = useCallback ( ( ) => {
50+ setIsOpen ( true ) ;
51+ } , [ setIsOpen ] ) ;
52+
53+ const onClose = useCallback ( ( ) => {
54+ setIsOpen ( false ) ;
55+ } , [ setIsOpen ] ) ;
56+
57+ const onInput = useCallback ( ( ) => {
58+ setIsOpen ( true ) ;
59+ } , [ setIsOpen ] ) ;
60+
61+ useDocSearchKeyboardEvents ( {
62+ isOpen,
63+ onOpen,
64+ onClose,
65+ onInput,
66+ searchButtonRef,
67+ } ) ;
68+
69+ const contextValue = useMemo (
70+ ( ) => ( {
71+ onOpen,
72+ searchButtonRef,
73+ } ) ,
74+ [ onOpen , searchButtonRef ]
75+ ) ;
76+
77+ return (
78+ < DocSearchContext . Provider value = { contextValue } >
79+ { children }
80+ { isOpen
81+ ? createPortal (
82+ < Suspense fallback = { null } >
83+ < OriginalDocSearchModal
84+ initialScrollY = { window . scrollY }
85+ onClose = { onClose }
86+ { ...docSearchProps }
87+ />
88+ </ Suspense > ,
89+ document . body
90+ )
91+ : null }
92+ </ DocSearchContext . Provider >
93+ ) ;
94+ }
95+
96+ export function DocSearchButton ( ) {
97+ const docSearchContext = useContext ( DocSearchContext ) ;
98+
99+ if ( ! docSearchContext ) {
100+ throw new Error ( "DocSearch must be used within a DocSearchModal" ) ;
28101 }
29102
103+ const { onOpen, searchButtonRef } = docSearchContext ;
104+
30105 return (
31106 < Suspense fallback = { < div className = "h-10" /> } >
32- < div className = "animate-[fadeIn_100ms_ease-in_1]" >
33- < OriginalDocSearch { ...docSearchProps } />
34- </ div >
107+ < OriginalDocSearchButton ref = { searchButtonRef } onClick = { onOpen } />
35108 </ Suspense >
36109 ) ;
37110}
0 commit comments