I want to build a full accesible website and of course I can build it using all patterns provided in the follow guideline: https://www.w3.org/WAI/ARIA/apg/patterns
For the moment I'm trying to implement the main menu navigation in the header by combining the menu pattern with the tabs pattern. Here the two guideline: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/ https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/
It seems that the common way to do build the menu is to follow the pattern with tags ul and li, adding extra attribute aria- and also some javascript code to enable the keyboard navigation.
However, I want to show a tab list when the dropw-down menu is openend (see the screenshot).
At the end I did it (see the html code posted) but I don't understand if I've broken the accessibility or not. Basically I put the tabs pattern into the drop-down menu but I didn't find any example of it on the web and futhermore I tested the code above with WAVE Evaluation Tool chrome extension which dosn't give me any accessibility error. I'm still struggling if it is fine or not.
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<!-- Bootstrap Icon library -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" integrity="sha384-He3RckdFB2wffiHOcESa3sf4Ida+ni/fw9SSzAcfY2EPnU1zkK/sLUzw2C5Tyuhj" crossorigin="anonymous">
<style>
[role="tabpanel"].is-hidden {
display: none;
}
</style>
<title>TITLE_GOES_HERE</title>
</head>
<body>
<header>
<nav aria-label="Mythical University" class="navbar navbar-expand-lg bg-light">
<ul id="exTest" class="disclosure-nav navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<button type="button"
aria-expanded="true"
aria-controls="id_about_menu">
About
</button>
<ul id="id_about_menu">
<li>
<a href="#mythical-page-content">
Overview
</a>
</li>
<li>
<a href="#mythical-page-content">
Administration
</a>
</li>
<li>
<a href="#mythical-page-content">
Facts
</a>
</li>
<li>
<a href="#mythical-page-content">
Campus Tours
</a>
</li>
</ul>
</li>
<li class="nav-item">
<button type="button"
aria-expanded="true"
aria-controls="id_admissions_menu">
tabs
</button>
<div role="region" id="id_admissions_menu">
<div class="tabs">
<h3 id="tablist-1">
Topics
</h3>
<div role="tablist" aria-labelledby="tablist-1" class="manual">
<button id="tab-1" type="button" role="tab" aria-selected="true" aria-controls="tabpanel-1">
<span class="focus">Maria Ahlefeldt</span>
</button>
<button id="tab-2" type="button" role="tab" aria-selected="false" aria-controls="tabpanel-2" tabindex="-1">
<span class="focus">Carl Andersen</span>
</button>
<button id="tab-3" type="button" role="tab" aria-selected="false" aria-controls="tabpanel-3" tabindex="-1">
<span class="focus">Ida da Fonseca</span>
</button>
</div>
<div id="tabpanel-1" role="tabpanel" aria-labelledby="tab-1">
<p>
<a href="https://en.wikipedia.org/wiki/Maria_Theresia_Ahlefeldt">Maria Theresia Ahlefeldt</a>
(16 January 1755 – 20 December 1810) was a Danish, (originally German), composer.
She is known as the first female composer in Denmark.
Maria Theresia composed music for several ballets, operas, and plays of the royal theatre.
She was given good critic as a composer and described as a “<span lang="da">virkelig Tonekunstnerinde</span>” ('a True Artist of Music').
</p>
</div>
<div id="tabpanel-2" role="tabpanel" aria-labelledby="tab-2" class="is-hidden">
<p>
<a href="https://en.wikipedia.org/wiki/Joachim_Andersen_(composer)">Carl Joachim Andersen</a>
(29 April 1847 – 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen.
Both as a virtuoso and as composer of flute music, he is considered one of the best of his time.
He was considered to be a tough leader and teacher and demanded as such a lot from his orchestras but through that style he reached a high level.
</p>
</div>
<div id="tabpanel-3" role="tabpanel" aria-labelledby="tab-3" class="is-hidden">
<p>
<a href="https://en.wikipedia.org/wiki/Ida_Henriette_da_Fonseca">Ida Henriette da Fonseca</a>
(July 27, 1802 – July 6, 1858) was a Danish opera singer and composer.
Ida Henriette da Fonseca was the daughter of Abraham da Fonseca (1776–1849) and Marie Sofie Kiærskou (1784–1863).
She and her sister Emilie da Fonseca were students of Giuseppe Siboni, choir master of the Opera in Copenhagen.
She was given a place at the royal Opera alongside her sister the same year she debuted in 1827.
</p>
</div>
</div>
</div>
</li>
</ul>
</nav>
</header>
<!-- Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* Supplemental JS for the disclosure menu keyboard behavior
*/
'use strict';
class DisclosureNav {
constructor(domNode) {
this.rootNode = domNode;
this.controlledNodes = [];
this.openIndex = null;
this.useArrowKeys = true;
this.topLevelNodes = [
...this.rootNode.querySelectorAll(
'.main-link, button[aria-expanded][aria-controls]'
),
];
this.topLevelNodes.forEach((node) => {
// handle button + menu
if (
node.tagName.toLowerCase() === 'button' &&
node.hasAttribute('aria-controls')
) {
const menu = node.parentNode.querySelector('ul');
if (menu) {
// save ref controlled menu
this.controlledNodes.push(menu);
// collapse menus
node.setAttribute('aria-expanded', 'false');
this.toggleMenu(menu, false);
// attach event listeners
menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
node.addEventListener('click', this.onButtonClick.bind(this));
node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
} else {
const menu = node.parentNode.querySelector('div[role="region"]');
if (menu) {
// save ref controlled menu
this.controlledNodes.push(menu);
// collapse menus
node.setAttribute('aria-expanded', 'false');
this.toggleMenu(menu, false);
// attach event listeners
menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
node.addEventListener('click', this.onButtonClick.bind(this));
node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
}
}
}
// handle links
else {
this.controlledNodes.push(null);
node.addEventListener('keydown', this.onLinkKeyDown.bind(this));
}
});
this.rootNode.addEventListener('focusout', this.onBlur.bind(this));
}
controlFocusByKey(keyboardEvent, nodeList, currentIndex) {
switch (keyboardEvent.key) {
case 'ArrowUp':
case 'ArrowLeft':
keyboardEvent.preventDefault();
if (currentIndex > -1) {
var prevIndex = Math.max(0, currentIndex - 1);
nodeList[prevIndex].focus();
}
break;
case 'ArrowDown':
case 'ArrowRight':
keyboardEvent.preventDefault();
if (currentIndex > -1) {
var nextIndex = Math.min(nodeList.length - 1, currentIndex + 1);
nodeList[nextIndex].focus();
}
break;
case 'Home':
keyboardEvent.preventDefault();
nodeList[0].focus();
break;
case 'End':
keyboardEvent.preventDefault();
nodeList[nodeList.length - 1].focus();
break;
}
}
// public function to close open menu
close() {
this.toggleExpand(this.openIndex, false);
}
onBlur(event) {
var menuContainsFocus = this.rootNode.contains(event.relatedTarget);
if (!menuContainsFocus && this.openIndex !== null) {
this.toggleExpand(this.openIndex, false);
}
}
onButtonClick(event) {
var button = event.target;
var buttonIndex = this.topLevelNodes.indexOf(button);
var buttonExpanded = button.getAttribute('aria-expanded') === 'true';
this.toggleExpand(buttonIndex, !buttonExpanded);
}
onButtonKeyDown(event) {
var targetButtonIndex = this.topLevelNodes.indexOf(document.activeElement);
// close on escape
if (event.key === 'Escape') {
this.toggleExpand(this.openIndex, false);
}
// move focus into the open menu if the current menu is open
else if (
this.useArrowKeys &&
this.openIndex === targetButtonIndex &&
event.key === 'ArrowDown'
) {
event.preventDefault();
this.controlledNodes[this.openIndex].querySelector('a').focus();
}
// handle arrow key navigation between top-level buttons, if set
else if (this.useArrowKeys) {
this.controlFocusByKey(event, this.topLevelNodes, targetButtonIndex);
}
}
onLinkKeyDown(event) {
var targetLinkIndex = this.topLevelNodes.indexOf(document.activeElement);
// handle arrow key navigation between top-level buttons, if set
if (this.useArrowKeys) {
this.controlFocusByKey(event, this.topLevelNodes, targetLinkIndex);
}
}
onMenuKeyDown(event) {
if (this.openIndex === null) {
return;
}
var menuLinks = Array.prototype.slice.call(
this.controlledNodes[this.openIndex].querySelectorAll('a')
);
var currentIndex = menuLinks.indexOf(document.activeElement);
// close on escape
if (event.key === 'Escape') {
this.topLevelNodes[this.openIndex].focus();
this.toggleExpand(this.openIndex, false);
}
// handle arrow key navigation within menu links, if set
else if (this.useArrowKeys) {
this.controlFocusByKey(event, menuLinks, currentIndex);
}
}
toggleExpand(index, expanded) {
// close open menu, if applicable
if (this.openIndex !== index) {
this.toggleExpand(this.openIndex, false);
}
// handle menu at called index
if (this.topLevelNodes[index]) {
this.openIndex = expanded ? index : null;
this.topLevelNodes[index].setAttribute('aria-expanded', expanded);
this.toggleMenu(this.controlledNodes[index], expanded);
}
}
toggleMenu(domNode, show) {
if (domNode) {
domNode.style.display = show ? 'block' : 'none';
}
}
updateKeyControls(useArrowKeys) {
this.useArrowKeys = useArrowKeys;
}
}
/* Initialize Disclosure Menus */
window.addEventListener(
'load',
function () {
var menus = document.querySelectorAll('.disclosure-nav');
var disclosureMenus = [];
for (var i = 0; i < menus.length; i++) {
disclosureMenus[i] = new DisclosureNav(menus[i]);
}
// listen to arrow key checkbox
var arrowKeySwitch = document.getElementById('arrow-behavior-switch');
if (arrowKeySwitch) {
arrowKeySwitch.addEventListener('change', function () {
var checked = arrowKeySwitch.checked;
for (var i = 0; i < disclosureMenus.length; i++) {
disclosureMenus[i].updateKeyControls(checked);
}
});
}
// fake link behavior
disclosureMenus.forEach((disclosureNav, i) => {
var links = menus[i].querySelectorAll('[href="#mythical-page-content"]');
var examplePageHeading = document.getElementById('mythical-page-heading');
for (var k = 0; k < links.length; k++) {
// The codepen export script updates the internal link href with a full URL
// we're just manually fixing that behavior here
links[k].href = '#mythical-page-content';
links[k].addEventListener('click', (event) => {
// change the heading text to fake a page change
var pageTitle = event.target.innerText;
examplePageHeading.innerText = pageTitle;
// handle aria-current
for (var n = 0; n < links.length; n++) {
links[n].removeAttribute('aria-current');
}
event.target.setAttribute('aria-current', 'page');
});
}
});
},
false
);
</script>
<script>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* File: tabs-manual.js
*
* Desc: Tablist widget that implements ARIA Authoring Practices
*/
'use strict';
class TabsManual {
constructor(groupNode) {
this.tablistNode = groupNode;
this.tabs = [];
this.firstTab = null;
this.lastTab = null;
this.tabs = Array.from(this.tablistNode.querySelectorAll('[role=tab]'));
this.tabpanels = [];
for (var i = 0; i < this.tabs.length; i += 1) {
var tab = this.tabs[i];
var tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
tab.tabIndex = -1;
tab.setAttribute('aria-selected', 'false');
this.tabpanels.push(tabpanel);
tab.addEventListener('keydown', this.onKeydown.bind(this));
tab.addEventListener('click', this.onClick.bind(this));
if (!this.firstTab) {
this.firstTab = tab;
}
this.lastTab = tab;
}
this.setSelectedTab(this.firstTab);
}
setSelectedTab(currentTab) {
for (var i = 0; i < this.tabs.length; i += 1) {
var tab = this.tabs[i];
if (currentTab === tab) {
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
this.tabpanels[i].classList.remove('is-hidden');
} else {
tab.setAttribute('aria-selected', 'false');
tab.tabIndex = -1;
this.tabpanels[i].classList.add('is-hidden');
}
}
}
moveFocusToTab(currentTab) {
currentTab.focus();
}
moveFocusToPreviousTab(currentTab) {
var index;
if (currentTab === this.firstTab) {
this.moveFocusToTab(this.lastTab);
} else {
index = this.tabs.indexOf(currentTab);
this.moveFocusToTab(this.tabs[index - 1]);
}
}
moveFocusToNextTab(currentTab) {
var index;
if (currentTab === this.lastTab) {
this.moveFocusToTab(this.firstTab);
} else {
index = this.tabs.indexOf(currentTab);
this.moveFocusToTab(this.tabs[index + 1]);
}
}
/* EVENT HANDLERS */
onKeydown(event) {
var tgt = event.currentTarget,
flag = false;
switch (event.key) {
case 'ArrowLeft':
this.moveFocusToPreviousTab(tgt);
flag = true;
break;
case 'ArrowRight':
this.moveFocusToNextTab(tgt);
flag = true;
break;
case 'Home':
this.moveFocusToTab(this.firstTab);
flag = true;
break;
case 'End':
this.moveFocusToTab(this.lastTab);
flag = true;
break;
default:
break;
}
if (flag) {
event.stopPropagation();
event.preventDefault();
}
}
// Since this example uses buttons for the tabs, the click onr also is activated
// with the space and enter keys
onClick(event) {
this.setSelectedTab(event.currentTarget);
}
}
// Initialize tablist
window.addEventListener('load', function () {
var tablists = document.querySelectorAll('[role=tablist].manual');
for (var i = 0; i < tablists.length; i++) {
new TabsManual(tablists[i]);
}
});
</script>
</body>
</html>
