fix: refactor downshift
Luke Karrys committed
Sep 25, 2023 at 09:23 UTC
4e468efffce089d444b3da1178789b5935faa6d5
8 files changed
+265
-283
package-lock.json
+26
-20
@@ -2052,11 +2052,11 @@
2052
}
2053
},
2054
"node_modules/@babel/runtime": {
2055
- "version": "7.20.7",
2056
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
2057
- "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
2055
+ "version": "7.23.1",
2056
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz",
2057
+ "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
2058
"dependencies": {
2059
- "regenerator-runtime": "^0.13.11"
2059
+ "regenerator-runtime": "^0.14.0"
2060
},
2061
"engines": {
2062
"node": ">=6.9.0"
@@ -2074,6 +2074,11 @@
2074
"node": ">=6.9.0"
2075
}
2076
},
2077
+ "node_modules/@babel/runtime/node_modules/regenerator-runtime": {
2078
+ "version": "0.14.0",
2079
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
2080
+ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
2081
+ },
2082
"node_modules/@babel/template": {
2083
"version": "7.20.7",
2084
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
@@ -10264,9 +10269,9 @@
10269
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
10270
},
10271
"node_modules/compute-scroll-into-view": {
10267
- "version": "1.0.20",
10268
- "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
10269
- "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
10272
+ "version": "3.0.3",
10273
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz",
10274
+ "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A=="
10275
},
10276
"node_modules/concat-map": {
10277
"version": "0.0.1",
@@ -11619,23 +11624,24 @@
11624
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="
11625
},
11626
"node_modules/downshift": {
11622
- "version": "3.4.8",
11623
- "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.4.8.tgz",
11624
- "integrity": "sha512-dZL3iNL/LbpHNzUQAaVq/eTD1ocnGKKjbAl/848Q0KEp6t81LJbS37w3f93oD6gqqAnjdgM7Use36qZSipHXBw==",
11627
+ "version": "8.2.1",
11628
+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-8.2.1.tgz",
11629
+ "integrity": "sha512-mHQUQMWCliXkoWvhxlRyEyKcdlbs8rZjZ+nSqGi8dKIv26gWtbQ8yVTPuYSHT7X8ff+FkEcskT9ktLuNKNoEHA==",
11630
"dependencies": {
11626
- "@babel/runtime": "^7.4.5",
11627
- "compute-scroll-into-view": "^1.0.9",
11628
- "prop-types": "^15.7.2",
11629
- "react-is": "^16.9.0"
11631
+ "@babel/runtime": "^7.22.15",
11632
+ "compute-scroll-into-view": "^3.0.3",
11633
+ "prop-types": "^15.8.1",
11634
+ "react-is": "^18.2.0",
11635
+ "tslib": "^2.6.2"
11636
},
11637
"peerDependencies": {
11632
- "react": ">=0.14.9"
11638
+ "react": ">=16.12.0"
11639
}
11640
},
11635
- "node_modules/downshift/node_modules/react-is": {
11636
- "version": "16.13.1",
11637
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
11638
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
11641
+ "node_modules/downshift/node_modules/tslib": {
11642
+ "version": "2.6.2",
11643
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
11644
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
11645
},
11646
"node_modules/duplexer": {
11647
"version": "0.1.2",
@@ -32506,7 +32512,7 @@
32512
"copy-to-clipboard": "^3.3.3",
32513
"date-fns": "^2.29.3",
32514
"details-element-polyfill": "^2.4.0",
32509
- "downshift": "^3.4.8",
32515
+ "downshift": "^8.2.1",
32516
"find-up": "^6.3.0",
32517
"framer-motion": "^3.10.6",
32518
"fuse.js": "^3.6.1",
theme/package.json
+1
-1
@@ -38,7 +38,7 @@
38
"copy-to-clipboard": "^3.3.3",
39
"date-fns": "^2.29.3",
40
"details-element-polyfill": "^2.4.0",
41
- "downshift": "^3.4.8",
41
+ "downshift": "^8.2.1",
42
"find-up": "^6.3.0",
43
"framer-motion": "^3.10.6",
44
"fuse.js": "^3.6.1",
theme/src/components/header.js
+7
-15
@@ -1,5 +1,5 @@
1
import {Box, Flex, Link, Sticky} from '@primer/components'
2
-import {SearchIcon, ThreeBarsIcon} from '@primer/octicons-react'
2
+import {ThreeBarsIcon} from '@primer/octicons-react'
3
import {Link as GatsbyLink} from 'gatsby'
4
import React from 'react'
5
import styled, {ThemeContext} from 'styled-components'
@@ -11,6 +11,8 @@ import NavDrawer, {useNavDrawerState} from './nav-drawer'
11
import NavDropdown, {NavDropdownItem} from './nav-dropdown'
12
import Search from './search'
13
import NpmLogo from './npm-logo'
14
+import useBreakpoint from '../use-breakpoint'
15
+import useSearch from '../use-search'
16
17
export const HEADER_HEIGHT = 66
18
@@ -22,8 +24,9 @@ const NpmHeaderBar = styled(Box)`
24
function Header({location, isSearchEnabled = true}) {
25
const theme = React.useContext(ThemeContext)
26
const [isNavDrawerOpen, setIsNavDrawerOpen] = useNavDrawerState(theme.breakpoints[2])
25
- const [isMobileSearchOpen, setIsMobileSearchOpen] = React.useState(false)
27
const siteMetadata = useSiteMetadata()
28
+ const isMobile = useBreakpoint(theme.breakpoints[2], 'max')
29
+ const search = useSearch({isMobile})
30
31
const logoStyle = {color: '#cb0000', marginRight: '16px'}
32
const titleStyle = {color: '#dddddd', fontWeight: '600', display: 'flex', alignItems: 'center'}
@@ -46,7 +49,7 @@ function Header({location, isSearchEnabled = true}) {
49
50
{isSearchEnabled ? (
51
<Box display={['none', null, null, 'block']} ml={4}>
49
- <Search />
52
+ <Search {...search} />
53
</Box>
54
) : null}
55
</Flex>
@@ -55,18 +58,7 @@ function Header({location, isSearchEnabled = true}) {
58
<HeaderNavItems items={headerNavItems} />
59
</Box>
60
<Flex display={['flex', null, null, 'none']}>
58
- {isSearchEnabled ? (
59
- <>
60
- <DarkButton
61
- aria-label="Search"
62
- aria-expanded={isMobileSearchOpen}
63
- onClick={() => setIsMobileSearchOpen(true)}
64
- >
65
- <SearchIcon />
66
- </DarkButton>
67
- <MobileSearch isOpen={isMobileSearchOpen} onDismiss={() => setIsMobileSearchOpen(false)} />
68
- </>
69
- ) : null}
61
+ {isSearchEnabled ? <MobileSearch {...search} /> : null}
62
<DarkButton
63
aria-label="Menu"
64
aria-expanded={isNavDrawerOpen}
theme/src/components/mobile-search.js
+72
-108
@@ -1,127 +1,91 @@
1
import {Absolute, Fixed, Flex} from '@primer/components'
2
-import {XIcon} from '@primer/octicons-react'
3
-import Downshift from 'downshift'
2
+import {XIcon, SearchIcon} from '@primer/octicons-react'
3
import {AnimatePresence, motion} from 'framer-motion'
5
-import {navigate} from 'gatsby'
4
import React from 'react'
5
import {FocusOn} from 'react-focus-on'
8
-import useSearch from '../use-search'
6
import DarkButton from './dark-button'
7
import DarkTextInput from './dark-text-input'
8
import SearchResults from './search-results'
9
import useSiteMetadata from '../use-site-metadata'
10
14
-function stateReducer(state, changes) {
15
- switch (changes.type) {
16
- case Downshift.stateChangeTypes.changeInput:
17
- if (!changes.inputValue) {
18
- // Close the menu if the input is empty.
19
- return {...changes, isOpen: false}
20
- }
21
- return changes
22
- case Downshift.stateChangeTypes.blurInput:
23
- // Don't let a blur event change the state of `inputValue` or `isOpen`.
24
- return {...changes, inputValue: state.inputValue, isOpen: state.isOpen}
25
- default:
26
- return changes
27
- }
28
-}
29
-
30
-function MobileSearch({isOpen, onDismiss}) {
31
- const [query, setQuery] = React.useState('')
32
- const results = useSearch(query)
11
+function MobileSearch({onDismiss, ...props}) {
12
const siteMetadata = useSiteMetadata()
13
+ const {reset, results, isOpen, getInputProps, getItemProps, getMenuProps, highlightedIndex} = props
14
35
- function handleDismiss() {
36
- setQuery('')
15
+ const handleDismiss = () => {
16
+ reset()
17
onDismiss()
18
}
19
20
return (
41
- <AnimatePresence>
42
- {isOpen ? (
43
- <FocusOn returnFocus={true} onEscapeKey={() => handleDismiss()}>
44
- <Fixed top={0} left={0} right={0} bottom={0} zIndex={1}>
45
- <Absolute
46
- as={motion.div}
47
- initial={{opacity: 0}}
48
- animate={{opacity: 1}}
49
- exit={{opacity: 0}}
50
- top={0}
51
- left={0}
52
- right={0}
53
- bottom={0}
54
- bg="rgba(0,0,0,0.5)"
55
- zIndex={-1}
56
- onClick={handleDismiss}
57
- />
58
- <Downshift
59
- inputValue={query}
60
- onInputValueChange={inputValue => setQuery(inputValue)}
61
- selectedItem={null}
62
- onSelect={item => {
63
- if (item) {
64
- navigate(item.path)
65
- handleDismiss()
66
- }
67
- }}
68
- itemToString={item => (item ? item.title : '')}
69
- stateReducer={stateReducer}
21
+ <FocusOn returnFocus={true} onEscapeKey={handleDismiss}>
22
+ <Fixed top="10px" left={0} right={0} bottom={0} zIndex={1}>
23
+ <Absolute
24
+ as={motion.div}
25
+ initial={{opacity: 0}}
26
+ animate={{opacity: 1}}
27
+ exit={{opacity: 0}}
28
+ top={0}
29
+ left={0}
30
+ right={0}
31
+ bottom={0}
32
+ bg="rgba(0,0,0,0.5)"
33
+ zIndex={-1}
34
+ onClick={handleDismiss}
35
+ />
36
+ <Flex flexDirection="column" height={isOpen ? '100%' : 'auto'}>
37
+ <Flex bg="gray.9" color="white" p={3} flex="0 0 auto">
38
+ <motion.div
39
+ initial={{scaleX: 0.1}}
40
+ animate={{scaleX: 1}}
41
+ exit={{scaleX: 0.1, transition: {duration: 0.1}}}
42
+ transition={{type: 'tween', duration: 0.2}}
43
+ style={{width: '100%', originX: '100%'}}
44
>
71
- {({getInputProps, getItemProps, getMenuProps, getRootProps, isOpen: isMenuOpen, highlightedIndex}) => (
72
- <Flex
73
- {...getRootProps({
74
- flexDirection: 'column',
75
- height: isMenuOpen ? '100%' : 'auto',
76
- })}
77
- >
78
- <Flex bg="gray.9" color="white" p={3} flex="0 0 auto">
79
- <motion.div
80
- initial={{scaleX: 0.1}}
81
- animate={{scaleX: 1}}
82
- exit={{scaleX: 0.1, transition: {duration: 0.1}}}
83
- transition={{type: 'tween', duration: 0.2}}
84
- style={{width: '100%', originX: '100%'}}
85
- >
86
- <DarkTextInput
87
- {...getInputProps({
88
- placeholder: `Search ${siteMetadata.title}`,
89
- width: '100%',
90
- })}
91
- />
92
- </motion.div>
93
- <DarkButton ml={3} aria-label="Cancel" onClick={handleDismiss}>
94
- <XIcon />
95
- </DarkButton>
96
- </Flex>
97
- {isMenuOpen ? (
98
- <Flex
99
- {...getMenuProps({
100
- bg: 'white',
101
- py: 1,
102
- flexDirection: 'column',
103
- flex: '1 1 auto',
104
- style: {
105
- overflow: 'auto',
106
- WebkitOverflowScrolling: 'touch',
107
- },
108
- })}
109
- >
110
- <SearchResults
111
- results={results}
112
- getItemProps={getItemProps}
113
- highlightedIndex={highlightedIndex}
114
- />
115
- </Flex>
116
- ) : null}
117
- </Flex>
118
- )}
119
- </Downshift>
120
- </Fixed>
121
- </FocusOn>
122
- ) : null}
123
- </AnimatePresence>
45
+ <DarkTextInput
46
+ {...getInputProps({
47
+ placeholder: `Search ${siteMetadata.title}`,
48
+ width: '100%',
49
+ })}
50
+ />
51
+ </motion.div>
52
+ <DarkButton ml={3} aria-label="Cancel" onClick={handleDismiss}>
53
+ <XIcon />
54
+ </DarkButton>
55
+ </Flex>
56
+ <Flex
57
+ {...getMenuProps({
58
+ bg: 'white',
59
+ py: isOpen ? 1 : 0,
60
+ flexDirection: 'column',
61
+ flex: '1 1 auto',
62
+ style: {
63
+ overflow: 'auto',
64
+ WebkitOverflowScrolling: 'touch',
65
+ },
66
+ })}
67
+ >
68
+ {isOpen ? <SearchResults {...{results, getItemProps, highlightedIndex}} /> : null}
69
+ </Flex>
70
+ </Flex>
71
+ </Fixed>
72
+ </FocusOn>
73
+ )
74
+}
75
+
76
+function MobileSearchWrapper(props) {
77
+ const [isOpen, setIsOpen] = React.useState(false)
78
+
79
+ return (
80
+ <>
81
+ <DarkButton aria-label="Search" aria-expanded={isOpen} onClick={() => setIsOpen(true)}>
82
+ <SearchIcon />
83
+ </DarkButton>
84
+ <AnimatePresence>
85
+ {isOpen ? <MobileSearch onDismiss={() => setIsOpen(false)} {...props} /> : null}
86
+ </AnimatePresence>
87
+ </>
88
)
89
}
90
127
-export default MobileSearch
91
+export default MobileSearchWrapper
theme/src/components/search.js
+24
-68
@@ -1,80 +1,36 @@
1
import {BorderBox, Position} from '@primer/components'
2
-import Downshift from 'downshift'
3
-import {navigate} from 'gatsby'
2
import React from 'react'
5
-import useSearch from '../use-search'
3
import useSiteMetadata from '../use-site-metadata'
4
import DarkTextInput from './dark-text-input'
5
import SearchResults from './search-results'
6
10
-function stateReducer(state, changes) {
11
- switch (changes.type) {
12
- case Downshift.stateChangeTypes.changeInput:
13
- if (!changes.inputValue) {
14
- // Close the menu if the input is empty.
15
- return {...changes, isOpen: false}
16
- }
17
- return changes
18
- default:
19
- return changes
20
- }
21
-}
22
-
23
-function Search() {
24
- const [query, setQuery] = React.useState('')
25
- const results = useSearch(query)
7
+function Search(props) {
8
const siteMetadata = useSiteMetadata()
9
+ const {getInputProps, getMenuProps, isOpen, results, getItemProps, highlightedIndex} = props
10
11
return (
29
- <Downshift
30
- inputValue={query}
31
- onInputValueChange={inputValue => setQuery(inputValue)}
32
- // We don't need Downshift to keep track of a selected item because as
33
- // soon as an item is selected we navigate to a new page.
34
- // Let's avoid any unexpected states related to the selected item
35
- // by setting it to always be `null`.
36
- selectedItem={null}
37
- onSelect={item => {
38
- if (item) {
39
- navigate(item.path)
40
- setQuery('')
41
- }
42
- }}
43
- itemToString={item => (item ? item.title : '')}
44
- stateReducer={stateReducer}
45
- >
46
- {({getInputProps, getItemProps, getMenuProps, getRootProps, isOpen, highlightedIndex}) => (
47
- <Position {...getRootProps({position: 'relative'})}>
48
- <DarkTextInput
49
- {...getInputProps({
50
- placeholder: `Search ${siteMetadata.title}`,
51
- width: 240,
52
- })}
53
- />
54
- {isOpen ? (
55
- <Position
56
- {...getMenuProps({
57
- position: 'absolute',
58
- left: 0,
59
- right: 0,
60
- pt: 2,
61
- })}
62
- >
63
- <BorderBox
64
- minWidth={300}
65
- maxHeight="70vh"
66
- py={1}
67
- boxShadow="medium"
68
- bg="white"
69
- style={{overflow: 'auto'}}
70
- >
71
- <SearchResults results={results} getItemProps={getItemProps} highlightedIndex={highlightedIndex} />
72
- </BorderBox>
73
- </Position>
74
- ) : null}
75
- </Position>
76
- )}
77
- </Downshift>
12
+ <Position position="relative">
13
+ <DarkTextInput
14
+ {...getInputProps({
15
+ placeholder: `Search ${siteMetadata.title}`,
16
+ width: 240,
17
+ })}
18
+ />
19
+ <Position
20
+ {...getMenuProps({
21
+ position: 'absolute',
22
+ left: 0,
23
+ right: 0,
24
+ pt: 2,
25
+ })}
26
+ >
27
+ {isOpen ? (
28
+ <BorderBox minWidth={300} maxHeight="70vh" py={1} boxShadow="medium" bg="white" style={{overflow: 'auto'}}>
29
+ <SearchResults {...{results, getItemProps, highlightedIndex}} />
30
+ </BorderBox>
31
+ ) : null}
32
+ </Position>
33
+ </Position>
34
)
35
}
36
theme/src/search.worker.js
+30
-30
@@ -1,35 +1,35 @@
1
import Fuse from 'fuse.js'
2
import debounce from 'lodash.debounce'
3
-;(function searchWorker() {
4
- let fuse = null
3
6
- // [MKT]: I landed on the debouce wait value of 50 based mostly on
7
- // experimentation. With both `leading` and `trailing` set to `true`, this
8
- // feels pretty snappy.
9
- //
10
- // From https://lodash.com/docs/#debounce:
11
- //
12
- // > Note: If `leading` and `trailing` options are `true`, `func` is invoked
13
- // > on the trailing edge of the timeout only if the debounced function is
14
- // > invoked more than once during the wait timeout.
15
- const performSearch = debounce(
16
- function performSearch(query) {
17
- const results = fuse.search(query).slice(0, 20)
18
- postMessage({results, query})
19
- },
20
- 50,
21
- {leading: true, trailing: true},
22
- )
4
+let fuse = null
5
24
- onmessage = function ({data}) {
25
- if (data.list) {
26
- fuse = new Fuse(data.list, {
27
- threshold: 0.2,
28
- keys: ['title', 'rawBody'],
29
- tokenize: true,
30
- })
31
- } else if (data.query) {
32
- performSearch(data.query)
33
- }
6
+// [MKT]: I landed on the debouce wait value of 50 based mostly on
7
+// experimentation. With both `leading` and `trailing` set to `true`, this
8
+// feels pretty snappy.
9
+//
10
+// From https://lodash.com/docs/#debounce:
11
+//
12
+// > Note: If `leading` and `trailing` options are `true`, `func` is invoked
13
+// > on the trailing edge of the timeout only if the debounced function is
14
+// > invoked more than once during the wait timeout.
15
+const performSearch = debounce(
16
+ query =>
17
+ postMessage({
18
+ query,
19
+ results: fuse.search(query).slice(0, 20),
20
+ }),
21
+ 50,
22
+ {leading: true, trailing: true},
23
+)
24
+
25
+onmessage = function ({data}) {
26
+ if (data.data) {
27
+ fuse = new Fuse(data.data, {
28
+ threshold: 0.2,
29
+ keys: ['title', 'rawBody'],
30
+ tokenize: true,
31
+ })
32
+ } else if (data.query) {
33
+ performSearch(data.query)
34
}
35
-})()
35
+}
theme/src/use-breakpoint.js
new
+29
@@ -0,0 +1,29 @@
1
+import React from 'react'
2
+
3
+function useBreakpoint(breakpoint, minMax = 'min') {
4
+ // Handle string values from themes with units at the end
5
+ if (typeof breakpoint === 'string') {
6
+ breakpoint = parseInt(breakpoint, 10)
7
+ }
8
+
9
+ const matchMedia = React.useMemo(() => {
10
+ if (typeof window === 'undefined') {
11
+ const eventTarget = new EventTarget()
12
+ eventTarget.matches = false
13
+ return eventTarget
14
+ }
15
+ return window.matchMedia(`(${minMax}-width: ${breakpoint - (minMax === 'min' ? 0 : 1)}px)`)
16
+ }, [breakpoint, minMax])
17
+
18
+ const [matches, setMatches] = React.useState(matchMedia.matches)
19
+ const handleChange = React.useCallback(() => setMatches(matchMedia.matches), [matchMedia])
20
+
21
+ React.useEffect(() => {
22
+ matchMedia.addEventListener('change', handleChange)
23
+ return () => matchMedia.removeEventListener('change', handleChange)
24
+ }, [matchMedia, handleChange])
25
+
26
+ return matches
27
+}
28
+
29
+export default useBreakpoint
theme/src/use-search.js
+76
-41
@@ -1,11 +1,15 @@
1
-import {graphql, useStaticQuery} from 'gatsby'
1
import React from 'react'
2
+import {useCombobox} from 'downshift'
3
+import {navigate, graphql, useStaticQuery} from 'gatsby'
4
4
-function useSearch(query) {
5
- const latestQuery = React.useRef(query)
5
+function useSearch({isMobile = false} = {}) {
6
+ const queryRef = React.useRef()
7
const workerRef = React.useRef()
8
8
- const data = useStaticQuery(graphql`
9
+ const [query, setQuery] = React.useState()
10
+ const [items, setItems] = React.useState(null)
11
+
12
+ const rawData = useStaticQuery(graphql`
13
{
14
allMdx {
15
nodes {
@@ -25,59 +29,90 @@ function useSearch(query) {
29
}
30
`)
31
28
- const mdxNodes = data.allMdx.nodes.reduce((map, obj) => {
29
- map[obj.id] = obj
30
- return map
31
- }, {})
32
-
33
- const list = React.useMemo(
34
- () =>
35
- data.allSitePage.nodes
36
- .filter(node => {
37
- return node.pageContext && node.pageContext.mdxId && mdxNodes[node.pageContext.mdxId] != null
38
- })
39
- .map(node => {
40
- const mdxNode = mdxNodes[node.pageContext.mdxId]
41
-
42
- const obj = {
43
- path: node.path,
44
- title: mdxNode.frontmatter.title,
45
- rawBody: mdxNode.rawBody,
46
- }
47
- return obj
48
- }),
49
- [data],
50
- )
32
+ const data = React.useMemo(() => {
33
+ const mdxNodes = rawData.allMdx.nodes.reduce((map, obj) => {
34
+ map[obj.id] = obj
35
+ return map
36
+ }, {})
37
52
- const [results, setResults] = React.useState(list)
38
+ return rawData.allSitePage.nodes
39
+ .filter(node => {
40
+ return node.pageContext && node.pageContext.mdxId && mdxNodes[node.pageContext.mdxId] != null
41
+ })
42
+ .map(node => {
43
+ const mdxNode = mdxNodes[node.pageContext.mdxId]
44
+ return {
45
+ path: node.path,
46
+ title: mdxNode.frontmatter.title,
47
+ rawBody: mdxNode.rawBody,
48
+ }
49
+ })
50
+ }, [rawData])
51
52
const handleSearchResults = React.useCallback(({data}) => {
55
- if (data.query && data.results && data.query === latestQuery.current) {
56
- setResults(data.results)
53
+ if (data.query && data.results && data.query === queryRef.current) {
54
+ setItems(data.results)
55
}
56
}, [])
57
58
React.useEffect(() => {
59
const worker = new Worker(new URL('./search.worker.js', import.meta.url))
62
- worker.addEventListener('message', handleSearchResults)
63
- worker.postMessage({list})
60
workerRef.current = worker
61
66
- return () => {
67
- workerRef.current.terminate()
68
- }
69
- }, [list, handleSearchResults])
62
+ worker.addEventListener('message', handleSearchResults)
63
+ worker.postMessage({data})
64
+
65
+ return () => worker.terminate()
66
+ }, [data, handleSearchResults])
67
68
React.useEffect(() => {
72
- latestQuery.current = query
73
- if (query && workerRef.current) {
69
+ queryRef.current = query
70
+
71
+ if (query) {
72
workerRef.current.postMessage({query})
73
} else {
76
- setResults(list)
74
+ setItems(null)
75
}
78
- }, [query, list])
76
+ }, [query])
77
+
78
+ const combobox = useCombobox({
79
+ // We don't need Downshift to keep track of a selected item because as
80
+ // soon as an item is selected we navigate to a new page.
81
+ // Let's avoid any unexpected states related to the selected item
82
+ // by setting it to always be `null`.
83
+ items: items || [],
84
+ onInputValueChange: ({inputValue}) => setQuery(inputValue),
85
+ onSelectedItemChange: ({selectedItem}) => {
86
+ if (selectedItem) {
87
+ navigate(selectedItem.path)
88
+ combobox.reset()
89
+ }
90
+ },
91
+ itemToString: item => (item ? item.title : ''),
92
+ stateReducer: (state, {type, changes}) => {
93
+ switch (type) {
94
+ case useCombobox.stateChangeTypes.InputChange:
95
+ if (!changes.inputValue) {
96
+ // Close the menu if the input is empty.
97
+ return {...changes, isOpen: false}
98
+ }
99
+ break
100
+ case useCombobox.stateChangeTypes.InputBlur:
101
+ if (isMobile) {
102
+ // Don't let a blur event change the state of `inputValue` or `isOpen`.
103
+ return {...changes, inputValue: state.inputValue, isOpen: state.isOpen}
104
+ }
105
+ break
106
+ }
107
+ return changes
108
+ },
109
+ })
110
80
- return results
111
+ return {
112
+ ...combobox,
113
+ results: items,
114
+ isOpen: !!(combobox.isOpen && items),
115
+ }
116
}
117
118
export default useSearch