[Extensions] Make extension message bubble factory platform-abstract
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / aria_util.js
blob7df05627b2cf2ccc9928f7182427529c30794d31
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.
5 /**
6  * @fileoverview A collection of JavaScript utilities used to simplify working
7  * with ARIA (http://www.w3.org/TR/wai-aria).
8  */
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');
18 /**
19  * Create the namespace
20  * @constructor
21  */
22 cvox.AriaUtil = function() {
26 /**
27  * A constant indicating no role name.
28  * @type {string}
29  */
30 cvox.AriaUtil.NO_ROLE_NAME = ' ';
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'
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'
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'} }
151  * Checks if a node should be treated as a hidden node because of its ARIA
152  * markup.
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;
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.
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;
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.
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;
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
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);
239  * Determines whether or not a node is or is the descendant of a node
240  * with a particular role.
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;
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;
277  * Returns true is the node is any kind of button.
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;
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);
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     }
315     if (role) {
316       roleName = cvox.AriaUtil.getRoleNameMsgForRole_(role);
317       if (!roleName) {
318         roleName = cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME[role];
319       }
320     }
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;
350  * Returns a string to be presented to the user that identifies what the
351  * targetNode's role is.
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 : '';
367  * Returns a string that gives information about the state of the targetNode.
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   }
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 msgId = attr.values[value];
385     if (msgId) {
386       state.push([msgId]);
387     }
388   }
389   if (targetNode.getAttribute('role') == 'grid') {
390       return cvox.AriaUtil.getGridState_(targetNode, targetNode);
391   }
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   }
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   }
411   var valueNow = targetNode.getAttribute('aria-valuenow');
412   var valueMin = targetNode.getAttribute('aria-valuemin');
413   var valueMax = targetNode.getAttribute('aria-valuemax');
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   }
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   }
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;
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   }
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     }
496     if (descendantRoleList) {
497       var listLength;
498       var currentIndex;
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       }
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;
535  * Returns a string that gives information about the state of the grid node.
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);
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 [];
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   }
574   var activeId = targetNode.getAttribute('aria-activedescendant');
575   if (!activeId) {
576     return null;
577   }
578   return activeId;
583  * Returns the list of elements that are one aria-level below.
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;
608  * Recursively finds the first node(s) that match the role.
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   }
640  * If the node is an object with an active descendant, returns the
641  * descendant node.
643  * This function will fully resolve an active descendant chain. If a circular
644  * chain is detected, it will return null.
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;
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   }
666   if (node == targetNode) {
667     return null;
668   }
669   return node;
674  * Given a node, returns true if it's an ARIA control widget. Control widgets
675  * are treated as leaf nodes.
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;
707  * Given a node, returns true if it's an ARIA composite control.
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;
733  * Given a node, returns its 'aria-live' value if it's a live region, or
734  * null otherwise.
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   }
763  * Given a node, returns its 'aria-atomic' value.
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;
784  * Given a node, returns its 'aria-busy' value.
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;
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.
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   }
824   var tokens = value.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '').split(' ');
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   }
837  * Given a node, return all live regions that are either rooted at this
838  * node or contain this node.
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   }
856   while (node) {
857     if (cvox.AriaUtil.getAriaLive(node)) {
858       result.push(node);
859       return result;
860     }
861     node = node.parentElement;
862   }
864   return result;
869  * Checks to see whether or not a node is an ARIA landmark.
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;
895  * Checks to see whether or not a node is an ARIA grid.
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;
915  * Returns the id of an earcon to play along with the description for a node.
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   }
949   return null;
954  * Returns the role of the node.
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.
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;
977  * Checks to see whether or not a node is an ARIA math node.
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';