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