From 093a61ffca6fb318966e5c99b031846cf1a04177 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 18 Mar 2020 15:34:09 -0500 Subject: [PATCH 01/69] updated menubar editor example --- .../menubar-editor.css} | 0 .../{menubar-2 => }/images/check-brown.png | Bin .../{menubar-2 => }/images/dot-brown.png | Bin .../images/down-arrow-brown.png | Bin .../{menubar-2 => }/images/separator.png | Bin examples/menubar/js/menubar-editor.js | 722 +++++++++++++ .../styleManager.js => js/style-manager.js} | 4 + .../menubar/menubar-2/js/MenubarAction.js | 210 ---- .../menubar/menubar-2/js/MenubarItemAction.js | 167 --- .../menubar/menubar-2/js/PopupMenuAction.js | 253 ----- .../menubar-2/js/PopupMenuItemAction.js | 206 ---- .../menubar/menubar-2/js/menubar-2-init.js | 15 - .../menubar-2.html => menubar-editor.html} | 34 +- package-lock.json | 986 +++++++----------- package.json | 10 +- 15 files changed, 1142 insertions(+), 1465 deletions(-) rename examples/menubar/{menubar-2/css/menubarAction.css => css/menubar-editor.css} (100%) rename examples/menubar/{menubar-2 => }/images/check-brown.png (100%) rename examples/menubar/{menubar-2 => }/images/dot-brown.png (100%) rename examples/menubar/{menubar-2 => }/images/down-arrow-brown.png (100%) rename examples/menubar/{menubar-2 => }/images/separator.png (100%) create mode 100644 examples/menubar/js/menubar-editor.js rename examples/menubar/{menubar-2/js/styleManager.js => js/style-manager.js} (97%) delete mode 100644 examples/menubar/menubar-2/js/MenubarAction.js delete mode 100644 examples/menubar/menubar-2/js/MenubarItemAction.js delete mode 100644 examples/menubar/menubar-2/js/PopupMenuAction.js delete mode 100644 examples/menubar/menubar-2/js/PopupMenuItemAction.js delete mode 100644 examples/menubar/menubar-2/js/menubar-2-init.js rename examples/menubar/{menubar-2/menubar-2.html => menubar-editor.html} (95%) diff --git a/examples/menubar/menubar-2/css/menubarAction.css b/examples/menubar/css/menubar-editor.css similarity index 100% rename from examples/menubar/menubar-2/css/menubarAction.css rename to examples/menubar/css/menubar-editor.css diff --git a/examples/menubar/menubar-2/images/check-brown.png b/examples/menubar/images/check-brown.png similarity index 100% rename from examples/menubar/menubar-2/images/check-brown.png rename to examples/menubar/images/check-brown.png diff --git a/examples/menubar/menubar-2/images/dot-brown.png b/examples/menubar/images/dot-brown.png similarity index 100% rename from examples/menubar/menubar-2/images/dot-brown.png rename to examples/menubar/images/dot-brown.png diff --git a/examples/menubar/menubar-2/images/down-arrow-brown.png b/examples/menubar/images/down-arrow-brown.png similarity index 100% rename from examples/menubar/menubar-2/images/down-arrow-brown.png rename to examples/menubar/images/down-arrow-brown.png diff --git a/examples/menubar/menubar-2/images/separator.png b/examples/menubar/images/separator.png similarity index 100% rename from examples/menubar/menubar-2/images/separator.png rename to examples/menubar/images/separator.png diff --git a/examples/menubar/js/menubar-editor.js b/examples/menubar/js/menubar-editor.js new file mode 100644 index 0000000000..713647de0c --- /dev/null +++ b/examples/menubar/js/menubar-editor.js @@ -0,0 +1,722 @@ +/* +* 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 +*/ + +var MenubarEditor = function (domNode, actionManager) { + + this.domNode = domNode; + this.actionManager = actionManager; + this.isMouseDownOnBackground = false; + + this.menuitemGroups = {}; + this.menuOrientation = {}; + this.isPopup = {}; + this.openPopups = false; + + this.firstChars = {}; // see Menubar init method + this.firstMenuitem = {}; // see Menubar init method + this.lastMenuitem = {}; // see Menubar init method + + this.initMenu(domNode) + domNode.addEventListener('focusin', this.handleMenubarFocusin.bind(this)); + domNode.addEventListener('focusout', this.handleMenubarFocusout.bind(this)); + + window.addEventListener('mousedown', this.handleBackgroundMousedown.bind(this), true); + window.addEventListener('mouseup', this.handleBackgroundMouseup.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; + + function findMenuitems(node, group) { + var role, flag, groupId; + + while (node) { + flag = true; + role = node.getAttribute('role'); + + if (role) { + role = role.trim().toLowerCase(); + } + + switch (role) { + case 'menu': + initMenu(node); + flag = false; + break; + + case 'group': + groupId = getGroupId(node); + menuitemGroups[groupId] = []; + break; + + case 'menuitem': + case 'menuitemradio': + case 'menuitemcheckbox': + 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]) { + menuitem.tabIndex = 0; + this.firstMenuitem[menuId] = menuitem; + } + this.lastMenuitem[menuId] = menuitem; + + } +}; + +/* MenubarEditor FOCUS MANAGEMENT METHODS */ + +MenubarEditor.prototype.setFocusToMenuitem = function (menuId, newMenuitem, currentMenuitem) { + + if (typeof currentMenuitem !== 'object') { + currentMenuitem = false; + } + + this.menuitemGroups[menuId].forEach(function(item) { + item.tabIndex = -1; + }); + + if (currentMenuitem && + this.hasPopup(currentMenuitem) && + this.isOpen(currentMenuitem)) { + this.closePopup(currentMenuitem); + } + + if (this.hasPopup(newMenuitem)) { + if (this.openPopups) { + this.openPopup(newMenuitem); + } + } + else { + var menu = this.getMenu(newMenuitem); + var cmi = menu.previousElementSibling; + if (!this.isOpen(cmi)) { + this.openPopup(cmi); + } + } + + newMenuitem.tabIndex = 0; + newMenuitem.focus(); + +}; + +MenubarEditor.prototype.setFocusToFirstMenuitem = function (menuId, currentMenuitem) { + this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId], currentMenuitem); +}; + +MenubarEditor.prototype.setFocusToLastMenuitem = function (menuId, currentMenuitem) { + this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId], currentMenuitem); +}; + +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, currentMenuitem); + + 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, currentMenuitem); + + 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], currentMenuitem); + } +}; + +// Utitlities + +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( 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.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 - 1) + '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.closePopupAll = function () { + + var popups = this.domNode.querySelectorAll('[aria-haspopup]'); + + for (var i = 0; i < popups.length; i++) { + var popup = popups[i]; + if (this.isOpen(popup)) { + this.closePopup(popup); + event.stopPropagation(); + event.preventDefault(); + } + } + +}; + +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.handleMenubarFocusin = function (event) { + // if the menubar or any of its menus has focus, add styling hook for hover + this.domNode.classList.add('focus'); +}; + +MenubarEditor.prototype.handleMenubarFocusout = function (event) { + // remove styling hook for hover on menubar item + this.domNode.classList.remove('focus'); +}; + +MenubarEditor.prototype.handleBackgroundMousedown = function (event) { + if (!this.domNode.contains(event.target)) { + this.isMouseDownOnBackground = true; + this.closePopupAll(); + } +}; + +MenubarEditor.prototype.handleBackgroundMouseup = function () { + this.isMouseDownOnBackground = false; +}; + + +MenubarEditor.prototype.handleKeydown = function (event) { + var tgt = event.currentTarget, + key = event.key, + flag = false, + menuId = this.getMenuId(tgt), + id, + popupMenuId, + mi, + role, + option, + value; + +// console.log('[handleMenubarKeydown][key]: ' + key); +// console.log('[handleMenubarKeydown][menuId]: ' + menuId); + + switch (key) { + case ' ': + case 'Enter': + if (this.hasPopup(tgt)) { + this.openPopups = true; + 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.openPopups = false; + 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)) { + this.openPopups = true; + popupMenuId = this.openPopup(tgt); + this.setFocusToFirstMenuitem(popupMenuId); + flag = true; + } + } + break; + + case 'Esc': + case 'Escape': + this.openPopups = false; + 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; + } + 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.openPopups = false; + 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, + role, + option, + value; + + if (this.hasPopup(tgt)) { + if (this.isOpen(tgt)) { + this.openPopups = false; + this.closePopup(tgt); + } + else { + this.closePopupAll(); + this.openPopups = true; + this.openPopup(tgt); + } + } + 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.openPopups = false; + this.closePopup(tgt); + } + + event.stopPropagation(); + event.preventDefault(); + +}; + +MenubarEditor.prototype.handleMenuitemMouseover = function (event) { + var tgt = event.currentTarget; + + if (this.hasPopup(tgt) && this.openPopups) { + if (!this.isOpen(tgt)) { + this.closePopupAll(); + this.openPopups = true; + this.openPopup(tgt); + } + } +}; + +// Initialize menubar editor + +window.addEventListener('load', function () { + var styleManager = new StyleManager('textarea1'); + var menubarEditor = new MenubarEditor(document.getElementById('menubar1'), styleManager); +}); + diff --git a/examples/menubar/menubar-2/js/styleManager.js b/examples/menubar/js/style-manager.js similarity index 97% rename from examples/menubar/menubar-2/js/styleManager.js rename to examples/menubar/js/style-manager.js index 90b98c3f7b..f47f14a0ac 100644 --- a/examples/menubar/menubar-2/js/styleManager.js +++ b/examples/menubar/js/style-manager.js @@ -111,6 +111,10 @@ 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(); diff --git a/examples/menubar/menubar-2/js/MenubarAction.js b/examples/menubar/menubar-2/js/MenubarAction.js deleted file mode 100644 index 1883da3c6a..0000000000 --- a/examples/menubar/menubar-2/js/MenubarAction.js +++ /dev/null @@ -1,210 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* -* File: MenubarAction.js -* -* Desc: Menubar widget that implements ARIA Authoring Practices -*/ - -/* -* @constructor MenubarAction -* -* @desc -* Wrapper object for a menubar -* -* @param domNode -* The DOM element node that serves as the menubar container. -* Each child element of domNode that represents a menubaritem -* must be an A element. -*/ -var MenubarAction = function (domNode) { - var msgPrefix = 'Menubar constructor argument domNode '; - - // Check whether domNode is a DOM element - if (!(domNode instanceof Element)) { - throw new TypeError(msgPrefix + 'is not a DOM Element.'); - } - - // Check whether domNode has descendant elements - if (domNode.childElementCount === 0) { - throw new Error(msgPrefix + 'has no element children.'); - } - - // Check whether domNode's descendant elements contain A elements - var e = domNode.firstElementChild; - while (e) { - var menubarItem = e.firstElementChild; - if (menubarItem && menubarItem.tagName !== 'A') { - throw new Error(msgPrefix + 'has child elements that are not A elements.'); - } - e = e.nextElementSibling; - } - - this.domNode = domNode; - - this.menubarItems = []; // see Menubar init method - this.firstChars = []; // see Menubar init method - - this.firstItem = null; // see Menubar init method - this.lastItem = null; // see Menubar init method -}; - -/* -* @method MenubarAction.prototype.init -* -* @desc -* Adds ARIA role to the menubar node -* Traverse menubar children for A elements to configure each A element as an ARIA menuitem -* and populate menuitems array. Initialize firstItem and lastItem properties. -*/ -MenubarAction.prototype.init = function (actionManager) { - var menubarItem, menuElement, textContent, numItems; - - this.actionManager = actionManager; - - this.domNode.setAttribute('role', 'menubar'); - - this.domNode.addEventListener('focusin', this.handleFocusin.bind(this)); - this.domNode.addEventListener('focusout', this.handleFocusout.bind(this)); - - // Traverse the element children of the menubar domNode: configure each with - // menuitem role behavior and store reference in menuitems array. - var e = this.domNode.firstElementChild; - - while (e) { - menuElement = e.firstElementChild; - - if (menuElement && menuElement.tagName === 'A') { - menubarItem = new MenubarItemAction(menuElement, this); - menubarItem.init(); - this.menubarItems.push(menubarItem); - textContent = menuElement.textContent.trim(); - this.firstChars.push(textContent.substring(0, 1).toLowerCase()); - } - - e = e.nextElementSibling; - } - - // Use populated menuitems array to initialize firstItem and lastItem. - numItems = this.menubarItems.length; - if (numItems > 0) { - this.firstItem = this.menubarItems[0]; - this.lastItem = this.menubarItems[numItems - 1]; - } - this.firstItem.domNode.tabIndex = 0; -}; - -/* FOCUS MANAGEMENT METHODS */ - -MenubarAction.prototype.setFocusToItem = function (newItem, hover) { - var isOpen = false; - var hasFocus = this.domNode.contains(document.activeElement); - for (var i = 0; i < this.menubarItems.length; i++) { - var mbi = this.menubarItems[i]; - isOpen = isOpen || (mbi.popupMenu && mbi.popupMenu.isOpen()); - if (!hover || hasFocus) { - mbi.domNode.tabIndex = -1; - } - if (mbi.popupMenu) { - mbi.popupMenu.close(); - } - } - if (!hover || hasFocus) { - newItem.domNode.focus(); - newItem.domNode.tabIndex = 0; - } - if (isOpen && newItem.popupMenu) { - newItem.popupMenu.open(); - } -}; - -MenubarAction.prototype.setFocusToFirstItem = function () { - this.setFocusToItem(this.firstItem); -}; -MenubarAction.prototype.setFocusToLastItem = function () { - this.setFocusToItem(this.lastItem); -}; - -MenubarAction.prototype.setFocusToPreviousItem = function (currentItem) { - var newItem, index; - - if (currentItem === this.firstItem) { - newItem = this.lastItem; - } - else { - index = this.menubarItems.indexOf(currentItem); - newItem = this.menubarItems[ index - 1 ]; - } - - this.setFocusToItem(newItem); -}; - -MenubarAction.prototype.setFocusToNextItem = function (currentItem) { - var newItem, index; - - if (currentItem === this.lastItem) { - newItem = this.firstItem; - } - else { - index = this.menubarItems.indexOf(currentItem); - newItem = this.menubarItems[ index + 1 ]; - } - this.setFocusToItem(newItem); -}; - -MenubarAction.prototype.setFocusByFirstCharacter = function (currentItem, char) { - var start, index; - - char = char.toLowerCase(); - - // Get start index for search based on position of currentItem - start = this.menubarItems.indexOf(currentItem) + 1; - if (start === this.menubarItems.length) { - start = 0; - } - - // Check remaining slots in the menu - index = this.getIndexFirstChars(start, char); - - // If not found in remaining slots, check from beginning - if (index === -1) { - index = this.getIndexFirstChars(0, char); - } - - // If match was found... - if (index > -1) { - this.menubarItems[index].domNode.focus(); - this.menubarItems[index].domNode.tabIndex = 0; - currentItem.tabIndex = -1; - } -}; - -MenubarAction.prototype.getIndexFirstChars = function (startIndex, char) { - for (var i = startIndex; i < this.firstChars.length; i++) { - if (char === this.firstChars[i]) { - return i; - } - } - return -1; -}; - -MenubarAction.prototype.handleFocusin = function (event) { - // if the menubar or any of its menus has focus, add styling hook for hover - this.domNode.classList.add('focus'); -}; - -MenubarAction.prototype.handleFocusout = function (event) { - // if the next element to get focus is not in the menubar or its menus, then close menu - if (!this.domNode.contains(event.relatedTarget)) { - for (var i = 0; i < this.menubarItems.length; i++) { - var mbi = this.menubarItems[i]; - if (mbi.popupMenu && mbi.popupMenu.isOpen()) { - mbi.popupMenu.close(); - } - } - } - // remove styling hook for hover on menubar item - this.domNode.classList.remove('focus'); -}; - diff --git a/examples/menubar/menubar-2/js/MenubarItemAction.js b/examples/menubar/menubar-2/js/MenubarItemAction.js deleted file mode 100644 index 9a6ab78e2f..0000000000 --- a/examples/menubar/menubar-2/js/MenubarItemAction.js +++ /dev/null @@ -1,167 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* -* File: MenubarItemAction.js -* -* Desc: Menubar Menuitem widget that implements ARIA Authoring Practices -*/ - -/* -* @constructor MenubarItemAction -* -* @desc -* Object that configures menubar item elements by setting tabIndex -* and registering itself to handle pertinent events. -* -* @param domNode -* The DOM element node that serves as the menubar item container. -* The menubarObj is responsible for checking that it has -* requisite metadata, e.g. role="menuitem". -* -* @param menubarObj -* The MenubarAction object that is a delegate for the menubar DOM element -* that contains the menubar item element. -*/ -var MenubarItemAction = function (domNode, menubarObj) { - - this.menubar = menubarObj; - this.domNode = domNode; - this.popupMenu = false; - - this.keyCode = Object.freeze({ - 'TAB': 9, - 'RETURN': 13, - 'ESC': 27, - 'SPACE': 32, - 'PAGEUP': 33, - 'PAGEDOWN': 34, - 'END': 35, - 'HOME': 36, - 'LEFT': 37, - 'UP': 38, - 'RIGHT': 39, - 'DOWN': 40 - }); -}; - -MenubarItemAction.prototype.init = function () { - this.domNode.tabIndex = -1; - - this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); - this.domNode.addEventListener('click', this.handleClick.bind(this)); - this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); - - // initialize pop up menus - - var nextElement = this.domNode.nextElementSibling; - - if (nextElement && nextElement.tagName === 'UL') { - this.popupMenu = new PopupMenuAction(nextElement, this, this.menubar.actionManager); - this.popupMenu.init(); - } - -}; - -MenubarItemAction.prototype.handleKeydown = function (event) { - var tgt = event.currentTarget, - char = event.key, - flag = false; - - function isPrintableCharacter (str) { - return str.length === 1 && str.match(/\S/); - } - - switch (event.keyCode) { - case this.keyCode.SPACE: - case this.keyCode.RETURN: - case this.keyCode.DOWN: - if (this.popupMenu) { - this.popupMenu.open(); - this.popupMenu.setFocusToFirstItem(); - flag = true; - } - break; - - case this.keyCode.ESC: - if (this.popupMenu) { - this.popupMenu.close(); - } - flag = true; - break; - - case this.keyCode.LEFT: - this.menubar.setFocusToPreviousItem(this); - flag = true; - break; - - case this.keyCode.RIGHT: - this.menubar.setFocusToNextItem(this); - flag = true; - break; - - case this.keyCode.UP: - if (this.popupMenu) { - this.popupMenu.open(); - this.popupMenu.setFocusToLastItem(); - flag = true; - } - break; - - case this.keyCode.HOME: - case this.keyCode.PAGEUP: - this.menubar.setFocusToFirstItem(); - if (this.popupMenu) { - this.popupMenu.close(); - } - flag = true; - break; - - case this.keyCode.END: - case this.keyCode.PAGEDOWN: - this.menubar.setFocusToLastItem(); - if (this.popupMenu) { - this.popupMenu.close(); - } - flag = true; - break; - - default: - if (isPrintableCharacter(char)) { - this.menubar.setFocusByFirstCharacter(this, char); - flag = true; - } - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } -}; - -MenubarItemAction.prototype.handleClick = function (event) { - if (this.popupMenu) { - if (!this.popupMenu.isOpen()) { - // clicking on menubar item opens menu (closes open menu first) - for (var i = 0; i < this.menubar.menubarItems.length; i++) { - var mbi = this.menubar.menubarItems[i]; - if (mbi.popupMenu && mbi.popupMenu.isOpen()) { - mbi.popupMenu.close(); - break; - } - } - this.popupMenu.open(); - } - else { - // clicking again on same menubar item closes menu - this.popupMenu.close(); - } - // prevent scroll to top of page when anchor element is clicked - event.preventDefault(); - } -}; - -MenubarItemAction.prototype.handleMouseover = function (event) { - this.menubar.setFocusToItem(this, true); -}; diff --git a/examples/menubar/menubar-2/js/PopupMenuAction.js b/examples/menubar/menubar-2/js/PopupMenuAction.js deleted file mode 100644 index 8d1e3ba372..0000000000 --- a/examples/menubar/menubar-2/js/PopupMenuAction.js +++ /dev/null @@ -1,253 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* -* File: PopupMenuAction.js -* -* Desc: Popup menu widget that implements ARIA Authoring Practices -*/ - -/* -* @constructor PopupMenuAction -* -* @desc -* Wrapper object for a simple popup menu (without nested submenus) -* -* @param domNode -* The DOM element node that serves as the popup menu container. Each -* child element of domNode that represents a menuitem must have a -* 'role' attribute with value 'menuitem'. -* -* @param controllerObj -* The object that is a wrapper for the DOM element that controls the -* menu, e.g. a button element, with an 'aria-controls' attribute that -* references this menu's domNode. See MenuButton.js -* -* The controller object is expected to have the following properties: -* 1. domNode: The controller object's DOM element node, needed for -* retrieving positioning information. -*/ -var PopupMenuAction = function (domNode, controllerObj, actionManager) { - var elementChildren, - msgPrefix = 'PopupMenu constructor argument domNode '; - - // Check whether domNode is a DOM element - if (!(domNode instanceof Element)) { - throw new TypeError(msgPrefix + 'is not a DOM Element.'); - } - - // Check whether domNode has child elements - if (domNode.childElementCount === 0) { - throw new Error(msgPrefix + 'has no element children.'); - } - - this.domNode = domNode; - this.controller = controllerObj; - this.actionManager = actionManager; - - this.menuitems = []; // see PopupMenu init method - this.firstChars = []; // see PopupMenu init method - - this.firstItem = null; // see PopupMenu init method - this.lastItem = null; // see PopupMenu init method -}; - -/* -* @method PopupMenuAction.prototype.init -* -* @desc -* Traverse domNode children to configure each menuitem and populate menuitems -* array. Initialize firstItem and lastItem properties. -*/ -PopupMenuAction.prototype.init = function () { - var childElement, menuElement, firstChildElement, menuItem, textContent, numItems, label; - - // Configure the domNode itself - this.domNode.tabIndex = -1; - - this.domNode.setAttribute('role', 'menu'); - - if (!this.domNode.getAttribute('aria-labelledby') && !this.domNode.getAttribute('aria-label') && !this.domNode.getAttribute('title')) { - label = this.controller.domNode.innerHTML; - this.domNode.setAttribute('aria-label', label); - } - - // Traverse the element children of domNode: configure each with - // menuitem role behavior and store reference in menuitems array. - menuElements = this.domNode.getElementsByTagName('LI'); - - for (var i = 0; i < menuElements.length; i++) { - - menuElement = menuElements[i]; - - if (!menuElement.firstElementChild && menuElement.getAttribute('role') !== 'separator') { - menuItem = new MenuItem(menuElement, this); - menuItem.init(); - this.menuitems.push(menuItem); - textContent = menuElement.textContent.trim(); - this.firstChars.push(textContent.substring(0, 1).toLowerCase()); - } - - } - - // Use populated menuitems array to initialize firstItem and lastItem. - numItems = this.menuitems.length; - if (numItems > 0) { - this.firstItem = this.menuitems[0]; - this.lastItem = this.menuitems[numItems - 1]; - } -}; - -PopupMenuAction.prototype.updateMenuStates = function () { - - var item = this.domNode.querySelector('[data-option="font-larger"]'); - if (item) { - if (this.actionManager.isMaxFontSize()) { - item.setAttribute('aria-disabled', 'true'); - } - else { - item.setAttribute('aria-disabled', 'false'); - } - } - - item = this.domNode.querySelector('[data-option="font-smaller"]'); - if (item) { - if (this.actionManager.isMinFontSize()) { - item.setAttribute('aria-disabled', 'true'); - } - else { - item.setAttribute('aria-disabled', 'false'); - } - } - - // Update the radio buttons for font, in case they were updated using the larger - // smaller font menu items - - var rbs = this.domNode.querySelectorAll('[data-option="font-size"] [role=menuitemradio]'); - - for (var i = 0; i < rbs.length; i++) { - var rb = rbs[i]; - - if (this.actionManager.fontSize === rb.textContent.toLowerCase()) { - rb.setAttribute('aria-checked', 'true'); - } - else { - rb.setAttribute('aria-checked', 'false'); - } - } - -}; - -/* FOCUS MANAGEMENT METHODS */ - -PopupMenuAction.prototype.setFocusToController = function (command) { - if (typeof command !== 'string') { - command = ''; - } - if (command === 'previous') { - this.controller.menubar.setFocusToPreviousItem(this.controller); - } - else if (command === 'next') { - this.controller.menubar.setFocusToNextItem(this.controller); - } - else { - this.controller.domNode.focus(); - } -}; - -PopupMenuAction.prototype.setFocusToItem = function (item) { - item.domNode.focus(); -}; - -PopupMenuAction.prototype.setFocusToFirstItem = function () { - this.firstItem.domNode.focus(); -}; - -PopupMenuAction.prototype.setFocusToLastItem = function () { - this.lastItem.domNode.focus(); -}; - -PopupMenuAction.prototype.setFocusToPreviousItem = function (currentItem) { - var index; - - if (currentItem === this.firstItem) { - this.lastItem.domNode.focus(); - } - else { - index = this.menuitems.indexOf(currentItem); - this.menuitems[index - 1].domNode.focus(); - } -}; - -PopupMenuAction.prototype.setFocusToNextItem = function (currentItem) { - var index; - - if (currentItem === this.lastItem) { - this.firstItem.domNode.focus(); - } - else { - index = this.menuitems.indexOf(currentItem); - this.menuitems[index + 1].domNode.focus(); - } -}; - -PopupMenuAction.prototype.setFocusByFirstCharacter = function (currentItem, char) { - var start, index; - - char = char.toLowerCase(); - - // Get start index for search based on position of currentItem - start = this.menuitems.indexOf(currentItem) + 1; - if (start === this.menuitems.length) { - start = 0; - } - - // Check remaining slots in the menu - index = this.getIndexFirstChars(start, char); - - // If not found in remaining slots, check from beginning - if (index === -1) { - index = this.getIndexFirstChars(0, char); - } - - // If match was found... - if (index > -1) { - this.menuitems[index].domNode.focus(); - } -}; - -PopupMenuAction.prototype.getIndexFirstChars = function (startIndex, char) { - for (var i = startIndex; i < this.firstChars.length; i++) { - if (char === this.firstChars[i]) { - return i; - } - } - return -1; -}; - -/* MENU DISPLAY METHODS */ - -PopupMenuAction.prototype.open = function () { - // get position and bounding rectangle of controller object's DOM node - var rect = this.controller.domNode.getBoundingClientRect(); - - // set CSS properties - this.domNode.style.position = 'absolute'; - this.domNode.style.top = (rect.height - 1) + 'px'; - this.domNode.style.left = '0px'; - this.domNode.style.zIndex = 100; - this.domNode.style.display = 'block'; - - // set aria-expanded attribute - this.controller.domNode.setAttribute('aria-expanded', 'true'); -}; - -PopupMenuAction.prototype.isOpen = function () { - return this.controller.domNode.getAttribute('aria-expanded') === 'true'; -}; - -PopupMenuAction.prototype.close = function () { - this.domNode.style.display = 'none'; - this.domNode.style.zIndex = 0; - this.controller.domNode.setAttribute('aria-expanded', 'false'); -}; diff --git a/examples/menubar/menubar-2/js/PopupMenuItemAction.js b/examples/menubar/menubar-2/js/PopupMenuItemAction.js deleted file mode 100644 index 0a7f866e3f..0000000000 --- a/examples/menubar/menubar-2/js/PopupMenuItemAction.js +++ /dev/null @@ -1,206 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* -* File: MenuItem.js -* -* Desc: Popup Menu Menuitem widget that implements ARIA Authoring Practices -*/ - -/* -* @constructor MenuItem -* -* @desc -* Wrapper object for a simple menu item in a popup menu -* -* @param domNode -* The DOM element node that serves as the menu item container. -* The menuObj PopupMenu is responsible for checking that it has -* requisite metadata, e.g. role="menuitem". -* -* @param menuObj -* The object that is a wrapper for the PopupMenu DOM element that -* contains the menu item DOM element. See PopupMenu.js -*/ -var MenuItem = function (domNode, menuObj) { - - this.domNode = domNode; - this.menu = menuObj; - - this.keyCode = Object.freeze({ - 'TAB': 9, - 'RETURN': 13, - 'ESC': 27, - 'SPACE': 32, - 'PAGEUP': 33, - 'PAGEDOWN': 34, - 'END': 35, - 'HOME': 36, - 'LEFT': 37, - 'UP': 38, - 'RIGHT': 39, - 'DOWN': 40 - }); -}; - -MenuItem.prototype.init = function () { - this.domNode.tabIndex = -1; - - if (!this.domNode.getAttribute('role')) { - this.domNode.setAttribute('role', 'menuitem'); - } - - this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); - this.domNode.addEventListener('click', this.handleClick.bind(this)); - this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); -}; - -MenuItem.prototype.activateMenuitem = function (node) { - - var role = node.getAttribute('role'); - var value = node.textContent; - var option = node.getAttribute('data-option'); - var item; - // flag is used to signal whether a menu should close or not - // i.e. don't close if checkbox or radio menuitem is toggled - var flag = true; - - if (typeof option !== 'string') { - option = node.parentNode.getAttribute('data-option'); - } - - if (role === 'menuitem') { - this.menu.actionManager.setOption(option, value); - } - else { - if (role === 'menuitemcheckbox') { - if (node.getAttribute('aria-checked') === 'true') { - this.menu.actionManager.setOption(option, false); - node.setAttribute('aria-checked', 'false'); - } - else { - this.menu.actionManager.setOption(option, true); - node.setAttribute('aria-checked', 'true'); - } - flag = false; - } - else { - if (role === 'menuitemradio') { - - this.menu.actionManager.setOption(option, value); - - item = node.parentNode.firstElementChild; - while (item) { - if (item.getAttribute('role') === 'menuitemradio') { - item.setAttribute('aria-checked', 'false'); - } - item = item.nextElementSibling; - } - node.setAttribute('aria-checked', 'true'); - flag = false; - } - } - } - - this.menu.updateMenuStates(); - - return flag; - -}; - -/* EVENT HANDLERS */ - -MenuItem.prototype.handleKeydown = function (event) { - var tgt = event.currentTarget, - char = event.key, - flag = false; - - function isPrintableCharacter (str) { - return str.length === 1 && str.match(/\S/); - } - - switch (event.keyCode) { - case this.keyCode.SPACE: - if (this.activateMenuitem(tgt)) { - this.menu.setFocusToController(); - this.menu.close(); - } - flag = true; - break; - - case this.keyCode.RETURN: - this.activateMenuitem(tgt); - this.menu.setFocusToController(); - this.menu.close(); - flag = true; - break; - - case this.keyCode.ESC: - this.menu.setFocusToController(); - this.menu.close(); - flag = true; - break; - - case this.keyCode.UP: - this.menu.setFocusToPreviousItem(this); - flag = true; - break; - - case this.keyCode.DOWN: - this.menu.setFocusToNextItem(this); - flag = true; - break; - - case this.keyCode.LEFT: - this.menu.setFocusToController('previous'); - this.menu.close(); - flag = true; - break; - - case this.keyCode.RIGHT: - this.menu.setFocusToController('next'); - this.menu.close(); - flag = true; - break; - - case this.keyCode.HOME: - case this.keyCode.PAGEUP: - this.menu.setFocusToFirstItem(); - flag = true; - break; - - case this.keyCode.END: - case this.keyCode.PAGEDOWN: - this.menu.setFocusToLastItem(); - flag = true; - break; - - case this.keyCode.TAB: - this.menu.setFocusToController(); - this.menu.close(); - // allow tab and shift+tab to navigate out of menu bar - break; - - default: - if (isPrintableCharacter(char)) { - this.menu.setFocusByFirstCharacter(this, char); - flag = true; - } - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } -}; - -MenuItem.prototype.handleClick = function (event) { - this.activateMenuitem(event.currentTarget); - this.menu.setFocusToController(); - this.menu.close(); -}; - -MenuItem.prototype.handleMouseover = function (event) { - this.menu.setFocusToItem(this); -}; diff --git a/examples/menubar/menubar-2/js/menubar-2-init.js b/examples/menubar/menubar-2/js/menubar-2-init.js deleted file mode 100644 index c0137b47b3..0000000000 --- a/examples/menubar/menubar-2/js/menubar-2-init.js +++ /dev/null @@ -1,15 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* -* File: menubar-2-init.js -* -* Desc: Creates a menubar to control the styling of text in a textarea element -*/ - -window.addEventListener('load', function () { - var menubar = new MenubarAction(document.getElementById('menubar1')); - var styleManager = new StyleManager('textarea1'); - menubar.init(styleManager); -}); - diff --git a/examples/menubar/menubar-2/menubar-2.html b/examples/menubar/menubar-editor.html similarity index 95% rename from examples/menubar/menubar-2/menubar-2.html rename to examples/menubar/menubar-editor.html index ab7280cc95..b11359583a 100644 --- a/examples/menubar/menubar-2/menubar-2.html +++ b/examples/menubar/menubar-editor.html @@ -6,19 +6,15 @@ - - - - + + + + - - - - - - - + + +