DEV Community

Cover image for Accessibility
Hari Bhandari
Hari Bhandari

Posted on

Accessibility

For accessibility, we will follow w3org aria practices.

WAI-ARIA Roles, States, and Properties:

According to WAI/ARIA,

  • 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.

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)}
/>
}
Enter fullscreen mode Exit fullscreen mode

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

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 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 AccordionItems index in the Descendants map, we can also store the corresponding AccordionButton ref along with the index.

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} ...../>
}
Enter fullscreen mode Exit fullscreen mode

Here, the Descendants map.ref.current will look something like:

{
    'id1': { index: 0, props: { element: button#button--sdf..},
    'id2': { index: 1, props: { element: button#button--kj...},
...
}
Enter fullscreen mode Exit fullscreen mode

Checkpoint 4ddd6a2c39c9e93b70732014948a316f1c568baa

Top comments (0)