Skip to content

Commit 23a2196

Browse files
committed
fix(#1999): refactor to use SearchAutocomplete and v3 of AlgoliaSearch API
1 parent 2564a11 commit 23a2196

File tree

5 files changed

+220
-224
lines changed

5 files changed

+220
-224
lines changed

packages/dev/docs/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"@spectrum-icons/illustrations": "^3.1.0",
2424
"@spectrum-icons/ui": "^3.1.0",
2525
"@spectrum-icons/workflow": "^4.0.0",
26+
"algoliasearch": "^4.14.1",
2627
"clsx": "^1.1.1",
2728
"globals-docs": "^2.4.1",
28-
"highlight.js": "9.18.1",
29+
"highlight.js": "^11.6.0",
2930
"markdown-to-jsx": "^6.11.0",
3031
"quicklink": "^2.0.0",
3132
"react": "^18.0.0",

packages/dev/docs/src/Layout.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ function Page({children, currentPage, publicUrl, styles, scripts}) {
206206
}
207207
}
208208
)}} />
209-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
210209
</head>
211210
<body>
212211
{children}
@@ -219,7 +218,6 @@ function Page({children, currentPage, publicUrl, styles, scripts}) {
219218
document.head.appendChild(script);
220219
});
221220
`}} />
222-
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" />
223221
</body>
224222
</html>
225223
);

packages/dev/docs/src/client.js

Lines changed: 78 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {ActionButton} from '@react-spectrum/button';
13+
import {ActionButton, defaultTheme, Provider, Text} from '@adobe/react-spectrum';
14+
import algoliasearch from 'algoliasearch/lite';
1415
import docsStyle from './docs.css';
16+
import {Item, SearchAutocomplete, Section} from '@react-spectrum/autocomplete';
1517
import {listen} from 'quicklink';
1618
import React, {useEffect, useRef, useState} from 'react';
1719
import ReactDOM from 'react-dom';
18-
import {SearchField} from '@react-spectrum/searchfield';
1920
import ShowMenu from '@spectrum-icons/workflow/ShowMenu';
2021
import {ThemeSwitcher} from './ThemeSwitcher';
2122
import {watchModals} from '@react-aria/aria-modal-polyfill';
@@ -192,119 +193,93 @@ function Hamburger() {
192193
}
193194

194195
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+
};
218202

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);
229218
}
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+
}
238226
}
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>
271235
}
272-
}
273-
});
236+
</Item>
237+
);
274238
});
239+
let suggestions = sections.map((section, index) => <Section key={`${index}-lvl0-${section.title}`} title={section.title}>{section.items}</Section>);
240+
setSuggestions(suggestions);
241+
};
275242

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);
296265
}
297-
});
298-
}, []);
266+
window.location.href = url;
267+
}
268+
};
299269

300270
return (
301-
<div role="search">
302-
<SearchField
271+
<Provider theme={defaultTheme} role="search">
272+
<SearchAutocomplete
303273
aria-label="Search"
274+
placeholder="Search"
304275
UNSAFE_className={docsStyle.docSearchBox}
305276
id="algolia-doc-search"
306-
placeholder="Search" />
307-
</div>
277+
value={searchValue}
278+
onInputChange={onInputChange}
279+
onSubmit={onSubmit}>
280+
{suggestions}
281+
</SearchAutocomplete>
282+
</Provider>
308283
);
309284
}
310285

packages/dev/docs/src/docs.css

Lines changed: 11 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -625,102 +625,20 @@ h2.sectionHeader {
625625
}
626626

627627
z-index: 1;
628-
629-
& :global(.algolia-autocomplete .ds-dropdown-menu) {
630-
border-color: var(--spectrum-alias-border-color-dark, var(--spectrum-global-color-gray-400));
631-
border-radius: var(--spectrum-global-dimension-size-65);
632-
border-style: solid;
633-
border-width: var(--spectrum-alias-border-size-thin, var(--spectrum-global-dimension-static-size-10));
634-
box-shadow: unset;
635-
filter: drop-shadow(0 1px 4px var(--spectrum-alias-dropshadow-color));
636-
637-
& [class^='ds-dataset-'] {
638-
background-color: var(--spectrum-global-color-gray-50);
639-
border: unset;
640-
}
641-
642-
&:before {
643-
background-color: var(--spectrum-global-color-gray-50);
644-
border-right-color: var(--spectrum-alias-border-color-dark, var(--spectrum-global-color-gray-400));
645-
border-top-color: var(--spectrum-alias-border-color-dark, var(--spectrum-global-color-gray-400));
646-
z-index: -1;
647-
}
648-
649-
& :global(.algolia-docsearch-suggestion) {
650-
background-color: var(--spectrum-global-color-gray-50);
651-
}
652-
653-
& :global(.algolia-docsearch-suggestion.algolia-docsearch-suggestion__main) {
654-
text-decoration: none;
655-
}
656-
657-
& :global(.algolia-docsearch-suggestion--category-header) {
658-
border-bottom-color: var(--spectrum-global-color-gray-300);
659-
color: var(--spectrum-global-color-gray-900);
660-
font-weight: var(--spectrum-global-font-weight-semi-bold);
661-
text-transform: uppercase;
662-
}
663-
664-
& :global(.algolia-docsearch-suggestion--highlight) {
665-
background: var(--spectrum-alias-text-highlight-color);
666-
color: var(--spectrum-global-color-blue-700);
667-
}
668-
669-
& :global(.algolia-docsearch-suggestion--subcategory-column) {
670-
color: var(--spectrum-global-color-gray-800);
671-
}
672-
673-
& :global(.algolia-docsearch-suggestion--subcategory-column:before) {
674-
background: var(--spectrum-global-color-gray-300);
675-
}
676-
677-
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion--subcategory-column:before) {
678-
background: var(--spectrum-alias-border-color-focus);
679-
width: var(--spectrum-selectlist-border-size-key-focus);
680-
}
681-
682-
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion--content:before) {
683-
background: var(--spectrum-alias-border-color-focus);
684-
width: var(--spectrum-selectlist-border-size-key-focus);
685-
left: calc(-1 * var(--spectrum-selectlist-border-size-key-focus));
686-
}
687-
688-
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content) {
689-
background-color: var(--spectrum-alias-background-color-hover-overlay);
690-
}
691-
692-
& :global(.algolia-docsearch-suggestion--content:before) {
693-
background: var(--spectrum-global-color-gray-300);
694-
}
695-
696-
& :global(.algolia-docsearch-suggestion--title) {
697-
color: var(--spectrum-global-color-gray-800);
698-
}
699-
}
700628
}
701629

702-
@media (max-width: 768px) {
703-
.pageHeader {
704-
& :global(.algolia-autocomplete) {
705-
& :global(.algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column) {
706-
color: var(--spectrum-global-color-gray-700);
707-
opacity: 1;
708-
}
709-
710-
& :global(.algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:after) {
711-
content: "|\00A0";
712-
}
713-
714-
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:after) {
715-
color: var(--spectrum-alias-border-color-focus);
716-
font-weight: bold;
717-
}
718-
}
630+
.docSearchBox {
631+
margin-inline-start: auto;
632+
width: var(--spectrum-global-dimension-size-3600);
633+
> div {
634+
width: 100%;
719635
}
720636
}
721637

722-
.docSearchBox {
723-
margin-inline-start: auto;
638+
.docSearchBoxMark {
639+
font-weight: bold;
640+
background: var(--spectrum-alias-text-highlight-color);
641+
color: var(--spectrum-global-color-blue-700);
724642
}
725643

726644
/* hamburger menu should be hidden so that it doesn't receive focus before the sidenav */
@@ -803,31 +721,11 @@ h2.sectionHeader {
803721
margin-right: auto;
804722
width: auto
805723
}
806-
807-
@media (min-width: 570px) {
808-
.pageHeader {
809-
& :global(.algolia-autocomplete .ds-dropdown-menu) {
810-
margin-inline-end: var(--spectrum-global-dimension-size-600);
811-
max-width: 500px;
812-
min-width: unset;
813-
position: fixed !important;
814-
top: var(--spectrum-global-dimension-size-500) !important;
815-
}
816-
}
817-
}
818724
}
819725

820726
@media (max-width: 569px) {
821-
.pageHeader {
822-
& :global(.algolia-autocomplete .ds-dropdown-menu) {
823-
min-width: unset;
824-
position: fixed !important;
825-
top: var(--spectrum-global-dimension-size-500) !important;
826-
827-
&:before {
828-
right: var(--spectrum-global-dimension-size-1200) !important;
829-
}
830-
}
727+
.docSearchBox {
728+
width: var(--spectrum-global-dimension-size-2400);
831729
}
832730
}
833731

0 commit comments

Comments
 (0)