dependabot/npm_and_yarn/main/proc-log-7.0.0
@reggi/path-to-regexp
dependabot/npm_and_yarn/main/copy-to-clipboard-4.0.2
dependabot/npm_and_yarn/main/eslint-10.4.0
dependabot/npm_and_yarn/main/npmcli/eslint-config-7.0.0
dependabot/npm_and_yarn/main/proc-log-7.0.0
dependabot/npm_and_yarn/npm_and_yarn-826852524d
dependabot/npm_and_yarn/npm_and_yarn-ab9a7f4bc2
deprecate-totp-2fa
dhei/classic-tokens
gat-bypass-2fa-docs
jpg619/fix-accessibility-content-flow
jpg619/version-bump-tar-2
kartykp/gat-bypass-2fa-docs
kartykp/upgrade-path-to-regex
main
maitxn/version-bump-tar
patch-1
reggi/cache-based-on-version
reggi/dev-engines
reggi/fix-transform-prettier
reggi/overrides
update-search-sensitivity
| 1 | import React from 'react' |
| 2 | import {useCombobox} from 'downshift' |
| 3 | import {navigate, graphql, useStaticQuery} from 'gatsby' |
| 4 | import {useIsMobile} from './use-breakpoint' |
| 5 | import usePage from './use-page' |
| 6 | import * as getNav from '../util/get-nav' |
| 7 | import {CLI_PATH} from '../constants' |
| 8 | |
| 9 | const useSearchData = () => { |
| 10 | const data = useStaticQuery(graphql` |
| 11 | { |
| 12 | allMdx { |
| 13 | nodes { |
| 14 | id |
| 15 | frontmatter { |
| 16 | title |
| 17 | } |
| 18 | body |
| 19 | } |
| 20 | } |
| 21 | allSitePage { |
| 22 | nodes { |
| 23 | path |
| 24 | pageContext |
| 25 | } |
| 26 | } |
| 27 | } |
| 28 | `) |
| 29 | |
| 30 | return React.useMemo(() => { |
| 31 | const mdxNodes = data.allMdx.nodes.reduce((map, obj) => { |
| 32 | map[obj.id] = obj |
| 33 | return map |
| 34 | }, {}) |
| 35 | |
| 36 | return data.allSitePage.nodes |
| 37 | .filter(node => mdxNodes[node.pageContext?.mdxId] != null) |
| 38 | .map(node => { |
| 39 | const mdxNode = mdxNodes[node.pageContext.mdxId] |
| 40 | return { |
| 41 | path: node.path, |
| 42 | title: mdxNode.frontmatter.title, |
| 43 | body: mdxNode.body, |
| 44 | } |
| 45 | }) |
| 46 | }, [data]) |
| 47 | } |
| 48 | |
| 49 | const useCliVersion = () => { |
| 50 | return getNav.getCurrentOrDefaultVariant( |
| 51 | getNav.getItem(getNav.getVariantRoot(`${CLI_PATH}/`, {stripTrailing: false})), |
| 52 | usePage().location.pathname, |
| 53 | ) |
| 54 | } |
| 55 | |
| 56 | const useSearchCombobox = (results, setQuery) => { |
| 57 | const isMobile = useIsMobile() |
| 58 | |
| 59 | const combobox = useCombobox({ |
| 60 | id: 'search-box', |
| 61 | items: results || [], |
| 62 | selectedItem: null, |
| 63 | onInputValueChange: ({inputValue}) => setQuery(inputValue), |
| 64 | onSelectedItemChange: ({selectedItem}) => { |
| 65 | if (selectedItem) { |
| 66 | navigate(selectedItem.path) |
| 67 | resetAndClose(true) |
| 68 | } |
| 69 | }, |
| 70 | itemToString: item => (item ? item.title : ''), |
| 71 | stateReducer: (state, {type, changes}) => { |
| 72 | switch (type) { |
| 73 | case useCombobox.stateChangeTypes.InputChange: |
| 74 | if (!changes.inputValue) { |
| 75 | // Close the menu if the input is empty. |
| 76 | return {...changes, isOpen: false} |
| 77 | } |
| 78 | break |
| 79 | case useCombobox.stateChangeTypes.InputBlur: |
| 80 | if (isMobile) { |
| 81 | // Don't let a blur event change the state of `inputValue` or `isOpen`. |
| 82 | return { |
| 83 | ...changes, |
| 84 | inputValue: state.inputValue, |
| 85 | isOpen: state.isOpen, |
| 86 | } |
| 87 | } |
| 88 | break |
| 89 | default: |
| 90 | break |
| 91 | } |
| 92 | return changes |
| 93 | }, |
| 94 | }) |
| 95 | |
| 96 | const [isMobileSearchOpen, setMobileSearchOpen] = React.useState(false) |
| 97 | const [isForceClose, setForceClose] = React.useState(false) |
| 98 | const forceCloseRef = React.useRef(false) |
| 99 | |
| 100 | const resetAndClose = React.useCallback( |
| 101 | force => { |
| 102 | combobox.reset() |
| 103 | if (force === true) { |
| 104 | setForceClose(true) |
| 105 | } else { |
| 106 | setMobileSearchOpen(false) |
| 107 | } |
| 108 | }, |
| 109 | [combobox, setMobileSearchOpen], |
| 110 | ) |
| 111 | |
| 112 | // if forceClose is set then we wait until the exit animation props have |
| 113 | // been removed in the component and then set mobile search to false |
| 114 | React.useEffect(() => { |
| 115 | if (isMobileSearchOpen && isForceClose && !forceCloseRef.current) { |
| 116 | setMobileSearchOpen(false) |
| 117 | } |
| 118 | forceCloseRef.current = isForceClose |
| 119 | }, [forceCloseRef, isForceClose, isMobileSearchOpen, setMobileSearchOpen]) |
| 120 | |
| 121 | // always reset force close any time mobile search is closed |
| 122 | React.useEffect(() => { |
| 123 | if (!isMobileSearchOpen) { |
| 124 | setForceClose(false) |
| 125 | } |
| 126 | }, [setForceClose, isMobileSearchOpen]) |
| 127 | |
| 128 | // Fixes focus behavior on iOS where the input gets focus styles but not the |
| 129 | // actual focus after animating open. |
| 130 | const inputRef = React.useRef() |
| 131 | React.useEffect(() => { |
| 132 | if (isMobileSearchOpen) { |
| 133 | inputRef.current.focus() |
| 134 | } |
| 135 | }, [inputRef, isMobileSearchOpen]) |
| 136 | |
| 137 | return { |
| 138 | ...combobox, |
| 139 | isMobileSearchOpen, |
| 140 | isForceClose, |
| 141 | setMobileSearchOpen, |
| 142 | resetAndClose, |
| 143 | getInputProps: (...props) => combobox.getInputProps({ref: inputRef, ...props}), |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | function useSearch() { |
| 148 | const [query, setQuery] = React.useState() |
| 149 | const [results, setResults] = React.useState(null) |
| 150 | const queryRef = React.useRef() |
| 151 | const items = useSearchData() |
| 152 | const {url: cliUrl} = useCliVersion() |
| 153 | const worker = React.useRef() |
| 154 | |
| 155 | const handleSearchResults = React.useCallback(({data}) => { |
| 156 | if (data.query && data.results && data.query === queryRef.current) { |
| 157 | setResults(data.results) |
| 158 | } |
| 159 | }, []) |
| 160 | |
| 161 | React.useEffect(() => { |
| 162 | worker.current = new Worker(new URL('../util/search.worker.js', import.meta.url)) |
| 163 | }, []) |
| 164 | |
| 165 | React.useEffect(() => { |
| 166 | worker.current.addEventListener('message', handleSearchResults) |
| 167 | }, [worker, handleSearchResults]) |
| 168 | |
| 169 | React.useEffect(() => { |
| 170 | worker.current.postMessage({items}) |
| 171 | }, [worker, items]) |
| 172 | |
| 173 | React.useEffect(() => { |
| 174 | worker.current.postMessage({cli: {root: CLI_PATH, current: cliUrl}}) |
| 175 | }, [worker, cliUrl]) |
| 176 | |
| 177 | React.useEffect(() => { |
| 178 | queryRef.current = query |
| 179 | |
| 180 | if (query) { |
| 181 | worker.current.postMessage({query}) |
| 182 | } else { |
| 183 | setResults(null) |
| 184 | } |
| 185 | }, [worker, query]) |
| 186 | |
| 187 | const combobox = useSearchCombobox(results, setQuery) |
| 188 | |
| 189 | return { |
| 190 | ...combobox, |
| 191 | results, |
| 192 | resultsOpen: !!(combobox.isOpen && results), |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | export default useSearch |