microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2137-privacy-planner

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

docs/docusaurus/src/theme/SearchBar/index.jsx

74lines · modecode

1import React, { useEffect, useRef } from 'react';
2import SearchBar from '@theme-original/SearchBar';
3
4// WI-07 accessibility wrapper for the local search input.
5//
6// The upstream @easyops-cn/autocomplete.js (via @easyops-cn/docusaurus-search-local)
7// already promotes the navbar search input to the WAI-ARIA APG Combobox pattern at
8// runtime: it sets role="combobox", aria-autocomplete, aria-expanded (toggled on
9// open/close), aria-activedescendant (on arrow navigation), and aria-owns pointing at
10// the generated role="listbox" element. The popup items receive role="option".
11//
12// Two divergences from the current APG Combobox pattern remain, and this wrapper
13// closes both without ejecting the upstream component (keeping the swizzle resilient
14// to package upgrades):
15// 1. The popup is wired with the legacy aria-owns attribute instead of aria-controls,
16// so we mirror aria-owns onto aria-controls and keep them in sync.
17// 2. The "See all results" footer link is rendered as a bare interactive child of the
18// role="listbox" element, which is not an allowed listbox child (WCAG 1.3.1 / axe
19// aria-required-children). We tag the footer's anchor itself with role="option" so
20// the listbox only owns valid leaf options; tagging the wrapping div instead would
21// leave the focusable anchor as a nested interactive descendant (axe nested-interactive).
22export default function SearchBarWrapper(props) {
23 const containerRef = useRef(null);
24
25 useEffect(() => {
26 const root = containerRef.current;
27 if (!root) {
28 return undefined;
29 }
30
31 const sync = () => {
32 const input = root.querySelector('input.navbar__search-input');
33 if (input) {
34 const owns = input.getAttribute('aria-owns');
35 if (owns) {
36 if (input.getAttribute('aria-controls') !== owns) {
37 input.setAttribute('aria-controls', owns);
38 }
39 } else if (input.hasAttribute('aria-controls')) {
40 input.removeAttribute('aria-controls');
41 }
42 }
43
44 const listbox = root.querySelector('[role="listbox"]');
45 if (listbox) {
46 const footerLink = listbox.querySelector('[class*="hitFooter"] a');
47 if (footerLink && footerLink.getAttribute('role') !== 'option') {
48 footerLink.setAttribute('role', 'option');
49 }
50 }
51 };
52
53 // The combobox attributes are applied lazily, the first time the input is
54 // focused and the search index loads, and the popup contents are rebuilt on
55 // every keystroke, so observe the whole search container rather than reading
56 // the initial state once.
57 sync();
58 const observer = new MutationObserver(sync);
59 observer.observe(root, {
60 subtree: true,
61 childList: true,
62 attributes: true,
63 attributeFilter: ['aria-owns'],
64 });
65
66 return () => observer.disconnect();
67 }, []);
68
69 return (
70 <div ref={containerRef} style={{ display: 'contents' }}>
71 <SearchBar {...props} />
72 </div>
73 );
74}
75