AccessibilityUtils.js (47818B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * Accessible states used to check node's state from the accessiblity API 9 * perspective. 10 * 11 * Note: if gecko is built with --disable-accessibility, the interfaces 12 * are not defined. This is why we use getters instead to be able to use 13 * these statically. 14 */ 15 16 this.AccessibilityUtils = (function () { 17 const FORCE_DISABLE_ACCESSIBILITY_PREF = "accessibility.force_disabled"; 18 19 // Accessible states. 20 const { STATE_FOCUSABLE, STATE_INVISIBLE, STATE_LINKED, STATE_UNAVAILABLE } = 21 Ci.nsIAccessibleStates; 22 23 // Accessible action for showing long description. 24 const CLICK_ACTION = "click"; 25 26 // Roles that are considered focusable with the keyboard. 27 const KEYBOARD_FOCUSABLE_ROLES = new Set([ 28 Ci.nsIAccessibleRole.ROLE_BUTTONMENU, 29 Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, 30 Ci.nsIAccessibleRole.ROLE_COMBOBOX, 31 Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX, 32 Ci.nsIAccessibleRole.ROLE_ENTRY, 33 Ci.nsIAccessibleRole.ROLE_LINK, 34 Ci.nsIAccessibleRole.ROLE_LISTBOX, 35 Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, 36 Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, 37 Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, 38 Ci.nsIAccessibleRole.ROLE_SEARCHBOX, 39 Ci.nsIAccessibleRole.ROLE_SLIDER, 40 Ci.nsIAccessibleRole.ROLE_SPINBUTTON, 41 Ci.nsIAccessibleRole.ROLE_SUMMARY, 42 Ci.nsIAccessibleRole.ROLE_SWITCH, 43 Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, 44 ]); 45 46 // Roles that are user interactive. 47 const INTERACTIVE_ROLES = new Set([ 48 ...KEYBOARD_FOCUSABLE_ROLES, 49 Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM, 50 Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION, 51 Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION, 52 Ci.nsIAccessibleRole.ROLE_MENUITEM, 53 Ci.nsIAccessibleRole.ROLE_OPTION, 54 Ci.nsIAccessibleRole.ROLE_OUTLINE, 55 Ci.nsIAccessibleRole.ROLE_OUTLINEITEM, 56 Ci.nsIAccessibleRole.ROLE_PAGETAB, 57 Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM, 58 Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, 59 Ci.nsIAccessibleRole.ROLE_RICH_OPTION, 60 ]); 61 62 // Roles that are considered interactive when they are focusable. 63 const INTERACTIVE_IF_FOCUSABLE_ROLES = new Set([ 64 // If article is focusable, we can assume it is inside a feed. 65 Ci.nsIAccessibleRole.ROLE_ARTICLE, 66 // Column header can be focusable. 67 Ci.nsIAccessibleRole.ROLE_COLUMNHEADER, 68 Ci.nsIAccessibleRole.ROLE_GRID_CELL, 69 Ci.nsIAccessibleRole.ROLE_MENUBAR, 70 Ci.nsIAccessibleRole.ROLE_MENUPOPUP, 71 Ci.nsIAccessibleRole.ROLE_PAGETABLIST, 72 // Row header can be focusable. 73 Ci.nsIAccessibleRole.ROLE_ROWHEADER, 74 Ci.nsIAccessibleRole.ROLE_SCROLLBAR, 75 Ci.nsIAccessibleRole.ROLE_SEPARATOR, 76 Ci.nsIAccessibleRole.ROLE_TOOLBAR, 77 ]); 78 79 // Roles that are considered form controls. 80 const FORM_ROLES = new Set([ 81 Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, 82 Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION, 83 Ci.nsIAccessibleRole.ROLE_COMBOBOX, 84 Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX, 85 Ci.nsIAccessibleRole.ROLE_ENTRY, 86 Ci.nsIAccessibleRole.ROLE_LISTBOX, 87 Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, 88 Ci.nsIAccessibleRole.ROLE_PROGRESSBAR, 89 Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, 90 Ci.nsIAccessibleRole.ROLE_SLIDER, 91 Ci.nsIAccessibleRole.ROLE_SPINBUTTON, 92 Ci.nsIAccessibleRole.ROLE_SWITCH, 93 ]); 94 95 const DEFAULT_ENV = Object.freeze({ 96 // Checks that accessible object has at least one accessible action. 97 actionCountRule: true, 98 // Checks that accessible object (and its corresponding node) is focusable 99 // (has focusable state and its node's tabindex is not set to -1). 100 focusableRule: true, 101 // Checks that clickable accessible object (and its corresponding node) has 102 // appropriate interactive semantics. 103 ifClickableThenInteractiveRule: true, 104 // Checks that accessible object has a role that is considered to be 105 // interactive. 106 interactiveRule: true, 107 // Checks that accessible object has a non-empty label. 108 labelRule: true, 109 // Checks that a node is enabled and is expected to be enabled via 110 // the accessibility API. 111 mustBeEnabled: true, 112 // Checks that a node has a corresponding accessible object. 113 mustHaveAccessibleRule: true, 114 // Checks that accessible object (and its corresponding node) have a non- 115 // negative tabindex. Platform accessibility API still sets focusable state 116 // on tabindex=-1 nodes. 117 nonNegativeTabIndexRule: true, 118 }); 119 120 let gA11YChecks = false; 121 122 let gEnv = { 123 ...DEFAULT_ENV, 124 }; 125 126 // This is set by AccessibilityUtils.init so that we always have a reference 127 // to SimpleTest regardless of changes to the global scope. 128 let SimpleTest = null; 129 130 /** 131 * Get role attribute for an accessible object if specified for its 132 * corresponding ``DOMNode``. 133 * 134 * @param {nsIAccessible} accessible 135 * Accessible for which to determine its role attribute value. 136 * 137 * @returns {string} 138 * Role attribute value if specified. 139 */ 140 function getAriaRoles(accessible) { 141 try { 142 return accessible.attributes.getStringProperty("xml-roles"); 143 } catch (e) { 144 // No xml-roles. nsPersistentProperties throws if the attribute for a key 145 // is not found. 146 } 147 148 return ""; 149 } 150 151 /** 152 * Get related accessible objects that are targets of labelled by relation e.g. 153 * labels. 154 * 155 * @param {nsIAccessible} accessible 156 * Accessible objects to get labels for. 157 * 158 * @returns {Array} 159 * A list of accessible objects that are labels for a given accessible. 160 */ 161 function getLabels(accessible) { 162 const relation = accessible.getRelationByType( 163 Ci.nsIAccessibleRelation.RELATION_LABELLED_BY 164 ); 165 return [...relation.getTargets().enumerate(Ci.nsIAccessible)]; 166 } 167 168 /** 169 * Test if an accessible has a ``hidden`` attribute. 170 * 171 * @param {nsIAccessible} accessible 172 * Accessible object. 173 * 174 * @return {boolean} 175 * True if the accessible object has a ``hidden`` attribute, false 176 * otherwise. 177 */ 178 function hasHiddenAttribute(accessible) { 179 let hidden = false; 180 try { 181 hidden = accessible.attributes.getStringProperty("hidden"); 182 } catch (e) {} 183 // If the property is missing, error will be thrown 184 return hidden && hidden === "true"; 185 } 186 187 /** 188 * Test if an accessible is hidden from the user. 189 * 190 * @param {nsIAccessible} accessible 191 * Accessible object. 192 * 193 * @return {boolean} 194 * True if accessible is hidden from user, false otherwise. 195 */ 196 function isHidden(accessible) { 197 if (!accessible) { 198 return true; 199 } 200 201 while (accessible) { 202 if (hasHiddenAttribute(accessible)) { 203 return true; 204 } 205 206 accessible = accessible.parent; 207 } 208 209 return false; 210 } 211 212 /** 213 * Check if an accessible has a given state. 214 * 215 * @param {nsIAccessible} accessible 216 * Accessible object to test. 217 * @param {number} stateToMatch 218 * State to match. 219 * 220 * @return {boolean} 221 * True if |accessible| has |stateToMatch|, false otherwise. 222 */ 223 function matchState(accessible, stateToMatch) { 224 const state = {}; 225 accessible.getState(state, {}); 226 227 return !!(state.value & stateToMatch); 228 } 229 230 /** 231 * Determine if an accessible is a keyboard focusable browser toolbar button. 232 * Browser toolbar buttons aren't keyboard focusable in the usual way. 233 * Instead, focus is managed by JS code which sets tabindex on a single 234 * button at a time. Thus, we need to special case the focusable check for 235 * these buttons. 236 */ 237 function isKeyboardFocusableBrowserToolbarButton(accessible) { 238 const node = accessible.DOMNode; 239 if (!node || !node.ownerGlobal) { 240 return false; 241 } 242 const toolbar = 243 node.closest("toolbar") || 244 node.flattenedTreeParentNode.closest("toolbar"); 245 if (!toolbar || toolbar.getAttribute("keyNav") != "true") { 246 return false; 247 } 248 // The Go button in the Url Bar is an example of a purposefully 249 // non-focusable image toolbar button that provides an mouse/touch-only 250 // control for the search query submission, while a keyboard user could 251 // press `Enter` to do it. Similarly, two scroll buttons that appear when 252 // toolbar is overflowing, and keyboard-only users would actually scroll 253 // tabs in the toolbar while trying to navigate to these controls. When 254 // toolbarbuttons are redundant for keyboard users, we do not want to 255 // create an extra tab stop for such controls, thus we are expecting the 256 // button markup to include `keyNav="false"` attribute to flag it. 257 if (node.getAttribute("keyNav") == "false") { 258 const ariaRoles = getAriaRoles(accessible); 259 return ( 260 ariaRoles.includes("button") || 261 accessible.role == Ci.nsIAccessibleRole.ROLE_PUSHBUTTON 262 ); 263 } 264 return node.ownerGlobal.ToolbarKeyboardNavigator._isButton(node); 265 } 266 267 /** 268 * Determine if an accessible is a keyboard focusable control within a Firefox 269 * View list. The main landmark of the Firefox View has role="application" for 270 * users to expect a custom keyboard navigation pattern. Controls within this 271 * area aren't keyboard focusable in the usual way. Instead, focus is managed 272 * by JS code which sets tabindex on a single control within each list at a 273 * time. Thus, we need to special case the focusable check for these controls. 274 */ 275 function isKeyboardFocusableFxviewControlInApplication(accessible) { 276 const node = accessible.DOMNode; 277 if (!node || !node.ownerGlobal) { 278 return false; 279 } 280 // Firefox View application rows currently include only buttons and links: 281 if ( 282 !node.className.includes("fxview-tab-row-") || 283 (accessible.role != Ci.nsIAccessibleRole.ROLE_PUSHBUTTON && 284 accessible.role != Ci.nsIAccessibleRole.ROLE_LINK) 285 ) { 286 return false; // Not a button or a link in a Firefox View app. 287 } 288 // ToDo: We may eventually need to support intervening generics between 289 // a list and its listitem here and/or aria-owns lists. 290 const listitemAcc = accessible.parent; 291 const listAcc = listitemAcc.parent; 292 if ( 293 (!listAcc || listAcc.role != Ci.nsIAccessibleRole.ROLE_LIST) && 294 (!listitemAcc || listitemAcc.role != Ci.nsIAccessibleRole.ROLE_LISTITEM) 295 ) { 296 return false; // This button/link isn't inside a listitem within a list. 297 } 298 // All listitems should be not focusable while both a button and a link 299 // within each list item might have tabindex="-1". 300 if ( 301 node.tabIndex && 302 matchState(accessible, STATE_FOCUSABLE) && 303 !matchState(listitemAcc, STATE_FOCUSABLE) 304 ) { 305 // ToDo: We may eventually need to support lists which use aria-owns here. 306 // Check that there is only one keyboard reachable control within the list. 307 const childCount = listAcc.childCount; 308 let foundFocusable = false; 309 for (let c = 0; c < childCount; c++) { 310 const listitem = listAcc.getChildAt(c); 311 const listitemChildCount = listitem.childCount; 312 for (let i = 0; i < listitemChildCount; i++) { 313 const listitemControl = listitem.getChildAt(i); 314 // Use tabIndex rather than a11y focusable state because all controls 315 // within the listitem might have tabindex="-1". 316 if (listitemControl.DOMNode.tabIndex == 0) { 317 if (foundFocusable) { 318 // Only one control within a list should be focusable. 319 // ToDo: Fine-tune the a11y-check error message generated in this case. 320 // Strictly speaking, it's not ideal that we're performing an action 321 // from an is function, which normally only queries something without 322 // any externally observable behaviour. That said, fixing that would 323 // involve different return values for different cases (not a list, 324 // too many focusable listitem controls, etc) so we could move the 325 // a11yFail call to the caller. 326 a11yFail( 327 "Only one control should be focusable in a list", 328 accessible 329 ); 330 return false; 331 } 332 foundFocusable = true; 333 } 334 } 335 } 336 return foundFocusable; 337 } 338 return false; 339 } 340 341 /** 342 * Determine if an accessible is a keyboard focusable option within a listbox. 343 * We use it in the Url bar results - these controls are't keyboard focusable 344 * in the usual way. Instead, focus is managed by JS code which sets tabindex 345 * on a single option at a time. Thus, we need to special case the focusable 346 * check for these option items. 347 */ 348 function isKeyboardFocusableOption(accessible) { 349 const node = accessible.DOMNode; 350 if (!node || !node.ownerGlobal) { 351 return false; 352 } 353 const urlbarListbox = node.closest(".urlbarView-results"); 354 if (!urlbarListbox || urlbarListbox.getAttribute("role") != "listbox") { 355 return false; 356 } 357 return node.getAttribute("role") == "option"; 358 } 359 360 /** 361 * Determine if an accessible is a keyboard focusable PanelMultiView control. 362 * These controls aren't keyboard focusable in the usual way. Instead, focus 363 * is managed by JS code which sets tabindex dynamically. Thus, we need to 364 * special case the focusable check for these controls. 365 */ 366 function isKeyboardFocusablePanelMultiViewControl(accessible) { 367 const node = accessible.DOMNode; 368 if (!node || !node.ownerGlobal) { 369 return false; 370 } 371 const panelview = node.closest("panelview"); 372 if (!panelview || panelview.hasAttribute("disablekeynav")) { 373 return false; 374 } 375 return ( 376 node.ownerGlobal.PanelView.forNode(panelview)._tabNavigableWalker.filter( 377 node 378 ) == NodeFilter.FILTER_ACCEPT 379 ); 380 } 381 382 /** 383 * Determine if an accessible is a button that is excluded from a focus 384 * order, because its adjacent sibling is a focusable spinner. Controls with 385 * role="spinbutton" are often placed between two buttons that could 386 * increase ("^") or decrease ("v") the value of this spinner. Those buttons 387 * are not expected to be focusable, because their functionality for keyboard 388 * users is redundant to the spinner. But they are exposed to assistive 389 * technology for touch, mouse, switch, and speech-to-text users. Thus, we 390 * need to special case the focusable check for these buttons adjacent to 391 * a spinner. 392 */ 393 function isKeyboardFocusableSpinbuttonSibling(accessible) { 394 const node = accessible.DOMNode; 395 if (!node || !node.ownerGlobal) { 396 return false; 397 } 398 399 // The control itself is a button: 400 if (accessible.role != Ci.nsIAccessibleRole.ROLE_PUSHBUTTON) { 401 return false; 402 } 403 404 // At least one sibling is a keyboard-focusable spinbutton: 405 for (const sibling of [ 406 node.previousElementSibling, 407 node.nextElementSibling, 408 ]) { 409 if (sibling && sibling.tabIndex >= 0 && sibling.role == "spinbutton") { 410 return true; 411 } 412 } 413 return false; 414 } 415 416 /** 417 * Determine if an accessible is a keyboard focusable tab within a tablist. 418 * Per the ARIA design pattern, these controls aren't keyboard focusable in 419 * the usual way. Instead, focus is managed by JS code which sets tabindex on 420 * a single tab at a time. Thus, we need to special case the focusable check 421 * for these tab controls. 422 */ 423 function isKeyboardFocusableTabInTablist(accessible) { 424 const node = accessible.DOMNode; 425 if (!node || !node.ownerGlobal) { 426 return false; 427 } 428 if (accessible.role != Ci.nsIAccessibleRole.ROLE_PAGETAB) { 429 return false; // Not a tab. 430 } 431 const tablist = findNonGenericParentAccessible(accessible); 432 if (!tablist || tablist.role != Ci.nsIAccessibleRole.ROLE_PAGETABLIST) { 433 return false; // The tab isn't inside a tablist. 434 } 435 // ToDo: We may eventually need to support tablists which use 436 // aria-activedescendant here. 437 // Check that there is only one keyboard reachable tab. 438 let foundFocusable = false; 439 for (const tab of findNonGenericChildrenAccessible(tablist)) { 440 // Allow whitespaces to be included in the tablist for styling purposes 441 const isWhitespace = 442 tab.role == Ci.nsIAccessibleRole.ROLE_TEXT_LEAF && 443 tab.DOMNode.textContent.trim().length === 0; 444 if (tab.role != Ci.nsIAccessibleRole.ROLE_PAGETAB && !isWhitespace) { 445 // The tablist includes children other than tabs or whitespaces 446 a11yFail("Only tabs should be included in a tablist", accessible); 447 } 448 // Use tabIndex rather than a11y focusable state because all tabs might 449 // have tabindex="-1". 450 if (tab.DOMNode.tabIndex == 0) { 451 if (foundFocusable) { 452 // Only one tab within a tablist should be focusable. 453 // ToDo: Fine-tune the a11y-check error message generated in this case. 454 // Strictly speaking, it's not ideal that we're performing an action 455 // from an is function, which normally only queries something without 456 // any externally observable behaviour. That said, fixing that would 457 // involve different return values for different cases (not a tab, 458 // too many focusable tabs, etc) so we could move the a11yFail call 459 // to the caller. 460 a11yFail("Only one tab should be focusable in a tablist", accessible); 461 return false; 462 } 463 foundFocusable = true; 464 } 465 } 466 return foundFocusable; 467 } 468 469 /** 470 * Determine if an accessible is a keyboard focusable button in the url bar. 471 * Url bar buttons aren't keyboard focusable in the usual way. Instead, 472 * focus is managed by JS code which sets tabindex on a single button at a 473 * time. Thus, we need to special case the focusable check for these buttons. 474 * This also applies to the search bar buttons that reuse the same pattern. 475 */ 476 function isKeyboardFocusableUrlbarButton(accessible) { 477 const node = accessible.DOMNode; 478 if (!node || !node.ownerGlobal) { 479 return false; 480 } 481 const isUrlBar = 482 node 483 .closest(".urlbarView > .search-one-offs") 484 ?.getAttribute("disabletab") == "true"; 485 const isSearchBar = 486 node 487 .closest("#PopupSearchAutoComplete > .search-one-offs") 488 ?.getAttribute("is_searchbar") == "true"; 489 return ( 490 (isUrlBar || isSearchBar) && 491 node.getAttribute("tabindex") == "-1" && 492 node.tagName == "button" && 493 node.classList.contains("searchbar-engine-one-off-item") 494 ); 495 } 496 497 /** 498 * Determine if an accessible is a keyboard focusable XUL tab. 499 * Only one tab is focusable at a time, but after focusing it, you can use 500 * the keyboard to focus other tabs. 501 */ 502 function isKeyboardFocusableXULTab(accessible) { 503 const node = accessible.DOMNode; 504 return node && XULElement.isInstance(node) && node.tagName == "tab"; 505 } 506 507 /** 508 * The gridcells are not expected to be interactive and focusable 509 * individually, but it is allowed to manually manage focus within the grid 510 * per ARIA Grid pattern (https://www.w3.org/WAI/ARIA/apg/patterns/grid/). 511 * Example of such grid would be a datepicker where one gridcell can be 512 * selected and the focus is moved with arrow keys once the user tabbed into 513 * the grid. In grids like a calendar, only one element would be included in 514 * the focus order and the rest of grid cells may not have an interactive 515 * accessible created. We need to special case the check for these gridcells. 516 */ 517 function isAccessibleGridcell(node) { 518 if (!node || !node.ownerGlobal) { 519 return false; 520 } 521 const accessible = getAccessible(node); 522 523 if (!accessible || accessible.role != Ci.nsIAccessibleRole.ROLE_GRID_CELL) { 524 return false; // Not a grid cell. 525 } 526 // ToDo: We may eventually need to support intervening generics between 527 // a grid cell and its grid container here. 528 const gridRow = accessible.parent; 529 if (!gridRow || gridRow.role != Ci.nsIAccessibleRole.ROLE_ROW) { 530 return false; // The grid cell isn't inside a row. 531 } 532 let grid = gridRow.parent; 533 if (!grid) { 534 return false; // The grid cell isn't inside a grid. 535 } 536 if (grid.role == Ci.nsIAccessibleRole.ROLE_GROUPING) { 537 // Grid built on the HTML table may include <tbody> wrapper: 538 grid = grid.parent; 539 if (!grid || grid.role != Ci.nsIAccessibleRole.ROLE_GRID) { 540 return false; // The grid cell isn't inside a grid. 541 } 542 } 543 // Check that there is only one keyboard reachable grid cell. 544 let foundFocusable = false; 545 for (const gridCell of grid.DOMNode.querySelectorAll( 546 "td, [role=gridcell]" 547 )) { 548 // Grid cells are not expected to have a "tabindex" attribute and to be 549 // included in the focus order, with the exception of the only one cell 550 // that is included in the page tab sequence to provide access to the grid. 551 if (gridCell.tabIndex == 0) { 552 if (foundFocusable) { 553 // Only one grid cell within a grid should be focusable. 554 // ToDo: Fine-tune the a11y-check error message generated in this case. 555 // Strictly speaking, it's not ideal that we're performing an action 556 // from an is function, which normally only queries something without 557 // any externally observable behaviour. That said, fixing that would 558 // involve different return values for different cases (not a grid 559 // cell, too many focusable grid cells, etc) so we could move the 560 // a11yFail call to the caller. 561 a11yFail( 562 "Only one grid cell should be focusable in a grid", 563 accessible 564 ); 565 return false; 566 } 567 foundFocusable = true; 568 } 569 } 570 return foundFocusable; 571 } 572 573 /** 574 * XUL treecol elements currently aren't focusable, making them inaccessible. 575 * For now, we don't flag these as a failure to avoid breaking multiple tests. 576 * ToDo: We should remove this exception after this is fixed in bug 1848397. 577 */ 578 function isInaccessibleXulTreecol(node) { 579 if (!node || !node.ownerGlobal) { 580 return false; 581 } 582 const listheader = node.flattenedTreeParentNode; 583 if (listheader.tagName !== "listheader" || node.tagName !== "treecol") { 584 return false; 585 } 586 return true; 587 } 588 589 /** 590 * Determine if a DOM node is a combobox container of the url bar. We 591 * intentionally leave this element unlabeled, because its child is a search 592 * input that is the target and main control of this component. In general, we 593 * want to avoid duplication in the label announcement when a user focuses the 594 * input. Both NVDA and VO ignore the label on at least one of these controls 595 * if both have a label. But the bigger concern here is that it's very 596 * difficult to keep the accessible name synchronized between the combobox and 597 * the input. Thus, we need to special case the label check for this control. 598 */ 599 function isUnlabeledUrlBarCombobox(node) { 600 if (!node || !node.ownerGlobal) { 601 return false; 602 } 603 let ariaRole = node.getAttribute("role"); 604 // There are only two cases of this pattern: <moz-input-box> and <searchbar> 605 const isMozInputBox = 606 node.tagName == "moz-input-box" && 607 node.classList.contains("urlbar-input-box"); 608 const isSearchbar = node.tagName == "searchbar" && node.id == "searchbar"; 609 return (isMozInputBox || isSearchbar) && ariaRole == "combobox"; 610 } 611 612 /** 613 * Determine if a DOM node is an option within the url bar. We know each 614 * url bar option is accessible, but it disappears as soon as it is clicked 615 * during tests and the a11y-checks do not have time to test the label, 616 * because the Fluent localization is not yet completed by then. Thus, we 617 * need to special case the label check for these controls. 618 */ 619 function isUnlabeledUrlBarOption(node) { 620 if (!node || !node.ownerGlobal) { 621 return false; 622 } 623 const role = getAccessible(node)?.role; 624 const isOption = 625 node.tagName == "span" && 626 node.getAttribute("role") == "option" && 627 node.classList.contains("urlbarView-row-inner"); 628 const isMenuItem = 629 node.tagName == "menuitem" && 630 role == Ci.nsIAccessibleRole.ROLE_MENUITEM && 631 node.classList.contains("urlbarView-result-menuitem"); 632 // Not all options have "data-l10n-id" attributes in the URL Bar, because 633 // some of options are autocomplete options based on the user input and 634 // they are not expected to be localized. 635 return isOption || isMenuItem; 636 } 637 638 /** 639 * Determine if a DOM node is a menuitem within the XUL menu. We know each 640 * menuitem is accessible, but it disappears as soon as it is clicked during 641 * tests and the a11y-checks do not have time to test the label, because the 642 * Fluent localization is not yet completed by then. Thus, we need to special 643 * case the label check for these controls. 644 */ 645 function isUnlabeledMenuitem(node) { 646 if (!node || !node.ownerGlobal) { 647 return false; 648 } 649 const hasLabel = node.querySelector("label, description"); 650 const isMenuItem = 651 node.getAttribute("role") == "menuitem" || 652 (node.tagName == "richlistitem" && 653 node.classList.contains("autocomplete-richlistitem")) || 654 (node.tagName == "menuitem" && 655 node.classList.contains("urlbarView-result-menuitem")); 656 657 let parentNode = node.getRootNode().host ?? node.parentNode; 658 const isParentMenu = 659 parentNode.getAttribute("role") == "menu" || 660 (parentNode.tagName == "richlistbox" && 661 parentNode.classList.contains("autocomplete-richlistbox")) || 662 (parentNode.tagName == "menupopup" && 663 parentNode.classList.contains("urlbarView-result-menu")); 664 return ( 665 isMenuItem && 666 isParentMenu && 667 hasLabel && 668 (node.hasAttribute("data-l10n-id") || node.tagName == "richlistitem") 669 ); 670 } 671 672 /** 673 * Determine if the node is a "Show All" or one of image buttons on the 674 * about:config page, or a "X" close button on moz-message-bar. We know these 675 * buttons are accessible, but they disappear/are replaced as soon as they 676 * are clicked during tests and the a11y-checks do not have time to test the 677 * label, because the Fluent localization is not yet completed by then. 678 * Thus, we need to special case the label check for these controls. 679 */ 680 function isUnlabeledImageButton(node) { 681 if (!node || !node.ownerGlobal) { 682 return false; 683 } 684 const isShowAllButton = node.id == "show-all"; 685 const isReplacedImageButton = 686 node.classList.contains("button-add") || 687 node.classList.contains("button-delete") || 688 node.classList.contains("button-reset"); 689 const isCloseMozMessageBarButton = 690 node.classList.contains("close") && 691 node.getAttribute("data-l10n-id") == "moz-message-bar-close-button"; 692 return ( 693 node.tagName.toLowerCase() == "button" && 694 node.hasAttribute("data-l10n-id") && 695 (isShowAllButton || isReplacedImageButton || isCloseMozMessageBarButton) 696 ); 697 } 698 699 /** 700 * Determine if a node is a XUL:button on a prompt popup. We know this button 701 * is accessible, but it disappears as soon as it is clicked during tests and 702 * the a11y-checks do not have time to test the label, because the Fluent 703 * localization is not yet completed by then. Thus, we need to special case 704 * the label check for these controls. 705 */ 706 function isUnlabeledXulButton(node) { 707 if (!node || !node.ownerGlobal) { 708 return false; 709 } 710 const hasLabel = node.querySelector("label, xul\\:label"); 711 const isButton = 712 node.getAttribute("role") == "button" || 713 node.tagName == "button" || 714 node.tagName == "xul:button"; 715 return isButton && hasLabel && node.hasAttribute("data-l10n-id"); 716 } 717 718 /** 719 * Determine if a node is a XUL element for which tabIndex should be ignored. 720 * Some XUL elements report -1 for the .tabIndex property, even though they 721 * are in fact keyboard focusable. 722 */ 723 function shouldIgnoreTabIndex(node) { 724 if (!XULElement.isInstance(node)) { 725 return false; 726 } 727 return node.tagName == "label" && node.getAttribute("is") == "text-link"; 728 } 729 730 /** 731 * Determine if accessible is focusable with the keyboard. 732 * 733 * @param {nsIAccessible} accessible 734 * Accessible for which to determine if it is keyboard focusable. 735 * 736 * @returns {boolean} 737 * True if focusable with the keyboard. 738 */ 739 function isKeyboardFocusable(accessible) { 740 if ( 741 isKeyboardFocusableBrowserToolbarButton(accessible) || 742 isKeyboardFocusableOption(accessible) || 743 isKeyboardFocusablePanelMultiViewControl(accessible) || 744 isKeyboardFocusableUrlbarButton(accessible) || 745 isKeyboardFocusableXULTab(accessible) || 746 isKeyboardFocusableTabInTablist(accessible) || 747 isKeyboardFocusableFxviewControlInApplication(accessible) || 748 isKeyboardFocusableSpinbuttonSibling(accessible) 749 ) { 750 return true; 751 } 752 // State will be focusable even if the tabindex is negative. 753 const node = accessible.DOMNode; 754 const role = accessible.role; 755 return ( 756 matchState(accessible, STATE_FOCUSABLE) && 757 // Platform accessibility will still report STATE_FOCUSABLE even with the 758 // tabindex="-1" so we need to check that it is >= 0 to be considered 759 // keyboard focusable. 760 (!gEnv.nonNegativeTabIndexRule || 761 node.tabIndex > -1 || 762 node.closest('[aria-activedescendant][tabindex="0"]') || 763 // If an ARIA toolbar uses a roving tabindex, some controls on the 764 // toolbar might not currently be focusable even though they can be 765 // reached with arrow keys and become focusable at that point. 766 ((role == Ci.nsIAccessibleRole.ROLE_PUSHBUTTON || 767 role == Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON) && 768 node.closest('[role="toolbar"]')) || 769 // <moz-radio-group> and <moz-visual-picker> also use a roving tabindex. 770 (role === Ci.nsIAccessibleRole.ROLE_RADIOBUTTON && 771 node.getRootNode().host?.localName === "moz-radio") || 772 (role === Ci.nsIAccessibleRole.ROLE_RADIOBUTTON && 773 node.getRootNode().host?.localName === "moz-visual-picker-item") || 774 shouldIgnoreTabIndex(node)) 775 ); 776 } 777 778 function buildMessage(message, DOMNode) { 779 if (DOMNode) { 780 const { id, tagName, className } = DOMNode; 781 message += `: id: ${id}, tagName: ${tagName}, className: ${className}`; 782 } 783 784 return message; 785 } 786 787 /** 788 * Fail a test with a given message because of an issue with a given 789 * accessible object. This is used for cases where there's an actual 790 * accessibility failure that prevents UI from being accessible to keyboard/AT 791 * users. 792 * 793 * @param {string} message 794 * @param {nsIAccessible} accessible 795 * Accessible to log along with the failure message. 796 */ 797 function a11yFail(message, { DOMNode }) { 798 SimpleTest.ok(false, buildMessage(message, DOMNode)); 799 } 800 801 /** 802 * Log a todo statement with a given message because of an issue with a given 803 * accessible object. This is used for cases where accessibility best 804 * practices are not followed or for something that is not as severe to be 805 * considered a failure. 806 * 807 * @param {string} message 808 * @param {nsIAccessible} accessible 809 * Accessible to log along with the todo message. 810 */ 811 function a11yWarn(message, { DOMNode }) { 812 SimpleTest.todo(false, buildMessage(message, DOMNode)); 813 } 814 815 /** 816 * Test if the node's unavailable via the accessibility API. 817 * 818 * @param {nsIAccessible} accessible 819 * Accessible object. 820 */ 821 function assertEnabled(accessible) { 822 if (gEnv.mustBeEnabled && matchState(accessible, STATE_UNAVAILABLE)) { 823 a11yFail( 824 "Node expected to be enabled but is disabled via the accessibility API", 825 accessible 826 ); 827 } 828 } 829 830 /** 831 * Test if it is possible to focus on a node with the keyboard. This method 832 * also checks for additional keyboard focus issues that might arise. 833 * 834 * @param {nsIAccessible} accessible 835 * Accessible object for a node. 836 */ 837 function assertFocusable(accessible) { 838 if ( 839 gEnv.mustBeEnabled && 840 gEnv.focusableRule && 841 !isKeyboardFocusable(accessible) 842 ) { 843 const ariaRoles = getAriaRoles(accessible); 844 // Do not force ARIA combobox or listbox to be focusable. 845 if (!ariaRoles.includes("combobox") && !ariaRoles.includes("listbox")) { 846 a11yFail("Node is not focusable via the accessibility API", accessible); 847 } 848 849 return; 850 } 851 852 if (!INTERACTIVE_IF_FOCUSABLE_ROLES.has(accessible.role)) { 853 // ROLE_TABLE is used for grids too which are considered interactive. 854 if ( 855 accessible.role === Ci.nsIAccessibleRole.ROLE_TABLE && 856 !getAriaRoles(accessible).includes("grid") 857 ) { 858 a11yWarn( 859 "Focusable nodes should have interactive semantics", 860 accessible 861 ); 862 863 return; 864 } 865 } 866 867 if (accessible.DOMNode.tabIndex > 0) { 868 a11yWarn("Avoid using tabindex attribute greater than zero", accessible); 869 } 870 } 871 872 /** 873 * Test if it is possible to interact with a node via the accessibility API. 874 * 875 * @param {nsIAccessible} accessible 876 * Accessible object for a node. 877 */ 878 function assertInteractive(accessible) { 879 if ( 880 gEnv.mustBeEnabled && 881 gEnv.actionCountRule && 882 accessible.actionCount === 0 883 ) { 884 a11yFail("Node does not support any accessible actions", accessible); 885 886 return; 887 } 888 889 if ( 890 gEnv.mustBeEnabled && 891 gEnv.interactiveRule && 892 !INTERACTIVE_ROLES.has(accessible.role) 893 ) { 894 if ( 895 // Labels that have a label for relation with their target are clickable. 896 (accessible.role !== Ci.nsIAccessibleRole.ROLE_LABEL || 897 accessible.getRelationByType( 898 Ci.nsIAccessibleRelation.RELATION_LABEL_FOR 899 ).targetsCount === 0) && 900 // Images that are inside an anchor (have linked state). 901 (accessible.role !== Ci.nsIAccessibleRole.ROLE_GRAPHIC || 902 !matchState(accessible, STATE_LINKED)) 903 ) { 904 // Look for click action in the list of actions. 905 for (let i = 0; i < accessible.actionCount; i++) { 906 if ( 907 gEnv.ifClickableThenInteractiveRule && 908 accessible.getActionName(i) === CLICK_ACTION 909 ) { 910 a11yFail( 911 "Clickable nodes must have interactive semantics", 912 accessible 913 ); 914 } 915 } 916 } 917 918 a11yFail( 919 "Node does not have a correct interactive role and may not be " + 920 "manipulated via the accessibility API", 921 accessible 922 ); 923 } 924 } 925 926 /** 927 * Test if the node is labelled appropriately for accessibility API. 928 * 929 * @param {nsIAccessible} accessible 930 * Accessible object for a node. 931 */ 932 function assertLabelled(accessible, allowRecurse = true) { 933 const { DOMNode } = accessible; 934 let name = accessible.name; 935 if (!name) { 936 // If text has just been inserted into the tree, the a11y engine might not 937 // have picked it up yet. 938 forceRefreshDriverTick(DOMNode); 939 try { 940 name = accessible.name; 941 } catch (e) { 942 // The Accessible died because the DOM node was removed or hidden. 943 if (gEnv.labelRule) { 944 // Some elements disappear as soon as they are clicked during tests, 945 // their accessible dies before the Fluent localization is completed. 946 // We want to exclude these groups of nodes from the label check. 947 // Note: In other cases, this first block isn't necessarily hit 948 // because Fluent isn't finished yet. This might happen if a text 949 // node was inserted (whether by Fluent or something else) but a11y 950 // hasn't picked it up yet, but the node gets hidden before a11y 951 // can pick it up. 952 if ( 953 isUnlabeledUrlBarOption(DOMNode) || 954 isUnlabeledMenuitem(DOMNode) || 955 isUnlabeledImageButton(DOMNode) 956 ) { 957 return; 958 } 959 a11yWarn("Unlabeled element removed before l10n finished", { 960 DOMNode, 961 }); 962 } 963 return; 964 } 965 const doc = DOMNode.ownerDocument; 966 if ( 967 !name && 968 allowRecurse && 969 gEnv.labelRule && 970 doc.hasPendingL10nMutations 971 ) { 972 // There are pending async l10n mutations which might result in a valid 973 // accessible name. Try this check again once l10n is finished. 974 doc.addEventListener( 975 "L10nMutationsFinished", 976 () => { 977 try { 978 accessible.name; 979 } catch (e) { 980 // The Accessible died because the DOM node was removed or hidden. 981 if ( 982 isUnlabeledUrlBarOption(DOMNode) || 983 isUnlabeledImageButton(DOMNode) || 984 isUnlabeledXulButton(DOMNode) 985 ) { 986 return; 987 } 988 a11yWarn("Unlabeled element removed before l10n finished", { 989 DOMNode, 990 }); 991 return; 992 } 993 assertLabelled(accessible, false); 994 }, 995 { once: true } 996 ); 997 return; 998 } 999 } 1000 if (name) { 1001 name = name.trim(); 1002 } 1003 if (gEnv.labelRule && !name) { 1004 // The URL and Search Bar comboboxes are purposefully unlabeled, 1005 // since they include labeled inputs that are receiving focus. 1006 // Or the Accessible died because the DOM node was removed or hidden. 1007 if ( 1008 isUnlabeledUrlBarCombobox(DOMNode) || 1009 isUnlabeledUrlBarOption(DOMNode) 1010 ) { 1011 return; 1012 } 1013 a11yFail("Interactive elements must be labeled", accessible); 1014 1015 return; 1016 } 1017 1018 if (FORM_ROLES.has(accessible.role)) { 1019 const labels = getLabels(accessible); 1020 const hasNameFromVisibleLabel = labels.some( 1021 label => !matchState(label, STATE_INVISIBLE) 1022 ); 1023 1024 if (!hasNameFromVisibleLabel) { 1025 a11yWarn("Form elements should have a visible text label", accessible); 1026 } 1027 } else if ( 1028 accessible.role === Ci.nsIAccessibleRole.ROLE_LINK && 1029 DOMNode.nodeName === "AREA" && 1030 DOMNode.hasAttribute("href") 1031 ) { 1032 const alt = DOMNode.getAttribute("alt"); 1033 if (alt && alt.trim() !== name) { 1034 a11yFail( 1035 "Use alt attribute to label area elements that have the href attribute", 1036 accessible 1037 ); 1038 } 1039 } 1040 } 1041 1042 /** 1043 * Test if the node's visible via accessibility API. 1044 * 1045 * @param {nsIAccessible} accessible 1046 * Accessible object for a node. 1047 */ 1048 function assertVisible(accessible) { 1049 if (isHidden(accessible)) { 1050 a11yFail( 1051 "Node is not currently visible via the accessibility API and may not " + 1052 "be manipulated by it", 1053 accessible 1054 ); 1055 } 1056 } 1057 1058 /** 1059 * Walk node ancestry and force refresh driver tick in every document. 1060 * 1061 * @param {DOMNode} node 1062 * Node for traversing the ancestry. 1063 */ 1064 function forceRefreshDriverTick(node) { 1065 const wins = []; 1066 let bc = BrowsingContext.getFromWindow(node.ownerDocument.defaultView); // eslint-disable-line 1067 while (bc) { 1068 wins.push(bc.associatedWindow); 1069 bc = bc.embedderWindowGlobal?.browsingContext; 1070 } 1071 1072 let win = wins.pop(); 1073 while (win) { 1074 // Stop the refresh driver from doing its regular ticks and force two 1075 // refresh driver ticks: first to let layout update and notify a11y, and 1076 // the second to let a11y process updates. 1077 win.windowUtils.advanceTimeAndRefresh(100); 1078 win.windowUtils.advanceTimeAndRefresh(100); 1079 // Go back to normal refresh driver ticks. 1080 win.windowUtils.restoreNormalRefresh(); 1081 win = wins.pop(); 1082 } 1083 } 1084 1085 /** 1086 * Get an accessible object for a node. 1087 * Note: this method will not resolve if accessible object does not become 1088 * available for a given node. 1089 * 1090 * @param {DOMNode} node 1091 * Node to get the accessible object for. 1092 * 1093 * @return {nsIAccessible} 1094 * Accessibility object for a given node. 1095 */ 1096 function getAccessible(node) { 1097 const accessibilityService = Cc[ 1098 "@mozilla.org/accessibilityService;1" 1099 ].getService(Ci.nsIAccessibilityService); 1100 if (!accessibilityService) { 1101 // This is likely a build with --disable-accessibility 1102 return null; 1103 } 1104 1105 let acc = accessibilityService.getAccessibleFor(node); 1106 if (acc) { 1107 return acc; 1108 } 1109 1110 // Force refresh tick throughout document hierarchy 1111 forceRefreshDriverTick(node); 1112 return accessibilityService.getAccessibleFor(node); 1113 } 1114 1115 /** 1116 * Find the nearest interactive accessible ancestor for a node. 1117 */ 1118 function findInteractiveAccessible(node) { 1119 let acc; 1120 // Walk DOM ancestors until we find one with an accessible. 1121 for (; node && !acc; node = node.flattenedTreeParentNode) { 1122 acc = getAccessible(node); 1123 } 1124 if (!acc) { 1125 // No accessible ancestor. 1126 return acc; 1127 } 1128 // Walk a11y ancestors until we find one which is interactive. 1129 for (; acc; acc = acc.parent) { 1130 const relation = acc.getRelationByType( 1131 Ci.nsIAccessibleRelation.RELATION_LABEL_FOR 1132 ); 1133 if ( 1134 acc.role === Ci.nsIAccessibleRole.ROLE_LABEL && 1135 relation.targetsCount > 0 1136 ) { 1137 // If a <label> was clicked to activate a radiobutton or a checkbox, 1138 // return the accessible of the related input. 1139 // Note: aria-labelledby doesn't give the node a role of label, so this 1140 // won't work for aria-labelledby cases. That said, aria-labelledby also 1141 // doesn't have implicit click behaviour either and there's not really 1142 // any way we can check for that. 1143 const targetAcc = relation.getTarget(0); 1144 return targetAcc; 1145 } 1146 if (INTERACTIVE_ROLES.has(acc.role)) { 1147 return acc; 1148 } 1149 } 1150 // No interactive ancestor. 1151 return null; 1152 } 1153 1154 /** 1155 * Find the nearest non-generic ancestor for a node to account for generic 1156 * containers to intervene between the ancestor and it child. 1157 */ 1158 function findNonGenericParentAccessible(childAcc) { 1159 for (let acc = childAcc.parent; acc; acc = acc.parent) { 1160 if (acc.computedARIARole != "generic") { 1161 return acc; 1162 } 1163 } 1164 return null; 1165 } 1166 1167 /** 1168 * Find the nearest non-generic children for a node to account for generic 1169 * containers to intervene between the ancestor and its children. 1170 */ 1171 function* findNonGenericChildrenAccessible(parentAcc) { 1172 const count = parentAcc.childCount; 1173 for (let c = 0; c < count; ++c) { 1174 const child = parentAcc.getChildAt(c); 1175 // When Gecko will consider only one role as generic, we'd use child.role 1176 if (child.computedARIARole == "generic") { 1177 yield* findNonGenericChildrenAccessible(child); 1178 } else { 1179 yield child; 1180 } 1181 } 1182 } 1183 1184 function runIfA11YChecks(task) { 1185 return (...args) => (gA11YChecks ? task(...args) : null); 1186 } 1187 1188 /** 1189 * AccessibilityUtils provides utility methods for retrieving accessible objects 1190 * and performing accessibility related checks. 1191 * Current methods: 1192 * assertCanBeClicked 1193 * setEnv 1194 * resetEnv 1195 * 1196 */ 1197 const AccessibilityUtils = { 1198 assertCanBeClicked(node) { 1199 // Click events might fire on an inaccessible or non-interactive 1200 // descendant, even if the test author targeted them at an interactive 1201 // element. For example, if there's a button with an image inside it, 1202 // node might be the image. 1203 const acc = findInteractiveAccessible(node); 1204 if (!acc) { 1205 if (isAccessibleGridcell(node) || isInaccessibleXulTreecol(node)) { 1206 return; 1207 } 1208 if (gEnv.mustHaveAccessibleRule) { 1209 a11yFail("Node is not accessible via accessibility API", { 1210 DOMNode: node, 1211 }); 1212 } 1213 1214 return; 1215 } 1216 1217 assertInteractive(acc); 1218 assertFocusable(acc); 1219 assertVisible(acc); 1220 assertEnabled(acc); 1221 assertLabelled(acc); 1222 }, 1223 1224 setEnv(env = DEFAULT_ENV) { 1225 gEnv = { 1226 ...DEFAULT_ENV, 1227 ...env, 1228 }; 1229 }, 1230 1231 resetEnv() { 1232 gEnv = { ...DEFAULT_ENV }; 1233 }, 1234 1235 reset(a11yChecks = false, testPath = "") { 1236 gA11YChecks = a11yChecks; 1237 1238 const { Services } = SpecialPowers; 1239 // Disable accessibility service if it is running and if a11y checks are 1240 // disabled. However, don't do this for accessibility engine tests. 1241 if ( 1242 !gA11YChecks && 1243 Services.appinfo.accessibilityEnabled && 1244 !testPath.startsWith("chrome://mochitests/content/browser/accessible/") 1245 ) { 1246 Services.prefs.setIntPref(FORCE_DISABLE_ACCESSIBILITY_PREF, 1); 1247 Services.prefs.clearUserPref(FORCE_DISABLE_ACCESSIBILITY_PREF); 1248 } 1249 1250 // Reset accessibility environment flags that might've been set within the 1251 // test. 1252 this.resetEnv(); 1253 }, 1254 1255 init(simpleTest) { 1256 this._shouldHandleClicks = true; 1257 // A top level xul window's DocShell doesn't have a chromeEventHandler 1258 // attribute. In that case, the chrome event handler is just the global 1259 // window object. 1260 this._handler ??= 1261 window.docShell.chromeEventHandler ?? window.docShell.domWindow; 1262 this._handler.addEventListener("click", this, true, true); 1263 SimpleTest = simpleTest; 1264 }, 1265 1266 uninit() { 1267 this._handler?.removeEventListener("click", this, true); 1268 this._handler = null; 1269 SimpleTest = null; 1270 }, 1271 1272 /** 1273 * Suppress (or disable suppression of) handling of captured click events. 1274 * This should only be called by EventUtils, etc. when a click event will 1275 * be generated but we know it is not actually a click intended to activate 1276 * a control; e.g. drag/drop. Tests that wish to disable specific checks 1277 * should use setEnv instead. 1278 */ 1279 suppressClickHandling(shouldSuppress) { 1280 this._shouldHandleClicks = !shouldSuppress; 1281 }, 1282 1283 handleEvent({ composedTarget }) { 1284 if (!this._shouldHandleClicks) { 1285 return; 1286 } 1287 if (composedTarget.tagName.toLowerCase() == "slot") { 1288 // The click occurred on a text node inside a slot. Since events don't 1289 // target text nodes, the event was retargeted to the slot. However, a 1290 // slot isn't itself rendered. To deal with this, use the slot's parent 1291 // instead. 1292 composedTarget = composedTarget.flattenedTreeParentNode; 1293 } 1294 const bounds = 1295 composedTarget.ownerGlobal?.windowUtils?.getBoundsWithoutFlushing( 1296 composedTarget 1297 ); 1298 if (bounds && (bounds.width == 0 || bounds.height == 0)) { 1299 // Some tests click hidden nodes. These clearly aren't testing the UI 1300 // for the node itself (and presumably there is a test somewhere else 1301 // that does). Therefore, we can't (and shouldn't) do a11y checks. 1302 return; 1303 } 1304 this.assertCanBeClicked(composedTarget); 1305 }, 1306 }; 1307 1308 AccessibilityUtils.assertCanBeClicked = runIfA11YChecks( 1309 AccessibilityUtils.assertCanBeClicked.bind(AccessibilityUtils) 1310 ); 1311 1312 AccessibilityUtils.setEnv = runIfA11YChecks( 1313 AccessibilityUtils.setEnv.bind(AccessibilityUtils) 1314 ); 1315 1316 AccessibilityUtils.resetEnv = runIfA11YChecks( 1317 AccessibilityUtils.resetEnv.bind(AccessibilityUtils) 1318 ); 1319 1320 return AccessibilityUtils; 1321 })();