Home | History | Annotate | Download | only in common
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview A collection of JavaScript utilities used to simplify working
      7  * with ARIA (http://www.w3.org/TR/wai-aria).
      8  */
      9 
     10 
     11 goog.provide('cvox.AriaUtil');
     12 goog.require('cvox.AbstractEarcons');
     13 goog.require('cvox.ChromeVox');
     14 goog.require('cvox.NodeState');
     15 goog.require('cvox.NodeStateUtil');
     16 
     17 
     18 /**
     19  * Create the namespace
     20  * @constructor
     21  */
     22 cvox.AriaUtil = function() {
     23 };
     24 
     25 
     26 /**
     27  * A constant indicating no role name.
     28  * @type {string}
     29  */
     30 cvox.AriaUtil.NO_ROLE_NAME = ' ';
     31 
     32 /**
     33  * A mapping from ARIA role names to their message ids.
     34  * Note: If you are adding a new mapping, the new message identifier needs a
     35  * corresponding braille message. For example, a message id 'tag_button'
     36  * requires another message 'tag_button_brl' within messages.js.
     37  * @type {Object.<string, string>}
     38  */
     39 cvox.AriaUtil.WIDGET_ROLE_TO_NAME = {
     40   'alert' : 'aria_role_alert',
     41   'alertdialog' : 'aria_role_alertdialog',
     42   'button' : 'aria_role_button',
     43   'checkbox' : 'aria_role_checkbox',
     44   'columnheader' : 'aria_role_columnheader',
     45   'combobox' : 'aria_role_combobox',
     46   'dialog' : 'aria_role_dialog',
     47   'grid' : 'aria_role_grid',
     48   'gridcell' : 'aria_role_gridcell',
     49   'link' : 'aria_role_link',
     50   'listbox' : 'aria_role_listbox',
     51   'log' : 'aria_role_log',
     52   'marquee' : 'aria_role_marquee',
     53   'menu' : 'aria_role_menu',
     54   'menubar' : 'aria_role_menubar',
     55   'menuitem' : 'aria_role_menuitem',
     56   'menuitemcheckbox' : 'aria_role_menuitemcheckbox',
     57   'menuitemradio' : 'aria_role_menuitemradio',
     58   'option' : cvox.AriaUtil.NO_ROLE_NAME,
     59   'progressbar' : 'aria_role_progressbar',
     60   'radio' : 'aria_role_radio',
     61   'radiogroup' : 'aria_role_radiogroup',
     62   'rowheader' : 'aria_role_rowheader',
     63   'scrollbar' : 'aria_role_scrollbar',
     64   'slider' : 'aria_role_slider',
     65   'spinbutton' : 'aria_role_spinbutton',
     66   'status' : 'aria_role_status',
     67   'tab' : 'aria_role_tab',
     68   'tablist' : 'aria_role_tablist',
     69   'tabpanel' : 'aria_role_tabpanel',
     70   'textbox' : 'aria_role_textbox',
     71   'timer' : 'aria_role_timer',
     72   'toolbar' : 'aria_role_toolbar',
     73   'tooltip' : 'aria_role_tooltip',
     74   'treeitem' : 'aria_role_treeitem'
     75 };
     76 
     77 
     78 /**
     79  * Note: If you are adding a new mapping, the new message identifier needs a
     80  * corresponding braille message. For example, a message id 'tag_button'
     81  * requires another message 'tag_button_brl' within messages.js.
     82  * @type {Object.<string, string>}
     83  */
     84 cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME = {
     85   'article' : 'aria_role_article',
     86   'application' : 'aria_role_application',
     87   'banner' : 'aria_role_banner',
     88   'columnheader' : 'aria_role_columnheader',
     89   'complementary' : 'aria_role_complementary',
     90   'contentinfo' : 'aria_role_contentinfo',
     91   'definition' : 'aria_role_definition',
     92   'directory' : 'aria_role_directory',
     93   'document' : 'aria_role_document',
     94   'form' : 'aria_role_form',
     95   'group' : 'aria_role_group',
     96   'heading' : 'aria_role_heading',
     97   'img' : 'aria_role_img',
     98   'list' : 'aria_role_list',
     99   'listitem' : 'aria_role_listitem',
    100   'main' : 'aria_role_main',
    101   'math' : 'aria_role_math',
    102   'navigation' : 'aria_role_navigation',
    103   'note' : 'aria_role_note',
    104   'region' : 'aria_role_region',
    105   'rowheader' : 'aria_role_rowheader',
    106   'search' : 'aria_role_search',
    107   'separator' : 'aria_role_separator'
    108 };
    109 
    110 
    111 /**
    112  * @type {Array.<Object>}
    113  */
    114 cvox.AriaUtil.ATTRIBUTE_VALUE_TO_STATUS = [
    115   { name: 'aria-autocomplete', values:
    116       {'inline' : 'aria_autocomplete_inline',
    117        'list' : 'aria_autocomplete_list',
    118        'both' : 'aria_autocomplete_both'} },
    119   { name: 'aria-checked', values:
    120       {'true' : 'aria_checked_true',
    121        'false' : 'aria_checked_false',
    122        'mixed' : 'aria_checked_mixed'} },
    123   { name: 'aria-disabled', values:
    124       {'true' : 'aria_disabled_true'} },
    125   { name: 'aria-expanded', values:
    126       {'true' : 'aria_expanded_true',
    127        'false' : 'aria_expanded_false'} },
    128   { name: 'aria-invalid', values:
    129       {'true' : 'aria_invalid_true',
    130        'grammar' : 'aria_invalid_grammar',
    131        'spelling' : 'aria_invalid_spelling'} },
    132   { name: 'aria-multiline', values:
    133       {'true' : 'aria_multiline_true'} },
    134   { name: 'aria-multiselectable', values:
    135       {'true' : 'aria_multiselectable_true'} },
    136   { name: 'aria-pressed', values:
    137       {'true' : 'aria_pressed_true',
    138        'false' : 'aria_pressed_false',
    139        'mixed' : 'aria_pressed_mixed'} },
    140   { name: 'aria-readonly', values:
    141       {'true' : 'aria_readonly_true'} },
    142   { name: 'aria-required', values:
    143       {'true' : 'aria_required_true'} },
    144   { name: 'aria-selected', values:
    145       {'true' : 'aria_selected_true',
    146        'false' : 'aria_selected_false'} }
    147 ];
    148 
    149 
    150 /**
    151  * Checks if a node should be treated as a hidden node because of its ARIA
    152  * markup.
    153  *
    154  * @param {Node} targetNode The node to check.
    155  * @return {boolean} True if the targetNode should be treated as hidden.
    156  */
    157 cvox.AriaUtil.isHiddenRecursive = function(targetNode) {
    158   if (cvox.AriaUtil.isHidden(targetNode)) {
    159     return true;
    160   }
    161   var parent = targetNode.parentElement;
    162   while (parent) {
    163     if ((parent.getAttribute('aria-hidden') == 'true') &&
    164         (parent.getAttribute('chromevoxignoreariahidden') != 'true')) {
    165       return true;
    166     }
    167     parent = parent.parentElement;
    168   }
    169   return false;
    170 };
    171 
    172 
    173 /**
    174  * Checks if a node should be treated as a hidden node because of its ARIA
    175  * markup. Does not check parents, so if you need to know if this is a
    176  * descendant of a hidden node, call isHiddenRecursive.
    177  *
    178  * @param {Node} targetNode The node to check.
    179  * @return {boolean} True if the targetNode should be treated as hidden.
    180  */
    181 cvox.AriaUtil.isHidden = function(targetNode) {
    182   if (!targetNode) {
    183     return true;
    184   }
    185   if (targetNode.getAttribute) {
    186     if ((targetNode.getAttribute('aria-hidden') == 'true') &&
    187         (targetNode.getAttribute('chromevoxignoreariahidden') != 'true')) {
    188       return true;
    189     }
    190   }
    191   return false;
    192 };
    193 
    194 
    195 /**
    196  * Checks if a node should be treated as a visible node because of its ARIA
    197  * markup, regardless of whatever other styling/attributes it may have.
    198  * It is possible to force a node to be visible by setting aria-hidden to
    199  * false.
    200  *
    201  * @param {Node} targetNode The node to check.
    202  * @return {boolean} True if the targetNode should be treated as visible.
    203  */
    204 cvox.AriaUtil.isForcedVisibleRecursive = function(targetNode) {
    205   var node = targetNode;
    206   while (node) {
    207     if (node.getAttribute) {
    208       // Stop and return the result based on the closest node that has
    209       // aria-hidden set.
    210       if (node.hasAttribute('aria-hidden') &&
    211           (node.getAttribute('chromevoxignoreariahidden') != 'true')) {
    212         return node.getAttribute('aria-hidden') == 'false';
    213       }
    214     }
    215     node = node.parentElement;
    216   }
    217   return false;
    218 };
    219 
    220 
    221 /**
    222  * Checks if a node should be treated as a leaf node because of its ARIA
    223  * markup. Does not check recursively, and does not check isControlWidget.
    224  * Note that elements with aria-label are treated as leaf elements. See:
    225  * http://www.w3.org/TR/wai-aria/roles#textalternativecomputation
    226  *
    227  * @param {Element} targetElement The node to check.
    228  * @return {boolean} True if the targetNode should be treated as a leaf node.
    229  */
    230 cvox.AriaUtil.isLeafElement = function(targetElement) {
    231   var role = targetElement.getAttribute('role');
    232   var hasArialLabel = targetElement.hasAttribute('aria-label') &&
    233       (targetElement.getAttribute('aria-label').length > 0);
    234   return (role == 'img' || role == 'progressbar' || hasArialLabel);
    235 };
    236 
    237 
    238 /**
    239  * Determines whether or not a node is or is the descendant of a node
    240  * with a particular role.
    241  *
    242  * @param {Node} node The node to be checked.
    243  * @param {string} roleName The role to check for.
    244  * @return {boolean} True if the node or one of its ancestor has the specified
    245  * role.
    246  */
    247 cvox.AriaUtil.isDescendantOfRole = function(node, roleName) {
    248   while (node) {
    249     if (roleName && node && (node.getAttribute('role') == roleName)) {
    250       return true;
    251     }
    252     node = node.parentNode;
    253   }
    254   return false;
    255 };
    256 
    257 
    258 /**
    259  * Helper function to return the role name message identifier for a role.
    260  * @param {string} role The role.
    261  * @return {?string} The role name message identifier.
    262  * @private
    263  */
    264 cvox.AriaUtil.getRoleNameMsgForRole_ = function(role) {
    265   var msgId = cvox.AriaUtil.WIDGET_ROLE_TO_NAME[role];
    266   if (!msgId) {
    267     return null;
    268   }
    269   if (msgId == cvox.AriaUtil.NO_ROLE_NAME) {
    270     // TODO(dtseng): This isn't the way to insert silence; beware!
    271     return ' ';
    272   }
    273   return msgId;
    274 };
    275 
    276 /**
    277  * Returns true is the node is any kind of button.
    278  *
    279  * @param {Node} node The node to check.
    280  * @return {boolean} True if the node is a button.
    281  */
    282 cvox.AriaUtil.isButton = function(node) {
    283   var role = cvox.AriaUtil.getRoleAttribute(node);
    284   if (role == 'button') {
    285     return true;
    286   }
    287   if (node.tagName == 'BUTTON') {
    288     return true;
    289   }
    290   if (node.tagName == 'INPUT') {
    291     return (node.type == 'submit' ||
    292             node.type == 'reset' ||
    293             node.type == 'button');
    294   }
    295   return false;
    296 };
    297 
    298 /**
    299  * Returns a role message identifier for a node.
    300  * For a localized string, see cvox.AriaUtil.getRoleName.
    301  * @param {Node} targetNode The node to get the role name for.
    302  * @return {string} The role name message identifier      for the targetNode.
    303  */
    304 cvox.AriaUtil.getRoleNameMsg = function(targetNode) {
    305   var roleName;
    306   if (targetNode && targetNode.getAttribute) {
    307     var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    308 
    309     // Special case for pop-up buttons.
    310     if (targetNode.getAttribute('aria-haspopup') == 'true' &&
    311         cvox.AriaUtil.isButton(targetNode)) {
    312       return 'aria_role_popup_button';
    313     }
    314 
    315     if (role) {
    316       roleName = cvox.AriaUtil.getRoleNameMsgForRole_(role);
    317       if (!roleName) {
    318         roleName = cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME[role];
    319       }
    320     }
    321 
    322     // To a user, a menu item within a menu bar is called a "menu";
    323     // any other menu item is called a "menu item".
    324     //
    325     // TODO(deboer): This block feels like a hack. dmazzoni suggests
    326     // using css-like syntax for names.  Investigate further if
    327     // we need more of these hacks.
    328     if (role == 'menuitem') {
    329       var container = targetNode.parentElement;
    330       while (container) {
    331         if (container.getAttribute &&
    332             (cvox.AriaUtil.getRoleAttribute(container) == 'menu' ||
    333              cvox.AriaUtil.getRoleAttribute(container) == 'menubar')) {
    334           break;
    335         }
    336         container = container.parentElement;
    337       }
    338       if (container && cvox.AriaUtil.getRoleAttribute(container) == 'menubar') {
    339         roleName = cvox.AriaUtil.getRoleNameMsgForRole_('menu');
    340       }  // else roleName is already 'Menu item', no need to change it.
    341     }
    342   }
    343   if (!roleName) {
    344     roleName = '';
    345   }
    346   return roleName;
    347 };
    348 
    349 /**
    350  * Returns a string to be presented to the user that identifies what the
    351  * targetNode's role is.
    352  *
    353  * @param {Node} targetNode The node to get the role name for.
    354  * @return {string} The role name for the targetNode.
    355  */
    356 cvox.AriaUtil.getRoleName = function(targetNode) {
    357   var roleMsg = cvox.AriaUtil.getRoleNameMsg(targetNode);
    358   var roleName = cvox.ChromeVox.msgs.getMsg(roleMsg);
    359   var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    360   if ((role == 'heading') && (targetNode.hasAttribute('aria-level'))) {
    361     roleName += ' ' + targetNode.getAttribute('aria-level');
    362   }
    363   return roleName ? roleName : '';
    364 };
    365 
    366 /**
    367  * Returns a string that gives information about the state of the targetNode.
    368  *
    369  * @param {Node} targetNode The node to get the state information for.
    370  * @param {boolean} primary Whether this is the primary node we're
    371  *     interested in, where we might want extra information - as
    372  *     opposed to an ancestor, where we might be more brief.
    373  * @return {cvox.NodeState} The status information about the node.
    374  */
    375 cvox.AriaUtil.getStateMsgs = function(targetNode, primary) {
    376   var state = [];
    377   if (!targetNode || !targetNode.getAttribute) {
    378     return state;
    379   }
    380 
    381   for (var i = 0, attr; attr = cvox.AriaUtil.ATTRIBUTE_VALUE_TO_STATUS[i];
    382       i++) {
    383     var value = targetNode.getAttribute(attr.name);
    384     var msg_id = attr.values[value];
    385     if (msg_id) {
    386       state.push([msg_id]);
    387     }
    388   }
    389   if (targetNode.getAttribute('role') == 'grid') {
    390       return cvox.AriaUtil.getGridState_(targetNode, targetNode);
    391   }
    392 
    393   var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    394   if (targetNode.getAttribute('aria-haspopup') == 'true') {
    395     if (role == 'menuitem') {
    396       state.push(['has_submenu']);
    397     } else if (cvox.AriaUtil.isButton(targetNode)) {
    398       // Do nothing - the role name will be 'pop-up button'.
    399     } else {
    400       state.push(['has_popup']);
    401     }
    402   }
    403 
    404   var valueText = targetNode.getAttribute('aria-valuetext');
    405   if (valueText) {
    406     // If there is a valueText, that always wins.
    407     state.push(['aria_value_text', valueText]);
    408     return state;
    409   }
    410 
    411   var valueNow = targetNode.getAttribute('aria-valuenow');
    412   var valueMin = targetNode.getAttribute('aria-valuemin');
    413   var valueMax = targetNode.getAttribute('aria-valuemax');
    414 
    415   // Scrollbar and progressbar should speak the percentage.
    416   // http://www.w3.org/TR/wai-aria/roles#scrollbar
    417   // http://www.w3.org/TR/wai-aria/roles#progressbar
    418   if ((valueNow != null) && (valueMin != null) && (valueMax != null)) {
    419     if ((role == 'scrollbar') || (role == 'progressbar')) {
    420       var percent = Math.round((valueNow / (valueMax - valueMin)) * 100);
    421       state.push(['state_percent', percent]);
    422       return state;
    423     }
    424   }
    425 
    426   // Return as many of the value attributes as possible.
    427   if (valueNow != null) {
    428     state.push(['aria_value_now', valueNow]);
    429   }
    430   if (valueMin != null) {
    431     state.push(['aria_value_min', valueMin]);
    432   }
    433   if (valueMax != null) {
    434     state.push(['aria_value_max', valueMax]);
    435   }
    436 
    437   // If this is a composite control or an item within a composite control,
    438   // get the index and count of the current descendant or active
    439   // descendant.
    440   var parentControl = targetNode;
    441   var currentDescendant = null;
    442 
    443   if (cvox.AriaUtil.isCompositeControl(parentControl) && primary) {
    444     currentDescendant = cvox.AriaUtil.getActiveDescendant(parentControl);
    445   } else {
    446     role = cvox.AriaUtil.getRoleAttribute(targetNode);
    447     if (role == 'option' ||
    448         role == 'menuitem' ||
    449         role == 'menuitemcheckbox' ||
    450         role == 'menuitemradio' ||
    451         role == 'radio' ||
    452         role == 'tab' ||
    453         role == 'treeitem') {
    454       currentDescendant = targetNode;
    455       parentControl = targetNode.parentElement;
    456       while (parentControl &&
    457              !cvox.AriaUtil.isCompositeControl(parentControl)) {
    458         parentControl = parentControl.parentElement;
    459         if (parentControl &&
    460             cvox.AriaUtil.getRoleAttribute(parentControl) == 'treeitem') {
    461           break;
    462         }
    463       }
    464     }
    465   }
    466 
    467   if (parentControl &&
    468       (cvox.AriaUtil.isCompositeControl(parentControl) ||
    469           cvox.AriaUtil.getRoleAttribute(parentControl) == 'treeitem') &&
    470       currentDescendant) {
    471     var parentRole = cvox.AriaUtil.getRoleAttribute(parentControl);
    472     var descendantRoleList;
    473     switch (parentRole) {
    474       case 'combobox':
    475       case 'listbox':
    476         descendantRoleList = ['option'];
    477         break;
    478       case 'menu':
    479         descendantRoleList = ['menuitem',
    480                              'menuitemcheckbox',
    481                              'menuitemradio'];
    482         break;
    483       case 'radiogroup':
    484         descendantRoleList = ['radio'];
    485         break;
    486       case 'tablist':
    487         descendantRoleList = ['tab'];
    488         break;
    489       case 'tree':
    490       case 'treegrid':
    491       case 'treeitem':
    492         descendantRoleList = ['treeitem'];
    493         break;
    494     }
    495 
    496     if (descendantRoleList) {
    497       var listLength;
    498       var currentIndex;
    499 
    500       var ariaLength =
    501           parseInt(currentDescendant.getAttribute('aria-setsize'), 10);
    502       if (!isNaN(ariaLength)) {
    503         listLength = ariaLength;
    504       }
    505       var ariaIndex =
    506           parseInt(currentDescendant.getAttribute('aria-posinset'), 10);
    507       if (!isNaN(ariaIndex)) {
    508         currentIndex = ariaIndex;
    509       }
    510 
    511       if (listLength == undefined || currentIndex == undefined) {
    512         var descendants = cvox.AriaUtil.getNextLevel(parentControl,
    513             descendantRoleList);
    514         if (listLength == undefined) {
    515           listLength = descendants.length;
    516         }
    517         if (currentIndex == undefined) {
    518           for (var j = 0; j < descendants.length; j++) {
    519             if (descendants[j] == currentDescendant) {
    520               currentIndex = j + 1;
    521             }
    522           }
    523         }
    524       }
    525       if (currentIndex && listLength) {
    526         state.push(['list_position', currentIndex, listLength]);
    527       }
    528     }
    529   }
    530   return state;
    531 };
    532 
    533 
    534 /**
    535  * Returns a string that gives information about the state of the grid node.
    536  *
    537  * @param {Node} targetNode The node to get the state information for.
    538  * @param {Node} parentControl The parent composite control.
    539  * @return {cvox.NodeState} The status information about the node.
    540  * @private
    541  */
    542 cvox.AriaUtil.getGridState_ = function(targetNode, parentControl) {
    543   var activeDescendant = cvox.AriaUtil.getActiveDescendant(parentControl);
    544 
    545   if (activeDescendant) {
    546     var descendantSelector = '*[role~="row"]';
    547     var rows = parentControl.querySelectorAll(descendantSelector);
    548     var currentIndex = null;
    549     for (var j = 0; j < rows.length; j++) {
    550       var gridcells = rows[j].querySelectorAll('*[role~="gridcell"]');
    551       for (var k = 0; k < gridcells.length; k++) {
    552         if (gridcells[k] == activeDescendant) {
    553           return /** @type {cvox.NodeState} */ (
    554                   [['aria_role_gridcell_pos', j + 1, k + 1]]);
    555         }
    556       }
    557     }
    558   }
    559   return [];
    560 };
    561 
    562 
    563 /**
    564  * Returns the id of a node's active descendant
    565  * @param {Node} targetNode The node.
    566  * @return {?string} The id of the active descendant.
    567  * @private
    568  */
    569 cvox.AriaUtil.getActiveDescendantId_ = function(targetNode) {
    570   if (!targetNode.getAttribute) {
    571     return null;
    572   }
    573 
    574   var activeId = targetNode.getAttribute('aria-activedescendant');
    575   if (!activeId) {
    576     return null;
    577   }
    578   return activeId;
    579 };
    580 
    581 
    582 /**
    583  * Returns the list of elements that are one aria-level below.
    584  *
    585  * @param {Node} parentControl The node whose descendants should be analyzed.
    586  * @param {Array.<string>} role The role(s) of descendant we are looking for.
    587  * @return {Array.<Node>} The array of matching nodes.
    588  */
    589 cvox.AriaUtil.getNextLevel = function(parentControl, role) {
    590   var result = [];
    591   var children = parentControl.childNodes;
    592   var length = children.length;
    593   for (var i = 0; i < children.length; i++) {
    594     if (cvox.AriaUtil.isHidden(children[i]) ||
    595         !cvox.DomUtil.isVisible(children[i])) {
    596       continue;
    597     }
    598     var nextLevel = cvox.AriaUtil.getNextLevelItems(children[i], role);
    599     if (nextLevel.length > 0) {
    600       result = result.concat(nextLevel);
    601     }
    602   }
    603   return result;
    604 };
    605 
    606 
    607 /**
    608  * Recursively finds the first node(s) that match the role.
    609  *
    610  * @param {Element} current The node to start looking at.
    611  * @param {Array.<string>} role The role(s) to match.
    612  * @return {Array.<Element>} The array of matching nodes.
    613  */
    614 cvox.AriaUtil.getNextLevelItems = function(current, role) {
    615   if (current.nodeType != 1) { // If reached a node that is not an element.
    616     return [];
    617   }
    618   if (role.indexOf(cvox.AriaUtil.getRoleAttribute(current)) != -1) {
    619     return [current];
    620   } else {
    621     var children = current.childNodes;
    622     var length = children.length;
    623     if (length == 0) {
    624       return [];
    625     } else {
    626       var resultArray = [];
    627       for (var i = 0; i < length; i++) {
    628         var result = cvox.AriaUtil.getNextLevelItems(children[i], role);
    629         if (result.length > 0) {
    630           resultArray = resultArray.concat(result);
    631         }
    632       }
    633       return resultArray;
    634     }
    635   }
    636 };
    637 
    638 
    639 /**
    640  * If the node is an object with an active descendant, returns the
    641  * descendant node.
    642  *
    643  * This function will fully resolve an active descendant chain. If a circular
    644  * chain is detected, it will return null.
    645  *
    646  * @param {Node} targetNode The node to get descendant information for.
    647  * @return {Node} The descendant node or null if no node exists.
    648  */
    649 cvox.AriaUtil.getActiveDescendant = function(targetNode) {
    650   var seenIds = {};
    651   var node = targetNode;
    652 
    653   while (node) {
    654     var activeId = cvox.AriaUtil.getActiveDescendantId_(node);
    655     if (!activeId) {
    656       break;
    657     }
    658     if (activeId in seenIds) {
    659       // A circlar activeDescendant is an error, so return null.
    660       return null;
    661     }
    662     seenIds[activeId] = true;
    663     node = document.getElementById(activeId);
    664   }
    665 
    666   if (node == targetNode) {
    667     return null;
    668   }
    669   return node;
    670 };
    671 
    672 
    673 /**
    674  * Given a node, returns true if it's an ARIA control widget. Control widgets
    675  * are treated as leaf nodes.
    676  *
    677  * @param {Node} targetNode The node to be checked.
    678  * @return {boolean} Whether the targetNode is an ARIA control widget.
    679  */
    680 cvox.AriaUtil.isControlWidget = function(targetNode) {
    681   if (targetNode && targetNode.getAttribute) {
    682     var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    683     switch (role) {
    684       case 'button':
    685       case 'checkbox':
    686       case 'combobox':
    687       case 'listbox':
    688       case 'menu':
    689       case 'menuitemcheckbox':
    690       case 'menuitemradio':
    691       case 'radio':
    692       case 'slider':
    693       case 'progressbar':
    694       case 'scrollbar':
    695       case 'spinbutton':
    696       case 'tab':
    697       case 'tablist':
    698       case 'textbox':
    699         return true;
    700     }
    701   }
    702   return false;
    703 };
    704 
    705 
    706 /**
    707  * Given a node, returns true if it's an ARIA composite control.
    708  *
    709  * @param {Node} targetNode The node to be checked.
    710  * @return {boolean} Whether the targetNode is an ARIA composite control.
    711  */
    712 cvox.AriaUtil.isCompositeControl = function(targetNode) {
    713   if (targetNode && targetNode.getAttribute) {
    714     var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    715     switch (role) {
    716       case 'combobox':
    717       case 'grid':
    718       case 'listbox':
    719       case 'menu':
    720       case 'menubar':
    721       case 'radiogroup':
    722       case 'tablist':
    723       case 'tree':
    724       case 'treegrid':
    725         return true;
    726     }
    727   }
    728   return false;
    729 };
    730 
    731 
    732 /**
    733  * Given a node, returns its 'aria-live' value if it's a live region, or
    734  * null otherwise.
    735  *
    736  * @param {Node} node The node to be checked.
    737  * @return {?string} The live region value, like 'polite' or
    738  *     'assertive', or null if 'off' or none.
    739  */
    740 cvox.AriaUtil.getAriaLive = function(node) {
    741   if (!node.hasAttribute)
    742     return null;
    743   var value = node.getAttribute('aria-live');
    744   if (value == 'off') {
    745     return null;
    746   } else if (value) {
    747     return value;
    748   }
    749   var role = cvox.AriaUtil.getRoleAttribute(node);
    750   switch (role) {
    751     case 'alert':
    752       return 'assertive';
    753     case 'log':
    754     case 'status':
    755       return 'polite';
    756     default:
    757       return null;
    758   }
    759 };
    760 
    761 
    762 /**
    763  * Given a node, returns its 'aria-atomic' value.
    764  *
    765  * @param {Node} node The node to be checked.
    766  * @return {boolean} The aria-atomic live region value, either true or false.
    767  */
    768 cvox.AriaUtil.getAriaAtomic = function(node) {
    769   if (!node.hasAttribute)
    770     return false;
    771   var value = node.getAttribute('aria-atomic');
    772   if (value) {
    773     return (value === 'true');
    774   }
    775   var role = cvox.AriaUtil.getRoleAttribute(node);
    776   if (role == 'alert') {
    777     return true;
    778   }
    779   return false;
    780 };
    781 
    782 
    783 /**
    784  * Given a node, returns its 'aria-busy' value.
    785  *
    786  * @param {Node} node The node to be checked.
    787  * @return {boolean} The aria-busy live region value, either true or false.
    788  */
    789 cvox.AriaUtil.getAriaBusy = function(node) {
    790   if (!node.hasAttribute)
    791     return false;
    792   var value = node.getAttribute('aria-busy');
    793   if (value) {
    794     return (value === 'true');
    795   }
    796   return false;
    797 };
    798 
    799 
    800 /**
    801  * Given a node, checks its aria-relevant attribute (with proper inheritance)
    802  * and determines whether the given change (additions, removals, text, all)
    803  * is relevant and should be announced.
    804  *
    805  * @param {Node} node The node to be checked.
    806  * @param {string} change The name of the change to check - one of
    807  *     'additions', 'removals', 'text', 'all'.
    808  * @return {boolean} True if that change is relevant to that node as part of
    809  *     a live region.
    810  */
    811 cvox.AriaUtil.getAriaRelevant = function(node, change) {
    812   if (!node.hasAttribute)
    813     return false;
    814   var value;
    815   if (node.hasAttribute('aria-relevant')) {
    816     value = node.getAttribute('aria-relevant');
    817   } else {
    818     value = 'additions text';
    819   }
    820   if (value == 'all') {
    821     value = 'additions removals text';
    822   }
    823 
    824   var tokens = value.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '').split(' ');
    825 
    826   if (change == 'all') {
    827     return (tokens.indexOf('additions') >= 0 &&
    828             tokens.indexOf('text') >= 0 &&
    829             tokens.indexOf('removals') >= 0);
    830   } else {
    831     return (tokens.indexOf(change) >= 0);
    832   }
    833 };
    834 
    835 
    836 /**
    837  * Given a node, return all live regions that are either rooted at this
    838  * node or contain this node.
    839  *
    840  * @param {Node} node The node to be checked.
    841  * @return {Array.<Element>} All live regions affected by this node changing.
    842  */
    843 cvox.AriaUtil.getLiveRegions = function(node) {
    844   var result = [];
    845   if (node.querySelectorAll) {
    846     var nodes = node.querySelectorAll(
    847         '[role="alert"], [role="log"],  [role="marquee"], ' +
    848         '[role="status"], [role="timer"],  [aria-live]');
    849     if (nodes) {
    850       for (var i = 0; i < nodes.length; i++) {
    851         result.push(nodes[i]);
    852       }
    853     }
    854   }
    855 
    856   while (node) {
    857     if (cvox.AriaUtil.getAriaLive(node)) {
    858       result.push(node);
    859       return result;
    860     }
    861     node = node.parentElement;
    862   }
    863 
    864   return result;
    865 };
    866 
    867 
    868 /**
    869  * Checks to see whether or not a node is an ARIA landmark.
    870  *
    871  * @param {Node} node The node to be checked.
    872  * @return {boolean} Whether or not the node is an ARIA landmark.
    873  */
    874 cvox.AriaUtil.isLandmark = function(node) {
    875     if (!node || !node.getAttribute) {
    876       return false;
    877     }
    878     var role = cvox.AriaUtil.getRoleAttribute(node);
    879     switch (role) {
    880       case 'application':
    881       case 'banner':
    882       case 'complementary':
    883       case 'contentinfo':
    884       case 'form':
    885       case 'main':
    886       case 'navigation':
    887       case 'search':
    888         return true;
    889     }
    890     return false;
    891 };
    892 
    893 
    894 /**
    895  * Checks to see whether or not a node is an ARIA grid.
    896  *
    897  * @param {Node} node The node to be checked.
    898  * @return {boolean} Whether or not the node is an ARIA grid.
    899  */
    900 cvox.AriaUtil.isGrid = function(node) {
    901     if (!node || !node.getAttribute) {
    902       return false;
    903     }
    904     var role = cvox.AriaUtil.getRoleAttribute(node);
    905     switch (role) {
    906       case 'grid':
    907       case 'treegrid':
    908         return true;
    909     }
    910     return false;
    911 };
    912 
    913 
    914 /**
    915  * Returns the id of an earcon to play along with the description for a node.
    916  *
    917  * @param {Node} node The node to get the earcon for.
    918  * @return {number?} The earcon id, or null if none applies.
    919  */
    920 cvox.AriaUtil.getEarcon = function(node) {
    921   if (!node || !node.getAttribute) {
    922     return null;
    923   }
    924   var role = cvox.AriaUtil.getRoleAttribute(node);
    925   switch (role) {
    926     case 'button':
    927       return cvox.AbstractEarcons.BUTTON;
    928     case 'checkbox':
    929     case 'radio':
    930     case 'menuitemcheckbox':
    931     case 'menuitemradio':
    932       var checked = node.getAttribute('aria-checked');
    933       if (checked == 'true') {
    934         return cvox.AbstractEarcons.CHECK_ON;
    935       } else {
    936         return cvox.AbstractEarcons.CHECK_OFF;
    937       }
    938     case 'combobox':
    939     case 'listbox':
    940       return cvox.AbstractEarcons.LISTBOX;
    941     case 'textbox':
    942       return cvox.AbstractEarcons.EDITABLE_TEXT;
    943     case 'listitem':
    944       return cvox.AbstractEarcons.BULLET;
    945     case 'link':
    946       return cvox.AbstractEarcons.LINK;
    947   }
    948 
    949   return null;
    950 };
    951 
    952 
    953 /**
    954  * Returns the role of the node.
    955  *
    956  * This is equivalent to targetNode.getAttribute('role')
    957  * except it also takes into account cases where ChromeVox
    958  * itself has changed the role (ie, adding role="application"
    959  * to BODY elements for better screen reader compatibility.
    960  *
    961  * @param {Node} targetNode The node to get the role for.
    962  * @return {string} role of the targetNode.
    963  */
    964 cvox.AriaUtil.getRoleAttribute = function(targetNode) {
    965   if (!targetNode.getAttribute) {
    966     return '';
    967   }
    968   var role = targetNode.getAttribute('role');
    969   if (targetNode.hasAttribute('chromevoxoriginalrole')) {
    970     role = targetNode.getAttribute('chromevoxoriginalrole');
    971   }
    972   return role;
    973 };
    974 
    975 
    976 /**
    977  * Checks to see whether or not a node is an ARIA math node.
    978  *
    979  * @param {Node} node The node to be checked.
    980  * @return {boolean} Whether or not the node is an ARIA math node.
    981  */
    982 cvox.AriaUtil.isMath = function(node) {
    983   if (!node || !node.getAttribute) {
    984     return false;
    985   }
    986   var role = cvox.AriaUtil.getRoleAttribute(node);
    987   return role == 'math';
    988 };
    989