blob: b73ab77463ba3ee7e2c6d493b44a9a757c368769 [file] [log] [blame]
// 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 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>}
*/
cvox.AriaUtil.WIDGET_ROLE_TO_NAME = {
'alert' : 'role_alert',
'alertdialog' : 'role_alertdialog',
'button' : 'role_button',
'checkbox' : 'role_checkbox',
'columnheader' : 'role_columnheader',
'combobox' : 'role_combobox',
'dialog' : 'role_dialog',
'grid' : 'role_grid',
'gridcell' : 'role_gridcell',
'link' : 'role_link',
'listbox' : 'role_listbox',
'log' : 'role_log',
'marquee' : 'role_marquee',
'menu' : 'role_menu',
'menubar' : 'role_menubar',
'menuitem' : 'role_menuitem',
'menuitemcheckbox' : 'role_menuitemcheckbox',
'menuitemradio' : 'role_menuitemradio',
'option' : 'role_option',
'progressbar' : 'role_progressbar',
'radio' : 'role_radio',
'radiogroup' : 'role_radiogroup',
'rowheader' : 'role_rowheader',
'scrollbar' : 'role_scrollbar',
'slider' : 'role_slider',
'spinbutton' : 'role_spinbutton',
'status' : 'role_status',
'tab' : 'role_tab',
'tablist' : 'role_tablist',
'tabpanel' : 'role_tabpanel',
'textbox' : 'role_textbox',
'timer' : 'role_timer',
'toolbar' : 'role_toolbar',
'tooltip' : 'role_tooltip',
'treeitem' : '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>}
*/
cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME = {
'article' : 'role_article',
'application' : 'role_application',
'banner' : 'role_banner',
'columnheader' : 'role_columnheader',
'complementary' : 'role_complementary',
'contentinfo' : 'role_contentinfo',
'definition' : 'role_definition',
'directory' : 'role_directory',
'document' : 'role_document',
'form' : 'role_form',
'group' : 'role_group',
'heading' : 'role_heading',
'img' : 'role_img',
'list' : 'role_list',
'listitem' : 'role_listitem',
'main' : 'role_main',
'math' : 'role_math',
'navigation' : 'role_navigation',
'note' : 'role_note',
'region' : 'role_region',
'rowheader' : 'role_rowheader',
'search' : 'role_search',
'separator' : '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.
*
* @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');
return role == 'img' || role == 'progressbar';
};
/**
* 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;
}
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 '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 = 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 msgId = attr.values[value];
if (msgId) {
state.push([msgId]);
}
}
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} */ (
[['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 {Node} 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 {cvox.Earcon?} 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.Earcon.BUTTON;
case 'checkbox':
case 'radio':
case 'menuitemcheckbox':
case 'menuitemradio':
var checked = node.getAttribute('aria-checked');
if (checked == 'true') {
return cvox.Earcon.CHECK_ON;
} else {
return cvox.Earcon.CHECK_OFF;
}
case 'combobox':
case 'listbox':
return cvox.Earcon.LISTBOX;
case 'textbox':
return cvox.Earcon.EDITABLE_TEXT;
case 'listitem':
return cvox.Earcon.LIST_ITEM;
case 'link':
return cvox.Earcon.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';
};