PageSourceSearch

https://www.aragon.es/o/aragon-theme/js/aria/PopupMenuAction.js

js aragon.es collected 2026-09-24 08:28:21 UTC 7,108 bytes, 217 lines download raw bytes

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.