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-expanded
set totrue
. If the panel is not visible,aria-expanded
is set tofalse
.The accordion header button element has
aria-controls
set 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-labelledby
with 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.
-
getAccordionHeaderId
generates a unique ID string to use as the value of theid
attribute of the header element. This ID will be used as the value of thearia-labelledby
attribute of the corresponding accordion panel. -
getAccordionPanelId
generates a unique ID string to use as the value of theid
attribute of accordion panel. This ID will be used as the value of thearia-controls
attribute of the corresponding accordion header.
Since there can be multiple accordion component instances on the page and we cannot guarantee that the accordion item values will be globally unique, each accordion instance needs to have a unique identifier. We can create a newID
function that generates a unique ID which under the hood is a globally-incrementing integer. The final ID string will be a concatenation of the accordion instance's ID, the item value, and whether it's an accordion header or 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.
HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="accordion" class="accordion"></div>
<!-- 下面是函数创建后的sing item DOM样例: -->
<!--
<div>
<div>
HTML
<span
aria-hidden="true"
class="accordion-icon"></span>
</div>
<div>
The HyperText Markup Language or HTML is the
standard markup language for documents designed to
be displayed in a web browser.
</div>
</div>
-->
<script src="index.js"></script>
</body>
</html>
JS:
const data = {
sections: [
{
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.',
},
],
};
(() => {
// Encapsulate the ID generation so that it can only be read and is protected from external modification.
const newID = (() => {
let id = 0;
return () => id++;
})();
const getAccordionHeaderId = (accordionId, value) =>
accordionId + '-header-' + value;
const getAccordionPanelId = (accordionId, value) =>
accordionId + '-panel-' + value;
function accordion($rootEl, { sections }) {
const accordionId = `accordion-${newID()}`;
function attachEvents() {
// Use Event Delegation.
$rootEl.addEventListener('click', (event) => {
const target = event.target;
if (
target.tagName !== 'BUTTON' ||
!target.classList.contains('accordion-item-title')
) {
return;
}
const isExpanded = target.getAttribute('aria-expanded') === 'true'; //<--diff
target.setAttribute('aria-expanded', !isExpanded); //<--diff
// Find the icon and toggle the direction.
const $icon = target.querySelector('.accordion-icon');
$icon.classList.toggle('accordion-icon--rotated', !isExpanded); //<--diff
//key point --->通过target.nextSibling找到内容,然后toggle内容的显示隐藏
const $accordionContents = target.nextSibling;
$accordionContents.hidden = !$accordionContents.hidden;
});
}
function init() {
const $accordionSections = document.createDocumentFragment();
sections.forEach(({ value, title, contents }) => {
const headerId = getAccordionHeaderId(accordionId, value); //<--diff
const panelId = getAccordionPanelId(accordionId, value); //<--diff
const $accordionSection = document.createElement('div');
$accordionSection.classList.add('accordion-item');
const $accordionTitleBtn = document.createElement('button');
$accordionTitleBtn.classList.add('accordion-item-title');
$accordionTitleBtn.type = 'button';
$accordionTitleBtn.setAttribute('data-value', value);
$accordionTitleBtn.id = headerId; //<--diff
$accordionTitleBtn.setAttribute('aria-controls', panelId); //<--diff
$accordionTitleBtn.setAttribute('aria-expanded', false); //<--diff
const $accordionIcon = document.createElement('span');
$accordionIcon.classList.add('accordion-icon');
$accordionIcon.setAttribute('aria-hidden', 'true');
$accordionTitleBtn.append(title, $accordionIcon);
const $accordionSectionContents = document.createElement('div');
$accordionSectionContents.classList.add('accordion-item-contents');
$accordionSectionContents.hidden = true;
$accordionSectionContents.textContent = contents;
$accordionSectionContents.role = 'region'; //<--diff
$accordionSectionContents.id = panelId; //<--diff
$accordionSectionContents.setAttribute('aria-labelledby', headerId); //<--diff
$accordionSection.append($accordionTitleBtn, $accordionSectionContents);
$accordionSections.append($accordionSection);
});
$rootEl.appendChild($accordionSections);
}
init();
attachEvents();
}
accordion(document.getElementById('accordion'), data);
})();
This content originally appeared on DEV Community and was authored by MING
MING | Sciencx (2023-03-01T21:25:23+00:00) Accordion II – JS. Retrieved from https://www.scien.cx/2023/03/01/accordion-ii-js/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.