tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

keyboard.js (14689B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 loader.lazyRequireGetter(
      8  this,
      9  "CssLogic",
     10  "resource://devtools/server/actors/inspector/css-logic.js",
     11  true
     12 );
     13 loader.lazyRequireGetter(
     14  this,
     15  "getMatchingCSSRules",
     16  "resource://devtools/shared/inspector/css-logic.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "nodeConstants",
     22  "resource://devtools/shared/dom-node-constants.js"
     23 );
     24 loader.lazyRequireGetter(
     25  this,
     26  ["isDefunct", "getAriaRoles"],
     27  "resource://devtools/server/actors/utils/accessibility.js",
     28  true
     29 );
     30 
     31 const {
     32  accessibility: {
     33    AUDIT_TYPE: { KEYBOARD },
     34    ISSUE_TYPE: {
     35      [KEYBOARD]: {
     36        FOCUSABLE_NO_SEMANTICS,
     37        FOCUSABLE_POSITIVE_TABINDEX,
     38        INTERACTIVE_NO_ACTION,
     39        INTERACTIVE_NOT_FOCUSABLE,
     40        MOUSE_INTERACTIVE_ONLY,
     41        NO_FOCUS_VISIBLE,
     42      },
     43    },
     44    SCORES: { FAIL, WARNING },
     45  },
     46 } = require("resource://devtools/shared/constants.js");
     47 
     48 // Accessible action for showing long description.
     49 const CLICK_ACTION = "click";
     50 
     51 /**
     52 * Focus specific pseudo classes that the keyboard audit simulates to determine
     53 * focus styling.
     54 */
     55 const FOCUS_PSEUDO_CLASS = ":focus";
     56 const MOZ_FOCUSRING_PSEUDO_CLASS = ":-moz-focusring";
     57 
     58 const KEYBOARD_FOCUSABLE_ROLES = new Set([
     59  Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
     60  Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
     61  Ci.nsIAccessibleRole.ROLE_COMBOBOX,
     62  Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX,
     63  Ci.nsIAccessibleRole.ROLE_ENTRY,
     64  Ci.nsIAccessibleRole.ROLE_LINK,
     65  Ci.nsIAccessibleRole.ROLE_LISTBOX,
     66  Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
     67  Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
     68  Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
     69  Ci.nsIAccessibleRole.ROLE_SLIDER,
     70  Ci.nsIAccessibleRole.ROLE_SEARCHBOX,
     71  Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
     72  Ci.nsIAccessibleRole.ROLE_SUMMARY,
     73  Ci.nsIAccessibleRole.ROLE_SWITCH,
     74  Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
     75 ]);
     76 
     77 const INTERACTIVE_ROLES = new Set([
     78  ...KEYBOARD_FOCUSABLE_ROLES,
     79  Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
     80  Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
     81  Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
     82  Ci.nsIAccessibleRole.ROLE_MENUITEM,
     83  Ci.nsIAccessibleRole.ROLE_OPTION,
     84  Ci.nsIAccessibleRole.ROLE_OUTLINE,
     85  Ci.nsIAccessibleRole.ROLE_OUTLINEITEM,
     86  Ci.nsIAccessibleRole.ROLE_PAGETAB,
     87  Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
     88  Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
     89  Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
     90 ]);
     91 
     92 const INTERACTIVE_IF_FOCUSABLE_ROLES = new Set([
     93  // If article is focusable, we can assume it is inside a feed.
     94  Ci.nsIAccessibleRole.ROLE_ARTICLE,
     95  // Column header can be focusable.
     96  Ci.nsIAccessibleRole.ROLE_COLUMNHEADER,
     97  Ci.nsIAccessibleRole.ROLE_GRID_CELL,
     98  Ci.nsIAccessibleRole.ROLE_MENUBAR,
     99  Ci.nsIAccessibleRole.ROLE_MENUPOPUP,
    100  Ci.nsIAccessibleRole.ROLE_PAGETABLIST,
    101  // Row header can be focusable.
    102  Ci.nsIAccessibleRole.ROLE_ROWHEADER,
    103  Ci.nsIAccessibleRole.ROLE_SCROLLBAR,
    104  Ci.nsIAccessibleRole.ROLE_SEPARATOR,
    105  Ci.nsIAccessibleRole.ROLE_TOOLBAR,
    106 ]);
    107 
    108 /**
    109 * Determine if a node is dead or is not an element node.
    110 *
    111 * @param   {DOMNode} node
    112 *          Node to be tested for validity.
    113 *
    114 * @returns {boolean}
    115 *          True if the node is either dead or is not an element node.
    116 */
    117 function isInvalidNode(node) {
    118  return (
    119    !node ||
    120    Cu.isDeadWrapper(node) ||
    121    node.nodeType !== nodeConstants.ELEMENT_NODE ||
    122    !node.ownerGlobal
    123  );
    124 }
    125 
    126 /**
    127 * Determine if accessible is focusable with the keyboard.
    128 *
    129 * @param   {nsIAccessible} accessible
    130 *          Accessible for which to determine if it is keyboard focusable.
    131 *
    132 * @returns {boolean}
    133 *          True if focusable with the keyboard.
    134 */
    135 function isKeyboardFocusable(accessible) {
    136  const state = {};
    137  accessible.getState(state, {});
    138  // State will be focusable even if the tabindex is negative.
    139  return (
    140    state.value & Ci.nsIAccessibleStates.STATE_FOCUSABLE &&
    141    // Platform accessibility will still report STATE_FOCUSABLE even with the
    142    // tabindex="-1" so we need to check that it is >= 0 to be considered
    143    // keyboard focusable.
    144    accessible.DOMNode.tabIndex > -1
    145  );
    146 }
    147 
    148 /**
    149 * Determine if a current node has focus specific styling by applying a
    150 * focus-related pseudo class (such as :focus or :-moz-focusring) to a focusable
    151 * node.
    152 *
    153 * @param   {DOMNode} focusableNode
    154 *          Node to apply focus-related pseudo class to.
    155 * @param   {DOMNode} currentNode
    156 *          Node to be checked for having focus specific styling.
    157 * @param   {string} pseudoClass
    158 *          A focus related pseudo-class to be simulated for style comparison.
    159 *
    160 * @returns {boolean}
    161 *          True if the currentNode has focus specific styling.
    162 */
    163 function hasStylesForFocusRelatedPseudoClass(
    164  focusableNode,
    165  currentNode,
    166  pseudoClass
    167 ) {
    168  const defaultRules = getMatchingCSSRules(currentNode);
    169 
    170  InspectorUtils.addPseudoClassLock(focusableNode, pseudoClass);
    171 
    172  // Determine a set of properties that are specific to CSS rules that are only
    173  // present when a focus related pseudo-class is locked in.
    174  const tempRules = getMatchingCSSRules(currentNode);
    175  const properties = new Set();
    176  for (const rule of tempRules) {
    177    if (!defaultRules.includes(rule)) {
    178      for (let index = 0; index < rule.style.length; index++) {
    179        properties.add(rule.style.item(index));
    180      }
    181    }
    182  }
    183 
    184  // If there are no focus specific CSS rules or properties, currentNode does
    185  // node have any focus specific styling, we are done.
    186  if (properties.size === 0) {
    187    InspectorUtils.removePseudoClassLock(focusableNode, pseudoClass);
    188    return false;
    189  }
    190 
    191  // Determine values for properties that are focus specific.
    192  const tempStyle = CssLogic.getComputedStyle(currentNode);
    193  const focusStyle = {};
    194  for (const name of properties.values()) {
    195    focusStyle[name] = tempStyle.getPropertyValue(name);
    196  }
    197 
    198  InspectorUtils.removePseudoClassLock(focusableNode, pseudoClass);
    199 
    200  // If values for focus specific properties are different from default style
    201  // values, assume we have focus spefic styles for the currentNode.
    202  const defaultStyle = CssLogic.getComputedStyle(currentNode);
    203  for (const name of properties.values()) {
    204    if (defaultStyle.getPropertyValue(name) !== focusStyle[name]) {
    205      return true;
    206    }
    207  }
    208 
    209  return false;
    210 }
    211 
    212 /**
    213 * Check if an element node (currentNode) has distinct focus styling. This
    214 * function also takes into account a case when focus styling is applied to a
    215 * descendant too.
    216 *
    217 * @param   {DOMNode} focusableNode
    218 *          Node to apply focus-related pseudo class to.
    219 * @param   {DOMNode} currentNode
    220 *          Node to be checked for having focus specific styling.
    221 *
    222 * @returns {boolean}
    223 *          True if the node or its descendant has distinct focus styling.
    224 */
    225 function hasFocusStyling(focusableNode, currentNode) {
    226  if (isInvalidNode(currentNode)) {
    227    return false;
    228  }
    229 
    230  // Check if an element node has distinct :-moz-focusring styling.
    231  const hasStylesForMozFocusring = hasStylesForFocusRelatedPseudoClass(
    232    focusableNode,
    233    currentNode,
    234    MOZ_FOCUSRING_PSEUDO_CLASS
    235  );
    236  if (hasStylesForMozFocusring) {
    237    return true;
    238  }
    239 
    240  // Check if an element node has distinct :focus styling.
    241  const hasStylesForFocus = hasStylesForFocusRelatedPseudoClass(
    242    focusableNode,
    243    currentNode,
    244    FOCUS_PSEUDO_CLASS
    245  );
    246  if (hasStylesForFocus) {
    247    return true;
    248  }
    249 
    250  // If no element specific focus styles where found, check if its element
    251  // children have them.
    252  for (
    253    let child = currentNode.firstElementChild;
    254    child;
    255    child = currentNode.nextnextElementSibling
    256  ) {
    257    if (hasFocusStyling(focusableNode, child)) {
    258      return true;
    259    }
    260  }
    261 
    262  return false;
    263 }
    264 
    265 /**
    266 * A rule that determines if a focusable accessible object has appropriate focus
    267 * styling.
    268 *
    269 * @param  {nsIAccessible} accessible
    270 *         Accessible to be checked for being focusable and having focus
    271 *         styling.
    272 *
    273 * @return {null | object}
    274 *         Null if accessible has keyboard focus styling, audit report object
    275 *         otherwise.
    276 */
    277 function focusStyleRule(accessible) {
    278  const { DOMNode } = accessible;
    279  if (isInvalidNode(DOMNode)) {
    280    return null;
    281  }
    282 
    283  // Ignore non-focusable elements.
    284  if (!isKeyboardFocusable(accessible)) {
    285    return null;
    286  }
    287 
    288  if (hasFocusStyling(DOMNode, DOMNode)) {
    289    return null;
    290  }
    291 
    292  // If no browser or author focus styling was found, check if the node is a
    293  // widget that is themed by platform native theme.
    294  if (InspectorUtils.isElementThemed(DOMNode)) {
    295    return null;
    296  }
    297 
    298  return { score: WARNING, issue: NO_FOCUS_VISIBLE };
    299 }
    300 
    301 /**
    302 * A rule that determines if an interactive accessible has any associated
    303 * accessible actions with it. If the element is interactive but and has no
    304 * actions, assistive technology users will not be able to interact with it.
    305 *
    306 * @param  {nsIAccessible} accessible
    307 *         Accessible to be checked for being interactive and having accessible
    308 *         actions.
    309 *
    310 * @return {null | object}
    311 *         Null if accessible is not interactive or if it is and it has
    312 *         accessible action associated with it, audit report object otherwise.
    313 */
    314 function interactiveRule(accessible) {
    315  if (!INTERACTIVE_ROLES.has(accessible.role)) {
    316    return null;
    317  }
    318 
    319  if (accessible.actionCount > 0) {
    320    return null;
    321  }
    322 
    323  return { score: FAIL, issue: INTERACTIVE_NO_ACTION };
    324 }
    325 
    326 /**
    327 * A rule that determines if an interactive accessible is also focusable when
    328 * not disabled.
    329 *
    330 * @param  {nsIAccessible} accessible
    331 *         Accessible to be checked for being interactive and being focusable
    332 *         when enabled.
    333 *
    334 * @return {null | object}
    335 *         Null if accessible is not interactive or if it is and it is focusable
    336 *         when enabled, audit report object otherwise.
    337 */
    338 function focusableRule(accessible) {
    339  if (!KEYBOARD_FOCUSABLE_ROLES.has(accessible.role)) {
    340    return null;
    341  }
    342 
    343  const state = {};
    344  accessible.getState(state, {});
    345  // We only expect in interactive accessible object to be focusable if it is
    346  // not disabled.
    347  if (state.value & Ci.nsIAccessibleStates.STATE_UNAVAILABLE) {
    348    return null;
    349  }
    350 
    351  if (isKeyboardFocusable(accessible)) {
    352    return null;
    353  }
    354 
    355  const ariaRoles = getAriaRoles(accessible);
    356  if (
    357    ariaRoles &&
    358    (ariaRoles.includes("combobox") || ariaRoles.includes("listbox"))
    359  ) {
    360    // Do not force ARIA combobox or listbox to be focusable.
    361    return null;
    362  }
    363 
    364  return { score: FAIL, issue: INTERACTIVE_NOT_FOCUSABLE };
    365 }
    366 
    367 /**
    368 * A rule that determines if a focusable accessible has an associated
    369 * interactive role.
    370 *
    371 * @param  {nsIAccessible} accessible
    372 *         Accessible to be checked for having an interactive role if it is
    373 *         focusable.
    374 *
    375 * @return {null | object}
    376 *         Null if accessible is not interactive or if it is and it has an
    377 *         interactive role, audit report object otherwise.
    378 */
    379 function semanticsRule(accessible) {
    380  if (
    381    INTERACTIVE_ROLES.has(accessible.role) ||
    382    // Visible listboxes will have focusable state when inside comboboxes.
    383    accessible.role === Ci.nsIAccessibleRole.ROLE_COMBOBOX_LIST
    384  ) {
    385    return null;
    386  }
    387 
    388  if (isKeyboardFocusable(accessible)) {
    389    if (INTERACTIVE_IF_FOCUSABLE_ROLES.has(accessible.role)) {
    390      return null;
    391    }
    392 
    393    // ROLE_TABLE is used for grids too which are considered interactive.
    394    if (accessible.role === Ci.nsIAccessibleRole.ROLE_TABLE) {
    395      const ariaRoles = getAriaRoles(accessible);
    396      if (ariaRoles && ariaRoles.includes("grid")) {
    397        return null;
    398      }
    399    }
    400 
    401    return { score: WARNING, issue: FOCUSABLE_NO_SEMANTICS };
    402  }
    403 
    404  const state = {};
    405  accessible.getState(state, {});
    406  if (
    407    // Ignore text leafs.
    408    accessible.role === Ci.nsIAccessibleRole.ROLE_TEXT_LEAF ||
    409    // Ignore accessibles with no accessible actions.
    410    accessible.actionCount === 0 ||
    411    // Ignore labels that have a label for relation with their target because
    412    // they are clickable.
    413    (accessible.role === Ci.nsIAccessibleRole.ROLE_LABEL &&
    414      accessible.getRelationByType(Ci.nsIAccessibleRelation.RELATION_LABEL_FOR)
    415        .targetsCount > 0) ||
    416    // Ignore images that are inside an anchor (have linked state).
    417    (accessible.role === Ci.nsIAccessibleRole.ROLE_GRAPHIC &&
    418      state.value & Ci.nsIAccessibleStates.STATE_LINKED)
    419  ) {
    420    return null;
    421  }
    422 
    423  // Ignore anything but a click action in the list of actions.
    424  for (let i = 0; i < accessible.actionCount; i++) {
    425    if (accessible.getActionName(i) === CLICK_ACTION) {
    426      return { score: FAIL, issue: MOUSE_INTERACTIVE_ONLY };
    427    }
    428  }
    429 
    430  return null;
    431 }
    432 
    433 /**
    434 * A rule that determines if an element associated with a focusable accessible
    435 * has a positive tabindex.
    436 *
    437 * @param  {nsIAccessible} accessible
    438 *         Accessible to be checked for having an element with positive tabindex
    439 *         attribute.
    440 *
    441 * @return {null | object}
    442 *         Null if accessible is not focusable or if it is and its element's
    443 *         tabindex attribute is less than 1, audit report object otherwise.
    444 */
    445 function tabIndexRule(accessible) {
    446  const { DOMNode } = accessible;
    447  if (isInvalidNode(DOMNode)) {
    448    return null;
    449  }
    450 
    451  if (!isKeyboardFocusable(accessible)) {
    452    return null;
    453  }
    454 
    455  if (DOMNode.tabIndex > 0) {
    456    return { score: WARNING, issue: FOCUSABLE_POSITIVE_TABINDEX };
    457  }
    458 
    459  return null;
    460 }
    461 
    462 function auditKeyboard(accessible) {
    463  if (isDefunct(accessible)) {
    464    return null;
    465  }
    466  // Do not test anything on accessible objects for documents or frames.
    467  if (
    468    accessible.role === Ci.nsIAccessibleRole.ROLE_DOCUMENT ||
    469    accessible.role === Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME
    470  ) {
    471    return null;
    472  }
    473 
    474  // Check if interactive accessible can be used by the assistive
    475  // technology.
    476  let issue = interactiveRule(accessible);
    477  if (issue) {
    478    return issue;
    479  }
    480 
    481  // Check if interactive accessible is also focusable when enabled.
    482  issue = focusableRule(accessible);
    483  if (issue) {
    484    return issue;
    485  }
    486 
    487  // Check if accessible object has an element with a positive tabindex.
    488  issue = tabIndexRule(accessible);
    489  if (issue) {
    490    return issue;
    491  }
    492 
    493  // Check if a focusable accessible has interactive semantics.
    494  issue = semanticsRule(accessible);
    495  if (issue) {
    496    return issue;
    497  }
    498 
    499  // Check if focusable accessible has associated focus styling.
    500  issue = focusStyleRule(accessible);
    501  if (issue) {
    502    return issue;
    503  }
    504 
    505  return issue;
    506 }
    507 
    508 module.exports.auditKeyboard = auditKeyboard;