Skip to content

Commit f63fbad

Browse files
authored
Merge pull request #1 from majornista/1999
fix(adobe#1999): Improve accessibility in DocSearch results
2 parents fe4fda2 + 2dbc959 commit f63fbad

File tree

3 files changed

+154
-16
lines changed

3 files changed

+154
-16
lines changed

packages/dev/docs/src/Layout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ function Nav({currentPageName, pages}) {
342342
</a>
343343
}
344344
<a href={isBlog ? '/index.html' : './index.html'} className={docStyles.homeBtn} id="nav-title-id">
345-
<svg viewBox="0 0 30 26" fill="#E1251B" aria-label="Adobe">
345+
<svg viewBox="0 0 30 26" fill="#E1251B" role="img" aria-label="Adobe">
346346
<polygon points="19,0 30,0 30,26" />
347347
<polygon points="11.1,0 0,0 0,26" />
348348
<polygon points="15,9.6 22.1,26 17.5,26 15.4,20.8 10.2,20.8" />

packages/dev/docs/src/client.js

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,25 @@ function Hamburger() {
7070
let nav = document.querySelector('.' + docsStyle.nav);
7171
let main = document.querySelector('main');
7272
let themeSwitcher = event.target.parentElement.nextElementSibling;
73-
73+
let themeSwitcherButton = themeSwitcher.querySelector('button');
7474
nav.classList.toggle(docsStyle.visible);
7575

7676
if (nav.classList.contains(docsStyle.visible)) {
7777
setIsPressed(true);
7878
main.setAttribute('aria-hidden', 'true');
7979
themeSwitcher.setAttribute('aria-hidden', 'true');
80-
themeSwitcher.querySelector('button').tabIndex = -1;
80+
if (themeSwitcherButton) {
81+
themeSwitcherButton.tabIndex = -1;
82+
}
8183
nav.tabIndex = -1;
8284
nav.focus();
8385
} else {
8486
setIsPressed(false);
8587
main.removeAttribute('aria-hidden');
8688
themeSwitcher.removeAttribute('aria-hidden');
87-
themeSwitcher.querySelector('button').removeAttribute('tabindex');
89+
if (themeSwitcherButton) {
90+
themeSwitcherButton.removeAttribute('tabindex');
91+
}
8892
nav.removeAttribute('tabindex');
8993
}
9094
};
@@ -98,16 +102,21 @@ function Hamburger() {
98102

99103
/* remove visible className and aria-attributes that make nav behave as a modal */
100104
let removeVisible = (isNotResponsive = false) => {
101-
hamburgerButton.setAttribute('aria-pressed', 'false');
102-
103-
if (nav.contains(document.activeElement) && !isNotResponsive) {
104-
hamburgerButton.focus();
105+
setIsPressed(false);
106+
let button = hamburgerButton.querySelector('button');
107+
if (button) {
108+
if (nav.contains(document.activeElement) && !isNotResponsive) {
109+
button.focus();
110+
}
105111
}
106112

107113
nav.classList.remove(docsStyle.visible);
108114
main.removeAttribute('aria-hidden');
109115
themeSwitcher.removeAttribute('aria-hidden');
110-
themeSwitcher.querySelector('button').removeAttribute('tabindex');
116+
let themeSwitcherButton = themeSwitcher.querySelector('button');
117+
if (themeSwitcherButton) {
118+
themeSwitcherButton.removeAttribute('tabindex');
119+
}
111120
nav.removeAttribute('tabindex');
112121
};
113122

@@ -171,7 +180,7 @@ function Hamburger() {
171180

172181
return (
173182
<div className={docsStyle.hamburgerButton} title="Open navigation panel" role="presentation">
174-
<ActionButton onPress={onPress} aria-label="Open navigation panel" aria-pressed={isPressed ? isPressed : undefined}>
183+
<ActionButton onPress={onPress} aria-label="Open navigation panel" aria-pressed={isPressed ? isPressed : 'false'}>
175184
<ShowMenu />
176185
</ActionButton>
177186
</div>
@@ -182,20 +191,116 @@ function DocSearch() {
182191
useEffect(() => {
183192
// the following comes from docsearch.min.js
184193
// eslint-disable-next-line no-undef
185-
docsearch({
194+
const search = docsearch({
186195
apiKey: '9b5a0967c8bb751b5048ecfc99917979',
187196
indexName: 'react-spectrum',
188197
inputSelector: '#algolia-doc-search',
189198
debug: false // Set debug to true to inspect the dropdown
190199
});
200+
201+
// autocomplete:opened event handler
202+
search.autocomplete.on('autocomplete:opened', event => {
203+
const input = event.target;
204+
205+
// WAI-ARIA 1.2 uses aria-controls rather than aria-owns on combobox.
206+
if (!input.hasAttribute('aria-controls') && input.hasAttribute('aria-owns')) {
207+
input.setAttribute('aria-controls', input.getAttribute('aria-owns'));
208+
}
209+
210+
// Listbox dropdown should have an accessibility name.
211+
const listbox = input.parentElement.querySelector(`#${input.getAttribute('aria-controls')}`);
212+
listbox.setAttribute('aria-label', 'Search results');
213+
});
214+
215+
// autocomplete:updated event handler
216+
search.autocomplete.on('autocomplete:updated', event => {
217+
const input = event.target;
218+
const listbox = input.parentElement.querySelector(`#${input.getAttribute('aria-controls')}`);
219+
220+
// Add aria-hidden to the logo in the footer so that it does not break the listbox accessibility tree structure.
221+
const footer = listbox.querySelector('.algolia-docsearch-footer');
222+
if (footer && !footer.hasAttribute('aria-hidden')) {
223+
footer.setAttribute('aria-hidden', 'true');
224+
footer.querySelector('a[href]').tabIndex = -1;
225+
}
226+
227+
// With no results, the message should be an option in the listbox.
228+
const noResults = listbox.querySelector('.algolia-docsearch-suggestion--no-results');
229+
if (noResults) {
230+
noResults.setAttribute('role', 'option');
231+
232+
// Use aria-live to ensure that the noResults message gets announced.
233+
noResults.querySelector('.algolia-docsearch-suggestion--title').setAttribute('aria-live', 'assertive');
234+
}
235+
236+
// Clean up WAI-ARIA listbox structure by setting role=presentation to non-semantic div and span elements.
237+
[...listbox.querySelectorAll('div:not([role]), span:not([role])')].forEach(element => element.setAttribute('role', 'presentation'));
238+
239+
// Clean up WAI-ARIA listbox structure by correcting improper nesting of interactive controls.
240+
[...listbox.querySelectorAll('.ds-suggestion[role="option"]')].forEach(element => {
241+
const link = element.querySelector('a.algolia-docsearch-suggestion');
242+
if (link) {
243+
244+
// Remove static aria-label="Link to the result" that causes all options to be named the same.
245+
link.removeAttribute('aria-label');
246+
247+
// The interactive element should have role="option", a unique id, and tabIndex.
248+
link.setAttribute('role', 'option');
249+
link.id = `${element.id}-link`;
250+
link.tabIndex = -1;
251+
252+
// containing element should have role="presentation"
253+
element.setAttribute('role', 'presentation');
254+
255+
// Move aria-selected to the link, and update aria-activedescendant on input.
256+
if (element.hasAttribute('aria-selected')) {
257+
link.setAttribute('aria-selected', element.getAttribute('aria-selected'));
258+
element.removeAttribute('aria-selected');
259+
input.setAttribute('aria-activedescendant', link.id);
260+
}
261+
262+
// Fix double voicing of options when subcategory matches suggestion title.
263+
const subcategoryColumn = link.querySelector('.algolia-docsearch-suggestion--subcategory-column');
264+
const suggestionTitle = link.querySelector('.algolia-docsearch-suggestion--title');
265+
if (subcategoryColumn.textContent.trim() === suggestionTitle.textContent.trim()) {
266+
subcategoryColumn.setAttribute('aria-hidden', 'true');
267+
}
268+
}
269+
});
270+
});
271+
272+
// When navigating listbox, move aria-selected to link.
273+
search.autocomplete.on('autocomplete:cursorchanged', event => {
274+
const input = event.target;
275+
const listbox = input.parentElement.querySelector(`#${input.getAttribute('aria-controls')}`);
276+
let element = listbox.querySelector('a.algolia-docsearch-suggestion[aria-selected]');
277+
if (element) {
278+
element.removeAttribute('aria-selected');
279+
}
280+
281+
element = listbox.querySelector('.ds-suggestion.ds-cursor[aria-selected]');
282+
if (element) {
283+
let link = element.querySelector('a.algolia-docsearch-suggestion');
284+
285+
// Move aria-selected to the link, and update aria-activedescendant on input.
286+
if (link) {
287+
link.id = `${element.id}-link`;
288+
link.setAttribute('aria-selected', 'true');
289+
input.setAttribute('aria-activedescendant', link.id);
290+
element.removeAttribute('aria-selected');
291+
}
292+
}
293+
});
191294
}, []);
192295

193296
return (
194-
<SearchField
195-
aria-label="Search"
196-
UNSAFE_className={docsStyle.docSearchBox}
197-
id="algolia-doc-search"
198-
placeholder="Search" />
297+
<div role="search">
298+
<SearchField
299+
aria-label="Search"
300+
UNSAFE_className={docsStyle.docSearchBox}
301+
id="algolia-doc-search"
302+
placeholder="Search" />
303+
</div>
199304
);
200305
}
201306

packages/dev/docs/src/docs.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ h2.sectionHeader {
587587
left: 0;
588588
}
589589

590+
z-index: 1;
591+
590592
& :global(.algolia-autocomplete .ds-dropdown-menu) {
591593
border-color: var(--spectrum-alias-border-color-dark, var(--spectrum-global-color-gray-400));
592594
border-radius: var(--spectrum-global-dimension-size-65);
@@ -635,6 +637,17 @@ h2.sectionHeader {
635637
background: var(--spectrum-global-color-gray-300);
636638
}
637639

640+
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion--subcategory-column:before) {
641+
background: var(--spectrum-alias-border-color-focus);
642+
width: var(--spectrum-selectlist-border-size-key-focus);
643+
}
644+
645+
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion--content:before) {
646+
background: var(--spectrum-alias-border-color-focus);
647+
width: var(--spectrum-selectlist-border-size-key-focus);
648+
left: calc(-1 * var(--spectrum-selectlist-border-size-key-focus));
649+
}
650+
638651
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content) {
639652
background-color: var(--spectrum-alias-background-color-hover-overlay);
640653
}
@@ -649,6 +662,26 @@ h2.sectionHeader {
649662
}
650663
}
651664

665+
@media (max-width: 768px) {
666+
.pageHeader {
667+
& :global(.algolia-autocomplete) {
668+
& :global(.algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column) {
669+
color: var(--spectrum-global-color-gray-700);
670+
opacity: 1;
671+
}
672+
673+
& :global(.algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:after) {
674+
content: "|\00A0";
675+
}
676+
677+
& :global(.ds-suggestion.ds-cursor .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:after) {
678+
color: var(--spectrum-alias-border-color-focus);
679+
font-weight: bold;
680+
}
681+
}
682+
}
683+
}
684+
652685
.docSearchBox {
653686
margin-inline-start: auto;
654687
}

0 commit comments

Comments
 (0)