1 import React from 'react'
2 import {Button, ActionList, Box, Text} from '@primer/react'
3 import {XIcon, SearchIcon} from '@primer/octicons-react'
4 import {AnimatePresence, motion} from 'framer-motion'
5 import {FocusOn} from 'react-focus-on'
6 import TextInput from './text-input'
7 import useSiteMetadata from '../hooks/use-site-metadata'
8 import {HEADER_BAR, HEADER_HEIGHT} from '../constants'
9 import {LightTheme} from '../theme'
10 import {LinkNoUnderline} from './link'
11 import * as getNav from '../util/get-nav'
12 import omit from '../util/omit'
13 import {announce} from '../util/aria-live'
14
15 const SearchResults = ({results, getItemProps, highlightedIndex}) => {
16 const siteMetadata = useSiteMetadata()
17 if (!results || results.length === 0) {
18 announce('No results')
19 return (
20 <Box sx={{fontSize: 2, px: 3, py: 3}} aria-live="polite">
21 No results
22 </Box>
23 )
24 }
25
26 return (
27 <ActionList>
28 {results.map((item, index) => {
29 // keep the variant in the breadcrumb if we have one and its not the
30 // same as the last breadcrumb. this makes sure that variant index pages
31 // don't all appear the same in the search results
32 const variant = getNav.getVariant(getNav.getVariantRoot(item.path), item.path)
33 const hierarchy = getNav.getItemBreadcrumbs(item.path)
34 if (!variant || variant !== hierarchy[hierarchy.length - 1].shortName) {
35 hierarchy.pop()
36 }
37
38 return (
39 <ActionList.Item
40 key={item.path}
41 // fixes a bug between downshift and react/primer where the search result item is always disabled
42 // this is safe to remove because we know we never have any disabled search results
43 {...omit(getItemProps({item, index}), 'aria-disabled')}
44 as={LinkNoUnderline}
45 to={item.path}
46 active={highlightedIndex === index}
47 >
48 <Box sx={{display: 'flex', flexDirection: 'column', flex: '0 0 auto'}}>
49 <Text sx={{fontSize: 0}}>
50 {hierarchy.length ? hierarchy.map(s => s.shortName || s.title).join(' / ') : siteMetadata.shortName}
51 </Text>
52 <Text>{item.title}</Text>
53 </Box>
54 </ActionList.Item>
55 )
56 })}
57 </ActionList>
58 )
59 }
60
61 export const Desktop = props => {
62 const siteMetadata = useSiteMetadata()
63 const {getInputProps, getMenuProps, resultsOpen, ...rest} = props
64
65 return (
66 <Box sx={{position: 'relative'}}>
67 <TextInput
68 sx={{width: '240px'}}
69 placeholder={`Search ${siteMetadata.title}`}
70 aria-label={`Search ${siteMetadata.title}`}
71 {...getInputProps()}
72 />
73 <Box sx={{position: 'absolute', left: 0, right: 0, pt: 1}} {...getMenuProps()}>
74 {resultsOpen ? (
75 <LightTheme
76 sx={{
77 overflow: 'auto',
78 minWidth: 300,
79 maxHeight: '70vh',
80 boxShadow: 'shadow.large',
81 borderColor: 'border.muted',
82 bg: 'canvas.overlay',
83 borderRadius: 2,
84 borderWidth: 1,
85 borderStyle: 'solid',
86 }}
87 >
88 <SearchResults {...rest} />
89 </LightTheme>
90 ) : null}
91 </Box>
92 </Box>
93 )
94 }
95
96 export const Mobile = ({
97 resultsOpen,
98 getInputProps,
99 getMenuProps,
100 isMobileSearchOpen,
101 setMobileSearchOpen,
102 isForceClose,
103 resetAndClose,
104 ...rest
105 }) => {
106 const siteMetadata = useSiteMetadata()
107 const getCloseAnimation = exit => (isForceClose ? undefined : {exit})
108
109 return (
110 <>
111 <Button aria-label="Search" aria-expanded={isMobileSearchOpen} onClick={() => setMobileSearchOpen(true)}>
112 <SearchIcon />
113 </Button>
114 <AnimatePresence>
115 {isMobileSearchOpen ? (
116 <FocusOn returnFocus={true} onEscapeKey={() => resetAndClose(true)}>
117 <Box
118 sx={{
119 position: 'fixed',
120 top: `${HEADER_BAR}px`,
121 left: 0,
122 right: 0,
123 bottom: 0,
124 zIndex: 1,
125 }}
126 >
127 <Box
128 sx={{
129 position: 'absolute',
130 top: 0,
131 left: 0,
132 right: 0,
133 bottom: 0,
134 bg: 'overlay.backdrop',
135 zIndex: -1,
136 }}
137 key="search-backdrop"
138 as={motion.div}
139 initial={{opacity: 0}}
140 animate={{opacity: 1}}
141 transition={{type: 'tween'}}
142 onClick={() => resetAndClose(true)}
143 {...getCloseAnimation({opacity: 0})}
144 />
145 <Box sx={{display: 'flex', flexDirection: 'column', height: resultsOpen ? '100%' : 'auto'}}>
146 <Box
147 sx={{
148 display: 'flex',
149 color: 'fg.default',
150 height: `${HEADER_HEIGHT}px`,
151 flex: '0 0 auto',
152 px: 3,
153 alignItems: 'center',
154 border: '1px solid',
155 borderTopWidth: 0,
156 borderLeftWidth: 0,
157 borderRightWidth: 0,
158 borderColor: 'border.muted',
159 position: 'relative',
160 }}
161 >
162 <motion.div
163 key="search-box"
164 initial={{scaleX: 0}}
165 animate={{scaleX: 1}}
166 {...getCloseAnimation({scaleX: 0})}
167 transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
168 style={{width: '100%', originX: '100%'}}
169 >
170 <Box
171 sx={{
172 position: 'absolute',
173 top: 0,
174 bottom: 0,
175 left: 0,
176 width: '70px',
177 bg: 'canvas.default',
178 zIndex: '-1',
179 }}
180 />
181 <TextInput
182 leadingVisual={SearchIcon}
183 placeholder={`Search ${siteMetadata.title}`}
184 aria-label={`Search ${siteMetadata.title}`}
185 sx={{width: '100%'}}
186 {...getInputProps()}
187 />
188 </motion.div>
189 <Box
190 key="button-blocker"
191 as={motion.div}
192 initial={{opacity: 0}}
193 animate={{opacity: 1}}
194 {...getCloseAnimation({scaleX: 0})}
195 transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
196 sx={{
197 position: 'absolute',
198 top: 0,
199 bottom: 0,
200 right: 0,
201 width: '70px',
202 bg: 'canvas.default',
203 zIndex: '-1',
204 }}
205 />
206 <Box
207 key="button"
208 as={motion.div}
209 initial={{opacity: 0}}
210 animate={{opacity: 1}}
211 {...getCloseAnimation({opacity: 0})}
212 transition={{type: 'tween', ease: 'easeOut', duration: 0.2}}
213 >
214 <Button sx={{ml: 3}} aria-label="Cancel" onClick={() => resetAndClose(false)}>
215 <XIcon />
216 </Button>
217 </Box>
218 </Box>
219 <LightTheme
220 sx={{
221 display: 'flex',
222 bg: 'canvas.default',
223 flexDirection: 'column',
224 flex: '1 1 auto',
225 overflow: 'auto',
226 }}
227 style={{
228 WebkitOverflowScrolling: 'touch',
229 }}
230 {...getMenuProps()}
231 >
232 {resultsOpen ? <SearchResults {...rest} /> : null}
233 </LightTheme>
234 </Box>
235 </Box>
236 </FocusOn>
237 ) : null}
238 </AnimatePresence>
239 </>
240 )
241 }