This content originally appeared on DEV Community and was authored by MING
Note: This is an advanced version of Accordion, you should complete that question first before attempting this question.
In Accordion, we built a functional accordion component that can expand/collapse each section's contents. However, building good UI components goes beyond functionality and we have to ensure our components have great accessibility as well by adding the right ARIA roles, states, and properties to the DOM elements.
Requirements
The ARIA Authoring Practices Guide has a long list of guidelines for the ARIA roles, states, and properties to add to the various elements of an accordion. We should implement the following (improvised) guidelines for this question:
The title of each accordion header is contained in a
<button>element.If the accordion panel associated with an accordion header is visible, the header button element has
aria-expandedset totrue. If the panel is not visible,aria-expandedis set tofalse.The accordion header button element has
aria-controlsset to the ID of the element containing the accordion panel content.Each element that serves as a container for panel content has role region and
aria-labelledbywith a value that refers to the button that controls display of the panel.
The skeleton code uses the solution of Accordion, but you are free to use your own solution as a starting point.
Solution
We'll build on top of Accordion's solution. Other than adding the right ARIA roles and states, which is straightforward, we also need to link the accordion headers with the corresponding accordion contents/panels. Hence we create two functions, getAccordionHeaderId and getAccordionPanelId to do this.
-
getAccordionHeaderIdgenerates a unique ID string to use as the value of the id attribute of the header element. This ID will be used as the value of thearia-labelledbyattribute of the corresponding accordion panel. -
getAccordionPanelIdgenerates a unique ID string to use as the value of the id attribute of accordion panel. This ID will be used as the value of thearia-controlsattribute of the corresponding accordion header.
Since there can be multiple Accordion component instances on the page and we cannot guarantee that the accordion section values will be globally unique, each Accordion instance needs to have a unique identifier. The useId React hook can be used to generate unique ID for each Accordion instance. The final ID string will be a concatenation of the Accordion instance's ID, the item value, and whether it's a header or a panel.
Test Cases
- Inspect the rendered HTML to see that the right attributes were added to the DOM.
- You can go a step further by using accessibility testing tools like axe to validate the a11y of the elements.
Accessibility
By using a <button> for the section titles, we have enabled the basic keyboard interactions necessary to achieve sufficient accessibility for accordion components. However, there are some useful optional keyboard interactions to add, which will be covered in Accordion III.
App.js
import React from 'react';
import { AccordionII } from './AccordionII';
const data = [
{
value: 'html',
title: 'HTML',
contents:
'The HyperText Markup Language or HTML is the standard markup language for documents designed to be displayed in a web browser.',
},
{
value: 'css',
title: 'CSS',
contents:
'Cascading Style Sheets is a style sheet language used for describing the presentation of a document written in a markup language such as HTML or XML.',
},
{
value: 'javascript',
title: 'JavaScript',
contents:
'JavaScript, often abbreviated as JS, is a programming language that is one of the core technologies of the World Wide Web, alongside HTML and CSS.',
},
];
/**
* AccordionII 与 AccordionI 的区别:只是增加了Accessbility的东西,function逻辑没变
*/
export const AccordionIIWrapper = () => {
return (
<div className='wrapper'>
<AccordionII sections={data} />
</div>
);
};
Accordion.js
import React, { useState, useId } from 'react';
import './accordion.css';
const getAccordionHeaderId = (accordionId, value) =>
accordionId + '-header-' + value;
const getAccordionPanelId = (accordionId, value) =>
accordionId + '-panel-' + value;
export const AccordionII = ({ sections }) => {
const accordionId = useId(); // <--- diff
const [openSections, setOpenSections] = useState(new Set());
const handleClick = (value) => {
const newOpenSections = new Set(openSections);
newOpenSections.has(value)
? newOpenSections.delete(value)
: newOpenSections.add(value);
// console.log(newOpenSections);
setOpenSections(newOpenSections);
};
return (
<div className='accordion'>
{sections.map(({ value, title, contents }) => {
const isExpanded = openSections.has(value);
const headerId = getAccordionHeaderId(accordionId, value); // <--- diff
const panelId = getAccordionPanelId(accordionId, value); // <--- diff
return (
<div className='accordion-item' key={value}>
<button
className='accordion-item-title'
type='button'
onClick={() => handleClick(value)}
id={headerId} // <--- diff
aria-controls={panelId} // <---- connect below id={panelId}
aria-expanded={isExpanded} // <--- diff
>
{title}
<span
aria-hidden={true}
className={[
'accordion-icon',
isExpanded && 'accordion-icon--rotated',
]
.filter(Boolean)
.join(' ')}
/>
</button>
<div
className='accordion-item-contents'
hidden={!isExpanded}
id={panelId} // <--- diff
role='region' // <--- diff
aria-labelledby={headerId} // <--- connect above id={headerId}
>
{contents}
</div>
</div>
);
})}
</div>
);
};
This content originally appeared on DEV Community and was authored by MING
MING | Sciencx (2023-03-01T20:53:29+00:00) Accordion II – React. Retrieved from https://www.scien.cx/2023/03/01/accordion-ii-react/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.