Skip to content

Commit 8b5a209

Browse files
authored
docs: refactor search modal (TanStack#773)
* docs: refactor search modal - state/Modal has been moved into a SearchProvider - <Search /> is basically just the UI "button" - keyboard shortcut works everywhere - rendering <Search /> twice won't trigger 2 modals * fix: weird layout squish
1 parent 5e86d33 commit 8b5a209

File tree

6 files changed

+195
-235
lines changed

6 files changed

+195
-235
lines changed

docs/src/components/Nav.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@ import * as React from 'react'
22
import Link from 'next/link'
33
import logoSrc from '../images/logo.svg'
44
import { siteConfig } from 'siteConfig'
5+
import { Search } from './Search'
56

67
export const Nav = () => (
78
<div className="bg-white border-b border-gray-200">
89
<div className="container mx-auto">
9-
<div className="grid grid-cols-1 md:grid-cols-12 md:gap-6">
10-
<div className="w-60 col-span-3 flex items-center h-16 pt-4 md:pt-0">
10+
<div className="flex flex-wrap items-center">
11+
<div className="w-60 flex items-center h-16 pt-4 md:pt-0">
1112
<Link href="/" as="/">
1213
<a>
1314
<span className="sr-only">Home</span>
14-
<img src={logoSrc} />
15+
<img src={logoSrc} alt="React Query" />
1516
</a>
1617
</Link>
1718
</div>
18-
<div className="md:col-span-9 items-center flex justify-between md:justify-end space-x-4 md:space-x-8 h-16">
19-
<div className="flex items-center space-x-4 md:space-x-8 text-sm md:text-base">
19+
20+
<div className="flex-grow hidden lg:block ml-8">
21+
<Search />
22+
</div>
23+
24+
<div className="flex flex-grow items-center justify-between w-3/4 md:w-auto md:justify-end space-x-4 md:space-x-8 h-16">
25+
<div className="flex space-x-4 md:space-x-8 text-sm md:text-base">
2026
<div>
2127
<Link href="/docs/overview">
2228
<a className="leading-6 font-medium">Docs</a>

docs/src/components/Search.js

Lines changed: 54 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,56 @@
1-
import * as React from 'react';
2-
import { createPortal } from 'react-dom';
3-
import Router from 'next/router';
4-
import Link from 'next/link';
5-
import Head from 'next/head';
6-
import { useDocSearchKeyboardEvents } from '@docsearch/react';
7-
import { siteConfig } from 'siteConfig';
1+
import * as React from 'react'
2+
import { useSearch } from './useSearch'
83

9-
function Hit({
10-
hit,
11-
children
12-
}) {
13-
return <Link href={hit.url.replace()}>
14-
<a>{children}</a>
15-
</Link>;
16-
}
17-
18-
const options = {
19-
appId: siteConfig.algolia.appId,
20-
apiKey: siteConfig.algolia.apiKey,
21-
indexName: siteConfig.algolia.indexName,
22-
rednerModal: true
23-
};
24-
let DocSearchModal = null;
25-
export const Search = ({
26-
appId,
27-
searchParameters = {
28-
hitsPerPage: 5
29-
},
30-
renderModal = true
31-
}) => {
32-
const [isLoaded, setIsLoaded] = React.useState(true);
33-
const [isShowing, setIsShowing] = React.useState(false);
34-
const scrollY = React.useRef(0);
35-
const importDocSearchModalIfNeeded = React.useCallback(function importDocSearchModalIfNeeded() {
36-
if (DocSearchModal) {
37-
return Promise.resolve();
38-
}
39-
40-
return Promise.all([import('@docsearch/react/modal')]).then(([{
41-
DocSearchModal: Modal
42-
}]) => {
43-
DocSearchModal = Modal;
44-
});
45-
}, []);
46-
const onOpen = React.useCallback(function onOpen() {
47-
importDocSearchModalIfNeeded().then(() => {
48-
setIsShowing(true);
49-
});
50-
}, [importDocSearchModalIfNeeded, setIsShowing]);
51-
const onClose = React.useCallback(function onClose() {
52-
setIsShowing(false);
53-
}, [setIsShowing]);
54-
useDocSearchKeyboardEvents({
55-
isOpen: isShowing,
56-
onOpen,
57-
onClose
58-
});
59-
return <>
60-
<Head>
61-
<link rel="preconnect" href={`https://${appId}-dsn.algolia.net`} crossOrigin="true" />
62-
</Head>
4+
export const Search = () => {
5+
const { onOpen } = useSearch()
636

64-
<div>
65-
<button type="button" className="group form-input hover:text-gray-600 hover:border-gray-300 transition duration-150 ease-in-out pointer flex items-center bg-gray-50 text-left w-full text-gray-500 rounded-lg text-sm align-middle" onClick={onOpen}>
66-
<svg width="1em" height="1em" className="mr-3 align-middle text-gray-600 flex-shrink-0 group-hover:text-gray-700" style={{
67-
marginBottom: 2
68-
}} viewBox="0 0 20 20">
69-
<path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" strokeWidth="2" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round"></path>
70-
</svg>
71-
Search docs
72-
<span className="ml-auto invisible lg:visible">
73-
<kbd className="border border-gray-300 mr-1 bg-gray-100 align-middle p-0 inline-flex justify-center items-center text-xs text-center mr-0 rounded group-hover:border-gray-300 transition duration-150 ease-in-out " style={{
74-
minWidth: '1.8em'
75-
}}>
76-
77-
</kbd>
78-
<kbd className="border border-gray-300 bg-gray-100 align-middle p-0 inline-flex justify-center items-center text-xs text-center ml-auto mr-0 rounded group-hover:border-gray-300 transition duration-150 ease-in-out " style={{
79-
minWidth: '1.8em'
80-
}}>
81-
K
82-
</kbd>
83-
</span>
84-
</button>
85-
</div>
86-
87-
{isLoaded && isShowing && createPortal(<DocSearchModal {...options} searchParameters={searchParameters} onClose={onClose} navigator={{
88-
navigate({
89-
suggestionUrl
90-
}) {
91-
Router.push(suggestionUrl);
92-
}
93-
94-
}} transformItems={items => {
95-
return items.map(item => {
96-
const url = new URL(item.url);
97-
return { ...item,
98-
url: item.url.replace(url.origin, '').replace('#__next', '').replace('/docs/#', '/docs/overview#')
99-
};
100-
});
101-
}} hitComponent={Hit} />, document.body)}
102-
</>;
103-
};
104-
Search.displayName = 'Search';
7+
return (
8+
<div>
9+
<button
10+
type="button"
11+
className="group form-input hover:text-gray-600 hover:border-gray-300 transition duration-150 ease-in-out pointer flex items-center bg-gray-50 text-left w-full text-gray-500 rounded-lg text-sm align-middle"
12+
onClick={onOpen}
13+
>
14+
<svg
15+
width="1em"
16+
height="1em"
17+
className="mr-3 align-middle text-gray-600 flex-shrink-0 group-hover:text-gray-700"
18+
style={{
19+
marginBottom: 2,
20+
}}
21+
viewBox="0 0 20 20"
22+
>
23+
<path
24+
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
25+
stroke="currentColor"
26+
fill="none"
27+
strokeWidth="2"
28+
fillRule="evenodd"
29+
strokeLinecap="round"
30+
strokeLinejoin="round"
31+
></path>
32+
</svg>
33+
Search docs
34+
<span className="ml-auto invisible lg:visible">
35+
<kbd
36+
className="border border-gray-300 mr-1 bg-gray-100 align-middle p-0 inline-flex justify-center items-center text-xs text-center mr-0 rounded group-hover:border-gray-300 transition duration-150 ease-in-out "
37+
style={{
38+
minWidth: '1.8em',
39+
}}
40+
>
41+
42+
</kbd>
43+
<kbd
44+
className="border border-gray-300 bg-gray-100 align-middle p-0 inline-flex justify-center items-center text-xs text-center ml-auto mr-0 rounded group-hover:border-gray-300 transition duration-150 ease-in-out "
45+
style={{
46+
minWidth: '1.8em',
47+
}}
48+
>
49+
K
50+
</kbd>
51+
</span>
52+
</button>
53+
</div>
54+
)
55+
}
56+
Search.displayName = 'Search'

docs/src/components/Sidebar.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import { useState } from 'react';
2-
import cn from 'classnames';
3-
import { Search } from './Search';
4-
export const Sidebar = ({
5-
active,
6-
children,
7-
fixed
8-
}) => {
9-
const [searching, setSearching] = useState(false);
10-
return <aside className={cn('sidebar bg-white top-24 flex-shrink-0 pr-2', {
11-
active,
12-
['pb-0 flex flex-col z-1 sticky']: fixed,
13-
fixed,
14-
searching
15-
})}>
16-
<div className="sidebar-search my-2">
17-
<Search renderModal={false} />
1+
import { useState } from 'react'
2+
import cn from 'classnames'
3+
import { Search } from './Search'
4+
export const Sidebar = ({ active, children, fixed }) => {
5+
const [searching, setSearching] = useState(false)
6+
return (
7+
<aside
8+
className={cn('sidebar bg-white top-24 flex-shrink-0 pr-2', {
9+
active,
10+
['pb-0 flex flex-col z-1 sticky']: fixed,
11+
fixed,
12+
searching,
13+
})}
14+
>
15+
<div className="sidebar-search my-2 lg:hidden">
16+
<Search />
1817
</div>
1918
<div className="sidebar-content overflow-y-auto pb-4">{children}</div>
2019
<style jsx>{`
@@ -44,5 +43,6 @@ export const Sidebar = ({
4443
}
4544
}
4645
`}</style>
47-
</aside>;
48-
};
46+
</aside>
47+
)
48+
}

docs/src/components/useSearch.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React from 'react'
2+
import { createPortal } from 'react-dom'
3+
import Router from 'next/router'
4+
import Head from 'next/head'
5+
import Link from 'next/link'
6+
import { useDocSearchKeyboardEvents } from '@docsearch/react'
7+
import { siteConfig } from 'siteConfig'
8+
9+
const SearchContext = React.createContext()
10+
let DocSearchModal = null
11+
12+
export const useSearch = () => React.useContext(SearchContext)
13+
14+
export function SearchProvider({
15+
children,
16+
searchParameters = {
17+
hitsPerPage: 5,
18+
},
19+
}) {
20+
const [isShowing, setIsShowing] = React.useState(false)
21+
22+
const onOpen = React.useCallback(function onOpen() {
23+
function importDocSearchModalIfNeeded() {
24+
if (DocSearchModal) {
25+
return Promise.resolve()
26+
}
27+
28+
return import('@docsearch/react/modal').then(
29+
({ DocSearchModal: Modal }) => (DocSearchModal = Modal)
30+
)
31+
}
32+
33+
importDocSearchModalIfNeeded().then(() => {
34+
setIsShowing(true)
35+
})
36+
}, [])
37+
38+
const onClose = React.useCallback(() => setIsShowing(false), [])
39+
40+
useDocSearchKeyboardEvents({
41+
isOpen: isShowing,
42+
onOpen,
43+
onClose,
44+
})
45+
46+
const options = {
47+
appId: siteConfig.algolia.appId,
48+
apiKey: siteConfig.algolia.apiKey,
49+
indexName: siteConfig.algolia.indexName,
50+
renderModal: true,
51+
}
52+
53+
return (
54+
<>
55+
<Head>
56+
<link
57+
key="algolia"
58+
rel="preconnect"
59+
href={`https://${options.appId}-dsn.algolia.net`}
60+
crossOrigin="true"
61+
/>
62+
</Head>
63+
64+
<SearchContext.Provider value={{ DocSearchModal, onOpen }}>
65+
{children}
66+
</SearchContext.Provider>
67+
68+
{isShowing &&
69+
createPortal(
70+
<DocSearchModal
71+
{...options}
72+
searchParameters={searchParameters}
73+
onClose={onClose}
74+
navigator={{
75+
navigate({ suggestionUrl }) {
76+
Router.push(suggestionUrl)
77+
},
78+
}}
79+
transformItems={items => {
80+
return items.map(item => {
81+
const url = new URL(item.url)
82+
return {
83+
...item,
84+
url: item.url
85+
.replace(url.origin, '')
86+
.replace('#__next', '')
87+
.replace('/docs/#', '/docs/overview#'),
88+
}
89+
})
90+
}}
91+
hitComponent={Hit}
92+
/>,
93+
document.body
94+
)}
95+
</>
96+
)
97+
}
98+
99+
function Hit({ hit, children }) {
100+
return (
101+
<Link href={hit.url.replace()}>
102+
<a>{children}</a>
103+
</Link>
104+
)
105+
}

docs/src/pages/_app.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react'
22
import '@docsearch/react/dist/style.css'
33
import '../styles/index.css'
44
import Head from 'next/head'
5+
import { SearchProvider } from 'components/useSearch'
56

67
function loadScript(src, attrs = {}) {
78
if (typeof document !== 'undefined') {
@@ -46,7 +47,9 @@ function MyApp({ Component, pageProps }) {
4647
}}
4748
/>
4849
</Head>
49-
<Component {...pageProps} />
50+
<SearchProvider>
51+
<Component {...pageProps} />
52+
</SearchProvider>
5053
</>
5154
)
5255
}

0 commit comments

Comments
 (0)