Skip to content

Commit

Permalink
Merge pull request #32 from 1Copenut/feature/region-vs-group
Browse files Browse the repository at this point in the history
[FEATURE] Accordion `role="region"` or `role="group"`
  • Loading branch information
1Copenut authored Oct 27, 2023
2 parents 0037ec4 + 4c58268 commit 306e29a
Show file tree
Hide file tree
Showing 8 changed files with 701 additions and 0 deletions.
3 changes: 3 additions & 0 deletions accordion-region-or-group/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/.DS_Store
**/node_modules/
*.*.swp
38 changes: 38 additions & 0 deletions accordion-region-or-group/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Accordion roles: region or group?

This code snippet explores the use of `role="region"` and `role="group"` for accordion (collapsible section) content blocks. [W3C's WAI-ARIA accordion example](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/examples/accordion/) uses the region role, but the Mozilla Dev Network (MDN) [accessibility guidance](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/region_role#accessibility_concerns) says to use it sparingly. I wanted to test different markup patterns and see how screen readers interpret information.

I assume the following markup and conditions:

1. All collapsible sections are toggled open and closed with a `button[aria-expanded="true | false"]`
2. Content blocks are simple `<div/>` elements with a `hidden` attribute toggled on and off using JavaScript
3. Content blocks receive programmatic focus when they are revealed in the DOM (semantically and visually)

## Screen reader findings
These findings are not exhaustive. As of October 27, 2023, I have only tested Safari + VoiceOver and Firefox + VoiceOver on MacOS Ventura. Future testing will include Chrome + JAWS and Firefox + NVDA. Mobile testing with iOS devices should also be done.

### Safari + VoiceOver

1. `div[role="region"]` Text content received screen reader pointer. Text was read aloud immediately. Container was not named and did not appear in the VO Landmarks menu.
2. `div[role="group"]` Text content received screen reader pointer. Text was read aloud immediately. Container was not named and did not appear in the VO Landmarks menu.
3. `div[role="region"] + aria-labelledby="BUTTON_ID"` Container received screen reader pointer. Container was named and button text appeared in the VO Landmarks menu. Advancing the screen reader pointer read the text aloud.
4. `div[role="group"] + aria-labelledby="BUTTON_ID"` Container received screen reader pointer. Container was not named. Named container did not appear in the VO Landmarks menu. Advancing the screen reader pointer read the text aloud.

**Conclusion:**<br/>
Example number three, the `div[role="region"] + aria-labelledby="BUTTON_ID"` was the best outcome in my experience. The expanded section was named using the button's accessible name. The expanded content block appeared in the VO Landmarks menu, making it reachable via asynchronous traversal.

**Minimal HTML markup:**
```html
<div class="cd-accordion--container">
<button id="button-3" class="cd-button--expand">
Div role "region" with aria-labelledby
<svg aria-hidden="true" focusable="false" viewBox="0 0 10 10">
<rect height="8" width="2" y="1" x="4" />
<rect height="2" width="8" y="4" x="1" />
</svg>
</button>
<div aria-labelledby="button-3" class="cd-accordion--content" role="region" hidden>
This is the content in group number three. It includes an aria-labelledby attribute.
</div>
</div>
```
52 changes: 52 additions & 0 deletions accordion-region-or-group/css/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
body {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
line-height: 1.5;
}

main {
margin: 0 auto;
width: 72ch;
}

p {
margin: 0 0 0.5rem;
}

a {
color: mediumblue;
}

button {
align-items: center;
background-color: whitesmoke;
border: none;
display: flex;
font-size: 1.25rem;
justify-content: space-between;
line-height: 1.65;
padding: 1rem;
width: 100%;

& svg {
pointer-events: none;
height: 2rem;
}
}

button:hover,
button:focus {
background-color: dodgerblue;
}

button[aria-expanded="true"] rect:first-of-type {
display: none !important;
}

div.cd-accordion--container {
border: 1px solid dodgerblue;
margin-bottom: 1.25rem;

& div {
padding: 1rem;
}
}
70 changes: 70 additions & 0 deletions accordion-region-or-group/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expandable sections: region or group?</title>
<link rel="stylesheet" href="css/main.css">
</head>

<body>
<main>
<h1>Expandable sections: region or group?</h1>

<div class="cd-accordion--container">
<button id="button-1" class="cd-button--expand">
Div role "region"
<svg aria-hidden="true" focusable="false" viewBox="0 0 10 10">
<rect height="8" width="2" y="1" x="4" />
<rect height="2" width="8" y="4" x="1" />
</svg>
</button>
<div role="region" class="cd-accordion--content" hidden>
This is the content in group number one.
</div>
</div>

<div class="cd-accordion--container">
<button id="button-2" class="cd-button--expand">
Div role "group"
<svg aria-hidden="true" focusable="false" viewBox="0 0 10 10">
<rect height="8" width="2" y="1" x="4" />
<rect height="2" width="8" y="4" x="1" />
</svg>
</button>
<div role="group" class="cd-accordion--content" hidden>
This is the content in group number two.
</div>
</div>

<div class="cd-accordion--container">
<button id="button-3" class="cd-button--expand">
Div role "region" with aria-labelledby
<svg aria-hidden="true" focusable="false" viewBox="0 0 10 10">
<rect height="8" width="2" y="1" x="4" />
<rect height="2" width="8" y="4" x="1" />
</svg>
</button>
<div aria-labelledby="button-3" class="cd-accordion--content" role="region" hidden>
This is the content in group number three. It includes an aria-labelledby attribute.
</div>
</div>

<div class="cd-accordion--container">
<button id="button-4" class="cd-button--expand">
Div role "group" with aria-labelledby
<svg aria-hidden="true" focusable="false" viewBox="0 0 10 10">
<rect height="8" width="2" y="1" x="4" />
<rect height="2" width="8" y="4" x="1" />
</svg>
</button>
<div aria-labelledby="button-4" class="cd-accordion--content" role="group" hidden>
This is the content in group number four. It includes an aria-labelledby attribute.
</div>
</div>
</main>
<script src="js/index.js" type="module"></script>
</body>

</html>
3 changes: 3 additions & 0 deletions accordion-region-or-group/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Accordion from "./modules/Accordion.js";

const toggles = new Accordion(".cd-button--expand", ".cd-accordion--content");
48 changes: 48 additions & 0 deletions accordion-region-or-group/js/modules/accordion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* I wrote the script, and got help from these fine articles:
* https://inclusive-components.design/collapsible-sections/
* https://gomakethings.com/detecting-click-events-on-svgs-with-vanilla-js-event-delegation/
*/

/**
* @param {string} buttonElems CSS selector for buttons to trigger accordion.
* @param {string} contentElems CSS selector for content blocks. Typically a DIV or SPAN.
*/
function Accordion(buttonElems, contentElems) {
const buttons = [...document.querySelectorAll(buttonElems)];
const contentBlocks = [...document.querySelectorAll(contentElems)];

buttons.forEach((button) => {
button.addEventListener("click", (e) => {
const targetButton = e.target;
let targetExpanded =
targetButton.getAttribute("aria-expanded") === "true" || false;

targetButton.setAttribute("aria-expanded", !targetExpanded);
this.toggleContent(targetButton, targetExpanded);
});
});

contentBlocks.forEach((block) => {
block.addEventListener("blur", (e) => {
e.target.removeAttribute("tabindex");
});
});
}

/**
*
* @param {HTMLButtonElement} target Button that fired the click event.
* @param {boolean} isContentHidden Is sibling content block hidden?
*/
Accordion.prototype.toggleContent = function (target, isContentHidden) {
const container = target.nextElementSibling;
container.toggleAttribute("hidden");

if (!isContentHidden) {
container.setAttribute("tabindex", "-1");
container.focus();
}
};

export default Accordion;
Loading

0 comments on commit 306e29a

Please sign in to comment.