diff --git a/examples/menubar/css/menubar-editor.css b/examples/menubar/css/menubar-editor.css
new file mode 100644
index 0000000000..01f257cbe1
--- /dev/null
+++ b/examples/menubar/css/menubar-editor.css
@@ -0,0 +1,183 @@
+.menubar-editor {
+ margin: 0;
+ padding: 2px;
+ width: 560px;
+}
+
+.menubar-editor.focus {
+ padding: 0;
+ border: solid 2px #034575;
+}
+
+.menubar-editor textarea {
+ padding: 4px;
+ margin: 0;
+ border: 2px solid #eee;
+ height: 400px;
+ width: 548px;
+ font-size: medium;
+ font-family: sans-serif;
+}
+
+.menubar-editor [role="menubar"] {
+ margin: 0;
+ padding: 2px;
+ border: 2px solid #eee;
+ font-size: 110%;
+ list-style: none;
+ background-color: #eee;
+ height: 32px;
+ display: block;
+}
+
+.menubar-editor [role="menubar"] li {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.menubar-editor [role="menubar"] > li {
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ left: 1px;
+}
+
+.menubar-editor [role="menubar"] > li > [role="menuitem"]::after {
+ content: url('../images/down-arrow.svg');
+ padding-left: 0.25em;
+}
+
+.menubar-editor [role="menubar"] > li > [role="menuitem"]:focus::after {
+ content: url('../images/down-arrow-focus.svg');
+}
+
+.menubar-editor [role="menubar"] > li > [role="menuitem"][aria-expanded="true"]::after {
+ content: url('../images/up-arrow-focus.svg');
+ }
+
+.menubar-editor [role="menubar"] [role="menu"] {
+ display: none;
+ margin: 0;
+ padding: 2px;
+ position: absolute;
+ border: 2px solid #034575;
+ background-color: #eee;
+}
+
+.menubar-editor [role="menubar"] [role="group"] {
+ margin: 0;
+ padding: 0;
+}
+
+.menubar-editor [role="menubar"] [role="menuitem"][aria-disabled="true"] {
+ color: #666;
+ text-decoration: line-through;
+}
+
+.menubar-editor [role="menubar"] [role="menuitem"],
+.menubar-editor [role="menubar"] [role="menuitemcheckbox"],
+.menubar-editor [role="menubar"] [role="menuitemradio"],
+.menubar-editor [role="menubar"] [role="separator"] {
+ padding: 6px;
+ background-color: #eee;
+ border: 0px solid #eee;
+ color: black;
+}
+
+.menubar-editor [role="menubar"] [role="menuitem"][aria-expanded="true"] {
+ padding: 4px;
+ border: 2px solid #034575;
+ background-color: #034575;
+ color: #fff;
+ outline: none;
+}
+
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitem"],
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitemcheckbox"],
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitemradio"],
+.menubar-editor [role="menubar"] [role="menu"] [role="separator"] {
+ padding-left: 27px;
+ width: 8em;
+}
+
+.menubar-editor [role="menubar"] [role="separator"] {
+ padding-top: 3px;
+ background-image: url('../images/separator.svg');
+ background-position: center;
+ background-repeat: repeat-x;
+}
+
+.menubar-editor [role="menubar"] [role="menu"] [aria-checked='true'] {
+ padding: 6px;
+ padding-left: 8px;
+ padding-right: 18px;
+}
+
+.menubar-editor [role="menubar"] [role='menuitemradio'][aria-checked='true']::before {
+ content: url('../images/radio-checked.svg');
+ padding-right: 3px;
+}
+
+.menubar-editor [role="menubar"] [role='menuitemcheckbox'][aria-checked='true']::before {
+ content: url('../images/checkbox-checked.svg');
+ padding-right: 3px;
+}
+
+
+/* focus and hover styling */
+
+
+.menubar-editor [role="menubar"] [role="menuitem"]:focus,
+.menubar-editor [role="menubar"] [role="menuitemcheckbox"]:focus,
+.menubar-editor [role="menubar"] [role="menuitemradio"]:focus {
+ padding: 4px;
+ border: 2px solid #034575;
+ background-color: #034575;
+ color: #fff;
+ outline: none;
+}
+
+.menubar-editor [role="menubar"] [role='menuitemradio'][aria-checked='true']:focus::before {
+ content: url('../images/radio-checked-focus.svg');
+ padding-right: 3px;
+}
+
+.menubar-editor [role="menubar"] [role='menuitemcheckbox'][aria-checked='true']:focus::before {
+ content: url('../images/checkbox-checked-focus.svg');
+ padding-right: 3px;
+}
+
+
+.menubar-editor [role="menubar"] [role="menuitem"]:hover {
+ padding: 4px;
+ border: 2px solid #034575;
+}
+
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitem"]:focus,
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitemcheckbox"]:focus,
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitemradio"]:focus {
+ padding-left: 25px;
+}
+
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitem"][aria-checked='true']:focus,
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitemcheckbox"][aria-checked='true']:focus,
+.menubar-editor [role="menubar"] [role="menu"] [role="menuitemradio"][aria-checked='true']:focus {
+ padding-left: 8px;
+ padding-right: 21px;
+}
+
+/*
+* Text area styles
+*/
+.menubar-editor .italic {
+ font-style: italic;
+}
+
+.menubar-editor .bold {
+ font-weight: bold;
+}
+
+.menubar-editor .underline {
+ text-decoration: underline;
+}
diff --git a/examples/menubar/images/checkbox-checked-focus.svg b/examples/menubar/images/checkbox-checked-focus.svg
new file mode 100644
index 0000000000..47f273cec4
--- /dev/null
+++ b/examples/menubar/images/checkbox-checked-focus.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/menubar/images/checkbox-checked.svg b/examples/menubar/images/checkbox-checked.svg
new file mode 100644
index 0000000000..4a68a8a972
--- /dev/null
+++ b/examples/menubar/images/checkbox-checked.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/menubar/images/down-arrow-focus.svg b/examples/menubar/images/down-arrow-focus.svg
new file mode 100644
index 0000000000..f637806269
--- /dev/null
+++ b/examples/menubar/images/down-arrow-focus.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/menubar/images/down-arrow.svg b/examples/menubar/images/down-arrow.svg
new file mode 100644
index 0000000000..c30c32e123
--- /dev/null
+++ b/examples/menubar/images/down-arrow.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/menubar/images/radio-checked-focus.svg b/examples/menubar/images/radio-checked-focus.svg
new file mode 100644
index 0000000000..04c1a03d0f
--- /dev/null
+++ b/examples/menubar/images/radio-checked-focus.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/menubar/images/radio-checked.svg b/examples/menubar/images/radio-checked.svg
new file mode 100644
index 0000000000..f34a549a37
--- /dev/null
+++ b/examples/menubar/images/radio-checked.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/menubar/images/separator.svg b/examples/menubar/images/separator.svg
new file mode 100644
index 0000000000..db78906f72
--- /dev/null
+++ b/examples/menubar/images/separator.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/menubar/images/up-arrow-focus.svg b/examples/menubar/images/up-arrow-focus.svg
new file mode 100644
index 0000000000..ede8a5d71e
--- /dev/null
+++ b/examples/menubar/images/up-arrow-focus.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/menubar/js/menubar-editor.js b/examples/menubar/js/menubar-editor.js
new file mode 100644
index 0000000000..3f26973421
--- /dev/null
+++ b/examples/menubar/js/menubar-editor.js
@@ -0,0 +1,725 @@
+/*
+* This content is licensed according to the W3C Software License at
+* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+*
+* File: menubar-editor.js
+*
+* Desc: Creates a menubar to control the styling of text in a textarea element
+*/
+
+'use strict';
+
+var MenubarEditor = function (domNode) {
+
+ this.domNode = domNode;
+ this.menubarNode = domNode.querySelector('[role=menubar]');
+ this.textareaNode = domNode.querySelector('textarea');
+ this.actionManager = new StyleManager(this.textareaNode);
+
+ this.popups = [];
+ this.menuitemGroups = {};
+ this.menuOrientation = {};
+ this.isPopup = {};
+
+ this.firstChars = {}; // see Menubar init method
+ this.firstMenuitem = {}; // see Menubar init method
+ this.lastMenuitem = {}; // see Menubar init method
+
+ this.initMenu(this.menubarNode)
+ this.domNode.addEventListener('focusin', this.handleFocusin.bind(this));
+ this.domNode.addEventListener('focusout', this.handleFocusout.bind(this));
+
+ window.addEventListener('mousedown', this.handleBackgroundMousedown.bind(this), true);
+};
+
+MenubarEditor.prototype.getMenuitems = function(domNode) {
+ var nodes = [];
+
+ var initMenu = this.initMenu.bind(this);
+ var getGroupId = this.getGroupId.bind(this);
+ var menuitemGroups = this.menuitemGroups;
+ var popups = this.popups;
+
+ function findMenuitems(node, group) {
+ var role, flag, groupId;
+
+ while (node) {
+ flag = true;
+ role = node.getAttribute('role');
+
+ switch (role) {
+ case 'menu':
+ node.tabIndex = -1;
+ initMenu(node);
+ flag = false;
+ break;
+
+ case 'group':
+ groupId = getGroupId(node);
+ menuitemGroups[groupId] = [];
+ break;
+
+ case 'menuitem':
+ case 'menuitemradio':
+ case 'menuitemcheckbox':
+ if (node.getAttribute('aria-haspopup') === 'true') {
+ popups.push(node);
+ }
+ nodes.push(node);
+ if (group) {
+ group.push(node);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (flag && node.firstElementChild) {
+ findMenuitems(node.firstElementChild, menuitemGroups[groupId]);
+ }
+
+ node = node.nextElementSibling;
+ }
+ }
+
+ findMenuitems(domNode.firstElementChild, false);
+
+ return nodes;
+};
+
+MenubarEditor.prototype.initMenu = function (menu) {
+ var i, menuitems, menuitem, role, nextElement;
+
+ var menuId = this.getMenuId(menu);
+
+ menuitems = this.getMenuitems(menu);
+ this.menuOrientation[menuId] = this.getMenuOrientation(menu);
+ this.isPopup[menuId] = menu.getAttribute('role') === 'menu';
+
+ this.menuitemGroups[menuId] = [];
+ this.firstChars[menuId] = [];
+ this.firstMenuitem[menuId] = null;
+ this.lastMenuitem[menuId] = null;
+
+ for(i = 0; i < menuitems.length; i++) {
+ menuitem = menuitems[i];
+ role = menuitem.getAttribute('role');
+
+ if (role.indexOf('menuitem') < 0) {
+ continue;
+ }
+
+ menuitem.tabIndex = -1;
+ this.menuitemGroups[menuId].push(menuitem);
+ this.firstChars[menuId].push(menuitem.textContent[0].toLowerCase());
+
+ menuitem.addEventListener('keydown', this.handleKeydown.bind(this));
+ menuitem.addEventListener('click', this.handleMenuitemClick.bind(this));
+
+ menuitem.addEventListener('mouseover', this.handleMenuitemMouseover.bind(this));
+
+ if( !this.firstMenuitem[menuId]) {
+ if (this.hasPopup(menuitem)) {
+ menuitem.tabIndex = 0;
+ }
+ this.firstMenuitem[menuId] = menuitem;
+ }
+ this.lastMenuitem[menuId] = menuitem;
+
+ }
+};
+
+/* MenubarEditor FOCUS MANAGEMENT METHODS */
+
+MenubarEditor.prototype.setFocusToMenuitem = function (menuId, newMenuitem) {
+
+ var isAnyPopupOpen = this.isAnyPopupOpen();
+
+ this.closePopupAll(newMenuitem);
+
+ if (this.hasPopup(newMenuitem)) {
+ if (isAnyPopupOpen) {
+ this.openPopup(newMenuitem);
+ }
+ }
+ else {
+ var menu = this.getMenu(newMenuitem);
+ var cmi = menu.previousElementSibling;
+ if (!this.isOpen(cmi)) {
+ this.openPopup(cmi);
+ }
+ }
+
+ if (this.hasPopup(newMenuitem)) {
+ if (this.menuitemGroups[menuId]) {
+ this.menuitemGroups[menuId].forEach(function(item) {
+ item.tabIndex = -1;
+ });
+ }
+ newMenuitem.tabIndex = 0;
+ }
+
+ newMenuitem.focus();
+
+};
+
+MenubarEditor.prototype.setFocusToFirstMenuitem = function (menuId) {
+ this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId]);
+};
+
+MenubarEditor.prototype.setFocusToLastMenuitem = function (menuId) {
+ this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId]);
+};
+
+MenubarEditor.prototype.setFocusToPreviousMenuitem = function (menuId, currentMenuitem) {
+ var newMenuitem, index;
+
+ if (currentMenuitem === this.firstMenuitem[menuId]) {
+ newMenuitem = this.lastMenuitem[menuId];
+ }
+ else {
+ index = this.menuitemGroups[menuId].indexOf(currentMenuitem);
+ newMenuitem = this.menuitemGroups[menuId][ index - 1 ];
+ }
+
+ this.setFocusToMenuitem(menuId, newMenuitem);
+
+ return newMenuitem;
+};
+
+MenubarEditor.prototype.setFocusToNextMenuitem = function (menuId, currentMenuitem) {
+ var newMenuitem, index;
+
+ if (currentMenuitem === this.lastMenuitem[menuId]) {
+ newMenuitem = this.firstMenuitem[menuId];
+ }
+ else {
+ index = this.menuitemGroups[menuId].indexOf(currentMenuitem);
+ newMenuitem = this.menuitemGroups[menuId][ index + 1 ];
+ }
+ this.setFocusToMenuitem(menuId, newMenuitem);
+
+ return newMenuitem;
+};
+
+MenubarEditor.prototype.setFocusByFirstCharacter = function (menuId, currentMenuitem, char) {
+ var start, index;
+
+ char = char.toLowerCase();
+
+ // Get start index for search based on position of currentItem
+ start = this.menuitemGroups[menuId].indexOf(currentMenuitem) + 1;
+ if (start >= this.menuitemGroups[menuId].length) {
+ start = 0;
+ }
+
+ // Check remaining slots in the menu
+ index = this.getIndexFirstChars(menuId, start, char);
+
+ // If not found in remaining slots, check from beginning
+ if (index === -1) {
+ index = this.getIndexFirstChars(menuId, 0, char);
+ }
+
+ // If match was found...
+ if (index > -1) {
+ this.setFocusToMenuitem(menuId, this.menuitemGroups[menuId][index]);
+ }
+};
+
+// Utilities
+
+MenubarEditor.prototype.getIndexFirstChars = function (menuId, startIndex, char) {
+ for (var i = startIndex; i < this.firstChars[menuId].length; i++) {
+ if (char === this.firstChars[menuId][i]) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+MenubarEditor.prototype.isPrintableCharacter = function(str) {
+ return str.length === 1 && str.match(/\S/);
+};
+
+MenubarEditor.prototype.getIdFromAriaLabel = function(node) {
+ var id = node.getAttribute('aria-label')
+ if (id) {
+ id = id.trim().toLowerCase().replace(' ', '-').replace('/', '-');
+ }
+ return id;
+};
+
+
+MenubarEditor.prototype.getMenuOrientation = function(node) {
+
+ var orientation = node.getAttribute('aria-orientation');
+
+ if (!orientation) {
+ var role = node.getAttribute('role');
+
+ switch (role) {
+ case 'menubar':
+ orientation = 'horizontal';
+ break;
+
+ case 'menu':
+ orientation = 'vertical';
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return orientation;
+};
+
+MenubarEditor.prototype.getDataOption = function(node) {
+
+ var option = false;
+ var hasOption = node.hasAttribute('data-option');
+ var role = node.hasAttribute('role');
+
+ if (!hasOption) {
+
+ while (node && !hasOption &&
+ (role !== 'menu') &&
+ (role !== 'menubar')) {
+ node = node.parentNode;
+ if (node) {
+ role = node.getAttribute('role');
+ hasOption = node.hasAttribute('data-option');
+ }
+ }
+ }
+
+ if (node) {
+ option = node.getAttribute('data-option');
+ }
+
+ return option;
+};
+
+MenubarEditor.prototype.getGroupId = function(node) {
+
+ var id = false;
+ var role = node.getAttribute('role');
+
+ while (node && (role !== 'group') &&
+ (role !== 'menu') &&
+ (role !== 'menubar')) {
+ node = node.parentNode;
+ if (node) {
+ role = node.getAttribute('role');
+ }
+ }
+
+ if (node) {
+ id = role + '-' + this.getIdFromAriaLabel(node);
+ }
+
+ return id;
+};
+
+MenubarEditor.prototype.getMenuId = function(node) {
+
+ var id = false;
+ var role = node.getAttribute('role');
+
+ while (node && (role !== 'menu') && (role !== 'menubar')) {
+ node = node.parentNode;
+ if (node) {
+ role = node.getAttribute('role');
+ }
+ }
+
+ if (node) {
+ id = role + '-' + this.getIdFromAriaLabel(node);
+ }
+
+ return id;
+};
+
+MenubarEditor.prototype.getMenu = function(menuitem) {
+
+ var id = false;
+ var menu = menuitem;
+ var role = menuitem.getAttribute('role');
+
+ while (menu && (role !== 'menu') && (role !== 'menubar')) {
+ menu = menu.parentNode
+ if (menu) {
+ role = menu.getAttribute('role');
+ }
+ }
+
+ return menu;
+};
+
+MenubarEditor.prototype.toggleCheckbox = function(menuitem) {
+ if (menuitem.getAttribute('aria-checked') === 'true') {
+ menuitem.setAttribute('aria-checked', 'false');
+ return false;
+ }
+ menuitem.setAttribute('aria-checked', 'true');
+ return true;
+};
+
+MenubarEditor.prototype.setRadioButton = function(menuitem) {
+ var groupId = this.getGroupId(menuitem);
+ var radiogroupItems = this.menuitemGroups[groupId];
+ radiogroupItems.forEach( function (item) {
+ item.setAttribute('aria-checked', 'false')
+ });
+ menuitem.setAttribute('aria-checked', 'true');
+ return menuitem.textContent;
+};
+
+MenubarEditor.prototype.updateFontSizeMenu = function(menuId) {
+
+ var fontSizeMenuitems = this.menuitemGroups[menuId];
+ var currentValue = this.actionManager.getFontSize();
+
+ for (var i = 0; i < fontSizeMenuitems.length; i++) {
+ var mi = fontSizeMenuitems[i];
+ var dataOption = mi.getAttribute('data-option');
+ var value = mi.textContent.trim().toLowerCase();
+
+ switch (dataOption) {
+ case 'font-smaller':
+ if (currentValue === 'x-small') {
+ mi.setAttribute('aria-disabled', 'true');
+ }
+ else {
+ mi.removeAttribute('aria-disabled');
+ }
+ break;
+
+ case 'font-larger':
+ if (currentValue === 'x-large') {
+ mi.setAttribute('aria-disabled', 'true');
+ }
+ else {
+ mi.removeAttribute('aria-disabled');
+ }
+ break;
+
+ default:
+ if (currentValue === value) {
+ mi.setAttribute('aria-checked', 'true');
+ }
+ else {
+ mi.setAttribute('aria-checked', 'false');
+ }
+ break;
+
+ }
+ }
+
+
+}
+
+// Popup menu methods
+
+MenubarEditor.prototype.isAnyPopupOpen = function () {
+ for (var i = 0; i < this.popups.length; i++) {
+ if (this.popups[i].getAttribute('aria-expanded') === 'true') {
+ return true;
+ }
+ }
+ return false;
+};
+
+MenubarEditor.prototype.openPopup = function (menuitem) {
+
+ // set aria-expanded attribute
+ var popupMenu = menuitem.nextElementSibling;
+
+ var rect = menuitem.getBoundingClientRect();
+
+ // set CSS properties
+ popupMenu.style.position = 'absolute';
+ popupMenu.style.top = (rect.height - 3) + 'px';
+ popupMenu.style.left = '0px';
+ popupMenu.style.zIndex = 100;
+ popupMenu.style.display = 'block';
+
+ menuitem.setAttribute('aria-expanded', 'true');
+
+ return this.getMenuId(popupMenu);
+
+};
+
+MenubarEditor.prototype.closePopup = function (menuitem) {
+ var menu, cmi;
+
+ if (this.hasPopup(menuitem)) {
+ if (this.isOpen(menuitem)) {
+ menuitem.setAttribute('aria-expanded', 'false');
+ menuitem.nextElementSibling.style.display = 'none';
+ menuitem.nextElementSibling.style.zIndex = 0;
+
+ }
+ }
+ else {
+ menu = this.getMenu(menuitem);
+ cmi = menu.previousElementSibling;
+ cmi.setAttribute('aria-expanded', 'false');
+ cmi.focus();
+ menu.style.display = 'none';
+ menu.style.zIndex = 0;
+ }
+ return cmi;
+};
+
+MenubarEditor.prototype.doesNotContain = function (popup, menuitem) {
+ if (menuitem) {
+ return !popup.nextElementSibling.contains(menuitem);
+ }
+ return true;
+};
+
+MenubarEditor.prototype.closePopupAll = function (menuitem) {
+ if (typeof menuitem !== 'object') {
+ menuitem = false;
+ }
+
+ for (var i = 0; i < this.popups.length; i++) {
+ var popup = this.popups[i];
+ if (this.isOpen(popup) && this.doesNotContain(popup, menuitem)) {
+ this.closePopup(popup);
+ }
+ }
+};
+
+MenubarEditor.prototype.hasPopup = function (menuitem) {
+ return menuitem.getAttribute('aria-haspopup') === 'true';
+};
+
+MenubarEditor.prototype.isOpen = function (menuitem) {
+ return menuitem.getAttribute('aria-expanded') === 'true';
+};
+
+// Menu event handlers
+
+MenubarEditor.prototype.handleFocusin = function (event) {
+ this.domNode.classList.add('focus');
+};
+
+MenubarEditor.prototype.handleFocusout = function (event) {
+ this.domNode.classList.remove('focus');
+};
+
+MenubarEditor.prototype.handleBackgroundMousedown = function (event) {
+ if (!this.menubarNode.contains(event.target)) {
+ this.closePopupAll();
+ }
+};
+
+MenubarEditor.prototype.handleKeydown = function (event) {
+ var tgt = event.currentTarget,
+ key = event.key,
+ flag = false,
+ menuId = this.getMenuId(tgt),
+ id,
+ popupMenuId,
+ mi,
+ role,
+ option,
+ value;
+
+ switch (key) {
+ case ' ':
+ case 'Enter':
+ if (this.hasPopup(tgt)) {
+ popupMenuId = this.openPopup(tgt);
+ this.setFocusToFirstMenuitem(popupMenuId);
+ }
+ else {
+ role = tgt.getAttribute('role');
+ option = this.getDataOption(tgt);
+ switch(role) {
+ case 'menuitem':
+ this.actionManager.setOption(option, tgt.textContent);
+ break;
+
+ case 'menuitemcheckbox':
+ value = this.toggleCheckbox(tgt);
+ this.actionManager.setOption(option, value);
+ break;
+
+ case 'menuitemradio':
+ value = this.setRadioButton(tgt);
+ this.actionManager.setOption(option, value);
+ break;
+
+ default:
+ break;
+ }
+
+ if (this.getMenuId(tgt) === 'menu-size') {
+ this.updateFontSizeMenu('menu-size');
+ }
+ this.closePopup(tgt);
+ }
+ flag = true;
+ break;
+
+ case 'ArrowDown':
+ case 'Down':
+ if (this.menuOrientation[menuId] === 'vertical') {
+ this.setFocusToNextMenuitem(menuId, tgt);
+ flag = true;
+ }
+ else {
+ if (this.hasPopup(tgt)) {
+ popupMenuId = this.openPopup(tgt);
+ this.setFocusToFirstMenuitem(popupMenuId);
+ flag = true;
+ }
+ }
+ break;
+
+ case 'Esc':
+ case 'Escape':
+ this.closePopup(tgt);
+ flag = true;
+ break;
+
+ case 'Left':
+ case 'ArrowLeft':
+ if (this.menuOrientation[menuId] === 'horizontal') {
+ this.setFocusToPreviousMenuitem(menuId, tgt);
+ flag = true;
+ }
+ else {
+ mi = this.closePopup(tgt);
+ id = this.getMenuId(mi);
+ mi = this.setFocusToPreviousMenuitem(id, mi);
+ this.openPopup(mi);
+ }
+ break;
+
+ case 'Right':
+ case 'ArrowRight':
+ if (this.menuOrientation[menuId] === 'horizontal') {
+ this.setFocusToNextMenuitem(menuId, tgt);
+ flag = true;
+ }
+ else {
+ mi = this.closePopup(tgt);
+ id = this.getMenuId(mi);
+ mi = this.setFocusToNextMenuitem(id, mi);
+ this.openPopup(mi);
+ }
+ break;
+
+ case 'Up':
+ case 'ArrowUp':
+ if (this.menuOrientation[menuId] === 'vertical') {
+ this.setFocusToPreviousMenuitem(menuId, tgt);
+ flag = true;
+ }
+ else {
+ if (this.hasPopup(tgt)) {
+ popupMenuId = this.openPopup(tgt);
+ this.setFocusToLastMenuitem(popupMenuId);
+ flag = true;
+ }
+ }
+ break;
+
+ case 'Home':
+ case 'PageUp':
+ this.setFocusToFirstMenuitem(menuId, tgt);
+ flag = true;
+ break;
+
+ case 'End':
+ case 'PageDown':
+ this.setFocusToLastMenuitem(menuId, tgt);
+ flag = true;
+ break;
+
+ case 'Tab':
+ this.closePopup(tgt);
+ break;
+
+ default:
+ if (this.isPrintableCharacter(key)) {
+ this.setFocusByFirstCharacter(menuId, tgt, key);
+ flag = true;
+ }
+ break;
+ }
+
+ if (flag) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+};
+
+MenubarEditor.prototype.handleMenuitemClick = function (event) {
+ var tgt = event.currentTarget;
+ var value;
+
+ if (this.hasPopup(tgt)) {
+ if (this.isOpen(tgt)) {
+ this.closePopup(tgt);
+ }
+ else {
+ var menuId = this.openPopup(tgt);
+ this.setFocusToMenuitem(menuId, tgt);
+ }
+ }
+ else {
+ var role = tgt.getAttribute('role');
+ var option = this.getDataOption(tgt);
+ switch(role) {
+ case 'menuitem':
+ this.actionManager.setOption(option, tgt.textContent);
+ break;
+
+ case 'menuitemcheckbox':
+ value = this.toggleCheckbox(tgt);
+ this.actionManager.setOption(option, value);
+ break;
+
+ case 'menuitemradio':
+ value = this.setRadioButton(tgt);
+ this.actionManager.setOption(option, value);
+ break;
+
+ default:
+ break;
+ }
+
+ if (this.getMenuId(tgt) === 'menu-size') {
+ this.updateFontSizeMenu('menu-size');
+ }
+ this.closePopup(tgt);
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+};
+
+MenubarEditor.prototype.handleMenuitemMouseover = function (event) {
+ var tgt = event.currentTarget;
+
+ if (this.isAnyPopupOpen() && this.getMenu(tgt)) {
+ this.setFocusToMenuitem(this.getMenu(tgt), tgt);
+ }
+};
+
+// Initialize menubar editor
+
+window.addEventListener('load', function () {
+ var menubarEditors = document.querySelectorAll('.menubar-editor');
+ for(var i=0; i < menubarEditors.length; i++) {
+ var menubarEditor = new MenubarEditor(menubarEditors[i]);
+ }
+});
diff --git a/examples/menubar/js/style-manager.js b/examples/menubar/js/style-manager.js
new file mode 100644
index 0000000000..36f6264a09
--- /dev/null
+++ b/examples/menubar/js/style-manager.js
@@ -0,0 +1,170 @@
+/*
+* This content is licensed according to the W3C Software License at
+* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+*
+* File: TextStyling.js
+*
+* Desc: Styling functions for changing the style of an item
+*/
+
+'use strict';
+
+var StyleManager = function (node) {
+ this.node = node;
+ this.fontSize = 'medium';
+};
+
+StyleManager.prototype.setFontFamily = function (value) {
+ this.node.style.fontFamily = value;
+};
+
+StyleManager.prototype.setTextDecoration = function (value) {
+ this.node.style.textDecoration = value;
+};
+
+StyleManager.prototype.setTextAlign = function (value) {
+ this.node.style.textAlign = value;
+};
+
+StyleManager.prototype.setFontSize = function (value) {
+ this.fontSize = value;
+ this.node.style.fontSize = value;
+};
+
+StyleManager.prototype.setColor = function (value) {
+ this.node.style.color = value;
+};
+
+StyleManager.prototype.setBold = function (flag) {
+
+ if (flag) {
+ this.node.style.fontWeight = 'bold';
+ }
+ else {
+ this.node.style.fontWeight = 'normal';
+ }
+};
+
+StyleManager.prototype.setItalic = function (flag) {
+
+ if (flag) {
+ this.node.style.fontStyle = 'italic';
+ }
+ else {
+ this.node.style.fontStyle = 'normal';
+ }
+};
+
+StyleManager.prototype.fontSmaller = function () {
+
+ switch (this.fontSize) {
+ case 'small':
+ this.setFontSize('x-small');
+ break;
+
+ case 'medium':
+ this.setFontSize('small');
+ break;
+
+ case 'large':
+ this.setFontSize('medium');
+ break;
+
+ case 'x-large':
+ this.setFontSize('large');
+ break;
+
+ default:
+ break;
+
+ } // end switch
+};
+
+StyleManager.prototype.fontLarger = function () {
+
+ switch (this.fontSize) {
+ case 'x-small':
+ this.setFontSize('small');
+ break;
+
+ case 'small':
+ this.setFontSize('medium');
+ break;
+
+ case 'medium':
+ this.setFontSize('large');
+ break;
+
+ case 'large':
+ this.setFontSize('x-large');
+ break;
+
+ default:
+ break;
+
+ } // end switch
+};
+
+StyleManager.prototype.isMinFontSize = function () {
+ return this.fontSize === 'x-small';
+};
+
+StyleManager.prototype.isMaxFontSize = function () {
+ return this.fontSize === 'x-large';
+};
+
+StyleManager.prototype.getFontSize = function () {
+ return this.fontSize;
+};
+
+StyleManager.prototype.setOption = function (option, value) {
+
+ option = option.toLowerCase();
+ if (typeof value === 'string') {
+ value = value.toLowerCase();
+ }
+
+ switch (option) {
+
+ case 'font-bold':
+ this.setBold(value);
+ break;
+
+ case 'font-color':
+ this.setColor(value);
+ break;
+
+ case 'font-family':
+ this.setFontFamily(value);
+ break;
+
+ case 'font-smaller':
+ this.fontSmaller();
+ break;
+
+ case 'font-larger':
+ this.fontLarger();
+ break;
+
+ case 'font-size':
+ this.setFontSize(value);
+ break;
+
+ case 'font-italic':
+ this.setItalic(value);
+ break;
+
+ case 'text-align':
+ this.setTextAlign(value);
+ break;
+
+ case 'text-decoration':
+ this.setTextDecoration(value);
+ break;
+
+ default:
+ break;
+
+ } // end switch
+
+};
diff --git a/examples/menubar/menubar-editor.html b/examples/menubar/menubar-editor.html
new file mode 100644
index 0000000000..8414e8efc8
--- /dev/null
+++ b/examples/menubar/menubar-editor.html
@@ -0,0 +1,822 @@
+
+
+
+
+ Editor Menubar Example | WAI-ARIA Authoring Practices 1.2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Editor Menubar Example
+
+ The following example demonstrates using the
+ menubar design pattern
+ to provide access to sets of actions.
+ Each item in the below menubar identifies a category of text formatting actions that can be executed from its submenu.
+ The submenus also demonstrate menuitemradio and menuitemcheckbox elements.
+
+ Similar examples include:
+
+
+
+
+
+ Accessibility Features
+
+ Users of assistive technologies can identify which format settings are selected because they are represented by menu item radio and menu item checkbox elements that have a checked state.
+ Disabled menu items are demonstrated in the font size menu, which includes two disabled menuitems.
+ To help communicate that the arrow keys are available for directional navigation within the menubar and its submenus, a border is added to the menubar container when focus is within the menubar.
+ To support operating system high contrast settings, focus is highlighted by adding and removing a border around the menu item with focus.
+ The down arrow and checked icons are made compatible with operating system high contrast settings and hidden from screen readers by using the CSS content property to render images.
+
+ Like desktop menubars, submenus open on mouse hover over a parent item in the menubar only if another submenu is already open.
+ That is, if all submenus are closed, a click on a parent menu item is required to display a submenu.
+ Minimizing automatic popups makes exploring with a screen magnifier easier.
+
+
+ In general, moving focus in response to mouse hover is avoided in accessible widgets; it causes unexpected context changes for keyboard users.
+ However, like desktop menubars, there are two conditions in this example menubar where focus moves in response to hover in order to help maintain context for users who use both keyboard and mouse:
+
+ After a parent menu item in the menubar has been activated and the user hovers over a different parent item in the menubar, focus will follow hover.
+ When a submenu is open and the user hovers over an item in the submenu, focus follows hover.
+
+
+
+
+
+
+ Keyboard Support
+ Menubar
+
+
+
+ Key
+ Function
+
+
+
+
+
+ Space Enter
+
+ Opens submenu and moves focus to first item in the submenu.
+
+
+
+ Escape
+
+
+ If a submenu is open, closes it. Otherwise, does nothing.
+
+
+
+
+ Right Arrow
+
+
+
+ Moves focus to the next item in the menubar.
+ If focus is on the last item, moves focus to the first item.
+
+
+
+
+
+ Left Arrow
+
+
+
+ Moves focus to the previous item in the menubar.
+ If focus is on the first item, moves focus to the last item.
+
+
+
+
+
+ Down Arrow
+
+ Opens submenu and moves focus to first item in the submenu.
+
+
+
+ Up Arrow
+
+ Opens submenu and moves focus to last item in the submenu.
+
+
+
+ Home
+
+ Moves focus to first item in the menubar.
+
+
+
+ End
+
+ Moves focus to last item in the menubar.
+
+
+
+ Character
+
+
+
+ Moves focus to next item in the menubar having a name that starts with the typed character.
+ If none of the items have a name starting with the typed character, focus does not move.
+
+
+
+
+
+
+ Submenu
+
+
+
+ Key
+ Function
+
+
+
+
+
+ Space Enter
+
+ Activates menu item, causing action to be executed, e.g., bold text, change font.
+
+
+
+ Escape
+
+
+
+ Closes submenu.
+ Moves focus to parent menubar item.
+
+
+
+
+
+ Right Arrow
+
+
+
+ Closes submenu.
+ Moves focus to next item in the menubar.
+ Opens submenu of newly focused menubar item, keeping focus on that parent menubar item.
+
+
+
+
+
+ Left Arrow
+
+
+
+ Closes submenu.
+ Moves focus to previous item in the menubar.
+ Opens submenu of newly focused menubar item, keeping focus on that parent menubar item.
+
+
+
+
+
+ Down Arrow
+
+
+
+ Moves focus to the next item in the submenu.
+ If focus is on the last item, moves focus to the first item.
+
+
+
+
+
+ Up Arrow
+
+
+
+ Moves focus to previous item in the submenu.
+ If focus is on the first item, moves focus to the last item.
+
+
+
+
+
+ Home
+
+ Moves focus to the first item in the submenu.
+
+
+
+ End
+
+ Moves focus to the last item in the submenu.
+
+
+
+ Character
+
+
+
+ Moves focus to the next item having a name that starts with the typed character.
+ If none of the items have a name starting with the typed character, focus does not move.
+
+
+
+
+
+
+
+
+ Role, Property, State, and Tabindex Attributes
+ Menubar
+
+
+
+ Role
+ Attribute
+ Element
+ Usage
+
+
+
+
+
+ menubar
+
+
+
+ ul
+
+
+
+ Identifies the element as a menubar container for a set of menuitem elements.
+ Is not focusable because focus is managed using roving tabindex.
+
+
+
+
+
+
+ aria-label="string "
+
+
+
+ ul
+
+
+
+
+ Defines an accessible name for the menubar.
+
+ Helps assistive technology users understand the purpose of the menubar and
+ distinguish it from any other menubars or similar elements on the page.
+
+
+
+
+
+ menuitem
+
+
+
+ span
+
+
+
+ Identifies the element as a menu item within the menubar.
+ Accessible name comes from the text content.
+
+
+
+
+
+
+ tabindex="-1"
+
+
+ span
+
+
+ Makes the menuitem element keyboard focusable but
+ not part of the Tab sequence of the page.
+
+
+
+
+
+ tabindex="0"
+
+
+ span
+
+
+
+
+ Makes the menuitem element keyboard focusable and
+ part of the tab sequence of the page.
+
+
+ Only one menuitem in the menubar has tabindex="0".
+
+
+ When the page loads, the first item in the menubar has tabindex="0".
+
+
+ Focus is managed using roving tabindex .
+
+
+
+
+
+
+
+ aria-haspopup="true"
+
+
+ span
+
+
+ Indicates that the menuitem has a submenu.
+
+
+
+
+
+ aria-expanded="true"
+
+
+ span
+
+ Indicates the menu is open.
+
+
+
+
+ aria-expanded="false"
+
+
+ span
+
+ Indicates the submenu is closed.
+
+
+
+ Submenu
+
+
+
+ Role
+ Attribute
+ Element
+ Usage
+
+
+
+
+
+ menu
+
+
+
+ ul
+
+
+
+ Identifies the element as a menu container for a set of menu items.
+ Is not focusable because focus is managed using roving tabindex.
+
+
+
+
+
+
+ aria-label="string "
+
+
+ ul
+
+
+ Defines an accessible name for the menu.
+
+
+
+
+ menuitem
+
+
+
+ li
+
+
+
+ Identifies the element as an item in the submenu.
+ Accessible name comes from the text content.
+
+
+
+
+
+
+ tabindex="-1"
+
+
+ li
+
+
+ Makes the item focusable but not part of the page tab sequence.
+
+
+
+
+
+ aria-disabled="false"
+
+
+ li
+
+
+ Used on the font size "Smaller" and "Larger" options to indicate they are active.
+
+
+
+
+
+ aria-disabled="true"
+
+
+ li
+
+
+ Used on the font size "Smaller" and "Larger" options to indicate one of the options is not active because the largest or smallest font has been selected.
+
+
+
+
+ menuitemcheckbox
+
+
+
+ li
+
+
+
+ Identifies the element as a menuitemcheckbox.
+ Accessible name comes from the text content.
+
+
+
+
+
+
+ tabindex="-1"
+
+
+ li
+
+
+ Makes the menuitemcheckbox focusable but not part of the page tab sequence.
+
+
+
+
+
+ aria-checked="true"
+
+
+ li
+
+
+
+
+ Indicates that the menuitemcheckbox is checked.
+
+
+ The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors.
+
+
+
+
+
+
+
+ aria-checked="false"
+
+
+ li
+
+
+
+
+ Indicates that the menuitemcheckbox is NOT checked.
+
+
+ The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors.
+
+
+
+
+
+
+ separator
+
+
+
+ li
+
+
+
+ Identifies the element as a visual separator between groups of items within a menu, such as groups of menuitemradio or menuitemcheckbox elements.
+ Is not focusable but may be perceivable by a screen reader user when using a reading cursor that does not depend on focus.
+
+
+
+
+
+ group
+
+
+
+ ul
+
+
+
+
+ Identifies the element as a container for a set of menuitemradio elements.
+
+
+ Enables browsers to compute values of aria-setsize and aria-posinset.
+
+
+
+
+
+
+
+ aria-label="string"
+
+
+ ul
+
+
+ Provides an accessible name for the group of menu items.
+
+
+
+
+ menuitemradio
+
+
+
+ li
+
+
+
+
+ Identifies the element as a menuitemradio element.
+
+
+ When all items in a submenu are members of the same radio group,
+ the group is defined by the menu element; a group element is not necessary.
+
+
+ Accessible name is computed from the text content.
+
+
+
+
+
+
+
+ tabindex="-1"
+
+
+ li
+
+
+ Makes the menuitemradio focusable but not part of the page tab sequence.
+
+
+
+
+
+ aria-checked="true"
+
+
+ li
+
+
+
+
+ Indicates the menuitemradio is checked.
+
+
+ The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors.
+
+
+
+
+
+
+
+ aria-checked="false"
+
+
+ li
+
+
+
+
+ Indicates that the menuitemradio is NOT checked.
+
+
+ The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors.
+
+
+
+
+
+
+ Textarea
+
+
+
+ Role
+ Attribute
+ Element
+ Usage
+
+
+
+
+
+
+ aria-label="string "
+
+
+ textarea
+
+
+ Defines an accessible name for the textarea.
+
+
+
+
+
+
+
+ Javascript and CSS Source Code
+
+
+
+
+ HTML Source Code
+
+
+
+
+
+
+
+
+
+ Menu or Menubar Design Pattern in WAI-ARIA Authoring Practices 1.2
+
+
+
+
diff --git a/test/tests/menubar_menubar-editor.js b/test/tests/menubar_menubar-editor.js
new file mode 100644
index 0000000000..95d94a13c3
--- /dev/null
+++ b/test/tests/menubar_menubar-editor.js
@@ -0,0 +1,1032 @@
+'use strict';
+
+const { ariaTest } = require('..');
+const { By, Key } = require('selenium-webdriver');
+const assertAttributeValues = require('../util/assertAttributeValues');
+const assertAriaLabelExists = require('../util/assertAriaLabelExists');
+const assertAriaRoles = require('../util/assertAriaRoles');
+const assertRovingTabindex = require('../util/assertRovingTabindex');
+
+const exampleFile = 'menubar/menubar-editor.html';
+
+const ex = {
+ textareaSelector: '#ex1 textarea',
+ menubarSelector: '#ex1 [role="menubar"]',
+ menubarMenuitemSelector: '#ex1 span[role="menuitem"]',
+ submenuMenuitemSelector: '#ex1 li[role="menuitem"]',
+ submenuSelector: '#ex1 [role="menu"]',
+ menuitemcheckboxSelector: '#ex1 [role="menuitemcheckbox"]',
+ groupSelector: '#ex1 [role="group"]',
+ menuitemradioSelector: '#ex1 [role="menuitemradio"]',
+ numMenus: 4,
+ numSubmenuItems: [4, 10, 4, 7],
+ allSubmenuItems: [
+ '#ex1 [role="menu"][aria-label="Font"] li[role="menuitemradio"]',
+ '#ex1 [role="menu"][aria-label="Style/Color"] li[role="menuitemradio"],#ex1 [role="menu"][aria-label="Style/Color"] li[role="menuitemcheckbox"]',
+ '#ex1 [role="menu"][aria-label="Text Align"] li[role="menuitemradio"]',
+ '#ex1 [role="menu"][aria-label="Size"] li[role="menuitem"],#ex1 [role="menu"][aria-label="Size"] li[role="menuitemradio"]'
+ ],
+ radioItemGroupings: [
+ {
+ menuIndex: 0,
+ itemsSelector: '#ex1 [role="menu"][aria-label="Font"] [role="menuitemradio"]',
+ defaultSelectedIndex: 0
+ },
+ {
+ menuIndex: 1,
+ itemsSelector: '#ex1 [role="group"][aria-label="Text Color"] [role="menuitemradio"]',
+ defaultSelectedIndex: 0
+ },
+ {
+ menuIndex: 1,
+ itemsSelector: '#ex1 [role="group"][aria-label="Text Decoration"] [role="menuitemradio"]',
+ defaultSelectedIndex: 0
+ },
+ {
+ menuIndex: 2,
+ itemsSelector: '#ex1 [role="menu"][aria-label="Text Align"] [role="menuitemradio"]',
+ defaultSelectedIndex: 0
+ },
+ {
+ menuIndex: 3,
+ itemsSelector: '#ex1 [role="group"][aria-label="Font Sizes"] [role="menuitemradio"]',
+ defaultSelectedIndex: 2
+ }
+ ]
+};
+
+const exampleInitialized = async function (t) {
+ const initializedSelector = ex.menubarMenuitemSelector + '[tabindex="0"]';
+
+ await t.context.session.wait(async function () {
+ const els = await t.context.queryElements(t, initializedSelector);
+ return els.length === 1;
+ }, t.context.waitTime, 'Timeout waiting for example to initialize');
+};
+
+const checkmarkVisible = async function (t, selector, index) {
+ return await t.context.session.executeScript(function () {
+ const [selector, index] = arguments;
+ const checkmarkContent = window.getComputedStyle(
+ document.querySelectorAll(selector)[index], ':before'
+ ).getPropertyValue('content');
+ if (checkmarkContent == 'none') {
+ return false;
+ }
+ return true;
+ }, selector, index);
+};
+
+const checkFocus = async function (t, selector, index) {
+ return await t.context.session.executeScript(function () {
+ const [selector, index] = arguments;
+ const items = document.querySelectorAll(selector);
+ return items[index] === document.activeElement;
+ }, selector, index);
+};
+
+// Attributes
+
+ariaTest('Test aria-label on textarea', exampleFile, 'textarea-aria-label', async (t) => {
+ t.plan(1);
+ await assertAriaLabelExists(t, ex.textareaSelector);
+});
+
+ariaTest('Test for role="menubar" on ul', exampleFile, 'menubar-role', async (t) => {
+
+
+ const menubars = await t.context.queryElements(t, ex.menubarSelector);
+
+ t.is(
+ menubars.length,
+ 1,
+ 'One "role=menubar" element should be found by selector: ' + ex.menubarSelector
+ );
+
+ t.is(
+ await menubars[0].getTagName(),
+ 'ul',
+ '"role=menubar" should be found on a "ul"'
+ );
+});
+
+ariaTest('Test aria-label on menubar', exampleFile, 'menubar-aria-label', async (t) => {
+ await assertAriaLabelExists(t, ex.menubarSelector);
+});
+
+ariaTest('Test for role="menuitem" on li', exampleFile, 'menubar-menuitem-role', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ t.is(
+ menuitems.length,
+ 4,
+ '"role=menuitem" elements should be found by selector: ' + ex.menubarMenuitemSelector
+ );
+
+ for (let menuitem of menuitems) {
+ t.truthy(
+ await menuitem.getText(),
+ '"role=menuitem" elements should all have accessible text content: ' + ex.menubarMenuitemSelector
+ );
+ }
+});
+
+ariaTest('Test roving tabindex', exampleFile, 'menubar-menuitem-tabindex', async (t) => {
+
+
+ // Wait for roving tabindex to be initialized by the javascript
+ await exampleInitialized(t);
+
+ await assertRovingTabindex(t, ex.menubarMenuitemSelector, Key.ARROW_RIGHT);
+});
+
+ariaTest('Test aria-haspopup set to true on menuitems',
+ exampleFile, 'menubar-menuitem-aria-haspopup', async (t) => {
+
+
+ await assertAttributeValues(t, ex.menubarMenuitemSelector, 'aria-haspopup', 'true');
+ });
+
+ariaTest('"aria-expanded" attribute on menubar menuitem', exampleFile, 'menubar-menuitem-aria-expanded', async (t) => {
+
+
+ // Before interating with page, make sure aria-expanded is set to false
+ await assertAttributeValues(t, ex.menubarMenuitemSelector, 'aria-expanded', 'false');
+
+ // AND make sure no submenus are visible
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+ for (let submenu of submenus) {
+ t.false(
+ await submenu.isDisplayed(),
+ 'No submenus (found by selector: ' + ex.submenuSelector + ') should be displayed on load'
+ );
+ }
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let menuIndex = 0; menuIndex < menuitems.length; menuIndex++) {
+
+ // Send ARROW_DOWN to open submenu
+ await menuitems[menuIndex].sendKeys(Key.ARROW_DOWN);
+
+ for (let item = 0; item < menuitems.length; item++) {
+
+ // Test attribute "aria-expanded" is only set for the opened submenu
+ const displayed = menuIndex === item ? true : false;
+ t.is(
+ await menuitems[item].getAttribute('aria-expanded'),
+ displayed.toString(),
+ 'focus is on element ' + menuIndex + ' of elements "' + ex.menubarMenuitemSelector +
+ '", therefore "aria-expanded" on menuitem ' + item + ' should be ' + displayed
+ );
+
+ // Test the submenu is indeed displayed
+ t.is(
+ await submenus[item].isDisplayed(),
+ displayed,
+ 'focus is on element ' + menuIndex + ' of elements "' + ex.menubarMenuitemSelector +
+ '", therefore isDisplay of submenu ' + item + ' should return ' + displayed
+ );
+ }
+
+ // Send the ESCAPE to close submenu
+ await menuitems[menuIndex].sendKeys(Key.ESCAPE);
+ }
+
+});
+
+
+ariaTest('Test for role="menu" on ul', exampleFile, 'menu-role', async (t) => {
+
+
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+
+ // Test elements with role="menu" exist
+ t.is(
+ submenus.length,
+ 4,
+ 'Four role="menu" elements should be found by selector: ' + ex.submenuSelector
+ );
+
+ // Test the role="menu" elements are all on "ul" items
+ for (let submenu of submenus) {
+ t.is(
+ await submenu.getTagName(),
+ 'ul',
+ '"role=menu" should be found on a "ul"'
+ );
+ }
+
+ await assertAttributeValues(t, ex.submenuSelector, 'tabindex', '-1');
+});
+
+ariaTest('Test for aria-label on role="menu"', exampleFile, 'menu-aria-label', async (t) => {
+
+ const submenusSelectors = [
+ '#ex1 li:nth-of-type(1) [role="menu"]',
+ '#ex1 li:nth-of-type(2) [role="menu"]',
+ '#ex1 li:nth-of-type(3) [role="menu"]',
+ '#ex1 li:nth-of-type(4) [role="menu"]'
+ ];
+
+ for (let submenuSelector of submenusSelectors) {
+ await assertAriaLabelExists(t, submenuSelector);
+ }
+});
+
+ariaTest('Test for submenu menuitems with accessible names', exampleFile, 'submenu-menuitem-role', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.submenuMenuitemSelector);
+
+ t.truthy(
+ menuitems.length,
+ '"role=menuitem" elements should be found by selector: ' + ex.submenuMenuitemSelector
+ );
+
+ // Test the accessible name of each menuitem
+
+ for (let menuitem of menuitems) {
+
+ // The menuitem is not visible, so we cannot use selenium's "getText" function
+ const menutext = await t.context.session.executeScript(function () {
+ const el = arguments[0];
+ return el.innerHTML;
+ }, menuitem);
+
+ t.truthy(
+ menutext,
+ '"role=menuitem" elements should all have accessible text content: ' + ex.submenuMenuitemSelector
+ );
+ }
+
+});
+
+ariaTest('Test tabindex="-1" for all submenu role="menuitem"s',
+ exampleFile, 'submenu-menuitem-tabindex', async (t) => {
+
+ await assertAttributeValues(t, ex.submenuMenuitemSelector, 'tabindex', '-1');
+ });
+
+ariaTest('Test aria-disabled="false" for all submenu role="menuitem"s',
+ exampleFile, 'submenu-menuitem-aria-disabled', async (t) => {
+
+ // "aria-disable" should be set to false by default
+ await assertAttributeValues(t, ex.submenuMenuitemSelector, 'aria-disabled', 'false');
+
+ const menus = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const sizeMenu = await t.context.session.findElement(By.css('[aria-label="Size"]'));
+ const menuitems = await t.context.queryElements(t, '[role="menuitem"]', sizeMenu);
+ const menuItemRadios = await t.context.queryElements(t, '[role="menuitemradio"]', sizeMenu);
+
+ // select X-Small size
+ await menus[3].sendKeys(Key.ARROW_DOWN);
+ await menuItemRadios[0].sendKeys(Key.ENTER);
+
+ const disabledFirstItem = await menuitems[0].getAttribute('aria-disabled');
+
+ // Test that the item was successfully disabled
+ t.true(
+ disabledFirstItem === 'true',
+ 'The first menuitem in the last dropdown should become disabled after X-Small is selected'
+ );
+
+ // Select the X-Large size
+ await menus[3].sendKeys(Key.ARROW_DOWN);
+ await menuItemRadios[menuItemRadios.length - 1].sendKeys(Key.ENTER);
+
+ const disabledSecondItem = await menuitems[1].getAttribute('aria-disabled');
+
+ // Test that the item was successfully disabled
+ t.true(
+ disabledSecondItem === 'true',
+ 'The second menuitem in the last dropdown should become disabled after X-Large is selected'
+ );
+
+
+ });
+
+ariaTest('Test for role="menuitemcheckbox" on li', exampleFile, 'menuitemcheckbox-role', async (t) => {
+
+ const checkboxes = await t.context.queryElements(t, ex.menuitemcheckboxSelector);
+
+ t.truthy(
+ checkboxes.length,
+ '"role=menuitemcheckbox" elements should be found by selector: ' + ex.menuitemcheckboxSelector
+ );
+
+ // Test the accessible name of each menuitem
+
+ for (let checkbox of checkboxes) {
+
+ // The menuitem is not visible, so we cannot use selenium's "getText" function
+ const text = await t.context.session.executeScript(function () {
+ const el = arguments[0];
+ return el.innerHTML;
+ }, checkbox);
+
+ t.truthy(
+ text,
+ '"role=menuitemcheckbox" elements should all have accessible text content: ' + ex.menuitemcheckboxSelector
+ );
+ }
+});
+
+ariaTest('Test tabindex="-1" for role="menuitemcheckbox"', exampleFile, 'menuitemcheckbox-tabindex', async (t) => {
+ await assertAttributeValues(t, ex.menuitemcheckboxSelector, 'tabindex', '-1');
+});
+
+ariaTest('Test "aria-checked" attirbute on role="menuitemcheckbox"',
+ exampleFile, 'menuitemcheckbox-aria-checked', async (t) => {
+
+ const menus = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ // Reveal the menuitemcheckbox elements in the second dropdown
+ await menus[1].sendKeys(Key.ARROW_DOWN);
+
+ // Confirm aria-checked is set to false by default
+ await assertAttributeValues(t, ex.menuitemcheckboxSelector, 'aria-checked', 'false');
+
+ // And corrospondingly, neither item should have a visible checkmark
+ for (let checkIndex = 0; checkIndex < 2; checkIndex++) {
+ const checkmark = await checkmarkVisible(t, ex.menuitemcheckboxSelector, checkIndex);
+ t.false(
+ checkmark,
+ 'All menuitemcheckbox items should not have checkmark prepended'
+ );
+ }
+
+ // Select both menuitems
+ const checkboxes = await t.context.queryElements(t, ex.menuitemcheckboxSelector);
+ await checkboxes[0].sendKeys(Key.ENTER);
+ await menus[1].sendKeys(Key.ARROW_DOWN);
+ await checkboxes[1].sendKeys(Key.ENTER);
+ await menus[1].sendKeys(Key.ARROW_DOWN);
+
+
+ // Confirm aria-checked is set to true
+ await assertAttributeValues(t, ex.menuitemcheckboxSelector, 'aria-checked', 'true');
+
+ // And corrospondingly, both items should have a visible checkmark
+ for (let checkIndex = 0; checkIndex < 2; checkIndex++) {
+ const checkmark = await checkmarkVisible(t, ex.menuitemcheckboxSelector, checkIndex);
+ t.true(
+ checkmark,
+ 'All menuitemcheckbox items should have checkmark prepended'
+ );
+ }
+ });
+
+
+ariaTest('Test role="separator" exists', exampleFile, 'separator-role', async (t) => {
+ await assertAriaRoles(t, 'ex1', 'separator', 3, 'li');
+});
+
+ariaTest('Test role="group" exists', exampleFile, 'group-role', async (t) => {
+ await assertAriaRoles(t, 'ex1', 'group', 4, 'ul');
+});
+
+ariaTest('Test aria-label on group', exampleFile, 'group-aria-label', async (t) => {
+ await assertAriaLabelExists(t, ex.groupSelector);
+});
+
+ariaTest('Test role="menuitemradio" exists with accessible name',
+ exampleFile, 'menuitemradio-role', async (t) => {
+
+ const items = await t.context.queryElements(t, ex.menuitemradioSelector);
+
+ // Test that the elements exist
+ t.truthy(
+ items.length,
+ '"role=menuitemradio" elements should be found by selector: ' + ex.menuitemradioSelector
+ );
+
+ // Test for accessible name
+
+ for (let item of items) {
+
+ // The menuitem is not visible, so we cannot use selenium's "getText" function
+ const text = await t.context.session.executeScript(function () {
+ const el = arguments[0];
+ return el.innerHTML;
+ }, item);
+
+ t.truthy(
+ text,
+ '"role=menuitemradio" elements should all have accessible text content: ' + ex.menuitemradio
+ );
+ }
+ });
+
+ariaTest('Test tabindex="-1" on role="menuitemradio"',
+ exampleFile, 'menuitemradio-tabindex', async (t) => {
+ await assertAttributeValues(t, ex.menuitemradioSelector, 'tabindex', '-1');
+ });
+
+ariaTest('Text "aria-checked" appropriately set on role="menitemradio"',
+ exampleFile, 'menuitemradio-aria-checked', async (t) => {
+
+ const menus = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let grouping of ex.radioItemGroupings) {
+
+ // Reveal the elements in the dropdown
+ await menus[grouping.menuIndex].sendKeys(Key.ARROW_DOWN);
+
+ const items = await t.context.queryElements(t, grouping.itemsSelector);
+
+ // Test for the initial state of checked/not checked for all radio menuitems
+
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
+
+ const selected = itemIndex === grouping.defaultSelectedIndex ? true : false;
+
+ t.is(
+ await items[itemIndex].getAttribute('aria-checked'),
+ selected.toString(),
+ 'Only item ' + grouping.defaultSelectedIndex + ' should have aria-select="true" in menu dropdown items: ' + grouping.itemsSelector
+ );
+
+ const checkmark = await checkmarkVisible(t, grouping.itemsSelector, itemIndex);
+ t.is(
+ checkmark,
+ selected,
+ 'Only item ' + grouping.defaultSelectedIndex + ' should be selected in menu dropdown items: ' + grouping.itemsSelector
+ );
+ }
+ }
+ });
+
+// KEYS
+
+ariaTest('Key ENTER open submenu', exampleFile, 'menubar-key-space-and-enter', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the ENTER key
+ await menuitems[menuIndex].sendKeys(Key.ENTER);
+
+ // Test that the submenu is displayed
+ t.true(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ENTER" to menuitem ' + menuIndex + ' in menubar should display submenu'
+ );
+
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], 0),
+ 'Sending key "ENTER" to menuitem ' + menuIndex + ' in menubar should send focus to the first element in the submenu'
+ );
+ }
+});
+
+ariaTest('Key SPACE open submenu', exampleFile, 'menubar-key-space-and-enter', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the SPACE key
+ await menuitems[menuIndex].sendKeys(' ');
+
+ // Test that the submenu is displayed
+ t.true(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "SPACE" to menuitem ' + menuIndex + ' in menubar should display submenu'
+ );
+
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], 0),
+ 'Sending key "SPACE" to menuitem ' + menuIndex + ' in menubar should send focus to the first element in the submenu'
+ );
+
+ }
+});
+
+
+ariaTest('Key ESCAPE closes menubar', exampleFile, 'menubar-key-escape', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the ENTER key, then the ESCAPE
+ await menuitems[menuIndex].sendKeys(Key.ENTER, Key.ESCAPE);
+
+ // Test that the submenu is not displayed
+ t.false(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ESCAPE" to menuitem ' + menuIndex + ' in menubar should close the open submenu'
+ );
+ }
+
+});
+
+
+ariaTest('Key ARROW_RIGHT moves focus to next menubar item',
+ exampleFile, 'menubar-key-right-arrow', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus + 1; menuIndex++) {
+
+ const currentIndex = menuIndex % ex.numMenus;
+ const nextIndex = (menuIndex + 1) % ex.numMenus;
+
+ // Send the ARROW_RIGHT key
+ await menuitems[currentIndex].sendKeys(Key.ARROW_RIGHT);
+
+ // Test the focus is on the next item mod the number of items to account for wrapping
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, nextIndex),
+ 'Sending key "ARROW_RIGHT" to menuitem ' + currentIndex + ' should move focus to menuitem ' + nextIndex
+ );
+ }
+ });
+
+ariaTest('Key ARROW_RIGHT moves focus to next menubar item',
+ exampleFile, 'menubar-key-left-arrow', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ // Send the ARROW_LEFT key to the first menuitem
+ await menuitems[0].sendKeys(Key.ARROW_LEFT);
+
+ // Test the focus is on the last menu item
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, ex.numMenus - 1),
+ 'Sending key "ARROW_LEFT" to menuitem 0 will change focus to menu item 3'
+ );
+
+
+ for (let menuIndex = ex.numMenus - 1; menuIndex > 0; menuIndex--) {
+
+ // Send the ARROW_LEFT key
+ await menuitems[menuIndex].sendKeys(Key.ARROW_LEFT);
+
+ // Test the focus is on the previous menuitem
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, menuIndex - 1),
+ 'Sending key "ARROW_RIGHT" to menuitem ' + menuIndex + ' should move focus to menuitem ' + (menuIndex - 1)
+ );
+ }
+ });
+
+ariaTest('Key ARROW_UP opens submenu, focus on last item',
+ exampleFile, 'menubar-key-up-arrow', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the ENTER key
+ await menuitems[menuIndex].sendKeys(Key.UP);
+
+ // Test that the submenu is displayed
+ t.true(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ENTER" to menuitem ' + menuIndex + ' in menubar should display submenu'
+ );
+
+ const numSubItems = (await t.context.queryElements(t, ex.allSubmenuItems[menuIndex])).length;
+
+ // Test that the focus is on the last item in the list
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], numSubItems - 1),
+ 'Sending key "ENTER" to menuitem ' + menuIndex + ' in menubar should send focus to the first element in the submenu'
+ );
+ }
+ });
+
+ariaTest('Key ARROW_DOWN opens submenu, focus on first item',
+ exampleFile, 'menubar-key-down-arrow', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the ENTER key
+ await menuitems[menuIndex].sendKeys(Key.DOWN);
+
+ // Test that the submenu is displayed
+ t.true(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ENTER" to menuitem ' + menuIndex + ' in menubar should display submenu'
+ );
+
+ // Test that the focus is on the first item in the list
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], 0),
+ 'Sending key "ENTER" to menuitem ' + menuIndex + ' in menubar should send focus to the first element in the submenu'
+ );
+ }
+ });
+
+ariaTest('Key HOME goes to first item in menubar', exampleFile, 'menubar-key-home', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the ARROW_RIGHT key to move the focus to later menu item for every test
+ for (let i = 0; i < menuIndex; i++) {
+ await menuitems[i].sendKeys(Key.ARROW_RIGHT);
+ }
+
+ // Send the key HOME
+ await menuitems[menuIndex].sendKeys(Key.HOME);
+
+ // Test that the focus is on the first item in the list
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, 0),
+ 'Sending key "HOME" to menuitem ' + menuIndex + ' in menubar should move the foucs to the first menuitem'
+ );
+ }
+});
+
+ariaTest('Key END goes to last item in menubar', exampleFile, 'menubar-key-end', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Send the ARROW_RIGHT key to move the focus to later menu item for every test
+ for (let i = 0; i < menuIndex; i++) {
+ await menuitems[i].sendKeys(Key.ARROW_RIGHT);
+ }
+
+ // Send the key END
+ await menuitems[menuIndex].sendKeys(Key.END);
+
+ // Test that the focus is on the last item in the list
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, ex.numMenus - 1),
+ 'Sending key "END" to menuitem ' + menuIndex + ' in menubar should move the foucs to the last menuitem'
+ );
+ }
+});
+
+ariaTest('Character sends to menubar changes focus in menubar',
+ exampleFile, 'menubar-key-character', async (t) => {
+
+
+ const charIndexTest = [
+ { sendChar: 'f', sendIndex: 0, endIndex: 0 },
+ { sendChar: 's', sendIndex: 0, endIndex: 1 },
+ { sendChar: 't', sendIndex: 0, endIndex: 2 },
+ { sendChar: 'f', sendIndex: 1, endIndex: 0 },
+ { sendChar: 's', sendIndex: 1, endIndex: 3 },
+ { sendChar: 'z', sendIndex: 0, endIndex: 0 },
+ { sendChar: 'z', sendIndex: 3, endIndex: 3 }
+ ];
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ for (let test of charIndexTest) {
+
+ // Send character to menuitem
+ await menuitems[test.sendIndex].sendKeys(test.sendChar);
+
+ // Test that the focus switches to the appropriate menuitem
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, test.endIndex),
+ 'Sending character ' + test.sendChar + ' to menuitem ' + test.sendIndex + ' in menubar should move the focus to menuitem ' + test.endIndex
+ );
+ }
+ });
+
+ariaTest('ENTER in submenu selects item', exampleFile, 'submenu-enter', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ENTER);
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+
+ // Get the current style attribute on the "Text Sample"
+ const originalStyle = await t.context.session
+ .findElement(By.css(ex.textareaSelector))
+ .getAttribute('style');
+
+ // send ENTER to the item
+ await item.sendKeys(Key.ENTER);
+
+ // Test that the submenu is closed
+ t.false(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ENTER" to submenuitem "' + itemText + '" should close list'
+ );
+
+ // Test that the focus is back on the menuitem in the menubar
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, menuIndex),
+ 'Sending key "ENTER" to submenuitem "' + itemText + '" should change the focus to menuitem ' + menuIndex + ' in the menubar'
+ );
+
+ let changedStyle = true;
+ if (itemIndex === 0 && menuIndex === 0) {
+ // Only when selecting the first (selected by default) font option will the style not change.
+ changedStyle = false;
+ }
+
+ // Get the current style attribute on the "Text Sample"
+ const currentStyle = await t.context.session
+ .findElement(By.css(ex.textareaSelector))
+ .getAttribute('style');
+
+ t.is(
+ currentStyle != originalStyle,
+ changedStyle,
+ 'Sending key "ENTER" to submenuitem "' + itemText + '" should change the style attribute on the Text Sampe.'
+ );
+ }
+ }
+
+});
+
+ariaTest('ESCAPE to submenu closes submenu', exampleFile, 'submenu-escape', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ENTER);
+
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+
+ // send escape to the item
+ await item.sendKeys(Key.ESCAPE);
+
+ // make sure focus is on the menuitem and the popup is submenu is closed
+ t.false(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ESCAPE" to submenuitem "' + itemText + '" should close list'
+ );
+
+ // Test that the focus is back on the menuitem in the menubar
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, menuIndex),
+ 'Sending key "ESCAPE" to submenuitem "' + itemText + '" should change the focus to menuitem ' + menuIndex + ' in the menubar'
+ );
+ }
+ }
+
+});
+
+ariaTest('ARROW_RIGHT to submenu closes submenu and opens next',
+ exampleFile, 'submenu-right-arrow', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ENTER);
+
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+ const nextMenuIndex = (menuIndex + 1) % ex.numMenus;
+
+ // send RIGHT to the item
+ await item.sendKeys(Key.ARROW_RIGHT);
+
+ // Test that the submenu is closed
+ t.false(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ARROW_RIGHT" to submenuitem "' + itemText + '" should close list'
+ );
+
+ // Test that the next submenu is open
+ t.true(
+ await submenus[nextMenuIndex].isDisplayed(),
+ 'Sending key "ARROW_RIGHT" to submenuitem "' + itemText + '" should open submenu ' + nextMenuIndex
+ );
+
+ // Test that the focus is on the menuitem in the menubar
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, nextMenuIndex),
+ 'Sending key "ARROW_RIGHT" to submenuitem "' + itemText + '" should send focus to menuitem' + nextMenuIndex + ' in the menubar'
+ );
+ }
+ }
+ });
+
+ariaTest('ARROW_RIGHT to submenu closes submenu and opens next',
+ exampleFile, 'submenu-left-arrow', async (t) => {
+
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ const submenus = await t.context.queryElements(t, ex.submenuSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ENTER);
+
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+ // Account for wrapping (index 0 should go to 3)
+ const nextMenuIndex = menuIndex === 0 ? 3 : menuIndex - 1;
+
+ // send LEFT to the item
+ await item.sendKeys(Key.ARROW_LEFT);
+
+ // Test that the submenu is closed
+ t.false(
+ await submenus[menuIndex].isDisplayed(),
+ 'Sending key "ARROW_LEFT" to submenuitem "' + itemText + '" should close list'
+ );
+
+ // Test that the next submenu is open
+ t.true(
+ await submenus[nextMenuIndex].isDisplayed(),
+ 'Sending key "ARROW_LEFT" to submenuitem "' + itemText + '" should open submenu ' + nextMenuIndex
+ );
+
+ // Test that the focus is on the menuitem in the menubar
+ t.true(
+ await checkFocus(t, ex.menubarMenuitemSelector, nextMenuIndex),
+ 'Sending key "ARROW_LEFT" to submenuitem "' + itemText + '" should send focus to menuitem' + nextMenuIndex + ' in the menubar'
+ );
+ }
+ }
+ });
+
+ariaTest('ARROW_DOWN moves focus to next item', exampleFile, 'submenu-down-arrow', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ARROW_DOWN);
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+ const nextItemIndex = (itemIndex + 1) % ex.numSubmenuItems[menuIndex];
+
+ // send DOWN to the item
+ await item.sendKeys(Key.ARROW_DOWN);
+
+ // Test that the focus is on the next item
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], nextItemIndex),
+ 'Sending key "ARROW_DOWN" to submenu item "' + itemText + '" should send focus to next submenu item.'
+ );
+ }
+ }
+});
+
+ariaTest('ARROW_DOWN moves focus to previous item', exampleFile, 'submenu-up-arrow', async (t) => {
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ARROW_DOWN);
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+ // Account for wrapping
+ const nextItemIndex = itemIndex === 0 ? ex.numSubmenuItems[menuIndex] - 1 : itemIndex - 1;
+
+ // Send UP to the item
+ await item.sendKeys(Key.ARROW_UP);
+
+ // Test that the focus is on the previous item
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], nextItemIndex),
+ 'Sending key "ARROW_UP" to submenu item "' + itemText + '" should send focus to next submenu item.'
+ );
+ }
+ }
+});
+
+ariaTest('HOME moves focus to first item', exampleFile, 'submenu-home', async (t) => {
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ARROW_DOWN);
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+ // Account for wrapping
+ const nextItemIndex = itemIndex + 1;
+
+ // Send UP to the item
+ await item.sendKeys(Key.HOME);
+
+ // Test that the focus is on the first item
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], 0),
+ 'Sending key "HOME" to submenu item "' + itemText + '" should send focus to first submenu item.'
+ );
+ }
+ }
+});
+
+ariaTest('END moves focus to last item', exampleFile, 'submenu-end', async (t) => {
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Open the submenu
+ await menuitems[menuIndex].sendKeys(Key.ARROW_DOWN);
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+ const lastIndex = items.length - 1;
+
+ for (let itemIndex = 0; itemIndex < ex.numSubmenuItems[menuIndex]; itemIndex++) {
+ const item = items[itemIndex];
+ const itemText = await item.getText();
+ // Account for wrapping
+ const nextItemIndex = itemIndex + 1;
+
+ // Send UP to the item
+ await item.sendKeys(Key.END);
+
+ // Test that the focus is on the first item
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], lastIndex),
+ 'Sending key "END" to submenu item "' + itemText + '" should send focus to last submenu item.'
+ );
+ }
+ }
+});
+
+ariaTest('Character sends to menubar changes focus in menubar',
+ exampleFile, 'submenu-character', async (t) => {
+
+
+ const charIndexTest = [
+ [ // Tests for menu dropdown 0
+ { sendChar: 's', sendIndex: 0, endIndex: 1 },
+ { sendChar: 's', sendIndex: 1, endIndex: 0 },
+ { sendChar: 'x', sendIndex: 0, endIndex: 0 }
+ ],
+ [ // Tests for menu dropdown 1
+ { sendChar: 'u', sendIndex: 0, endIndex: 9 },
+ { sendChar: 'y', sendIndex: 9, endIndex: 9 }
+ ],
+ [ // Tests for menu dropdown 2
+ { sendChar: 'r', sendIndex: 0, endIndex: 2 },
+ { sendChar: 'z', sendIndex: 2, endIndex: 2 }
+ ],
+ [ // Tests for menu dropdown 3
+ { sendChar: 'x', sendIndex: 0, endIndex: 2 },
+ { sendChar: 'x', sendIndex: 2, endIndex: 6 }
+ ]
+ ];
+
+ const menuitems = await t.context.queryElements(t, ex.menubarMenuitemSelector);
+ for (let menuIndex = 0; menuIndex < ex.numMenus; menuIndex++) {
+
+ // Open the dropdown
+ await menuitems[menuIndex].sendKeys(Key.ARROW_DOWN);
+ const items = await t.context.queryElements(t, ex.allSubmenuItems[menuIndex]);
+
+ for (let test of charIndexTest[menuIndex]) {
+
+ // Send character to menuitem
+ const itemText = await items[test.sendIndex].getText();
+ await items[test.sendIndex].sendKeys(test.sendChar);
+
+ // Test that the focus switches to the appropriate menuitem
+ t.true(
+ await checkFocus(t, ex.allSubmenuItems[menuIndex], test.endIndex),
+ 'Sending character ' + test.sendChar + ' to menuitem ' + itemText + ' should move the focus to menuitem ' + test.endIndex
+ );
+ }
+ }
+ });