Skip to content

Instantly share code, notes, and snippets.

@seanredmond
Last active December 4, 2017 15:07
Show Gist options
  • Save seanredmond/d55eab28bbf771734769 to your computer and use it in GitHub Desktop.
Save seanredmond/d55eab28bbf771734769 to your computer and use it in GitHub Desktop.
Accessible dialog test
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A11y Popup Test</title>
<style>
div > a[role="button"] {
border: 2px solid black;
padding: 1em;
text-decoration: none;
font-weight: bold;
background-color: white;
color: black;
}
div > a[role="button"]:hover, a[role="button"]:active, a[role="button"]:focus {
background-color: black;
color: white;
border: 2px solid red;
}
.toggleMenu {
display: none;
position: relative;
z-index: 10;
top: 15px;
}
.toggleMenu.open {
display: block;
border: 2px solid black;
width: 7em;
list-style: none;
margin-top: 0;
padding: 0;
}
.toggleMenu li a {
background-color: white;
color: black;
text-decoration: none;
display: block;
width: 6em;
padding: 0.25em 0.25em 0.25em 0.5em;
}
.toggleMenu li a:active, .toggleMenu li a:focus, .toggleMenu li a:hover {
background-color: black;
color: white;
}
a:focus {
border: 2px solid red;
}
</style>
</head>
<body>
<h1>Popup Button Pattern</h1>
<p>A button that opens a popup:</p>
<div>
<a href="http://example.com/" id="toggleButton1" class="toggle" role="button" aria-haspopup="true" aria-controls="popupMenu1" aria-expanded="false">Choose an Option</a>
<ul id="popupMenu1" class="toggleMenu">
<li><a href="#">Option 1</a></li>
<li><a href="#">Option 2</a></li>
<li><a href="#" role="button">Close</a></li>
</ul>
</div>
<div>
<h2>Notes</h2>
<h3>NoJS &amp; Link vs. Button</h3>
<p>Without javascript, the toggle won't open the popup, so we need a default action for the toggle, which we can override (&quot;enhance&quot;) with JS. We can make a toggle a link pointing to a page where the user can do what the would do in the popup, or the main thing they would do.</p>
<p>A button would be the first choice for the toggle element when it can open the dropdown (since it affects the state on the current page), but a button can't act like a link (without JS). However, a link <em>can</em> act like a button with <tt>&lt;a role=&quot;button&quot;&gt;</tt>. The base function is a link with an <tt>href</tt>. With Javascript we can enhance it by setting a click handler. With <tt>preventDefault</tt> in the handler the Javascript will &quot;enhance away&quot; the base behaviour.</p>
<h3>ARIA roles</h3>
<p>In addition to <tt>role=&quot;button&quot;</tt> we will set <tt>aria-haspopup=&quot;true&quot;</tt> and <tt>aria-expanded=&quot;false&quot;</tt>. In combination with the button role, this will cause a screenreader to announce sometheing like &quot;Choose an Option, collapsed, popup button&quot; (VoiceOver).</p>
<p>With <tt>aria-haspopup</tt>, <tt>aria-controls</tt> must have the id of the popup element. It will be convenient for finding the element in JS.</p>
<h3>Function</h3>
<p>We want to be able to operate the dropdown by clicking or tapping the dropdown, or by focussing the toggle with the keyboard and hitting Enter or Space. A link is not natively activated by a Space like a button, even with <tt>role=&quot;button&quot;</tt>so we need to add a handler for this.</p>
<p>When the popup is opened with a keyboard, we want to focus the first item in the dropdown.</p>
<p>Once the dropdown is open, the last element in the dropdown is a cloe button. Tab-navigation should cycle through the links, returning to the first element once you tab past the last (and vice-versa) so that with a keyboard you have to explicitly close the dropdown via the close button. You should also be able to use the Escape key, to click or tap on the open button, or to click or tap outside the dropdown. When the dropdown has closed, focus should return to the toggle.</p>
<p>The current pattern uses only the toggleand when the popup is open the label changes to &quot;close.&quot; This an option but it has some drawbacks:</p>
<ul>
<li>Because of the <tt>aria-haspopup</tt> attribute, VoiceOver says contradictorily, &quot;Close, button, opens popup.&quot;</li>
<li>Tabbing through with a keyboard will propbably sound the same either way, but If you are navigating on a touch device, swiping downward over the popup it may be more understable that you've reached the end when you get to the close button.</li>
<li>There is a little more state to manage because you are changing labels.</li>
</ul>
<p>For illustration, here is a version without a separate close button:</p>
<div>
<div>
<a href="http://example.com/" role="button" id="toggleButton2" class="toggle" aria-haspopup="true" aria-controls="popupMenu2" aria-expanded="false">Choose an Option</a>
<ul id="popupMenu2" class="toggleMenu">
<li><a href="#">Option 1</a></li>
<li><a href="#">Option 2</a></li>
</ul>
</div>
</div>
<p><a href="https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-actions-active-descendant.html">ARIA example</a></p>
<p>iOS Safari only recognizes aria-haspopup=&quot;true&quot;</p>
</div>
<script src="popup.js"></script>
</body>
</html>
// Code for illustration only.
// Not very generic and there are probably better ways to do all of this.
(function () {
"use strict";
// Check whether the popup has popped up already by examining aria-expanded
function isExpanded(n) {
// If it exists it could be true or false...
if (n.attributes["aria-expanded"] !== undefined) {
return n.attributes["aria-expanded"].value === "true";
}
// ...but if it doesn't exist, it's false
return false;
}
function togglePopup1(e) {
var toggle = e.target;
var popup = document.getElementById(
toggle.attributes["aria-controls"].value);
var firstItem = popup.children[0].children[0];
var lastItem = popup.children[popup.children.length -1].children[0];
var closer = lastItem;
// Tab from last item back to top
function tabCycle(e) {
if (e.key === "Tab" && e.shiftKey === false) {
e.preventDefault();
firstItem.focus();
}
}
// Shift+Tab in reverse from first item to last
function shiftTabCycle(e) {
if (e.key === "Tab" && e.shiftKey === true) {
e.preventDefault();
lastItem.focus();
}
}
// Close the popup of you click or tap outside of it
function detectClickOutside(e) {
if (e.target === toggle) {
return false;
}
if (popup.contains(e.target)) {
return false;
}
closePopup();
return true;
}
function detectEsc(e) {
if (e.key === "Escape") {
closePopup();
}
}
// Close the popup and clean up after it
function closePopup() {
// Stop saying the popup has popped up
toggle.setAttribute("aria-expanded", false);
// Close the popup
popup.classList.remove("open");
// Cleanup event listeners
lastItem.removeEventListener("keydown", tabCycle);
toggle.removeEventListener("keydown", shiftTabCycle);
closer.removeEventListener("click", closePopup);
closer.removeEventListener("keydown", clickOnSpace);
document.removeEventListener("click", detectClickOutside);
document.removeEventListener("keydown", detectEsc);
// Move focus to toggle in case the popup was closed with its internal
// close button
toggle.focus();
}
// Pop up the popup
function openPopup() {
// Let readers know the menu is open
toggle.setAttribute("aria-expanded", true);
// Manage focus while tabbing
lastItem.addEventListener("keydown", tabCycle);
firstItem.addEventListener("keydown", shiftTabCycle);
// Close popup if you click outside of it
document.addEventListener("click", detectClickOutside);
// Close popup with Esc key
document.addEventListener("keydown", detectEsc);
// Close popup eith close button
closer.addEventListener("click", closePopup);
// Ass space to trigger close button
closer.addEventListener("keydown", clickOnSpace);
// show popup
popup.classList.add("open");
}
if (isExpanded(toggle)) {
closePopup();
} else {
openPopup();
// Move focus to the first item in the popup.
firstItem.focus();
}
e.preventDefault();
}
function togglePopup2(e) {
var toggle = e.target;
var popup = document.getElementById(
toggle.attributes["aria-controls"].value);
var firstItem = popup.children[0].children[0];
var lastItem = popup.children[popup.children.length -1].children[0];
// Tab from last item back to top
function tabCycle(e) {
if (e.key === "Tab" && e.shiftKey === false) {
e.preventDefault();
toggle.focus();
}
}
// Shift+Tab in reverse from first item to last
function shiftTabCycle(e) {
if (e.key === "Tab" && e.shiftKey === true) {
e.preventDefault();
lastItem.focus();
}
}
// Close the popup of you click or tap outside of it
function detectClickOutside(e) {
if (e.target === toggle) {
return false;
}
if (popup.contains(e.target)) {
return false;
}
closePopup();
return true;
}
function detectEsc(e) {
if (e.key === "Escape") {
closePopup();
}
}
// Close the popup and clean up after it
function closePopup() {
// Restore button label
toggle.text = "Choose an Option";
// Stop saying the popup has popped up
toggle.removeAttribute("aria-expanded");
// Close the popup
popup.classList.remove("open");
// Cleanup event listeners
lastItem.removeEventListener("keydown", tabCycle);
toggle.removeEventListener("keydown", shiftTabCycle);
document.removeEventListener("click", detectClickOutside);
document.removeEventListener("keydown", detectEsc);
}
// Pop up the popup
function openPopup() {
// Change button label
toggle.text = "Close";
// Let readers know the menu is open
toggle.setAttribute("aria-expanded", true);
// Manage focus while tabbing
lastItem.addEventListener("keydown", tabCycle);
toggle.addEventListener("keydown", shiftTabCycle);
// Close popup if you click outside of it
document.addEventListener("click", detectClickOutside);
// Close popup with Esc key
document.addEventListener("keydown", detectEsc);
// show popup
popup.classList.add("open");
}
if (isExpanded(toggle)) {
closePopup();
} else {
openPopup();
// Move focus to the first item in the popup.
firstItem.focus();
}
e.preventDefault();
}
// Make space par act like a click
function clickOnSpace(e) {
if (e.key === " ") {
e.preventDefault();
e.target.click();
}
}
// Toggle the popup when you activate the link
document.getElementById("toggleButton1")
.addEventListener("click", togglePopup1);
// Activate link with the spacebar so it acts like a button
document.getElementById("toggleButton1")
.addEventListener("keydown", clickOnSpace);
// Toggle the popup when you activate the link
document.getElementById("toggleButton2")
.addEventListener("click", togglePopup2);
// Activate link with the spacebar so it acts like a button
document.getElementById("toggleButton2")
.addEventListener("keydown", clickOnSpace);
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment