A Pen by Dustin Turner on CodePen.
Created
August 22, 2025 17:41
-
-
Save Dustin4444/4091cc9b3e088903fb5e9c77af6d65e1 to your computer and use it in GitHub Desktop.
pen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="menu-button-actions"> | |
| <button type="button" id="menubutton1" aria-haspopup="true" aria-expanded="false" aria-controls="menu1"> | |
| Actions | |
| <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="menu1" role="menu" tabindex="-1" aria-labelledby="menubutton1" aria-activedescendant="mi1"> | |
| <li id="mi1" role="menuitem">Action 1</li> | |
| <li id="mi2" role="menuitem">Action 2</li> | |
| <li id="mi3" role="menuitem">Action 3</li> | |
| <li id="mi4" role="menuitem">Action 4</li> | |
| </ul> | |
| </div> | |
| <p> | |
| <label>Last Action: <input class="action" id="action_output" type="text" value=""></label> | |
| </p> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * This content is licensed according to the W3C Software License at | |
| * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document | |
| * | |
| * File: menu-button-actives-active-descendant.js | |
| * | |
| * Desc: Creates a menu button that opens a menu of actions using aria-activedescendants | |
| */ | |
| "use strict"; | |
| class MenuButtonActionsActiveDescendant { | |
| constructor(domNode, performMenuAction) { | |
| this.domNode = domNode; | |
| this.performMenuAction = performMenuAction; | |
| this.buttonNode = domNode.querySelector("button"); | |
| this.menuNode = domNode.querySelector('[role="menu"]'); | |
| this.currentMenuitem = {}; | |
| 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)); | |
| this.menuNode.addEventListener("keydown", this.onMenuKeydown.bind(this)); | |
| var nodes = domNode.querySelectorAll('[role="menuitem"]'); | |
| 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("click", this.onMenuitemClick.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 | |
| ); | |
| } | |
| setFocusToMenuitem(newMenuitem) { | |
| for (var i = 0; i < this.menuitemNodes.length; i++) { | |
| var menuitem = this.menuitemNodes[i]; | |
| if (menuitem === newMenuitem) { | |
| this.currentMenuitem = newMenuitem; | |
| menuitem.classList.add("focus"); | |
| this.menuNode.setAttribute("aria-activedescendant", newMenuitem.id); | |
| } else { | |
| menuitem.classList.remove("focus"); | |
| } | |
| } | |
| } | |
| setFocusToFirstMenuitem() { | |
| this.setFocusToMenuitem(this.firstMenuitem); | |
| } | |
| setFocusToLastMenuitem() { | |
| this.setFocusToMenuitem(this.lastMenuitem); | |
| } | |
| setFocusToPreviousMenuitem() { | |
| var newMenuitem, index; | |
| if (this.currentMenuitem === this.firstMenuitem) { | |
| newMenuitem = this.lastMenuitem; | |
| } else { | |
| index = this.menuitemNodes.indexOf(this.currentMenuitem); | |
| newMenuitem = this.menuitemNodes[index - 1]; | |
| } | |
| this.setFocusToMenuitem(newMenuitem); | |
| return newMenuitem; | |
| } | |
| setFocusToNextMenuitem() { | |
| var newMenuitem, index; | |
| if (this.currentMenuitem === this.lastMenuitem) { | |
| newMenuitem = this.firstMenuitem; | |
| } else { | |
| index = this.menuitemNodes.indexOf(this.currentMenuitem); | |
| newMenuitem = this.menuitemNodes[index + 1]; | |
| } | |
| this.setFocusToMenuitem(newMenuitem); | |
| return newMenuitem; | |
| } | |
| setFocusByFirstCharacter(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(this.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; | |
| } | |
| // Popup menu methods | |
| openPopup() { | |
| this.menuNode.style.display = "block"; | |
| this.buttonNode.setAttribute("aria-expanded", "true"); | |
| this.menuNode.focus(); | |
| this.setFocusToFirstMenuitem(); | |
| } | |
| closePopup() { | |
| if (this.isOpen()) { | |
| this.buttonNode.setAttribute("aria-expanded", "false"); | |
| this.menuNode.setAttribute("aria-activedescendant", ""); | |
| for (let i = 0; i < this.menuitemNodes.length; i++) { | |
| this.menuitemNodes[i].classList.remove("focus"); | |
| } | |
| this.menuNode.style.display = "none"; | |
| this.buttonNode.focus(); | |
| } | |
| } | |
| isOpen() { | |
| return this.buttonNode.getAttribute("aria-expanded") === "true"; | |
| } | |
| // Menu event handlers | |
| onFocusin() { | |
| this.domNode.classList.add("focus"); | |
| } | |
| onFocusout() { | |
| this.domNode.classList.remove("focus"); | |
| } | |
| onButtonKeydown(event) { | |
| var 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(); | |
| 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(); | |
| } else { | |
| this.openPopup(); | |
| } | |
| event.stopPropagation(); | |
| event.preventDefault(); | |
| } | |
| onMenuKeydown(event) { | |
| var 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(key); | |
| flag = true; | |
| } | |
| if (event.key === "Tab") { | |
| this.closePopup(); | |
| flag = true; | |
| } | |
| } else { | |
| switch (key) { | |
| case " ": | |
| case "Enter": | |
| this.closePopup(); | |
| this.performMenuAction(this.currentMenuitem); | |
| flag = true; | |
| break; | |
| case "Esc": | |
| case "Escape": | |
| this.closePopup(); | |
| flag = true; | |
| break; | |
| case "Up": | |
| case "ArrowUp": | |
| this.setFocusToPreviousMenuitem(); | |
| flag = true; | |
| break; | |
| case "ArrowDown": | |
| case "Down": | |
| this.setFocusToNextMenuitem(); | |
| 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(key); | |
| flag = true; | |
| } | |
| break; | |
| } | |
| } | |
| if (flag) { | |
| event.stopPropagation(); | |
| event.preventDefault(); | |
| } | |
| } | |
| onMenuitemMouseover(event) { | |
| var tgt = event.currentTarget; | |
| this.setFocusToMenuitem(tgt); | |
| } | |
| onMenuitemClick(event) { | |
| var tgt = event.currentTarget; | |
| this.closePopup(); | |
| this.performMenuAction(tgt); | |
| } | |
| onBackgroundMousedown(event) { | |
| if (!this.domNode.contains(event.target)) { | |
| if (this.isOpen()) { | |
| this.closePopup(); | |
| } | |
| } | |
| } | |
| } | |
| // Initialize menu buttons | |
| window.addEventListener("load", function () { | |
| document.getElementById("action_output").value = "none"; | |
| function performMenuAction(node) { | |
| document.getElementById("action_output").value = node.textContent.trim(); | |
| } | |
| var menuButtons = document.querySelectorAll(".menu-button-actions"); | |
| for (var i = 0; i < menuButtons.length; i++) { | |
| new MenuButtonActionsActiveDescendant(menuButtons[i], performMenuAction); | |
| } | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script src="https://www.w3.orgcontent/shared/js/utils.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| .menu-button-actions { | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .menu-button-actions button { | |
| margin: 0; | |
| padding: 6px; | |
| display: inline-block; | |
| position: relative; | |
| background-color: #034575; | |
| border: 1px solid #034575; | |
| font-size: 0.9em; | |
| color: white; | |
| border-radius: 5px; | |
| } | |
| .menu-button-actions [role="menu"] { | |
| display: none; | |
| position: absolute; | |
| margin: 0; | |
| padding: 7px 4px; | |
| border: 2px solid #034575; | |
| border-radius: 5px; | |
| background-color: #eee; | |
| } | |
| .menu-button-actions [role="menuitem"], | |
| .menu-button-actions [role="separator"] { | |
| margin: 0; | |
| padding: 6px; | |
| display: block; | |
| width: 4em; | |
| background-color: #eee; | |
| color: black; | |
| border-radius: 5px; | |
| } | |
| .menu-button-actions [role="separator"] { | |
| padding-top: 3px; | |
| background-image: url("../images/separator.svg"); | |
| background-position: center; | |
| background-repeat: repeat-x; | |
| } | |
| .menu-button-actions button svg.down { | |
| padding-left: 0.125em; | |
| fill: currentcolor; | |
| stroke: currentcolor; | |
| } | |
| .menu-button-actions button[aria-expanded="true"] svg.down { | |
| transform: rotate(180deg); | |
| } | |
| /* focus styling */ | |
| .menu-button-actions button:hover, | |
| .menu-button-actions button:focus, | |
| .menu-button-actions button[aria-expanded="true"] { | |
| padding: 4px; | |
| border: 3px solid #034575; | |
| background: #eee; | |
| color: #222; | |
| outline: none; | |
| margin: 0; | |
| } | |
| .menu-button-actions [role="menuitem"].focus, | |
| .menu-button-actions [role="menuitem"]:focus { | |
| padding: 4px; | |
| border: 2px solid #034575; | |
| background: #034575; | |
| color: #fff; | |
| outline: none; | |
| margin: 0; | |
| } | |
| input.action:focus { | |
| outline: 2px solid #034575; | |
| background: #def; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <link href="https://www.w3.org/content/shared/css/core.css" rel="stylesheet" /> | |
| <link href="https://www.w3.org/StyleSheets/TR/2016/base.css" rel="stylesheet" /> | |
| <link href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment