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:

+ + +
+

Example

+ +
+ + +

+ More information on + Gettysburg Address +

+ + +
+ +
+ +
+

Accessibility Features

+
    +
  1. 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.
  2. +
  3. Disabled menu items are demonstrated in the font size menu, which includes two disabled menuitems.
  4. +
  5. 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.
  6. +
  7. To support operating system high contrast settings, focus is highlighted by adding and removing a border around the menu item with focus.
  8. +
  9. 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.
  10. +
  11. + 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. +
  12. +
  13. + 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: +
      +
    1. 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.
    2. +
    3. When a submenu is open and the user hovers over an item in the submenu, focus follows hover.
    4. +
    +
  14. +
+
+ +
+

Keyboard Support

+

Menubar

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
+ 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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
+ 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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
+ 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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
+ 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

+ + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
+ aria-label="string" + + textarea + + Defines an accessible name for the textarea. +
+
+ +
+

Javascript and CSS Source Code

+ +
+ +
+

HTML Source Code

+ +
+ + + + +
+
+ + + + diff --git a/index.html b/index.html index 1382fa0f97..59ac08c11b 100644 --- a/index.html +++ b/index.html @@ -7292,7 +7292,7 @@

C.4 Enabling funders - +

D. References

D.1