From a161bc69c17bad429253cf3b7c99986eeda1fa67 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Mon, 14 Jan 2019 21:47:01 -0600 Subject: [PATCH] Toolbar Example: Fix bugs and redesign to add more types of widgets (#pull 950) completes work to: * Resolve keyboard, aria-disabled, and aria-label bugs listed in issue #847. * Add additional widget types requested in issue #541, including toggle, checkbox, spinner, menu button, radio group, and link. * The above are all issues discussed in toolbar review issue #539. Note: missing regression tests are tracked by issue #965. --- examples/toolbar/css/menuButton.css | 61 +- examples/toolbar/css/toolbar.css | 156 ++- examples/toolbar/help.html | 25 + .../toolbar/images/menuitemradio-checked.svg | 3 + .../images/menuitemradio-unchecked.svg | 3 + .../toolbar/images/pulldown-icon-focus.svg | 3 + examples/toolbar/images/pulldown-icon.svg | 3 + examples/toolbar/js/FontMenu.js | 241 ++++ examples/toolbar/js/FontMenuButton.js | 83 ++ examples/toolbar/js/FontMenuItem.js | 172 +++ examples/toolbar/js/FormatToolbar.js | 344 ++++++ examples/toolbar/js/FormatToolbarItem.js | 216 ++++ examples/toolbar/js/SpinButton.js | 143 +++ examples/toolbar/js/main.js | 13 - examples/toolbar/js/menuButton.js | 566 --------- examples/toolbar/js/toolbar.js | 116 -- examples/toolbar/toolbar.html | 1065 ++++++++++++++--- test/tests/toolbar.js | 45 +- test/util/assertRovingTabindex.js | 2 +- 19 files changed, 2321 insertions(+), 939 deletions(-) create mode 100644 examples/toolbar/help.html create mode 100644 examples/toolbar/images/menuitemradio-checked.svg create mode 100644 examples/toolbar/images/menuitemradio-unchecked.svg create mode 100644 examples/toolbar/images/pulldown-icon-focus.svg create mode 100644 examples/toolbar/images/pulldown-icon.svg create mode 100644 examples/toolbar/js/FontMenu.js create mode 100644 examples/toolbar/js/FontMenuButton.js create mode 100644 examples/toolbar/js/FontMenuItem.js create mode 100644 examples/toolbar/js/FormatToolbar.js create mode 100644 examples/toolbar/js/FormatToolbarItem.js create mode 100644 examples/toolbar/js/SpinButton.js delete mode 100644 examples/toolbar/js/main.js delete mode 100644 examples/toolbar/js/menuButton.js delete mode 100644 examples/toolbar/js/toolbar.js diff --git a/examples/toolbar/css/menuButton.css b/examples/toolbar/css/menuButton.css index fd33fb7120..760784eb3c 100644 --- a/examples/toolbar/css/menuButton.css +++ b/examples/toolbar/css/menuButton.css @@ -1,13 +1,14 @@ -button { - cursor: pointer; - font-size: 110%; +[role='toolbar'] .menu-popup { + position: relative; } -.menu-wrapper { - position: relative -} +[role='toolbar'] .menu-popup [role="menu"] { + padding: 0; + width: 9.5em; + border: 2px solid #DDD; + border-radius:5px; + background-color: white; -ul[role="menu"] { display: none; position: absolute; margin: 0; @@ -15,21 +16,47 @@ ul[role="menu"] { z-index: 1; } -ul[role="menu"] li { +[role='toolbar'] .menu-popup [role="menu"].focus { + border-color: #005A9C; +} + +[role='toolbar'] .menu-popup [role="menu"] li { + padding: 0; + margin: 0; + display: block; + text-align: left; list-style: none; } -[role="menu"] { +[role='toolbar'] .menu-popup [role="menu"] [role="menuitemradio"] { padding: 0; - width: 8em; - border: thin solid black; - background-color: #EEEEEE; + padding-top: 1px; + padding-bottom: 1px; + padding-left: 2px; + padding-right: 2px; + outline:0; + border: none; + border-radius: 0; } -[role="menuitem"] { - padding: 0.25em; + +[role='toolbar'] .menu-popup [role="menu"] [role="menuitemradio"]:before { + content: url('../images/menuitemradio-unchecked.svg'); +} + +[role='toolbar'] .menu-popup [role="menu"] [role="menuitemradio"][aria-checked="true"]:before { + content: url('../images/menuitemradio-checked.svg'); } -[role="menuitem"]:focus, -[role="menuitem"]:hover { - background-color: #FFFFFF; +[role='toolbar'] .menu-popup [role="menu"] [role="menuitemradio"]:focus, +[role='toolbar'] .menu-popup [role="menu"] [role="menuitemradio"]:hover { + background:rgb(226, 239, 255); + border-top: 1px solid #005A9C; + border-bottom: 1px solid #005A9C; + padding-top: 0px; + padding-bottom: 0px; } + +[role='toolbar'] .menu-popup [role="menu"] [role="menuitemradio"]:focus { + border-color: #005A9C; +} + diff --git a/examples/toolbar/css/toolbar.css b/examples/toolbar/css/toolbar.css index 77dffcc8f8..fb136419d1 100644 --- a/examples/toolbar/css/toolbar.css +++ b/examples/toolbar/css/toolbar.css @@ -1,49 +1,137 @@ -.annotate { - color: #366ED4; - font-style: italic; +[role="toolbar"] { + border:1px #ECECEA solid; + border-radius:5px; + padding: 6px; + height: 44px; + width: 1050px; + background-color:#ECECEA; + border:2px solid transparent; } -.toolbar { - border-left: 1px solid #aaa; - margin-top: 10px; - font-size: 0; +[role="toolbar"].focus { + border-color:#005A9C; + border-width: 3px; + padding:5px; } -.toolbar-item { - display: inline-block; - padding: 0.5em 1em; +[role="toolbar"] .group { + padding:0.25em; + display: block; + float: left; +} + +[role="toolbar"] .group:not(:first-child) { + margin-left: 0.75em; +} + +[role="toolbar"] button, +[role="toolbar"] [role="radio"], +[role="toolbar"] label, +[role="toolbar"] .spinbutton, +[role="toolbar"] a, +[role="toolbar"] .input { + border: 1px solid rgb(255, 255, 255); + outline:none; + display: inline-block; + padding: 6px 12px; + border-radius:5px; + text-align: center; + background: rgb(255, 255, 255); + color: #222428; + font-size: 14px; + line-height: 1.5em; + margin-right:0.25em; +} + +[role="toolbar"] button[aria-pressed="true"], +[role="toolbar"] [role="radio"][aria-checked="true"] { + border-color:#555; + font-weight: bold; + background-color: #F4F4F4; +} + + +[role="toolbar"] button[aria-disabled="true"] { + color: #889; + cursor: not-allowed; +} - background: #eee; - border: 1px solid #aaa; - border-left: none; - color: blue; - font-size: 16px; - line-height: 1.5em; - vertical-align: top; +[role="toolbar"] button[aria-disabled="true"]:focus { + border-color:#005A9C; } -.toolbar-item.selected { - background-color: #ccc; - color: black; - font-weight: bold; +[role="toolbar"] button:focus, +[role="toolbar"] [role="radio"]:focus, +[role="toolbar"] .spinbutton:focus, +[role="toolbar"] .focus, +[role="toolbar"] a:focus{ + border-width:2px; + border-color:#005A9C; + background:rgb(226, 239, 255); + padding: 5px 11px; } -.menu-wrapper { +[role="toolbar"] button:hover, +[role="toolbar"] [role="radio"]:hover, +[role="toolbar"] .spinbutton:hover, +[role="toolbar"] label.input:hover, +[role="toolbar"] a:hover{ + border-color:#005A9C; + background:rgb(226, 239, 255); +} + +[role="toolbar"] button::-moz-focus-inner { + border: 0; +} + +[role="toolbar"] [role="spinbutton"] .value, +[role="toolbar"] [role="spinbutton"] .increase, +[role="toolbar"] [role="spinbutton"] .decrease { + width: 60px; display: inline-block; - font-size: 16px; + padding: 0; + margin: 0; +} + +[role="toolbar"] button[aria-haspopup] span { + float: right; +} + +[role="toolbar"] button[aria-haspopup] span:after { + content: url(../images/pulldown-icon.svg); +} + +[role="toolbar"] button[aria-haspopup]:focus span:after { + content: url(../images/pulldown-icon-focus.svg); } -.menu-button { - padding-right: 2.5em; - position: relative; +[role="toolbar"] [role="spinbutton"] .increase, +[role="toolbar"] [role="spinbutton"] .decrease { + width: 20px; + border: 1px solid #ECECEA; + border-radius: 3px; + background-color:#ECECEA; + border: 1px solid #ECECEA; } -.menu-button::after { - content: " "; - border-left: 0.4em solid transparent; - border-right: 0.4em solid transparent; - border-top: 0.4em solid black; - position: absolute; - right: 1em; - top: 1.1em; +[role="toolbar"] [role="spinbutton"] .increase:hover, +[role="toolbar"] [role="spinbutton"] .decrease:hover, +[role="toolbar"] [role="spinbutton"]:focus .increase, +[role="toolbar"] [role="spinbutton"]:focus .decrease{ + fill:#005A9C; + border-color: #005A9C; } + + +textarea { + width: 990px; + padding: .25em; + border: 2px solid black; + height: 400px; + font-size: 14pt; + font-family: sans-serif; + border-radius: 5px; +} + + + diff --git a/examples/toolbar/help.html b/examples/toolbar/help.html new file mode 100644 index 0000000000..9f916e2773 --- /dev/null +++ b/examples/toolbar/help.html @@ -0,0 +1,25 @@ + + + + +Toolbar Example | WAI-ARIA Authoring Practices 1.1 + + + + + + + + + + + + + + +

Toolbar Help

+ +

Learn more about toolbar behaviors by reading the ARIA Authoring Practices Guide.

+ + + diff --git a/examples/toolbar/images/menuitemradio-checked.svg b/examples/toolbar/images/menuitemradio-checked.svg new file mode 100644 index 0000000000..8e8ae3b781 --- /dev/null +++ b/examples/toolbar/images/menuitemradio-checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/toolbar/images/menuitemradio-unchecked.svg b/examples/toolbar/images/menuitemradio-unchecked.svg new file mode 100644 index 0000000000..143e662852 --- /dev/null +++ b/examples/toolbar/images/menuitemradio-unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/toolbar/images/pulldown-icon-focus.svg b/examples/toolbar/images/pulldown-icon-focus.svg new file mode 100644 index 0000000000..7da8d1950f --- /dev/null +++ b/examples/toolbar/images/pulldown-icon-focus.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/toolbar/images/pulldown-icon.svg b/examples/toolbar/images/pulldown-icon.svg new file mode 100644 index 0000000000..78ca969f8c --- /dev/null +++ b/examples/toolbar/images/pulldown-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/toolbar/js/FontMenu.js b/examples/toolbar/js/FontMenu.js new file mode 100644 index 0000000000..2b34b30495 --- /dev/null +++ b/examples/toolbar/js/FontMenu.js @@ -0,0 +1,241 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: FontMenu.js +*/ + +var FontMenu = function (domNode, controllerObj) { + var elementChildren, + msgPrefix = 'FontMenu 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.'); + } + + // Check whether domNode child elements are A elements + var childElement = domNode.firstElementChild; + + while (childElement) { + var menuitem = childElement.firstElementChild; + + if (menuitem && menuitem === 'A') { + throw new Error(msgPrefix + 'Cannot have descendant elements are A elements.'); + } + childElement = childElement.nextElementSibling; + } + + this.domNode = domNode; + this.controller = controllerObj; + + 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 + + this.hasFocus = false; // See MenuItem handleFocus, handleBlur + this.hasHover = false; // See PopupMenu handleMouseover, handleMouseout +}; + +/* +* @method FontMenu.prototype.init +* +* @desc +* Add domNode event listeners for mouseover and mouseout. Traverse +* domNode children to configure each menuitem and populate menuitems +* array. Initialize firstItem and lastItem properties. +*/ +FontMenu.prototype.init = function () { + var menuitemElements, menuitemElement, menuItem, textContent, numItems; + + // Configure the domNode itself + this.domNode.setAttribute('tabindex', '-1'); + + this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); + + // Traverse the element children of domNode: configure each with + // menuitem role behavior and store reference in menuitems array. + var menuitemElements = this.domNode.querySelectorAll('[role="menuitemradio"]'); + + for (var i = 0; i < menuitemElements.length; i++) { + menuitemElement = menuitemElements[i]; + menuItem = new FontMenuItem(menuitemElement, this); + menuItem.init(); + this.menuitems.push(menuItem); + textContent = menuitemElement.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]; + } +}; + +/* EVENT HANDLERS */ + +FontMenu.prototype.handleMouseover = function (event) { + this.hasHover = true; +}; + +FontMenu.prototype.handleMouseout = function (event) { + this.hasHover = false; + setTimeout(this.close.bind(this, false), 300); +}; + +/* FOCUS MANAGEMENT METHODS */ + +FontMenu.prototype.setFocusToController = function (command) { + if (typeof command !== 'string') { + command = ''; + } + + if (command === 'previous') { + this.controller.toolbar.setFocusToPrevious(this.controller.toolbarItem); + } + else { + if (command === 'next') { + this.controller.toolbar.setFocusToNext(this.controller.toolbarItem); + } + else { + this.controller.domNode.focus(); + } + } +}; + +FontMenu.prototype.setFontFamily = function (menuitem, font) { + for (var i = 0; i < this.menuitems.length; i++) { + var mi = this.menuitems[i]; + mi.domNode.setAttribute('aria-checked', mi === menuitem); + } + this.controller.setFontFamily(font); +}; + +FontMenu.prototype.setFocusToFirstItem = function () { + this.firstItem.domNode.focus(); +}; + +FontMenu.prototype.setFocusToLastItem = function () { + this.lastItem.domNode.focus(); +}; + +FontMenu.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(); + } +}; + +FontMenu.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(); + } +}; + +FontMenu.prototype.setFocusToCheckedItem = function () { + for (var index = 0; index < this.menuitems.length; index++) { + if (this.menuitems[index].domNode.getAttribute('aria-checked') === 'true') { + this.menuitems[index].domNode.focus(); + } + } +}; + +FontMenu.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(); + } +}; + +FontMenu.prototype.getIndexFirstChars = function (startIndex, char) { + for (var i = startIndex; i < this.firstChars.length; i++) { + if (char === this.firstChars[i]) { + return i; + } + } + return -1; +}; + +/* Focus methods */ + +FontMenu.prototype.setFocus = function () { + this.hasFocus = true; + this.domNode.classList.add('focus'); + this.controller.toolbar.domNode.classList.add('focus'); +}; + +FontMenu.prototype.removeFocus = function () { + this.hasFocus = false; + this.domNode.classList.remove('focus'); + this.controller.toolbar.domNode.classList.remove('focus'); + setTimeout(this.close.bind(this, false), 300); +}; + +/* MENU DISPLAY METHODS */ + +FontMenu.prototype.isOpen = function () { + return this.controller.domNode.getAttribute('aria-expanded') === 'true'; +}; + +FontMenu.prototype.open = function () { + // Get bounding rectangle of controller object's DOM node + var rect = this.controller.domNode.getBoundingClientRect(); + + // Set CSS properties + this.domNode.style.display = 'block'; + this.domNode.style.position = 'absolute'; + this.domNode.style.top = (rect.height - 1) + 'px'; + this.domNode.style.left = '0px'; + this.domNode.style.zIndex = 100; + + // Set aria-expanded attribute + this.controller.domNode.setAttribute('aria-expanded', 'true'); +}; + +FontMenu.prototype.close = function (force) { + if (typeof force !== 'boolean') { + force = false; + } + + if (force || (!this.hasFocus && !this.hasHover && !this.controller.hasHover)) { + this.domNode.style.display = 'none'; + this.controller.domNode.removeAttribute('aria-expanded'); + } +}; diff --git a/examples/toolbar/js/FontMenuButton.js b/examples/toolbar/js/FontMenuButton.js new file mode 100644 index 0000000000..7c5d114003 --- /dev/null +++ b/examples/toolbar/js/FontMenuButton.js @@ -0,0 +1,83 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: FontMenuButton.js +*/ + + +FontMenuButton = function (node, toolbar, toolbarItem) { + this.domNode = node; + this.fontMenu = false; + this.toolbar = toolbar; + this.toolbarItem = toolbarItem; + + this.buttonAction = 'font-family'; + this.value = ''; + + this.keyCode = Object.freeze({ + 'TAB': 9, + 'RETURN': 13, + 'ESC': 27, + 'SPACE': 32, + 'UP': 38, + 'DOWN': 40 + }); +}; + +FontMenuButton.prototype.init = function () { + var id = this.domNode.getAttribute('aria-controls'); + + if (id) { + var node = document.getElementById(id); + + if (node) { + this.fontMenu = new FontMenu(node, this); + this.fontMenu.init(); + } + } + + this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); +}; + +FontMenuButton.prototype.handleKeyDown = function (event) { + var flag = false; + + switch (event.keyCode) { + case this.keyCode.SPACE: + case this.keyCode.RETURN: + case this.keyCode.DOWN: + case this.keyCode.UP: + this.fontMenu.open(); + this.fontMenu.setFocusToCheckedItem(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +FontMenuButton.prototype.handleClick = function (event, menuButton) { + if (this.fontMenu.isOpen()) { + this.fontMenu.close(); + } + else { + this.fontMenu.open(); + } +}; + +FontMenuButton.prototype.setFontFamily = function (font) { + this.value = font; + this.domNode.innerHTML = font.toUpperCase() + ''; + this.domNode.style.fontFamily = font; + this.domNode.setAttribute('aria-label', 'Font: ' + font); + this.toolbar.activateItem(this); +}; + diff --git a/examples/toolbar/js/FontMenuItem.js b/examples/toolbar/js/FontMenuItem.js new file mode 100644 index 0000000000..ca26b5226c --- /dev/null +++ b/examples/toolbar/js/FontMenuItem.js @@ -0,0 +1,172 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: FontMenuItem.js +*/ + + +/* +* @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 PopupMenuAction.js +*/ +var FontMenuItem = function (domNode, fontMenu) { + + this.domNode = domNode; + this.fontMenu = fontMenu; + this.font = ''; + + 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 + }); +}; + +FontMenuItem.prototype.init = function () { + this.domNode.setAttribute('tabindex', '-1'); + + if (!this.domNode.getAttribute('role')) { + this.domNode.setAttribute('role', 'menuitemradio'); + } + + this.font = this.domNode.textContent.trim().toLowerCase(); + + this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); + this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); + +}; + +/* EVENT HANDLERS */ + +FontMenuItem.prototype.handleKeydown = function (event) { + var tgt = event.currentTarget, + flag = false, + char = event.key, + clickEvent; + + function isPrintableCharacter (str) { + return str.length === 1 && str.match(/\S/); + } + + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + + if (event.shiftKey) { + if (isPrintableCharacter(char)) { + this.fontMenu.setFocusByFirstCharacter(this, char); + } + } + else { + + switch (event.keyCode) { + case this.keyCode.SPACE: + case this.keyCode.RETURN: + this.handleClick(event); + flag = true; + break; + + case this.keyCode.ESC: + this.fontMenu.setFocusToController(); + this.fontMenu.close(true); + flag = true; + break; + + case this.keyCode.UP: + this.fontMenu.setFocusToPreviousItem(this); + flag = true; + break; + + case this.keyCode.DOWN: + this.fontMenu.setFocusToNextItem(this); + flag = true; + break; + + case this.keyCode.RIGHT: + flag = true; + break; + + case this.keyCode.LEFT: + flag = true; + break; + + case this.keyCode.HOME: + case this.keyCode.PAGEUP: + this.fontMenu.setFocusToFirstItem(); + flag = true; + break; + + case this.keyCode.END: + case this.keyCode.PAGEDOWN: + this.fontMenu.setFocusToLastItem(); + flag = true; + break; + + case this.keyCode.TAB: + this.fontMenu.setFocusToController(); + this.fontMenu.close(true); + break; + + default: + if (isPrintableCharacter(char)) { + this.fontMenu.setFocusByFirstCharacter(this, char); + } + break; + } + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +FontMenuItem.prototype.handleClick = function (event) { + this.fontMenu.setFontFamily(this, this.font); + this.fontMenu.setFocusToController(); + this.fontMenu.close(true); +}; + +FontMenuItem.prototype.handleFocus = function (event) { + this.fontMenu.setFocus(); +}; + +FontMenuItem.prototype.handleBlur = function (event) { + this.fontMenu.removeFocus(); +}; + +FontMenuItem.prototype.handleMouseover = function (event) { + this.fontMenu.hasHover = true; + this.fontMenu.open(); + +}; + +FontMenuItem.prototype.handleMouseout = function (event) { + this.fontMenu.hasHover = false; + setTimeout(this.fontMenu.close.bind(this.fontMenu, false), 300); +}; diff --git a/examples/toolbar/js/FormatToolbar.js b/examples/toolbar/js/FormatToolbar.js new file mode 100644 index 0000000000..8e888207e2 --- /dev/null +++ b/examples/toolbar/js/FormatToolbar.js @@ -0,0 +1,344 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: FormatToolbar.js +*/ + +/** + * @constructor + * + * @desc + * Format Toolbar object representing the state and interactions for a toolbar widget + * to format the text in a textarea element + * + * @param domNode + * The DOM node pointing to the element with the toolbar tole + */ +FormatToolbar = function (domNode) { + this.domNode = domNode; + this.firstItem = null; + this.lastItem = null; + + this.toolbarItems = []; + this.alignItems = []; + this.textarea = null; + + this.copyButton = null; + this.cutButton = null; + + this.start = null; + this.end = null; + this.ourClipboard = ''; + this.selected = null; + + this.nightModeCheck = null; + +}; + +FormatToolbar.prototype.init = function () { + var i, items, toolbarItem, menuButton; + + this.textarea = document.getElementById(this.domNode.getAttribute('aria-controls')); + this.textarea.style.width = (this.domNode.getBoundingClientRect().width - 12) + 'px'; + this.textarea.addEventListener('mouseup', this.selectTextContent.bind(this)); + this.textarea.addEventListener('keyup', this.selectTextContent.bind(this)); + + this.selected = this.textarea.selectText; + + this.copyButton = this.domNode.querySelector('.copy'); + this.cutButton = this.domNode.querySelector('.cut'); + this.pasteButton = this.domNode.querySelector('.paste'); + + this.nightModeCheck = this.domNode.querySelector('.nightmode'); + items = this.domNode.querySelectorAll('.item'); + + + for (i = 0; i < items.length; i++) { + + toolbarItem = new FormatToolbarItem(items[i], this); + toolbarItem.init(); + + if (items[i].hasAttribute('aria-haspopup')) { + menuButton = new FontMenuButton(items[i], this, toolbarItem); + menuButton.init(); + } + + if (i === 0) { + this.firstItem = toolbarItem; + } + this.lastItem = toolbarItem; + this.toolbarItems.push(toolbarItem); + } + + var spinButtons = this.domNode.querySelectorAll('[role=spinbutton]');; + + for (var i = 0; i < spinButtons.length; i++) { + var s = new SpinButton(spinButtons[i], this); + s.init(); + } + +}; + +FormatToolbar.prototype.selectTextContent = function () { + this.start = this.textarea.selectionStart; + this.end = this.textarea.selectionEnd; + this.selected = this.textarea.value.substring(this.start, this.end); + this.updateDisable(this.copyButton, this.cutButton, this.pasteButton, this.selected); + +}; +FormatToolbar.prototype.updateDisable = function (copyButton, cutButton, pasteButton, selectedContent) { + var start = this.textarea.selectionStart; + var end = this.textarea.selectionEnd; + if (start !== end) { + copyButton.setAttribute('aria-disabled', false); + cutButton.setAttribute('aria-disabled', false); + } + else { + copyButton.setAttribute('aria-disabled', true); + cutButton.setAttribute('aria-disabled', true); + } + if (this.ourClipboard.length > 0) { + pasteButton.setAttribute('aria-disabled', false); + } +}; + +FormatToolbar.prototype.selectText = function (start, end, textarea) { + if (typeof(textarea.selectionStart != undefined)) { + textarea.focus(); + textarea.selectionStart = start; + textarea.selectionEnd = end; + return true; + } +}; +FormatToolbar.prototype.copyTextContent = function (toolbarItem) { + if (this.copyButton.getAttribute('aria-disabled') === 'true') { + return; + } + this.selectText(this.start, this.end, this.textarea); + this.ourClipboard = this.selected; + this.updateDisable(this.copyButton, this.cutButton, this.pasteButton, this.selected); + +}; + +FormatToolbar.prototype.cutTextContent = function (toolbarItem) { + if (this.cutButton.getAttribute('aria-disabled') === 'true') { + return; + } + this.copyTextContent(toolbarItem); + var str = this.textarea.value; + this.textarea.value = str.replace(str.substring(this.start, this.end),''); + this.selected = ''; + this.updateDisable(this.copyButton, this.cutButton, this.pasteButton, this.selected); +}; + +FormatToolbar.prototype.pasteTextContent = function () { + if (this.pasteButton.getAttribute('aria-disabled') === 'true') { + return; + } + var str = this.textarea.value; + this.textarea.value = str.slice(0,this.textarea.selectionStart) + this.ourClipboard + str.slice(this.textarea.selectionEnd); + this.textarea.focus(); + this.updateDisable(this.copyButton, this.cutButton, this.pasteButton, this.selected); +}; + + +FormatToolbar.prototype.toggleBold = function (toolbarItem) { + if (toolbarItem.isPressed()) { + this.textarea.style.fontWeight = 'normal'; + toolbarItem.resetPressed(); + } + else { + this.textarea.style.fontWeight = 'bold'; + toolbarItem.setPressed(); + } +}; + +FormatToolbar.prototype.toggleUnderline = function (toolbarItem) { + if (toolbarItem.isPressed()) { + this.textarea.style.textDecoration = 'none'; + toolbarItem.resetPressed(); + } + else { + this.textarea.style.textDecoration = 'underline'; + toolbarItem.setPressed(); + } +}; + +FormatToolbar.prototype.toggleItalic = function (toolbarItem) { + if (toolbarItem.isPressed()) { + this.textarea.style.fontStyle = 'normal'; + toolbarItem.resetPressed(); + } + else { + this.textarea.style.fontStyle = 'italic'; + toolbarItem.setPressed(); + } +}; + +FormatToolbar.prototype.changeFontSize = function (value) { + this.textarea.style.fontSize = value + 'pt'; +}; + +FormatToolbar.prototype.toggleNightMode = function (toolbarItem) { + if (this.nightModeCheck.checked) { + this.textarea.style.color = '#eee'; + this.textarea.style.background = 'black'; + } + else { + this.textarea.style.color = 'black'; + this.textarea.style.background = 'white'; + } +}; + +FormatToolbar.prototype.redirectLink = function (toolbarItem) { + window.open( + toolbarItem.domNode.href, + '_blank' + ); +}; + +FormatToolbar.prototype.setAlignment = function (toolbarItem) { + for (var i = 0; i < this.alignItems.length; i++) { + this.alignItems[i].resetChecked(); + } + switch (toolbarItem.value) { + case 'left': + this.textarea.style.textAlign = 'left'; + toolbarItem.setChecked(); + break; + case 'center': + this.textarea.style.textAlign = 'center'; + toolbarItem.setChecked(); + break; + case 'right': + this.textarea.style.textAlign = 'right'; + toolbarItem.setChecked(); + break; + + default: + break; + } +}; + +FormatToolbar.prototype.setFocusToFirstAlignItem = function () { + this.setFocusItem(this.alignItems[0]); +}; + +FormatToolbar.prototype.setFocusToLastAlignItem = function () { + this.setFocusItem(this.alignItems[2]); +}; + +FormatToolbar.prototype.setFontFamily = function (font) { + this.textarea.style.fontFamily = font; +}; + +FormatToolbar.prototype.activateItem = function (toolbarItem) { + switch (toolbarItem.buttonAction) { + case 'bold': + this.toggleBold(toolbarItem); + break; + case 'underline': + this.toggleUnderline(toolbarItem); + break; + case 'italic': + this.toggleItalic(toolbarItem); + break; + case 'align': + this.setAlignment(toolbarItem); + break; + case 'copy': + this.copyTextContent(toolbarItem); + break; + case 'cut': + this.cutTextContent(toolbarItem); + break; + case 'paste': + this.pasteTextContent(toolbarItem); + break; + case 'font-family': + this.setFontFamily(toolbarItem.value); + break; + case 'nightmode': + this.toggleNightMode(toolbarItem); + break; + case 'link': + this.redirectLink(toolbarItem); + break; + default: + break; + + } +}; + +/** + * @desc + * Focus on the specified item + * + * @param element + * The item to focus on + */ +FormatToolbar.prototype.setFocusItem = function (item) { + for (var i = 0; i < this.toolbarItems.length; i++) { + this.toolbarItems[i].domNode.setAttribute('tabindex', '-1'); + } + + item.domNode.setAttribute('tabindex', '0'); + item.domNode.focus(); +}; + +FormatToolbar.prototype.setFocusToNext = function (currentItem) { + var index, newItem; + + if (currentItem === this.lastItem) { + newItem = this.firstItem; + } + else { + index = this.toolbarItems.indexOf(currentItem); + newItem = this.toolbarItems[index + 1]; + } + this.setFocusItem(newItem); +}; + +FormatToolbar.prototype.setFocusToPrevious = function (currentItem) { + var index, newItem; + + if (currentItem === this.firstItem) { + newItem = this.lastItem; + } + else { + index = this.toolbarItems.indexOf(currentItem); + newItem = this.toolbarItems[index - 1]; + } + this.setFocusItem(newItem); +}; + +FormatToolbar.prototype.setFocusToFirst = function (currentItem) { + this.setFocusItem(this.firstItem); +}; + +FormatToolbar.prototype.setFocusToLast = function (currentItem) { + this.setFocusItem(this.lastItem); +}; + + +// Initialize toolbars + +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* ARIA Toolbar Examples +* @function onload +* @desc Initialize the toolbar example once the page has loaded +*/ + +window.addEventListener('load', function () { + var toolbars = document.querySelectorAll('[role="toolbar"].format'); + + for (var i = 0; i < toolbars.length; i++) { + var toolbar = new FormatToolbar(toolbars[i]); + + toolbar.init(); + } +}); diff --git a/examples/toolbar/js/FormatToolbarItem.js b/examples/toolbar/js/FormatToolbarItem.js new file mode 100644 index 0000000000..f3e82c4fe9 --- /dev/null +++ b/examples/toolbar/js/FormatToolbarItem.js @@ -0,0 +1,216 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: FontToolbarItem.js +*/ + +FormatToolbarItem = function (domNode, toolbar) { + this.domNode = domNode; + this.toolbar = toolbar; + this.buttonAction = ''; + this.value = ''; + + + 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 + }); +}; + +FormatToolbarItem.prototype.init = function () { + this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); + + if (this.domNode.classList.contains('bold')) { + this.buttonAction = 'bold'; + } + + if (this.domNode.classList.contains('italic')) { + this.buttonAction = 'italic'; + } + + if (this.domNode.classList.contains('underline')) { + this.buttonAction = 'underline'; + } + + if (this.domNode.classList.contains('align-left')) { + this.buttonAction = 'align'; + this.value = 'left'; + this.toolbar.alignItems.push(this); + } + + if (this.domNode.classList.contains('align-center')) { + this.buttonAction = 'align'; + this.value = 'center'; + this.toolbar.alignItems.push(this); + } + + if (this.domNode.classList.contains('align-right')) { + this.buttonAction = 'align'; + this.value = 'right'; + this.toolbar.alignItems.push(this); + } + if (this.domNode.classList.contains('nightmode')) { + this.buttonAction = 'nightmode'; + } + if (this.domNode.classList.contains('link')) { + this.buttonAction = 'link'; + } + if (this.domNode.classList.contains('copy')) { + this.buttonAction = 'copy'; + } + if (this.domNode.classList.contains('paste')) { + this.buttonAction = 'paste'; + } + if (this.domNode.classList.contains('cut')) { + this.buttonAction = 'cut'; + } + if (this.domNode.classList.contains('spinbutton')) { + this.buttonAction = 'changeFontSize'; + } +}; + +FormatToolbarItem.prototype.isPressed = function () { + return this.domNode.getAttribute('aria-pressed') === 'true'; +}; + +FormatToolbarItem.prototype.setPressed = function () { + this.domNode.setAttribute('aria-pressed', 'true'); +}; + +FormatToolbarItem.prototype.resetPressed = function () { + this.domNode.setAttribute('aria-pressed', 'false'); +}; + + +FormatToolbarItem.prototype.setChecked = function () { + this.domNode.setAttribute('aria-checked', 'true'); + this.domNode.checked = true; + +}; + +FormatToolbarItem.prototype.resetChecked = function () { + this.domNode.setAttribute('aria-checked', 'false'); + this.domNode.checked = false; +}; + +FormatToolbarItem.prototype.disable = function () { + this.domNode.setAttribute('aria-disabled', 'true'); +}; + +FormatToolbarItem.prototype.enable = function () { + this.domNode.removeAttribute('aria-disabled'); +}; + +// Events + +FormatToolbarItem.prototype.handleBlur = function (event) { + this.toolbar.domNode.classList.remove('focus'); + + if (this.domNode.classList.contains('nightmode')) { + this.domNode.parentNode.classList.remove('focus'); + } +}; + +FormatToolbarItem.prototype.handleFocus = function (event) { + this.toolbar.domNode.classList.add('focus'); + + if (this.domNode.classList.contains('nightmode')) { + this.domNode.parentNode.classList.add('focus'); + } + +}; + +FormatToolbarItem.prototype.handleKeyDown = function (event) { + var flag = false; + + switch (event.keyCode) { + + case this.keyCode.RETURN: + case this.keyCode.SPACE: + if ((this.buttonAction !== '') && + (this.buttonAction !== 'bold') && + (this.buttonAction !== 'italic') && + (this.buttonAction !== 'underline')) { + this.toolbar.activateItem(this); + if (this.buttonAction !== 'nightmode') { + flag = true; + } + } + break; + + case this.keyCode.RIGHT: + this.toolbar.setFocusToNext(this); + flag = true; + break; + + case this.keyCode.LEFT: + this.toolbar.setFocusToPrevious(this); + flag = true; + break; + + case this.keyCode.HOME: + this.toolbar.setFocusToFirst(this); + flag = true; + break; + + case this.keyCode.END: + this.toolbar.setFocusToLast(this); + flag = true; + break; + + case this.keyCode.UP: + if (this.buttonAction === 'align') { + if (this.domNode.classList.contains('align-left')) { + this.toolbar.setFocusToLastAlignItem(); + } + else { + this.toolbar.setFocusToPrevious(this); + } + flag = true; + } + break; + case this.keyCode.DOWN: + if (this.buttonAction === 'align') { + if (this.domNode.classList.contains('align-right')) { + this.toolbar.setFocusToFirstAlignItem(); + } + else { + this.toolbar.setFocusToNext(this); + } + flag = true; + } + break; + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + +}; + +FormatToolbarItem.prototype.handleClick = function (e) { + + if (this.buttonAction == 'link') { + return; + } + + this.toolbar.setFocusItem(this); + this.toolbar.activateItem(this); +}; diff --git a/examples/toolbar/js/SpinButton.js b/examples/toolbar/js/SpinButton.js new file mode 100644 index 0000000000..6440bc4e43 --- /dev/null +++ b/examples/toolbar/js/SpinButton.js @@ -0,0 +1,143 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* +* File: SpinButton.js +*/ + +// Create SpinButton that contains value, valuemin, valuemax, and valuenow +var SpinButton = function (domNode, toolbar) { + + this.domNode = domNode; + this.toolbar = toolbar; + + this.valueDomNode = domNode.querySelector('.value'); + this.increaseDomNode = domNode.querySelector('.increase'); + this.decreaseDomNode = domNode.querySelector('.decrease'); + + this.valueMin = 8; + this.valueMax = 40; + this.valueNow = 12; + this.valueText = this.valueNow + ' Point'; + + this.keyCode = Object.freeze({ + 'UP': 38, + 'DOWN': 40, + 'PAGEUP': 33, + 'PAGEDOWN': 34, + 'END': 35, + 'HOME': 36 + }); +}; + +// Initialize slider +SpinButton.prototype.init = function () { + + if (this.domNode.getAttribute('aria-valuemin')) { + this.valueMin = parseInt((this.domNode.getAttribute('aria-valuemin'))); + } + + if (this.domNode.getAttribute('aria-valuemax')) { + this.valueMax = parseInt((this.domNode.getAttribute('aria-valuemax'))); + } + + if (this.domNode.getAttribute('aria-valuenow')) { + this.valueNow = parseInt((this.domNode.getAttribute('aria-valuenow'))); + } + + this.setValue(this.valueNow); + + this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); + + this.increaseDomNode.addEventListener('click', this.handleIncreaseClick.bind(this)); + this.decreaseDomNode.addEventListener('click', this.handleDecreaseClick.bind(this)); + +}; + +SpinButton.prototype.setValue = function (value) { + + if (value > this.valueMax) { + value = this.valueMax; + } + + if (value < this.valueMin) { + value = this.valueMin; + } + + this.valueNow = value; + this.valueText = value + ' Point'; + + this.domNode.setAttribute('aria-valuenow', this.valueNow); + this.domNode.setAttribute('aria-valuetext', this.valueText); + + if (this.valueDomNode) { + this.valueDomNode.innerHTML = this.valueText; + } + + this.toolbar.changeFontSize(value); + +}; + +SpinButton.prototype.handleKeyDown = function (event) { + + var flag = false; + + switch (event.keyCode) { + case this.keyCode.DOWN: + this.setValue(this.valueNow - 1); + flag = true; + break; + + case this.keyCode.UP: + this.setValue(this.valueNow + 1); + flag = true; + break; + + case this.keyCode.PAGEDOWN: + this.setValue(this.valueNow - 5); + flag = true; + break; + + case this.keyCode.PAGEUP: + this.setValue(this.valueNow + 5); + flag = true; + break; + + case this.keyCode.HOME: + this.setValue(this.valueMin); + flag = true; + break; + + case this.keyCode.END: + this.setValue(this.valueMax); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.preventDefault(); + event.stopPropagation(); + } + +}; + +SpinButton.prototype.handleIncreaseClick = function (event) { + + this.setValue(this.valueNow + 1); + + event.preventDefault(); + event.stopPropagation(); + +}; + +SpinButton.prototype.handleDecreaseClick = function (event) { + + this.setValue(this.valueNow - 1); + + event.preventDefault(); + event.stopPropagation(); + +}; diff --git a/examples/toolbar/js/main.js b/examples/toolbar/js/main.js deleted file mode 100644 index 8a09ec9f45..0000000000 --- a/examples/toolbar/js/main.js +++ /dev/null @@ -1,13 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* -* ARIA Toolbar Examples -* @function onload -* @desc Initialize the toolbar example once the page has loaded -*/ - -window.addEventListener('load', function () { - var ex1 = document.getElementById('ex1'); - var ex1Toolbar = new aria.Toolbar(ex1.querySelector('[role="toolbar"]')); -}); diff --git a/examples/toolbar/js/menuButton.js b/examples/toolbar/js/menuButton.js deleted file mode 100644 index cd68793b8a..0000000000 --- a/examples/toolbar/js/menuButton.js +++ /dev/null @@ -1,566 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -*/ - -/** - * ARIA Menu Button example - * @function onload - * @desc after page has loaded initialize all menu buttons based on the selector "[aria-haspopup][aria-controls]" - */ - -window.addEventListener('load', function () { - - var menuButtons = document.querySelectorAll('[aria-haspopup][aria-controls]'); - - [].forEach.call(menuButtons, function (menuButton) { - if ( - menuButton && - menuButton.tagName.toLowerCase() === 'button' || - menuButton.getAttribute('role').toLowerCase() === 'button' - ) { - var mb = new aria.widget.MenuButton(menuButton); - mb.initMenuButton(); - } - }); -}); - -/** - * @namespace aria - */ - -var aria = aria || {}; - -/* ---------------------------------------------------------------- */ -/* ARIA Utils Namespace */ -/* ---------------------------------------------------------------- */ - -/** - * @constructor Menu - * - * @memberOf aria.Utils - * - * @desc Computes absolute position of an element - */ - -aria.Utils = aria.Utils || {}; - -aria.Utils.findPos = function (element) { - var xPosition = 0; - var yPosition = 0; - - while (element) { - xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft); - yPosition += (element.offsetTop - element.scrollTop + element.clientTop); - element = element.offsetParent; - } - return {x: xPosition, y: yPosition}; -}; - -/* ---------------------------------------------------------------- */ -/* ARIA Widget Namespace */ -/* ---------------------------------------------------------------- */ - -aria.widget = aria.widget || {}; - -/* ---------------------------------------------------------------- */ -/* Menu Button Widget */ -/* ---------------------------------------------------------------- */ - -/** - * @constructor Menu - * - * @memberOf aria.Widget - * - * @desc Creates a Menu Button widget using ARIA - */ - -aria.widget.Menu = function (node, menuButton) { - - 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 - }); - - // Check fo DOM element node - if (typeof node !== 'object' || !node.getElementsByClassName) { - return false; - } - - this.menuNode = node; - node.tabIndex = -1; - - this.menuButton = menuButton; - - this.firstMenuItem = false; - this.lastMenuItem = false; - -}; - -/** - * @method initMenuButton - * - * @memberOf aria.widget.Menu - * - * @desc Adds event handlers to button elements - */ - -aria.widget.Menu.prototype.initMenu = function () { - - var self = this; - - var cn = this.menuNode.firstChild; - - while (cn) { - if (cn.nodeType === Node.ELEMENT_NODE) { - if (cn.getAttribute('role') === 'menuitem') { - cn.tabIndex = -1; - if (!this.firstMenuItem) { - this.firstMenuItem = cn; - } - this.lastMenuItem = cn; - - var eventKeyDown = function (event) { - self.eventKeyDown(event, self); - }; - cn.addEventListener('keydown', eventKeyDown); - - var eventMouseClick = function (event) { - self.eventMouseClick(event, self); - }; - cn.addEventListener('click', eventMouseClick); - - var eventBlur = function (event) { - self.eventBlur(event, self); - }; - cn.addEventListener('blur', eventBlur); - - var eventFocus = function (event) { - self.eventFocus(event, self); - }; - cn.addEventListener('focus', eventFocus); - - } - } - cn = cn.nextSibling; - } -}; - -/** - * @method nextMenuItem - * - * @memberOf aria.widget.Menu - * - * @desc Moves focus to next menuItem - */ - -aria.widget.Menu.prototype.nextMenuItem = function (currentMenuItem) { - var mi = currentMenuItem.nextSibling; - - while (mi) { - if ( - (mi.nodeType === Node.ELEMENT_NODE) && - (mi.getAttribute('role') === 'menuitem') - ) { - mi.focus(); - break; - } - mi = mi.nextSibling; - } - - if (!mi && this.firstMenuItem) { - this.firstMenuItem.focus(); - } -}; - -/** - * @method previousMenuItem - * - * @memberOf aria.widget.Menu - * - * @desc Moves focus to previous menuItem - */ - -aria.widget.Menu.prototype.previousMenuItem = function (currentMenuItem) { - var mi = currentMenuItem.previousSibling; - - while (mi) { - if ( - mi.nodeType === Node.ELEMENT_NODE && - mi.getAttribute('role') === 'menuitem' - ) { - mi.focus(); - break; - } - mi = mi.previousSibling; - } - - if (!mi && this.lastMenuItem) { - this.lastMenuItem.focus(); - } -}; - -/** - * @method eventKeyDown - * - * @memberOf aria.widget.Menu - * - * @desc Keydown event handler for Menu Object - * NOTE: The menu parameter is needed to provide a reference to the specific - * menu - */ - -aria.widget.Menu.prototype.eventKeyDown = function (event, menu) { - - var ct = event.currentTarget; - var flag = false; - - switch (event.keyCode) { - - case menu.keyCode.SPACE: - case menu.keyCode.RETURN: - menu.eventMouseClick(event, menu); - menu.menuButton.closeMenu(true); - flag = true; - break; - - case menu.keyCode.ESC: - menu.menuButton.closeMenu(true); - menu.menuButton.buttonNode.focus(); - flag = true; - break; - - case menu.keyCode.UP: - case menu.keyCode.LEFT: - menu.previousMenuItem(ct); - flag = true; - break; - - case menu.keyCode.DOWN: - case menu.keyCode.RIGHT: - menu.nextMenuItem(ct); - flag = true; - break; - - case menu.keyCode.TAB: - menu.menuButton.closeMenu(true, false); - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - -}; - -/** - * @method eventMouseClick - * - * @memberOf aria.widget.Menu - * - * @desc onclick event handler for Menu Object - * NOTE: The menu parameter is needed to provide a reference to the specific - * menu - */ - -aria.widget.Menu.prototype.eventMouseClick = function (event, menu) { - - var clickedItemText = event.target.innerText; - this.menuButton.buttonNode.innerText = clickedItemText; - menu.menuButton.closeMenu(true); - -}; - -/** - * @method eventBlur - * - * @memberOf aria.widget.Menu - * - * @desc eventBlur event handler for Menu Object - * NOTE: The menu parameter is needed to provide a reference to the specific - * menu - */ -aria.widget.Menu.prototype.eventBlur = function (event, menu) { - menu.menuHasFocus = false; - setTimeout(function () { - if (!menu.menuHasFocus) { - menu.menuButton.closeMenu(false, false); - } - }, 200); -}; - -/** - * @method eventFocus - * - * @memberOf aria.widget.Menu - * - * @desc eventFoucs event handler for Menu Object - * NOTE: The menu parameter is needed to provide a reference to the specific - * menu - */ -aria.widget.Menu.prototype.eventFocus = function (event, menu) { - menu.menuHasFocus = true; -}; - -/* ---------------------------------------------------------------- */ -/* Menu Button Widget */ -/* ---------------------------------------------------------------- */ - -/** - * @constructor Menu Button - * - * @memberOf aria.Widget - * - * @desc Creates a Menu Button widget using ARIA - */ - -aria.widget.MenuButton = function (node) { - - this.keyCode = Object.freeze({ - 'TAB': 9, - 'RETURN': 13, - 'ESC': 27, - 'SPACE': 32, - 'UP': 38, - 'DOWN': 40 - }); - - // Check fo DOM element node - if (typeof node !== 'object' || !node.getElementsByClassName) { - return false; - } - - this.done = true; - this.mouseInMouseButton = false; - this.menuHasFocus = false; - this.buttonNode = node; - this.isLink = false; - - if (node.tagName.toLowerCase() === 'a') { - var url = node.getAttribute('href'); - if (url && url.length && (url.length > 0)) { - this.isLink = true; - } - } - -}; - -/** - * @method initMenuButton - * - * @memberOf aria.widget.MenuButton - * - * @desc Adds event handlers to button elements - */ - -aria.widget.MenuButton.prototype.initMenuButton = function () { - var id = this.buttonNode.getAttribute('aria-controls'); - - if (id) { - this.menuNode = document.getElementById(id); - - if (this.menuNode) { - this.menu = new aria.widget.Menu(this.menuNode, this); - this.menu.initMenu(); - this.menuShowing = false; - } - } - - this.closeMenu(true, false); - - var self = this; - - var eventKeyDown = function (event) { - self.eventKeyDown(event, self); - }; - this.buttonNode.addEventListener('keydown', eventKeyDown); - - var eventMouseClick = function (event) { - self.eventMouseClick(event, self); - }; - this.buttonNode.addEventListener('click', eventMouseClick); -}; - -/** - * @method openMenu - * - * @memberOf aria.widget.MenuButton - * - * @desc Opens the menu - */ - -aria.widget.MenuButton.prototype.openMenu = function () { - if (this.menuNode) { - this.menuNode.style.display = 'block'; - this.menuShowing = true; - } -}; - -/** - * @method closeMenu - * - * @memberOf aria.widget.MenuButton - * - * @desc Close the menu - */ - -aria.widget.MenuButton.prototype.closeMenu = function (force, focusMenuButton) { - if (typeof force !== 'boolean') { - force = false; - } - if (typeof focusMenuButton !== 'boolean') { - focusMenuButton = true; - } - - if ( - force || - ( - !this.mouseInMenuButton && - this.menuNode && - !this.menu.mouseInMenu && - !this.menu.menuHasFocus - ) - ) { - this.menuNode.style.display = 'none'; - if (focusMenuButton) { - this.buttonNode.focus(); - } - this.menuShowing = false; - } -}; - -/** - * @method toggleMenu - * - * @memberOf aria.widget.MenuButton - * - * @desc Close or open the menu depending on current state - */ - -aria.widget.MenuButton.prototype.toggleMenu = function () { - - if (this.menuNode) { - if (this.menuNode.style.display === 'block') { - this.menuNode.style.display = 'none'; - } - else { - this.menuNode.style.display = 'block'; - } - } - -}; - -/** - * @method moveFocusToFirstMenuItem - * - * @memberOf aria.widget.MenuButton - * - * @desc Move keyboard focus to first menu item - */ - -aria.widget.MenuButton.prototype.moveFocusToFirstMenuItem = function () { - if (this.menu.firstMenuItem) { - this.openMenu(); - this.menu.firstMenuItem.focus(); - } - -}; - -/** - * @method moveFocusToLastMenuItem - * - * @memberOf aria.widget.MenuButton - * - * @desc Move keyboard focus to last menu item - */ - -aria.widget.MenuButton.prototype.moveFocusToLastMenuItem = function () { - - if (this.menu.lastMenuItem) { - this.openMenu(); - this.menu.lastMenuItem.focus(); - } - -}; - -/** - * @method eventKeyDown - * - * @memberOf aria.widget.MenuButton - * - * @desc Keydown event handler for MenuButton Object - * NOTE: The menuButton parameter is needed to provide a reference to the specific - * menuButton - */ - -aria.widget.MenuButton.prototype.eventKeyDown = function (event, menuButton) { - - var flag = false; - - switch (event.keyCode) { - - case menuButton.keyCode.SPACE: - menuButton.moveFocusToFirstMenuItem(); - flag = true; - break; - - case menuButton.keyCode.RETURN: - menuButton.moveFocusToFirstMenuItem(); - flag = true; - break; - - case menuButton.keyCode.UP: - if (this.menuShowing) { - menuButton.moveFocusToLastMenuItem(); - flag = true; - } - break; - - case menuButton.keyCode.DOWN: - if (this.menuShowing) { - menuButton.moveFocusToFirstMenuItem(); - flag = true; - } - break; - - case menuButton.keyCode.TAB: - menuButton.closeMenu(true, false); - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - -}; - -/** - * @method eventMouseClick - * - * @memberOf aria.widget.MenuButton - * - * @desc Click event handler for MenuButton Object - * NOTE: The menuButton parameter is needed to provide a reference to the specific - * menuButton - */ -aria.widget.MenuButton.prototype.eventMouseClick = function (event, menuButton) { - menuButton.moveFocusToFirstMenuItem(); -}; diff --git a/examples/toolbar/js/toolbar.js b/examples/toolbar/js/toolbar.js deleted file mode 100644 index 783be064b8..0000000000 --- a/examples/toolbar/js/toolbar.js +++ /dev/null @@ -1,116 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -*/ -/** - * @namespace aria - */ -var aria = aria || {}; - -/** - * @constructor - * - * @desc - * Toolbar object representing the state and interactions for a toolbar widget - * - * @param toolbarNode - * The DOM node pointing to the toolbar - */ -aria.Toolbar = function (toolbarNode) { - this.toolbarNode = toolbarNode; - this.items = this.toolbarNode.querySelectorAll('.toolbar-item'); - this.selectedItem = this.toolbarNode.querySelector('.selected'); - this.registerEvents(); -}; - -/** - * @desc - * Register events for the toolbar interactions - */ -aria.Toolbar.prototype.registerEvents = function () { - this.toolbarNode.addEventListener('keydown', this.checkFocusChange.bind(this)); - this.toolbarNode.addEventListener('click', this.checkClickItem.bind(this)); -}; - -/** - * @desc - * Handle various keyboard controls; LEFT/RIGHT will shift focus; DOWN - * activates a menu button if it is the focused item. - * - * @param evt - * The keydown event object - */ -aria.Toolbar.prototype.checkFocusChange = function (evt) { - var key = evt.which || evt.keyCode; - var nextIndex, nextItem; - - switch (key) { - case aria.KeyCode.LEFT: - case aria.KeyCode.RIGHT: - nextIndex = Array.prototype.indexOf.call(this.items, this.selectedItem); - nextIndex = key === aria.KeyCode.LEFT ? nextIndex - 1 : nextIndex + 1; - nextIndex = Math.max(Math.min(nextIndex, this.items.length - 1), 0); - - nextItem = this.items[nextIndex]; - this.selectItem(nextItem); - this.focusItem(nextItem); - break; - case aria.KeyCode.DOWN: - // if selected item is menu button, pressing DOWN should act like a click - if (aria.Utils.hasClass(this.selectedItem, 'menu-button')) { - evt.preventDefault(); - this.selectedItem.click(); - } - break; - } -}; - -/** - * @desc - * Selects a toolbar item if it is clicked - * - * @param evt - * The click event object - */ -aria.Toolbar.prototype.checkClickItem = function (evt) { - if (aria.Utils.hasClass(evt.target, 'toolbar-item')) { - this.selectItem(evt.target); - } -}; - -/** - * @desc - * Deselect the specified item - * - * @param element - * The item to deselect - */ -aria.Toolbar.prototype.deselectItem = function (element) { - aria.Utils.removeClass(element, 'selected'); - element.setAttribute('tabindex', '-1'); -}; - -/** - * @desc - * Deselect the currently selected item and select the specified item - * - * @param element - * The item to select - */ -aria.Toolbar.prototype.selectItem = function (element) { - this.deselectItem(this.selectedItem); - aria.Utils.addClass(element, 'selected'); - element.setAttribute('tabindex', '0'); - this.selectedItem = element; -}; - -/** - * @desc - * Focus on the specified item - * - * @param element - * The item to focus on - */ -aria.Toolbar.prototype.focusItem = function (element) { - element.focus(); -}; diff --git a/examples/toolbar/toolbar.html b/examples/toolbar/toolbar.html index 36e32394da..cfb7b647ef 100644 --- a/examples/toolbar/toolbar.html +++ b/examples/toolbar/toolbar.html @@ -7,17 +7,24 @@ + + + - - - - + + + + + + + +