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