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