Toggle navigation
WebEdit
New page
Samples
Login
Copy of 'Copy of 'Navigation Menu Button: Keyboard, ARIA and High Contrast Support''
Save
Show
Copy
Unsaved changes!
Settings
Hide
Title
Title
Description
Description
Web Key
Slug
Public
Last updated: May 5, 2024, 8:20 p.m.
HTML Head
HTML Body
<h1>Navigation Menu Button: Keyboard, ARIA and High Contrast Support</h1> <div class="menu-button-links"> <button type="button" id="id-button" aria-haspopup="true" aria-controls="id-menu"> WAI-ARIA Quick Links <svg xmlns="http://www.w3.org/2000/svg" class="down" width="12" height="9" viewBox="0 0 12 9"> <polygon points="1 0, 11 0, 6 8"></polygon> </svg> </button> <ul id="id-menu" role="menu" aria-labelledby="id-button"> <li role="none"> <a role="menuitem" href="https://www.w3.org/"> W3C Home Page </a> </li> <li role="none"> <a role="menuitem" href="https://www.w3.org/standards/webdesign/accessibility"> W3C Web Accessibility Initiative </a> </li> <li role="none"> <a role="menuitem" href="https://www.w3.org/TR/wai-aria/"> ARIA Specification </a> </li> <li role="none"> <a role="menuitem" href="https://w3c.github.io/aria-practices/"> Authoring Practices </a> </li> <li role="none"> <a role="menuitem" href="https://www.w3.org/TR/html-aria/#el-li"> HTML Accessibility API Mappings </a> </li> <li role="none"> <a role="menuitem" href="https://w3c.github.io/core-aam/#mapping_role"> Core ARIA Accessibility API Mappings </a> </li> <li role="none"> <a role="menuitem" href="https://www.w3.org/TR/accname-aam-1.1/"> Accessible Name and Description </a> </li> </ul> </div> <h2>High Contrast Features</h2> <ul> <li>When <code>button</code> does <em>not</em> have focus, CSS <code>border</code> property is set to <code>1px</code>.</li> <li>When <code>button</code> does have focus, CSS <code>border</code> property is set to <code>3px</code> and padding is reduced by <code>2px</code>.</li> <li>When <code>menuitem</code> does <em>not</em> have focus, CSS <code>border</code> property is set to <code>none</code>.</li> <li>When <code>menuitem</code> does have focus, CSS <code>border</code> property is set to <code>2px</code> and padding is reduced by <code>2px</code>.</li> </ul> <h2>ARIA Markup Features</h2> <h3>Button Element</h3> <ul> <li>Button element has the default role of <code>button</code>.</li> <li> <code>aria-haspopup="true"</code> attribute. </li> <li> <code>aria-controls</code> attribute references the <code>id</code> of the element with the <code>menu</code> role. </li> <li> <code>aria-expanded="true"</code> when menu is open, otherwise attribute is not present. </li> <li>Accessible name for the button comes from the text content of the button element.</li> </ul> <h3>Menu and Menuitem Elements</h3> <ul> <li>Container <code>ul</code> element has the <code>role="menu"</code>.</li> <li>Container <code>ul</code> element has the <code>aria-labelledby="id-mb"</code> to provide an accessible name for the menu.</li> <li> <code>a</code> elements have the <code>role="menuitem"</code>. </li> <li> <code>li</code> elements have the <code>role="none"</code>, since the list item semantics are not needed due to the <code>menuitem</code> roles being on the <code>a</code> elements. </li> <li>Accessible name for the menu items comes from the text content of the anchor elements.</li> </ul> <h2>Keyboard Features</h2> <p>Includes keyboard support required by the menu button design pattern: <ul> <li> Button opens menu moves keyboard focus to a menu option. <ul> <li><kbd>Space</kbd></li> <li><kbd>Enter</kbd> </li> <li> <kbd>Up arrow</kbd> </li> <li> <kbd>Down arrow</kbd> </li> </ul> </li> <li> Move through menu options with cursor keys. <ul> <li> <kbd>Up arrow</kbd> </li> <li> <kbd>Down arrow</kbd> </li> <li> <kbd>Home</kbd> </li> <li> <kbd>End</kbd> </li> </ul> </li> <li>Move through menu options with first letter keys.</li> <ul> <li> <kbd>A-Z</kbd> </li> <li> <kbd>a-z</kbd> </li> </ul> <li> Close menu and move focus back to button. <ul> <li> <kbd>Escape</kbd> </li> </ul> </li> </ul> <h2>Accessibility Issues</h2> <ul> <li>Poor high contrast support for focus styling.</li> </ul>
CSS
.menu-button-links { margin: 0; font-size: 110%; } .menu-button-links button { margin: 0; padding: 6px; display: inline-block; position: relative; background-color: #034575; border: 1px solid white; font-size: 0.9em; color: white; border-radius: 5px; } .menu-button-links [role="menu"] { margin: 0; padding: 7px 4px; list-style: none; display: none; position: absolute; border: 2px solid #034575; border-radius: 5px; background-color: #eee; } .menu-button-links [role="menu"] a { margin: 0; padding: 6px; display: block; width: 24em; background-color: #eee; border: none; color: black; border-radius: 5px; text-decoration: none; } .menu-button-links button svg.down { padding-left: 0.125em; fill: currentColor; stroke: currentColor; } .menu-button-links button[aria-expanded] svg.down { transform: rotate(180deg); } /* Focus and hover styling */ .menu-button-links button:focus, .menu-button-links button:hover { padding: 4px; background-color: white; border: 3px solid #034575; color: #222222; } .menu-button-links [role="menu"] a:focus, .menu-button-links [role="menu"] a:hover { padding: 4px; background-color: white; border: 2px solid #034575; color: #222222; }
JavaScript
class MenuButtonLinks { constructor(domNode) { this.domNode = domNode; this.buttonNode = domNode.querySelector('button'); this.menuNode = domNode.querySelector('[role="menu"]'); this.menuitemNodes = [] this.firstMenuitem = false; this.lastMenuitem = false; this.firstChars = []; this.buttonNode.addEventListener('keydown', this.onButtonKeydown.bind(this)); this.buttonNode.addEventListener('click', this.onButtonClick.bind(this)); var nodes = this.menuNode.querySelectorAll('a'); for (var i = 0; i < nodes.length; i++) { var menuitem = nodes[i]; this.menuitemNodes.push(menuitem); menuitem.tabIndex = -1; this.firstChars.push(menuitem.textContent.trim()[0].toLowerCase()); menuitem.addEventListener('keydown', this.onMenuitemKeydown.bind(this)); menuitem.addEventListener('mouseover', this.onMenuitemMouseover.bind(this)); if( !this.firstMenuitem) { this.firstMenuitem = menuitem; } this.lastMenuitem = menuitem; } domNode.addEventListener('focusin', this.onFocusin.bind(this)); domNode.addEventListener('focusout', this.onFocusout.bind(this)); window.addEventListener('mousedown', this.onBackgroundMousedown.bind(this), true); } // Popup menu methods openPopup() { var rect = this.menuNode.getBoundingClientRect(); this.menuNode.style.display = 'block'; this.buttonNode.setAttribute('aria-expanded', 'true'); } closePopup() { if (this.isOpen()) { this.buttonNode.removeAttribute('aria-expanded'); this.menuNode.style.display = 'none'; } } isOpen() { return this.buttonNode.getAttribute('aria-expanded') === 'true'; } // Focus management methods setFocusToMenuitem(newMenuitem) { this.menuitemNodes.forEach(function(item) { if (item === newMenuitem) { item.tabIndex = 0; newMenuitem.focus(); } else { item.tabIndex = -1; } }); } setFocusToFirstMenuitem(currentMenuitem) { this.setFocusToMenuitem(this.firstMenuitem); } setFocusToLastMenuitem(currentMenuitem) { this.setFocusToMenuitem(this.lastMenuitem); } setFocusToPreviousMenuitem(currentMenuitem) { var newMenuitem, index; if (currentMenuitem === this.firstMenuitem) { newMenuitem = this.lastMenuitem; } else { index = this.menuitemNodes.indexOf(currentMenuitem); newMenuitem = this.menuitemNodes[ index - 1 ]; } this.setFocusToMenuitem(newMenuitem); return newMenuitem; } setFocusToNextMenuitem(currentMenuitem) { var newMenuitem, index; if (currentMenuitem === this.lastMenuitem) { newMenuitem = this.firstMenuitem; } else { index = this.menuitemNodes.indexOf(currentMenuitem); newMenuitem = this.menuitemNodes[ index + 1 ]; } this.setFocusToMenuitem(newMenuitem); return newMenuitem; } setFocusByFirstCharacter(currentMenuitem, char) { var start, index; if (char.length > 1) { return; } char = char.toLowerCase(); // Get start index for search based on position of currentItem start = this.menuitemNodes.indexOf(currentMenuitem) + 1; if (start >= this.menuitemNodes.length) { start = 0; } // Check remaining slots in the menu index = this.firstChars.indexOf(char, start); // If not found in remaining slots, check from beginning if (index === -1) { index = this.firstChars.indexOf(char, 0); } // If match was found... if (index > -1) { this.setFocusToMenuitem(this.menuitemNodes[index]); } } // Utilities getIndexFirstChars(startIndex, char) { for (var i = startIndex; i < this.firstChars.length; i++) { if (char === this.firstChars[i]) { return i; } } return -1; } // Menu event handlers onFocusin(event) { this.domNode.classList.add('focus'); } onFocusout(event) { this.domNode.classList.remove('focus'); } onButtonKeydown(event) { var tgt = event.currentTarget, key = event.key, flag = false; switch (key) { case ' ': case 'Enter': case 'ArrowDown': case 'Down': this.openPopup(); this.setFocusToFirstMenuitem(); flag = true; break; case 'Esc': case 'Escape': this.closePopup(); this.buttonNode.focus(); flag = true; break; case 'Up': case 'ArrowUp': this.openPopup(); this.setFocusToLastMenuitem(); flag = true; break; default: break; } if (flag) { event.stopPropagation(); event.preventDefault(); } } onButtonClick(event) { if (this.isOpen()) { this.closePopup(); this.buttonNode.focus(); } else { this.openPopup(); this.setFocusToFirstMenuitem(); } event.stopPropagation(); event.preventDefault(); } onMenuitemKeydown(event) { var tgt = event.currentTarget, key = event.key, flag = false; function isPrintableCharacter (str) { return str.length === 1 && str.match(/\S/); } if (event.ctrlKey || event.altKey || event.metaKey) { return; } if (event.shiftKey) { if (isPrintableCharacter(key)) { this.setFocusByFirstCharacter(tgt, key); flag = true; } if (event.key === 'Tab') { this.buttonNode.focus(); this.closePopup(); flag = true; } } else { switch (key) { case ' ': window.location.href=tgt.href; break; case 'Esc': case 'Escape': this.closePopup(); this.buttonNode.focus(); flag = true; break; case 'Up': case 'ArrowUp': this.setFocusToPreviousMenuitem(tgt); flag = true; break; case 'ArrowDown': case 'Down': this.setFocusToNextMenuitem(tgt); flag = true; break; case 'Home': case 'PageUp': this.setFocusToFirstMenuitem(); flag = true; break; case 'End': case 'PageDown': this.setFocusToLastMenuitem(); flag = true; break; case 'Tab': this.closePopup(); break; default: if (isPrintableCharacter(key)) { this.setFocusByFirstCharacter(tgt, key); flag = true; } break; } } if (flag) { event.stopPropagation(); event.preventDefault(); } } onMenuitemMouseover(event) { var tgt = event.currentTarget; tgt.focus(); } onBackgroundMousedown(event) { if (!this.domNode.contains(event.target)) { if (this.isOpen()) { this.closePopup(); this.buttonNode.focus(); } } } } // Initialize navigation menu buttons window.addEventListener('load', function () { var menuButtons = document.querySelectorAll('.menu-button-links'); for(var i=0; i < menuButtons.length; i++) { var menuButton = new MenuButtonLinks(menuButtons[i]); } });