|
10 | 10 | * governing permissions and limitations under the License.
|
11 | 11 | */
|
12 | 12 |
|
13 |
| -import {ActionButton} from '@react-spectrum/button'; |
| 13 | +import {ActionButton, defaultTheme, Provider, Text} from '@adobe/react-spectrum'; |
| 14 | +import algoliasearch from 'algoliasearch/lite'; |
14 | 15 | import docsStyle from './docs.css';
|
| 16 | +import {Item, SearchAutocomplete, Section} from '@react-spectrum/autocomplete'; |
15 | 17 | import {listen} from 'quicklink';
|
16 | 18 | import React, {useEffect, useRef, useState} from 'react';
|
17 | 19 | import ReactDOM from 'react-dom';
|
18 |
| -import {SearchField} from '@react-spectrum/searchfield'; |
19 | 20 | import ShowMenu from '@spectrum-icons/workflow/ShowMenu';
|
20 | 21 | import {ThemeSwitcher} from './ThemeSwitcher';
|
21 | 22 | import {watchModals} from '@react-aria/aria-modal-polyfill';
|
@@ -192,119 +193,93 @@ function Hamburger() {
|
192 | 193 | }
|
193 | 194 |
|
194 | 195 | function DocSearch() {
|
195 |
| - useEffect(() => { |
196 |
| - // the following comes from docsearch.min.js |
197 |
| - // eslint-disable-next-line no-undef |
198 |
| - const search = docsearch({ |
199 |
| - apiKey: '9b5a0967c8bb751b5048ecfc99917979', |
200 |
| - indexName: 'react-spectrum', |
201 |
| - inputSelector: '#algolia-doc-search', |
202 |
| - debug: false // Set debug to true to inspect the dropdown |
203 |
| - }); |
204 |
| - |
205 |
| - // autocomplete:opened event handler |
206 |
| - search.autocomplete.on('autocomplete:opened', event => { |
207 |
| - const input = event.target; |
208 |
| - |
209 |
| - // WAI-ARIA 1.2 uses aria-controls rather than aria-owns on combobox. |
210 |
| - if (!input.hasAttribute('aria-controls') && input.hasAttribute('aria-owns')) { |
211 |
| - input.setAttribute('aria-controls', input.getAttribute('aria-owns')); |
212 |
| - } |
213 |
| - |
214 |
| - // Listbox dropdown should have an accessibility name. |
215 |
| - const listbox = input.parentElement.querySelector(`#${input.getAttribute('aria-controls')}`); |
216 |
| - listbox.setAttribute('aria-label', 'Search results'); |
217 |
| - }); |
| 196 | + const client = algoliasearch('1V1Q59JVTR', '44a7e2e7508ff185f25ac64c0a675f98'); |
| 197 | + const searchIndex = client.initIndex('react-spectrum'); |
| 198 | + const searchOptions = { |
| 199 | + highlightPreTag: `<mark class="${docsStyle.docSearchBoxMark}">`, |
| 200 | + highlightPostTag: '</mark>' |
| 201 | + }; |
218 | 202 |
|
219 |
| - // autocomplete:updated event handler |
220 |
| - search.autocomplete.on('autocomplete:updated', event => { |
221 |
| - const input = event.target; |
222 |
| - const listbox = input.parentElement.querySelector(`#${input.getAttribute('aria-controls')}`); |
223 |
| - |
224 |
| - // Add aria-hidden to the logo in the footer so that it does not break the listbox accessibility tree structure. |
225 |
| - const footer = listbox.querySelector('.algolia-docsearch-footer'); |
226 |
| - if (footer && !footer.hasAttribute('aria-hidden')) { |
227 |
| - footer.setAttribute('aria-hidden', 'true'); |
228 |
| - footer.querySelector('a[href]').tabIndex = -1; |
| 203 | + const [searchValue, setSearchValue] = useState(''); |
| 204 | + const [predictions, setPredictions] = useState(null); |
| 205 | + const [suggestions, setSuggestions] = useState(null); |
| 206 | + |
| 207 | + let updatePredictions = ({hits}) => { |
| 208 | + setPredictions(hits); |
| 209 | + let sections = []; |
| 210 | + hits.forEach(prediction => { |
| 211 | + let hierarchy = prediction.hierarchy; |
| 212 | + let objectID = prediction.objectID; |
| 213 | + let lvl0 = hierarchy.lvl0; |
| 214 | + let section = sections.find(section => section.title === lvl0); |
| 215 | + if (!section) { |
| 216 | + section = {title: lvl0, items: []}; |
| 217 | + sections.push(section); |
229 | 218 | }
|
230 |
| - |
231 |
| - // With no results, the message should be an option in the listbox. |
232 |
| - const noResults = listbox.querySelector('.algolia-docsearch-suggestion--no-results'); |
233 |
| - if (noResults) { |
234 |
| - noResults.setAttribute('role', 'option'); |
235 |
| - |
236 |
| - // Use aria-live to ensure that the noResults message gets announced. |
237 |
| - noResults.querySelector('.algolia-docsearch-suggestion--title').setAttribute('aria-live', 'assertive'); |
| 219 | + let text = []; |
| 220 | + let textValue = []; |
| 221 | + for (let i = 1; i < 6; i++) { |
| 222 | + if (hierarchy[`lvl${i}`]) { |
| 223 | + text.push(prediction._highlightResult.hierarchy[`lvl${i}`].value); |
| 224 | + textValue.push(hierarchy[`lvl${i}`]); |
| 225 | + } |
238 | 226 | }
|
239 |
| - |
240 |
| - // Clean up WAI-ARIA listbox structure by setting role=presentation to non-semantic div and span elements. |
241 |
| - [...listbox.querySelectorAll('div:not([role]), span:not([role])')].forEach(element => element.setAttribute('role', 'presentation')); |
242 |
| - |
243 |
| - // Clean up WAI-ARIA listbox structure by correcting improper nesting of interactive controls. |
244 |
| - [...listbox.querySelectorAll('.ds-suggestion[role="option"]')].forEach(element => { |
245 |
| - const link = element.querySelector('a.algolia-docsearch-suggestion'); |
246 |
| - if (link) { |
247 |
| - |
248 |
| - // Remove static aria-label="Link to the result" that causes all options to be named the same. |
249 |
| - link.removeAttribute('aria-label'); |
250 |
| - |
251 |
| - // The interactive element should have role="option", a unique id, and tabIndex. |
252 |
| - link.setAttribute('role', 'option'); |
253 |
| - link.id = `${element.id}-link`; |
254 |
| - link.tabIndex = -1; |
255 |
| - |
256 |
| - // containing element should have role="presentation" |
257 |
| - element.setAttribute('role', 'presentation'); |
258 |
| - |
259 |
| - // Move aria-selected to the link, and update aria-activedescendant on input. |
260 |
| - if (element.hasAttribute('aria-selected')) { |
261 |
| - link.setAttribute('aria-selected', element.getAttribute('aria-selected')); |
262 |
| - element.removeAttribute('aria-selected'); |
263 |
| - input.setAttribute('aria-activedescendant', link.id); |
264 |
| - } |
265 |
| - |
266 |
| - // Fix double voicing of options when subcategory matches suggestion title. |
267 |
| - const subcategoryColumn = link.querySelector('.algolia-docsearch-suggestion--subcategory-column'); |
268 |
| - const suggestionTitle = link.querySelector('.algolia-docsearch-suggestion--title'); |
269 |
| - if (subcategoryColumn.textContent.trim() === suggestionTitle.textContent.trim()) { |
270 |
| - subcategoryColumn.setAttribute('aria-hidden', 'true'); |
| 227 | + section.items.push( |
| 228 | + <Item key={objectID} textValue={textValue.join(' | ')}> |
| 229 | + <Text><span dangerouslySetInnerHTML={{__html: text.join(' | ')}} /></Text> |
| 230 | + { |
| 231 | + prediction.content && |
| 232 | + <Text slot="description"> |
| 233 | + <span dangerouslySetInnerHTML={{__html: prediction._snippetResult.content.value}} /> |
| 234 | + </Text> |
271 | 235 | }
|
272 |
| - } |
273 |
| - }); |
| 236 | + </Item> |
| 237 | + ); |
274 | 238 | });
|
| 239 | + let suggestions = sections.map((section, index) => <Section key={`${index}-lvl0-${section.title}`} title={section.title}>{section.items}</Section>); |
| 240 | + setSuggestions(suggestions); |
| 241 | + }; |
275 | 242 |
|
276 |
| - // When navigating listbox, move aria-selected to link. |
277 |
| - search.autocomplete.on('autocomplete:cursorchanged', event => { |
278 |
| - const input = event.target; |
279 |
| - const listbox = input.parentElement.querySelector(`#${input.getAttribute('aria-controls')}`); |
280 |
| - let element = listbox.querySelector('a.algolia-docsearch-suggestion[aria-selected]'); |
281 |
| - if (element) { |
282 |
| - element.removeAttribute('aria-selected'); |
283 |
| - } |
284 |
| - |
285 |
| - element = listbox.querySelector('.ds-suggestion.ds-cursor[aria-selected]'); |
286 |
| - if (element) { |
287 |
| - let link = element.querySelector('a.algolia-docsearch-suggestion'); |
288 |
| - |
289 |
| - // Move aria-selected to the link, and update aria-activedescendant on input. |
290 |
| - if (link) { |
291 |
| - link.id = `${element.id}-link`; |
292 |
| - link.setAttribute('aria-selected', 'true'); |
293 |
| - input.setAttribute('aria-activedescendant', link.id); |
294 |
| - element.removeAttribute('aria-selected'); |
295 |
| - } |
| 243 | + let onInputChange = (query) => { |
| 244 | + if (!query && predictions) { |
| 245 | + setPredictions(null); |
| 246 | + setSuggestions(null); |
| 247 | + } |
| 248 | + setSearchValue(query); |
| 249 | + searchIndex |
| 250 | + .search( |
| 251 | + query, |
| 252 | + searchOptions |
| 253 | + ) |
| 254 | + .then(updatePredictions); |
| 255 | + }; |
| 256 | + |
| 257 | + let onSubmit = (value, key) => { |
| 258 | + if (key) { |
| 259 | + let prediction = predictions.find(prediction => key === prediction.objectID); |
| 260 | + let url = prediction.url; |
| 261 | + if ( |
| 262 | + url.includes('https://react-spectrum.adobe.com') && |
| 263 | + !url.includes(window.location.host)) { |
| 264 | + url = url.replace('https://react-spectrum.adobe.com', window.location.origin); |
296 | 265 | }
|
297 |
| - }); |
298 |
| - }, []); |
| 266 | + window.location.href = url; |
| 267 | + } |
| 268 | + }; |
299 | 269 |
|
300 | 270 | return (
|
301 |
| - <div role="search"> |
302 |
| - <SearchField |
| 271 | + <Provider theme={defaultTheme} role="search"> |
| 272 | + <SearchAutocomplete |
303 | 273 | aria-label="Search"
|
| 274 | + placeholder="Search" |
304 | 275 | UNSAFE_className={docsStyle.docSearchBox}
|
305 | 276 | id="algolia-doc-search"
|
306 |
| - placeholder="Search" /> |
307 |
| - </div> |
| 277 | + value={searchValue} |
| 278 | + onInputChange={onInputChange} |
| 279 | + onSubmit={onSubmit}> |
| 280 | + {suggestions} |
| 281 | + </SearchAutocomplete> |
| 282 | + </Provider> |
308 | 283 | );
|
309 | 284 | }
|
310 | 285 |
|
|
0 commit comments