summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/chromeos/chromevox/common/aria_util.js
diff options
context:
space:
mode:
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.js988
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';
+};