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
11goog.provide('cvox.AriaUtil');
12goog.require('cvox.AbstractEarcons');
13goog.require('cvox.ChromeVox');
14goog.require('cvox.NodeState');
15goog.require('cvox.NodeStateUtil');
16
17
18/**
19 * Create the namespace
20 * @constructor
21 */
22cvox.AriaUtil = function() {
23};
24
25
26/**
27 * A constant indicating no role name.
28 * @type {string}
29 */
30cvox.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 */
39cvox.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 */
84cvox.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 */
114cvox.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 */
157cvox.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 */
181cvox.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 */
204cvox.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 */
230cvox.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 */
247cvox.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 */
264cvox.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 */
282cvox.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 */
304cvox.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 */
356cvox.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 */
375cvox.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 */
542cvox.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 */
569cvox.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 */
589cvox.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 */
614cvox.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 */
649cvox.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 */
680cvox.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 */
712cvox.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 */
740cvox.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 */
768cvox.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 */
789cvox.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 */
811cvox.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 */
843cvox.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 */
874cvox.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 */
900cvox.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 */
920cvox.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 */
964cvox.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 */
982cvox.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