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