Accessibility
Tags: react
Published: 2024-01-23 04:30:18

For accessibility, we will follow [w3org aria practices](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/).

#### WAI-ARIA Roles, States, and Properties:
According to [WAI/ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/), 
- for a visible accordion panel the corresponding header button should have `aria-expanded=true`, and for a non-visible panel, `aria-expanded=false`
- each accordion header should be contained in an element with `role=button`
- The accordion header button element should have `aria-controls` set to the ID of the corresponding accordion panel

There are more aria guidelines and for more please check this [link](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/).

```jsx
function Accordion(..) {
    const id = useId()
    ....
    <AccordionContext.Provder value={{accordionId: id, ...}}>
    ..
}
function AccordionItem(....) {
      const itemId = makeId(accordionId, index);
      const panelId = makeId("panel", itemId);
      const buttonId = makeId("button", itemId);
      //
    <Comp
        data-state="open" // "open" or "closed"
        data-disabled={disabled}
        data-disabled={disabled ? "" : undefined}
        data-read-only={readOnly ? "" : undefined}
    >
...
}
function AccordionButton(..) {
    ....
    return (
        <Com
            aria-controls={panelId}
            aria-expanded={state === AccordionStates.Open}
            ...
    />
}
function AccordionPanel(..) {
    <Comp
        role="region"
        aria-labelledby={buttonId}
        data-disabled={disabled || undefined}
        data-state={getDataState(state)}
/>
}
```
Here, along with the aria attributes I have added `data-*` attributes that can be used as CSS selectors. Unique IDs are being used for accessibility to point out related accordion buttons and panels.

Checkpoint [33715f20ee90cd5db009647ef747fabf48b0bd87](https://github.com/HariBhandari07/headless-accordion/commit/33715f20ee90cd5db009647ef747fabf48b0bd87)

#### Keyboard navigation
For Keyboard navigation, we will implement the following feature:

(assuming the accordion is already focused)
- `Up Arrow`: Move focus to the previous focusable accordion header
- `Down Arrow`: Move focus to the next focusable accordion header
- `Home`: Move focus to the first focusable accordion header
- `End`: Move focus to the last focusable accordion header
- `Enter` or `Space`: Open collapsed accordion panel and vice-versa
- `Tab`: Move focus to the next focusable element
- `Shift + Tab`: Move focus to the previous focusable element

For more info check [WAI Aria](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/#keyboard_interaction) Keyboard Interaction section

Breaking down the problem: if a user presses `Up Arrow`, we get the index of the previous focusable `AccordionItem` and the focus on its `AccordionButton`. If a user presses `Down Arrow`, we get the index of the next focusable `AccordionItem` and the focus on its `AccordionButton`. Since we already keep track of `AccordionItem`s index in the Descendants `map`, we can also store the corresponding `AccordionButton` ref along with the index.

```jsx
function AccordionItem(....) {
    const buttonRef = useRef<HTMLElement>(null);
    const index = useDescendant({ element: buttonRef.current });

    <AccordionItemContext.Provider value={{buttonRef, ....}} />
}

function AccordionButton(....) {
    const { buttonRef } = useAccordionItemContext()
    const { map } = useDescendantContext()

    const handleKeyDown = (e) => {
        // handle keyboard navigation
        ....
        map.current[elementId].props.element.focus()
    }
    return (
        <Comp ref={buttonRef} onKeyDown={handleKeyDown} ...../>
}
```

Here, the  Descendants `map.ref.current` will look something like:
```js
{
    'id1': { index: 0, props: { element: button#button--sdf..},
    'id2': { index: 1, props: { element: button#button--kj...},
...
}
```

Checkpoint [4ddd6a2c39c9e93b70732014948a316f1c568baa](https://github.com/HariBhandari07/headless-accordion/commit/4ddd6a2c39c9e93b70732014948a316f1c568baa)