diff options
Diffstat (limited to 'chromium/chrome/browser/resources/chromeos/chromevox/common/aria_util.js')
-rw-r--r-- | chromium/chrome/browser/resources/chromeos/chromevox/common/aria_util.js | 988 |
1 files changed, 988 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/common/aria_util.js b/chromium/chrome/browser/resources/chromeos/chromevox/common/aria_util.js new file mode 100644 index 00000000000..47d2ee96c30 --- /dev/null +++ b/chromium/chrome/browser/resources/chromeos/chromevox/common/aria_util.js @@ -0,0 +1,988 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview A collection of JavaScript utilities used to simplify working + * with ARIA (http://www.w3.org/TR/wai-aria). + */ + + +goog.provide('cvox.AriaUtil'); +goog.require('cvox.AbstractEarcons'); +goog.require('cvox.ChromeVox'); +goog.require('cvox.NodeState'); +goog.require('cvox.NodeStateUtil'); + + +/** + * Create the namespace + * @constructor + */ +cvox.AriaUtil = function() { +}; + + +/** + * A constant indicating no role name. + * @type {string} + */ +cvox.AriaUtil.NO_ROLE_NAME = ' '; + +/** + * A mapping from ARIA role names to their message ids. + * Note: If you are adding a new mapping, the new message identifier needs a + * corresponding braille message. For example, a message id 'tag_button' + * requires another message 'tag_button_brl' within messages.js. + * @type {Object.<string, string>} + */ +cvox.AriaUtil.WIDGET_ROLE_TO_NAME = { + 'alert' : 'aria_role_alert', + 'alertdialog' : 'aria_role_alertdialog', + 'button' : 'aria_role_button', + 'checkbox' : 'aria_role_checkbox', + 'columnheader' : 'aria_role_columnheader', + 'combobox' : 'aria_role_combobox', + 'dialog' : 'aria_role_dialog', + 'grid' : 'aria_role_grid', + 'gridcell' : 'aria_role_gridcell', + 'link' : 'aria_role_link', + 'listbox' : 'aria_role_listbox', + 'log' : 'aria_role_log', + 'marquee' : 'aria_role_marquee', + 'menu' : 'aria_role_menu', + 'menubar' : 'aria_role_menubar', + 'menuitem' : 'aria_role_menuitem', + 'menuitemcheckbox' : 'aria_role_menuitemcheckbox', + 'menuitemradio' : 'aria_role_menuitemradio', + 'option' : cvox.AriaUtil.NO_ROLE_NAME, + 'progressbar' : 'aria_role_progressbar', + 'radio' : 'aria_role_radio', + 'radiogroup' : 'aria_role_radiogroup', + 'rowheader' : 'aria_role_rowheader', + 'scrollbar' : 'aria_role_scrollbar', + 'slider' : 'aria_role_slider', + 'spinbutton' : 'aria_role_spinbutton', + 'status' : 'aria_role_status', + 'tab' : 'aria_role_tab', + 'tablist' : 'aria_role_tablist', + 'tabpanel' : 'aria_role_tabpanel', + 'textbox' : 'aria_role_textbox', + 'timer' : 'aria_role_timer', + 'toolbar' : 'aria_role_toolbar', + 'tooltip' : 'aria_role_tooltip', + 'treeitem' : 'aria_role_treeitem' +}; + + +/** + * Note: If you are adding a new mapping, the new message identifier needs a + * corresponding braille message. For example, a message id 'tag_button' + * requires another message 'tag_button_brl' within messages.js. + * @type {Object.<string, string>} + */ +cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME = { + 'article' : 'aria_role_article', + 'application' : 'aria_role_application', + 'banner' : 'aria_role_banner', + 'columnheader' : 'aria_role_columnheader', + 'complementary' : 'aria_role_complementary', + 'contentinfo' : 'aria_role_contentinfo', + 'definition' : 'aria_role_definition', + 'directory' : 'aria_role_directory', + 'document' : 'aria_role_document', + 'form' : 'aria_role_form', + 'group' : 'aria_role_group', + 'heading' : 'aria_role_heading', + 'img' : 'aria_role_img', + 'list' : 'aria_role_list', + 'listitem' : 'aria_role_listitem', + 'main' : 'aria_role_main', + 'math' : 'aria_role_math', + 'navigation' : 'aria_role_navigation', + 'note' : 'aria_role_note', + 'region' : 'aria_role_region', + 'rowheader' : 'aria_role_rowheader', + 'search' : 'aria_role_search', + 'separator' : 'aria_role_separator' +}; + + +/** + * @type {Array.<Object>} + */ +cvox.AriaUtil.ATTRIBUTE_VALUE_TO_STATUS = [ + { name: 'aria-autocomplete', values: + {'inline' : 'aria_autocomplete_inline', + 'list' : 'aria_autocomplete_list', + 'both' : 'aria_autocomplete_both'} }, + { name: 'aria-checked', values: + {'true' : 'aria_checked_true', + 'false' : 'aria_checked_false', + 'mixed' : 'aria_checked_mixed'} }, + { name: 'aria-disabled', values: + {'true' : 'aria_disabled_true'} }, + { name: 'aria-expanded', values: + {'true' : 'aria_expanded_true', + 'false' : 'aria_expanded_false'} }, + { name: 'aria-invalid', values: + {'true' : 'aria_invalid_true', + 'grammar' : 'aria_invalid_grammar', + 'spelling' : 'aria_invalid_spelling'} }, + { name: 'aria-multiline', values: + {'true' : 'aria_multiline_true'} }, + { name: 'aria-multiselectable', values: + {'true' : 'aria_multiselectable_true'} }, + { name: 'aria-pressed', values: + {'true' : 'aria_pressed_true', + 'false' : 'aria_pressed_false', + 'mixed' : 'aria_pressed_mixed'} }, + { name: 'aria-readonly', values: + {'true' : 'aria_readonly_true'} }, + { name: 'aria-required', values: + {'true' : 'aria_required_true'} }, + { name: 'aria-selected', values: + {'true' : 'aria_selected_true', + 'false' : 'aria_selected_false'} } +]; + + +/** + * Checks if a node should be treated as a hidden node because of its ARIA + * markup. + * + * @param {Node} targetNode The node to check. + * @return {boolean} True if the targetNode should be treated as hidden. + */ +cvox.AriaUtil.isHiddenRecursive = function(targetNode) { + if (cvox.AriaUtil.isHidden(targetNode)) { + return true; + } + var parent = targetNode.parentElement; + while (parent) { + if ((parent.getAttribute('aria-hidden') == 'true') && + (parent.getAttribute('chromevoxignoreariahidden') != 'true')) { + return true; + } + parent = parent.parentElement; + } + return false; +}; + + +/** + * Checks if a node should be treated as a hidden node because of its ARIA + * markup. Does not check parents, so if you need to know if this is a + * descendant of a hidden node, call isHiddenRecursive. + * + * @param {Node} targetNode The node to check. + * @return {boolean} True if the targetNode should be treated as hidden. + */ +cvox.AriaUtil.isHidden = function(targetNode) { + if (!targetNode) { + return true; + } + if (targetNode.getAttribute) { + if ((targetNode.getAttribute('aria-hidden') == 'true') && + (targetNode.getAttribute('chromevoxignoreariahidden') != 'true')) { + return true; + } + } + return false; +}; + + +/** + * Checks if a node should be treated as a visible node because of its ARIA + * markup, regardless of whatever other styling/attributes it may have. + * It is possible to force a node to be visible by setting aria-hidden to + * false. + * + * @param {Node} targetNode The node to check. + * @return {boolean} True if the targetNode should be treated as visible. + */ +cvox.AriaUtil.isForcedVisibleRecursive = function(targetNode) { + var node = targetNode; + while (node) { + if (node.getAttribute) { + // Stop and return the result based on the closest node that has + // aria-hidden set. + if (node.hasAttribute('aria-hidden') && + (node.getAttribute('chromevoxignoreariahidden') != 'true')) { + return node.getAttribute('aria-hidden') == 'false'; + } + } + node = node.parentElement; + } + return false; +}; + + +/** + * Checks if a node should be treated as a leaf node because of its ARIA + * markup. Does not check recursively, and does not check isControlWidget. + * Note that elements with aria-label are treated as leaf elements. See: + * http://www.w3.org/TR/wai-aria/roles#textalternativecomputation + * + * @param {Element} targetElement The node to check. + * @return {boolean} True if the targetNode should be treated as a leaf node. + */ +cvox.AriaUtil.isLeafElement = function(targetElement) { + var role = targetElement.getAttribute('role'); + var hasArialLabel = targetElement.hasAttribute('aria-label') && + (targetElement.getAttribute('aria-label').length > 0); + return (role == 'img' || role == 'progressbar' || hasArialLabel); +}; + + +/** + * Determines whether or not a node is or is the descendant of a node + * with a particular role. + * + * @param {Node} node The node to be checked. + * @param {string} roleName The role to check for. + * @return {boolean} True if the node or one of its ancestor has the specified + * role. + */ +cvox.AriaUtil.isDescendantOfRole = function(node, roleName) { + while (node) { + if (roleName && node && (node.getAttribute('role') == roleName)) { + return true; + } + node = node.parentNode; + } + return false; +}; + + +/** + * Helper function to return the role name message identifier for a role. + * @param {string} role The role. + * @return {?string} The role name message identifier. + * @private + */ +cvox.AriaUtil.getRoleNameMsgForRole_ = function(role) { + var msgId = cvox.AriaUtil.WIDGET_ROLE_TO_NAME[role]; + if (!msgId) { + return null; + } + if (msgId == cvox.AriaUtil.NO_ROLE_NAME) { + // TODO(dtseng): This isn't the way to insert silence; beware! + return ' '; + } + return msgId; +}; + +/** + * Returns true is the node is any kind of button. + * + * @param {Node} node The node to check. + * @return {boolean} True if the node is a button. + */ +cvox.AriaUtil.isButton = function(node) { + var role = cvox.AriaUtil.getRoleAttribute(node); + if (role == 'button') { + return true; + } + if (node.tagName == 'BUTTON') { + return true; + } + if (node.tagName == 'INPUT') { + return (node.type == 'submit' || + node.type == 'reset' || + node.type == 'button'); + } + return false; +}; + +/** + * Returns a role message identifier for a node. + * For a localized string, see cvox.AriaUtil.getRoleName. + * @param {Node} targetNode The node to get the role name for. + * @return {string} The role name message identifier for the targetNode. + */ +cvox.AriaUtil.getRoleNameMsg = function(targetNode) { + var roleName; + if (targetNode && targetNode.getAttribute) { + var role = cvox.AriaUtil.getRoleAttribute(targetNode); + + // Special case for pop-up buttons. + if (targetNode.getAttribute('aria-haspopup') == 'true' && + cvox.AriaUtil.isButton(targetNode)) { + return 'aria_role_popup_button'; + } + + if (role) { + roleName = cvox.AriaUtil.getRoleNameMsgForRole_(role); + if (!roleName) { + roleName = cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME[role]; + } + } + + // To a user, a menu item within a menu bar is called a "menu"; + // any other menu item is called a "menu item". + // + // TODO(deboer): This block feels like a hack. dmazzoni suggests + // using css-like syntax for names. Investigate further if + // we need more of these hacks. + if (role == 'menuitem') { + var container = targetNode.parentElement; + while (container) { + if (container.getAttribute && + (cvox.AriaUtil.getRoleAttribute(container) == 'menu' || + cvox.AriaUtil.getRoleAttribute(container) == 'menubar')) { + break; + } + container = container.parentElement; + } + if (container && cvox.AriaUtil.getRoleAttribute(container) == 'menubar') { + roleName = cvox.AriaUtil.getRoleNameMsgForRole_('menu'); + } // else roleName is already 'Menu item', no need to change it. + } + } + if (!roleName) { + roleName = ''; + } + return roleName; +}; + +/** + * Returns a string to be presented to the user that identifies what the + * targetNode's role is. + * + * @param {Node} targetNode The node to get the role name for. + * @return {string} The role name for the targetNode. + */ +cvox.AriaUtil.getRoleName = function(targetNode) { + var roleMsg = cvox.AriaUtil.getRoleNameMsg(targetNode); + var roleName = cvox.ChromeVox.msgs.getMsg(roleMsg); + var role = cvox.AriaUtil.getRoleAttribute(targetNode); + if ((role == 'heading') && (targetNode.hasAttribute('aria-level'))) { + roleName += ' ' + targetNode.getAttribute('aria-level'); + } + return roleName ? roleName : ''; +}; + +/** + * Returns a string that gives information about the state of the targetNode. + * + * @param {Node} targetNode The node to get the state information for. + * @param {boolean} primary Whether this is the primary node we're + * interested in, where we might want extra information - as + * opposed to an ancestor, where we might be more brief. + * @return {cvox.NodeState} The status information about the node. + */ +cvox.AriaUtil.getStateMsgs = function(targetNode, primary) { + var state = []; + if (!targetNode || !targetNode.getAttribute) { + return state; + } + + for (var i = 0, attr; attr = cvox.AriaUtil.ATTRIBUTE_VALUE_TO_STATUS[i]; + i++) { + var value = targetNode.getAttribute(attr.name); + var msg_id = attr.values[value]; + if (msg_id) { + state.push([msg_id]); + } + } + if (targetNode.getAttribute('role') == 'grid') { + return cvox.AriaUtil.getGridState_(targetNode, targetNode); + } + + var role = cvox.AriaUtil.getRoleAttribute(targetNode); + if (targetNode.getAttribute('aria-haspopup') == 'true') { + if (role == 'menuitem') { + state.push(['has_submenu']); + } else if (cvox.AriaUtil.isButton(targetNode)) { + // Do nothing - the role name will be 'pop-up button'. + } else { + state.push(['has_popup']); + } + } + + var valueText = targetNode.getAttribute('aria-valuetext'); + if (valueText) { + // If there is a valueText, that always wins. + state.push(['aria_value_text', valueText]); + return state; + } + + var valueNow = targetNode.getAttribute('aria-valuenow'); + var valueMin = targetNode.getAttribute('aria-valuemin'); + var valueMax = targetNode.getAttribute('aria-valuemax'); + + // Scrollbar and progressbar should speak the percentage. + // http://www.w3.org/TR/wai-aria/roles#scrollbar + // http://www.w3.org/TR/wai-aria/roles#progressbar + if ((valueNow != null) && (valueMin != null) && (valueMax != null)) { + if ((role == 'scrollbar') || (role == 'progressbar')) { + var percent = Math.round((valueNow / (valueMax - valueMin)) * 100); + state.push(['state_percent', percent]); + return state; + } + } + + // Return as many of the value attributes as possible. + if (valueNow != null) { + state.push(['aria_value_now', valueNow]); + } + if (valueMin != null) { + state.push(['aria_value_min', valueMin]); + } + if (valueMax != null) { + state.push(['aria_value_max', valueMax]); + } + + // If this is a composite control or an item within a composite control, + // get the index and count of the current descendant or active + // descendant. + var parentControl = targetNode; + var currentDescendant = null; + + if (cvox.AriaUtil.isCompositeControl(parentControl) && primary) { + currentDescendant = cvox.AriaUtil.getActiveDescendant(parentControl); + } else { + role = cvox.AriaUtil.getRoleAttribute(targetNode); + if (role == 'option' || + role == 'menuitem' || + role == 'menuitemcheckbox' || + role == 'menuitemradio' || + role == 'radio' || + role == 'tab' || + role == 'treeitem') { + currentDescendant = targetNode; + parentControl = targetNode.parentElement; + while (parentControl && + !cvox.AriaUtil.isCompositeControl(parentControl)) { + parentControl = parentControl.parentElement; + if (parentControl && + cvox.AriaUtil.getRoleAttribute(parentControl) == 'treeitem') { + break; + } + } + } + } + + if (parentControl && + (cvox.AriaUtil.isCompositeControl(parentControl) || + cvox.AriaUtil.getRoleAttribute(parentControl) == 'treeitem') && + currentDescendant) { + var parentRole = cvox.AriaUtil.getRoleAttribute(parentControl); + var descendantRoleList; + switch (parentRole) { + case 'combobox': + case 'listbox': + descendantRoleList = ['option']; + break; + case 'menu': + descendantRoleList = ['menuitem', + 'menuitemcheckbox', + 'menuitemradio']; + break; + case 'radiogroup': + descendantRoleList = ['radio']; + break; + case 'tablist': + descendantRoleList = ['tab']; + break; + case 'tree': + case 'treegrid': + case 'treeitem': + descendantRoleList = ['treeitem']; + break; + } + + if (descendantRoleList) { + var listLength; + var currentIndex; + + var ariaLength = + parseInt(currentDescendant.getAttribute('aria-setsize'), 10); + if (!isNaN(ariaLength)) { + listLength = ariaLength; + } + var ariaIndex = + parseInt(currentDescendant.getAttribute('aria-posinset'), 10); + if (!isNaN(ariaIndex)) { + currentIndex = ariaIndex; + } + + if (listLength == undefined || currentIndex == undefined) { + var descendants = cvox.AriaUtil.getNextLevel(parentControl, + descendantRoleList); + if (listLength == undefined) { + listLength = descendants.length; + } + if (currentIndex == undefined) { + for (var j = 0; j < descendants.length; j++) { + if (descendants[j] == currentDescendant) { + currentIndex = j + 1; + } + } + } + } + if (currentIndex && listLength) { + state.push(['list_position', currentIndex, listLength]); + } + } + } + return state; +}; + + +/** + * Returns a string that gives information about the state of the grid node. + * + * @param {Node} targetNode The node to get the state information for. + * @param {Node} parentControl The parent composite control. + * @return {cvox.NodeState} The status information about the node. + * @private + */ +cvox.AriaUtil.getGridState_ = function(targetNode, parentControl) { + var activeDescendant = cvox.AriaUtil.getActiveDescendant(parentControl); + + if (activeDescendant) { + var descendantSelector = '*[role~="row"]'; + var rows = parentControl.querySelectorAll(descendantSelector); + var currentIndex = null; + for (var j = 0; j < rows.length; j++) { + var gridcells = rows[j].querySelectorAll('*[role~="gridcell"]'); + for (var k = 0; k < gridcells.length; k++) { + if (gridcells[k] == activeDescendant) { + return /** @type {cvox.NodeState} */ ( + [['aria_role_gridcell_pos', j + 1, k + 1]]); + } + } + } + } + return []; +}; + + +/** + * Returns the id of a node's active descendant + * @param {Node} targetNode The node. + * @return {?string} The id of the active descendant. + * @private + */ +cvox.AriaUtil.getActiveDescendantId_ = function(targetNode) { + if (!targetNode.getAttribute) { + return null; + } + + var activeId = targetNode.getAttribute('aria-activedescendant'); + if (!activeId) { + return null; + } + return activeId; +}; + + +/** + * Returns the list of elements that are one aria-level below. + * + * @param {Node} parentControl The node whose descendants should be analyzed. + * @param {Array.<string>} role The role(s) of descendant we are looking for. + * @return {Array.<Node>} The array of matching nodes. + */ +cvox.AriaUtil.getNextLevel = function(parentControl, role) { + var result = []; + var children = parentControl.childNodes; + var length = children.length; + for (var i = 0; i < children.length; i++) { + if (cvox.AriaUtil.isHidden(children[i]) || + !cvox.DomUtil.isVisible(children[i])) { + continue; + } + var nextLevel = cvox.AriaUtil.getNextLevelItems(children[i], role); + if (nextLevel.length > 0) { + result = result.concat(nextLevel); + } + } + return result; +}; + + +/** + * Recursively finds the first node(s) that match the role. + * + * @param {Element} current The node to start looking at. + * @param {Array.<string>} role The role(s) to match. + * @return {Array.<Element>} The array of matching nodes. + */ +cvox.AriaUtil.getNextLevelItems = function(current, role) { + if (current.nodeType != 1) { // If reached a node that is not an element. + return []; + } + if (role.indexOf(cvox.AriaUtil.getRoleAttribute(current)) != -1) { + return [current]; + } else { + var children = current.childNodes; + var length = children.length; + if (length == 0) { + return []; + } else { + var resultArray = []; + for (var i = 0; i < length; i++) { + var result = cvox.AriaUtil.getNextLevelItems(children[i], role); + if (result.length > 0) { + resultArray = resultArray.concat(result); + } + } + return resultArray; + } + } +}; + + +/** + * If the node is an object with an active descendant, returns the + * descendant node. + * + * This function will fully resolve an active descendant chain. If a circular + * chain is detected, it will return null. + * + * @param {Node} targetNode The node to get descendant information for. + * @return {Node} The descendant node or null if no node exists. + */ +cvox.AriaUtil.getActiveDescendant = function(targetNode) { + var seenIds = {}; + var node = targetNode; + + while (node) { + var activeId = cvox.AriaUtil.getActiveDescendantId_(node); + if (!activeId) { + break; + } + if (activeId in seenIds) { + // A circlar activeDescendant is an error, so return null. + return null; + } + seenIds[activeId] = true; + node = document.getElementById(activeId); + } + + if (node == targetNode) { + return null; + } + return node; +}; + + +/** + * Given a node, returns true if it's an ARIA control widget. Control widgets + * are treated as leaf nodes. + * + * @param {Node} targetNode The node to be checked. + * @return {boolean} Whether the targetNode is an ARIA control widget. + */ +cvox.AriaUtil.isControlWidget = function(targetNode) { + if (targetNode && targetNode.getAttribute) { + var role = cvox.AriaUtil.getRoleAttribute(targetNode); + switch (role) { + case 'button': + case 'checkbox': + case 'combobox': + case 'listbox': + case 'menu': + case 'menuitemcheckbox': + case 'menuitemradio': + case 'radio': + case 'slider': + case 'progressbar': + case 'scrollbar': + case 'spinbutton': + case 'tab': + case 'tablist': + case 'textbox': + return true; + } + } + return false; +}; + + +/** + * Given a node, returns true if it's an ARIA composite control. + * + * @param {Node} targetNode The node to be checked. + * @return {boolean} Whether the targetNode is an ARIA composite control. + */ +cvox.AriaUtil.isCompositeControl = function(targetNode) { + if (targetNode && targetNode.getAttribute) { + var role = cvox.AriaUtil.getRoleAttribute(targetNode); + switch (role) { + case 'combobox': + case 'grid': + case 'listbox': + case 'menu': + case 'menubar': + case 'radiogroup': + case 'tablist': + case 'tree': + case 'treegrid': + return true; + } + } + return false; +}; + + +/** + * Given a node, returns its 'aria-live' value if it's a live region, or + * null otherwise. + * + * @param {Node} node The node to be checked. + * @return {?string} The live region value, like 'polite' or + * 'assertive', or null if 'off' or none. + */ +cvox.AriaUtil.getAriaLive = function(node) { + if (!node.hasAttribute) + return null; + var value = node.getAttribute('aria-live'); + if (value == 'off') { + return null; + } else if (value) { + return value; + } + var role = cvox.AriaUtil.getRoleAttribute(node); + switch (role) { + case 'alert': + return 'assertive'; + case 'log': + case 'status': + return 'polite'; + default: + return null; + } +}; + + +/** + * Given a node, returns its 'aria-atomic' value. + * + * @param {Node} node The node to be checked. + * @return {boolean} The aria-atomic live region value, either true or false. + */ +cvox.AriaUtil.getAriaAtomic = function(node) { + if (!node.hasAttribute) + return false; + var value = node.getAttribute('aria-atomic'); + if (value) { + return (value === 'true'); + } + var role = cvox.AriaUtil.getRoleAttribute(node); + if (role == 'alert') { + return true; + } + return false; +}; + + +/** + * Given a node, returns its 'aria-busy' value. + * + * @param {Node} node The node to be checked. + * @return {boolean} The aria-busy live region value, either true or false. + */ +cvox.AriaUtil.getAriaBusy = function(node) { + if (!node.hasAttribute) + return false; + var value = node.getAttribute('aria-busy'); + if (value) { + return (value === 'true'); + } + return false; +}; + + +/** + * Given a node, checks its aria-relevant attribute (with proper inheritance) + * and determines whether the given change (additions, removals, text, all) + * is relevant and should be announced. + * + * @param {Node} node The node to be checked. + * @param {string} change The name of the change to check - one of + * 'additions', 'removals', 'text', 'all'. + * @return {boolean} True if that change is relevant to that node as part of + * a live region. + */ +cvox.AriaUtil.getAriaRelevant = function(node, change) { + if (!node.hasAttribute) + return false; + var value; + if (node.hasAttribute('aria-relevant')) { + value = node.getAttribute('aria-relevant'); + } else { + value = 'additions text'; + } + if (value == 'all') { + value = 'additions removals text'; + } + + var tokens = value.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '').split(' '); + + if (change == 'all') { + return (tokens.indexOf('additions') >= 0 && + tokens.indexOf('text') >= 0 && + tokens.indexOf('removals') >= 0); + } else { + return (tokens.indexOf(change) >= 0); + } +}; + + +/** + * Given a node, return all live regions that are either rooted at this + * node or contain this node. + * + * @param {Node} node The node to be checked. + * @return {Array.<Element>} All live regions affected by this node changing. + */ +cvox.AriaUtil.getLiveRegions = function(node) { + var result = []; + if (node.querySelectorAll) { + var nodes = node.querySelectorAll( + '[role="alert"], [role="log"], [role="marquee"], ' + + '[role="status"], [role="timer"], [aria-live]'); + if (nodes) { + for (var i = 0; i < nodes.length; i++) { + result.push(nodes[i]); + } + } + } + + while (node) { + if (cvox.AriaUtil.getAriaLive(node)) { + result.push(node); + return result; + } + node = node.parentElement; + } + + return result; +}; + + +/** + * Checks to see whether or not a node is an ARIA landmark. + * + * @param {Node} node The node to be checked. + * @return {boolean} Whether or not the node is an ARIA landmark. + */ +cvox.AriaUtil.isLandmark = function(node) { + if (!node || !node.getAttribute) { + return false; + } + var role = cvox.AriaUtil.getRoleAttribute(node); + switch (role) { + case 'application': + case 'banner': + case 'complementary': + case 'contentinfo': + case 'form': + case 'main': + case 'navigation': + case 'search': + return true; + } + return false; +}; + + +/** + * Checks to see whether or not a node is an ARIA grid. + * + * @param {Node} node The node to be checked. + * @return {boolean} Whether or not the node is an ARIA grid. + */ +cvox.AriaUtil.isGrid = function(node) { + if (!node || !node.getAttribute) { + return false; + } + var role = cvox.AriaUtil.getRoleAttribute(node); + switch (role) { + case 'grid': + case 'treegrid': + return true; + } + return false; +}; + + +/** + * Returns the id of an earcon to play along with the description for a node. + * + * @param {Node} node The node to get the earcon for. + * @return {number?} The earcon id, or null if none applies. + */ +cvox.AriaUtil.getEarcon = function(node) { + if (!node || !node.getAttribute) { + return null; + } + var role = cvox.AriaUtil.getRoleAttribute(node); + switch (role) { + case 'button': + return cvox.AbstractEarcons.BUTTON; + case 'checkbox': + case 'radio': + case 'menuitemcheckbox': + case 'menuitemradio': + var checked = node.getAttribute('aria-checked'); + if (checked == 'true') { + return cvox.AbstractEarcons.CHECK_ON; + } else { + return cvox.AbstractEarcons.CHECK_OFF; + } + case 'combobox': + case 'listbox': + return cvox.AbstractEarcons.LISTBOX; + case 'textbox': + return cvox.AbstractEarcons.EDITABLE_TEXT; + case 'listitem': + return cvox.AbstractEarcons.BULLET; + case 'link': + return cvox.AbstractEarcons.LINK; + } + + return null; +}; + + +/** + * Returns the role of the node. + * + * This is equivalent to targetNode.getAttribute('role') + * except it also takes into account cases where ChromeVox + * itself has changed the role (ie, adding role="application" + * to BODY elements for better screen reader compatibility. + * + * @param {Node} targetNode The node to get the role for. + * @return {string} role of the targetNode. + */ +cvox.AriaUtil.getRoleAttribute = function(targetNode) { + if (!targetNode.getAttribute) { + return ''; + } + var role = targetNode.getAttribute('role'); + if (targetNode.hasAttribute('chromevoxoriginalrole')) { + role = targetNode.getAttribute('chromevoxoriginalrole'); + } + return role; +}; + + +/** + * Checks to see whether or not a node is an ARIA math node. + * + * @param {Node} node The node to be checked. + * @return {boolean} Whether or not the node is an ARIA math node. + */ +cvox.AriaUtil.isMath = function(node) { + if (!node || !node.getAttribute) { + return false; + } + var role = cvox.AriaUtil.getRoleAttribute(node); + return role == 'math'; +}; |