1export function PopupMenuAction(aria) { 2 /* 3 * This content is licensed according to the W3C Software License at 4 * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document 5 * 6 * File: PopupMenuAction.js 7 * 8 * Desc: Popup menu widget that implements ARIA Authoring Practices 9 */ 10 11 /* 12 * @constructor PopupMenuAction 13 * 14 * @desc 15 * Wrapper object for a simple popup menu (without nested submenus) 16 * 17 * @param domNode 18 * The DOM element node that serves as the popup menu container. Each 19 * child element of domNode that represents a menuitem must have a 20 * 'role' attribute with value 'menuitem'. 21 * 22 * @param controllerObj 23 * The object that is a wrapper for the DOM element that controls the 24 * menu, e.g. a button element, with an 'aria-controls' attribute that 25 * references this menu's domNode. See MenuButton.js 26 * 27 * The controller object is expected to have the following properties: 28 * 1. domNode: The controller object's DOM element node, needed for 29 * retrieving positioning information. 30 */ 31 aria.PopupMenuAction = function (domNode, controllerObj, actionManager) { 32 var elementChildren, 33 msgPrefix = 'PopupMenu constructor argument domNode '; 34 35 // Check whether domNode is a DOM element 36 if (!domNode instanceof Element) { 37 throw new TypeError(msgPrefix + 'is not a DOM Element.'); 38 } 39 40 // Check whether domNode has child elements 41 if (domNode.childElementCount === 0) { 42 throw new Error(msgPrefix + 'has no element children.'); 43 } 44 45 this.domNode = domNode; 46 this.controller = controllerObj; 47 this.actionManager = actionManager; 48 49 this.menuitems = []; // see PopupMenu init method 50 this.firstChars = []; // see PopupMenu init method 51 52 this.firstItem = null; // see PopupMenu init method 53 this.lastItem = null; // see PopupMenu init method 54 }; 55 56 /* 57 * @method PopupMenuAction.prototype.init 58 * 59 * @desc 60 * Traverse domNode children to configure each menuitem and populate menuitems 61 * array. Initialize firstItem and lastItem properties. 62 */ 63 aria.PopupMenuAction.prototype.init = function () { 64 var childElement, menuElement, firstChildElement, menuItem, textContent, numItems, label; 65 66 // Configure the domNode itself 67 this.domNode.tabIndex = -1; 68 69 this.domNode.setAttribute('role', 'menu'); 70 71 if (!this.domNode.getAttribute('aria-labelledby') && !this.domNode.getAttribute('aria-label') && !this.domNode.getAttribute('title')) { 72 label = this.controller.domNode.innerHTML; 73 this.domNode.setAttribute('aria-label', label); 74 } 75 76 // Traverse the element children of domNode: configure each with 77 // menuitem role behavior and store reference in menuitems array. 78 var menuElements = this.domNode.getElementsByTagName('LI'); 79 80 for (var i = 0; i < menuElements.length; i++) { 81 82 menuElement = menuElements[i]; 83 84 if (!menuElement.firstElementChild && menuElement.getAttribute('role') !== 'separator') { 85 menuItem = new aria.MenuItem(menuElement, this); 86 menuItem.init(); 87 this.menuitems.push(menuItem); 88 textContent = menuElement.textContent.trim(); 89 this.firstChars.push(textContent.substring(0, 1).toLowerCase()); 90 } 91 92 } 93 94 // Use populated menuitems array to initialize firstItem and lastItem. 95 numItems = this.menuitems.length; 96 if (numItems > 0) { 97 this.firstItem = this.menuitems[0]; 98 this.lastItem = this.menuitems[numItems - 1]; 99 } 100 }; 101 102 aria.PopupMenuAction.prototype.updateMenuStates = function () { 103 // updateMenuStates 104 }; 105 106 /* FOCUS MANAGEMENT METHODS */ 107 108 aria.PopupMenuAction.prototype.setFocusToController = function (command) { 109 if (typeof command !== 'string') { 110 command = ''; 111 } 112 if (command === 'previous') { 113 this.controller.menubar.setFocusToPreviousItem(this.controller); 114 } 115 else if (command === 'next') {
116 this.controller.menubar.setFocusToNextItem(this.controller); 117 } 118 else { 119 this.controller.domNode.focus(); 120 } 121 }; 122 123 aria.PopupMenuAction.prototype.setFocusToItem = function (item) { 124 item.domNode.focus(); 125 }; 126 127 aria.PopupMenuAction.prototype.setFocusToFirstItem = function () { 128 this.firstItem.domNode.focus(); 129 }; 130 131 aria.PopupMenuAction.prototype.setFocusToLastItem = function () { 132 this.lastItem.domNode.focus(); 133 }; 134 135 aria.PopupMenuAction.prototype.setFocusToPreviousItem = function (currentItem) { 136 var index; 137 138 if (currentItem === this.firstItem) { 139 this.lastItem.domNode.focus(); 140 } 141 else { 142 index = this.menuitems.indexOf(currentItem); 143 this.menuitems[index - 1].domNode.focus(); 144 } 145 }; 146 147 aria.PopupMenuAction.prototype.setFocusToNextItem = function (currentItem) { 148 var index; 149 150 if (currentItem === this.lastItem) { 151 this.firstItem.domNode.focus(); 152 } 153 else { 154 index = this.menuitems.indexOf(currentItem); 155 this.menuitems[index + 1].domNode.focus(); 156 } 157 }; 158 159 aria.PopupMenuAction.prototype.setFocusByFirstCharacter = function (currentItem, char) { 160 var start, index, char = char.toLowerCase(); 161 162 // Get start index for search based on position of currentItem 163 start = this.menuitems.indexOf(currentItem) + 1; 164 if (start === this.menuitems.length) { 165 start = 0; 166 } 167 168 // Check remaining slots in the menu 169 index = this.getIndexFirstChars(start, char); 170 171 // If not found in remaining slots, check from beginning 172 if (index === -1) { 173 index = this.getIndexFirstChars(0, char); 174 } 175 176 // If match was found... 177 if (index > -1) { 178 this.menuitems[index].domNode.focus(); 179 } 180 }; 181 182 aria.PopupMenuAction.prototype.getIndexFirstChars = function (startIndex, char) { 183 for (var i = startIndex; i < this.firstChars.length; i++) { 184 if (char === this.firstChars[i]) { 185 return i; 186 } 187 } 188 return -1; 189 }; 190 191 /* MENU DISPLAY METHODS */ 192 193 aria.PopupMenuAction.prototype.open = function () { 194 // get position and bounding rectangle of controller object's DOM node 195 var rect = this.controller.domNode.getBoundingClientRect(); 196 197 // set CSS properties 198 this.domNode.style.position = 'absolute'; 199 this.domNode.style.top = (rect.height - 1) + 'px'; 200 this.domNode.style.left = '0px'; 201 this.domNode.style.zIndex = 100; 202 this.domNode.style.display = 'block'; 203 204 // set aria-expanded attribute 205 this.controller.domNode.setAttribute('aria-expanded', 'true'); 206 }; 207 208 aria.PopupMenuAction.prototype.isOpen = function () { 209 return this.controller.domNode.getAttribute('aria-expanded') === 'true'; 210 }; 211 212 aria.PopupMenuAction.prototype.close = function () { 213 this.domNode.style.display = 'none'; 214 this.domNode.style.zIndex = 0; 215 this.controller.domNode.setAttribute('aria-expanded', 'false'); 216 }; 217}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.