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)}
/>
}
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
orSpace
: 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 AccordionItem
s 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} ...../>
}
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...},
...
}
Checkpoint 4ddd6a2c39c9e93b70732014948a316f1c568baa
Top comments (0)