tor-browser

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

text-label.js (14594B)


      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 const {
      8  accessibility: {
      9    AUDIT_TYPE: { TEXT_LABEL },
     10    ISSUE_TYPE,
     11    SCORES: { BEST_PRACTICES, FAIL, WARNING },
     12  },
     13 } = require("resource://devtools/shared/constants.js");
     14 
     15 const {
     16  AREA_NO_NAME_FROM_ALT,
     17  DIALOG_NO_NAME,
     18  DOCUMENT_NO_TITLE,
     19  EMBED_NO_NAME,
     20  FIGURE_NO_NAME,
     21  FORM_FIELDSET_NO_NAME,
     22  FORM_FIELDSET_NO_NAME_FROM_LEGEND,
     23  FORM_NO_NAME,
     24  FORM_NO_VISIBLE_NAME,
     25  FORM_OPTGROUP_NO_NAME_FROM_LABEL,
     26  FRAME_NO_NAME,
     27  HEADING_NO_CONTENT,
     28  HEADING_NO_NAME,
     29  IFRAME_NO_NAME_FROM_TITLE,
     30  IMAGE_NO_NAME,
     31  INTERACTIVE_NO_NAME,
     32  MATHML_GLYPH_NO_NAME,
     33  TOOLBAR_NO_NAME,
     34 } = ISSUE_TYPE[TEXT_LABEL];
     35 
     36 /**
     37 * Check if the accessible is visible to the assistive technology.
     38 *
     39 * @param {nsIAccessible} accessible
     40 *        Accessible object to be tested for visibility.
     41 *
     42 * @returns {boolean}
     43 *         True if accessible object is visible to assistive technology.
     44 */
     45 function isVisible(accessible) {
     46  const state = {};
     47  accessible.getState(state, {});
     48  return !(state.value & Ci.nsIAccessibleStates.STATE_INVISIBLE);
     49 }
     50 
     51 /**
     52 * Get related accessible objects that are targets of labelled by relation e.g.
     53 * labels.
     54 *
     55 * @param {nsIAccessible} accessible
     56 *        Accessible objects to get labels for.
     57 *
     58 * @returns {Array}
     59 *          A list of accessible objects that are labels for a given accessible.
     60 */
     61 function getLabels(accessible) {
     62  const relation = accessible.getRelationByType(
     63    Ci.nsIAccessibleRelation.RELATION_LABELLED_BY
     64  );
     65  return [...relation.getTargets().enumerate(Ci.nsIAccessible)];
     66 }
     67 
     68 /**
     69 * Get a trimmed name of the accessible object.
     70 *
     71 * @param {nsIAccessible} accessible
     72 *        Accessible objects to get a name for.
     73 *
     74 * @returns {null | string}
     75 *          Trimmed name of the accessible object if available.
     76 */
     77 function getAccessibleName(accessible) {
     78  return accessible.name && accessible.name.trim();
     79 }
     80 
     81 /**
     82 * A text label rule for accessible objects that must have a non empty
     83 * accessible name.
     84 *
     85 * @returns {null | object}
     86 *          Failure audit report if accessible object has no or empty name, null
     87 *          otherwise.
     88 */
     89 const mustHaveNonEmptyNameRule = function (issue, accessible) {
     90  const name = getAccessibleName(accessible);
     91  return name ? null : { score: FAIL, issue };
     92 };
     93 
     94 /**
     95 * A text label rule for accessible objects that should have a non empty
     96 * accessible name as a best practice.
     97 *
     98 * @returns {null | object}
     99 *          Best practices audit report if accessible object has no or empty
    100 *          name, null otherwise.
    101 */
    102 const shouldHaveNonEmptyNameRule = function (issue, accessible) {
    103  const name = getAccessibleName(accessible);
    104  return name ? null : { score: BEST_PRACTICES, issue };
    105 };
    106 
    107 /**
    108 * A text label rule for accessible objects that can be activated via user
    109 * action and must have a non-empty name.
    110 *
    111 * @returns {null | object}
    112 *          Failure audit report if interactive accessible object has no or
    113 *          empty name, null otherwise.
    114 */
    115 const interactiveRule = mustHaveNonEmptyNameRule.bind(
    116  null,
    117  INTERACTIVE_NO_NAME
    118 );
    119 
    120 /**
    121 * A text label rule for accessible objects that correspond to dialogs and thus
    122 * should have a non-empty name.
    123 *
    124 * @returns {null | object}
    125 *          Best practices audit report if dialog accessible object has no or
    126 *          empty name, null otherwise.
    127 */
    128 const dialogRule = shouldHaveNonEmptyNameRule.bind(null, DIALOG_NO_NAME);
    129 
    130 /**
    131 * A text label rule for accessible objects that provide visual information
    132 * (images, canvas, etc.) and must have a defined name (that can be empty, e.g.
    133 * "").
    134 *
    135 * @returns {null | object}
    136 *          Failure audit report if interactive accessible object has no name,
    137 *          null otherwise.
    138 */
    139 const imageRule = function (accessible) {
    140  const name = getAccessibleName(accessible);
    141  return name != null ? null : { score: FAIL, issue: IMAGE_NO_NAME };
    142 };
    143 
    144 /**
    145 * A text label rule for accessible objects that correspond to form elements.
    146 * These objects must have a non-empty name and must have a visible label.
    147 *
    148 * @returns {null | object}
    149 *          Failure audit report if form element accessible object has no name,
    150 *          warning if the name does not come from a visible label, null
    151 *          otherwise.
    152 */
    153 const formRule = function (accessible) {
    154  const name = getAccessibleName(accessible);
    155  if (!name) {
    156    return { score: FAIL, issue: FORM_NO_NAME };
    157  }
    158 
    159  const labels = getLabels(accessible);
    160  const hasNameFromVisibleLabel = labels.some(label => isVisible(label));
    161 
    162  return hasNameFromVisibleLabel
    163    ? null
    164    : { score: WARNING, issue: FORM_NO_VISIBLE_NAME };
    165 };
    166 
    167 /**
    168 * A text label rule for elements that map to ROLE_GROUPING:
    169 * * <OPTGROUP> must have a non-empty name and must be provided via the
    170 *   "label" attribute.
    171 * * <FIELDSET> must have a non-empty name and must be provided via the
    172 *   corresponding <LEGEND> element.
    173 *
    174 * @returns {null | object}
    175 *          Failure audit report if form grouping accessible object has no name,
    176 *          or has a name that is not derived from a required location, null
    177 *          otherwise.
    178 */
    179 const formGroupingRule = function (accessible) {
    180  const name = getAccessibleName(accessible);
    181  const { DOMNode } = accessible;
    182 
    183  switch (DOMNode.nodeName) {
    184    case "OPTGROUP":
    185      return name && DOMNode.label && DOMNode.label.trim() === name
    186        ? null
    187        : {
    188            score: FAIL,
    189            issue: FORM_OPTGROUP_NO_NAME_FROM_LABEL,
    190          };
    191    case "FIELDSET": {
    192      if (!name) {
    193        return { score: FAIL, issue: FORM_FIELDSET_NO_NAME };
    194      }
    195 
    196      const labels = getLabels(accessible);
    197      const hasNameFromLegend = labels.some(
    198        label =>
    199          label.DOMNode.nodeName === "LEGEND" &&
    200          label.name &&
    201          label.name.trim() === name &&
    202          isVisible(label)
    203      );
    204 
    205      return hasNameFromLegend
    206        ? null
    207        : {
    208            score: WARNING,
    209            issue: FORM_FIELDSET_NO_NAME_FROM_LEGEND,
    210          };
    211    }
    212    default:
    213      return null;
    214  }
    215 };
    216 
    217 /**
    218 * A text label rule for elements that map to ROLE_TEXT_CONTAINER:
    219 * * <METER> mapps to ROLE_TEXT_CONTAINER and must have a name provided via
    220 *   the visible label. Note: Will only work when bug 559770 is resolved (right
    221 *   now, unlabelled meters are not mapped to an accessible object).
    222 *
    223 * @returns {null | object}
    224 *          Failure audit report depending on requirements for dialogs or form
    225 *          meter element, null otherwise.
    226 */
    227 const textContainerRule = function (accessible) {
    228  const { DOMNode } = accessible;
    229 
    230  switch (DOMNode.nodeName) {
    231    case "DIALOG":
    232      return dialogRule(accessible);
    233    case "METER":
    234      return formRule(accessible);
    235    default:
    236      return null;
    237  }
    238 };
    239 
    240 /**
    241 * A text label rule for elements that map to ROLE_INTERNAL_FRAME:
    242 *  * <OBJECT> maps to ROLE_INTERNAL_FRAME. Check the type attribute and whether
    243 *    it includes "image/" (e.g. image/jpeg, image/png, image/gif). If so, audit
    244 *    it the same way other image roles are audited.
    245 *  * <EMBED> maps to ROLE_INTERNAL_FRAME and must have a non-empty name.
    246 *  * <FRAME> and <IFRAME> map to ROLE_INTERNAL_FRAME and must have a non-empty
    247 *    title attribute.
    248 *
    249 * @returns {null | object}
    250 *          Failure audit report if the internal frame accessible object name is
    251 *          not provided or if it is not derived from a required location, null
    252 *          otherwise.
    253 */
    254 const internalFrameRule = function (accessible) {
    255  const { DOMNode } = accessible;
    256  switch (DOMNode.nodeName) {
    257    case "FRAME":
    258      return mustHaveNonEmptyNameRule(FRAME_NO_NAME, accessible);
    259    case "IFRAME": {
    260      const name = getAccessibleName(accessible);
    261      const title = DOMNode.title && DOMNode.title.trim();
    262 
    263      return title && title === name
    264        ? null
    265        : { score: FAIL, issue: IFRAME_NO_NAME_FROM_TITLE };
    266    }
    267    case "OBJECT": {
    268      const type = DOMNode.getAttribute("type");
    269      if (!type || !type.startsWith("image/")) {
    270        return null;
    271      }
    272 
    273      return imageRule(accessible);
    274    }
    275    case "EMBED": {
    276      const type = DOMNode.getAttribute("type");
    277      if (!type || !type.startsWith("image/")) {
    278        return mustHaveNonEmptyNameRule(EMBED_NO_NAME, accessible);
    279      }
    280      return imageRule(accessible);
    281    }
    282    default:
    283      return null;
    284  }
    285 };
    286 
    287 /**
    288 * A text label rule for accessible objects that represent documents and should
    289 * have title element provided.
    290 *
    291 * @returns {null | object}
    292 *          Failure audit report if document accessible object has no or empty
    293 *          title, null otherwise.
    294 */
    295 const documentRule = function (accessible) {
    296  const title = accessible.DOMNode.title && accessible.DOMNode.title.trim();
    297  return title ? null : { score: FAIL, issue: DOCUMENT_NO_TITLE };
    298 };
    299 
    300 /**
    301 * A text label rule for accessible objects that correspond to headings and thus
    302 * must be non-empty.
    303 *
    304 * @returns {null | object}
    305 *          Failure audit report if heading accessible object has no or
    306 *          empty name or if its text content is empty, null otherwise.
    307 */
    308 const headingRule = function (accessible) {
    309  const name = getAccessibleName(accessible);
    310  if (!name) {
    311    return { score: FAIL, issue: HEADING_NO_NAME };
    312  }
    313 
    314  const content =
    315    accessible.DOMNode.textContent && accessible.DOMNode.textContent.trim();
    316  return content ? null : { score: WARNING, issue: HEADING_NO_CONTENT };
    317 };
    318 
    319 /**
    320 * A text label rule for accessible objects that represent toolbars and must
    321 * have a non-empty name if there is more than one toolbar present.
    322 *
    323 * @returns {null | object}
    324 *          Failure audit report if toolbar accessible object is not the only
    325 *          toolbar in the document and has no or empty title, null otherwise.
    326 */
    327 const toolbarRule = function (accessible) {
    328  const toolbars =
    329    accessible.DOMNode.ownerDocument.querySelectorAll(`[role="toolbar"]`);
    330 
    331  return toolbars.length > 1
    332    ? mustHaveNonEmptyNameRule(TOOLBAR_NO_NAME, accessible)
    333    : null;
    334 };
    335 
    336 /**
    337 * A text label rule for accessible objects that represent link (anchors, areas)
    338 * and must have a non-empty name.
    339 *
    340 * @returns {null | object}
    341 *          Failure audit report if link accessible object has no or empty name,
    342 *          or in case when it's an <area> element with href attribute the name
    343 *          is not specified by an alt attribute, null otherwise.
    344 */
    345 const linkRule = function (accessible) {
    346  const { DOMNode } = accessible;
    347  if (DOMNode.nodeName === "AREA" && DOMNode.hasAttribute("href")) {
    348    const alt = DOMNode.getAttribute("alt");
    349    const name = getAccessibleName(accessible);
    350    return alt && alt.trim() === name
    351      ? null
    352      : { score: FAIL, issue: AREA_NO_NAME_FROM_ALT };
    353  }
    354 
    355  return interactiveRule(accessible);
    356 };
    357 
    358 /**
    359 * A text label rule for accessible objects that are used to display
    360 * non-standard symbols where existing Unicode characters are not available and
    361 * must have a non-empty name.
    362 *
    363 * @returns {null | object}
    364 *          Failure audit report if mglyph accessible object has no or empty
    365 *          name, and no or empty alt attribute, null otherwise.
    366 */
    367 const mathmlGlyphRule = function (accessible) {
    368  const name = getAccessibleName(accessible);
    369  if (name) {
    370    return null;
    371  }
    372 
    373  const { DOMNode } = accessible;
    374  const alt = DOMNode.getAttribute("alt");
    375  return alt && alt.trim()
    376    ? null
    377    : { score: FAIL, issue: MATHML_GLYPH_NO_NAME };
    378 };
    379 
    380 const RULES = {
    381  [Ci.nsIAccessibleRole.ROLE_BUTTONMENU]: interactiveRule,
    382  [Ci.nsIAccessibleRole.ROLE_CANVAS]: imageRule,
    383  [Ci.nsIAccessibleRole.ROLE_CHECKBUTTON]: formRule,
    384  [Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]: interactiveRule,
    385  [Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION]: formRule,
    386  [Ci.nsIAccessibleRole.ROLE_COLUMNHEADER]: interactiveRule,
    387  [Ci.nsIAccessibleRole.ROLE_COMBOBOX]: formRule,
    388  [Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION]: interactiveRule,
    389  [Ci.nsIAccessibleRole.ROLE_DIAGRAM]: imageRule,
    390  [Ci.nsIAccessibleRole.ROLE_DIALOG]: dialogRule,
    391  [Ci.nsIAccessibleRole.ROLE_DOCUMENT]: documentRule,
    392  [Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX]: formRule,
    393  [Ci.nsIAccessibleRole.ROLE_ENTRY]: formRule,
    394  [Ci.nsIAccessibleRole.ROLE_FIGURE]: shouldHaveNonEmptyNameRule.bind(
    395    null,
    396    FIGURE_NO_NAME
    397  ),
    398  [Ci.nsIAccessibleRole.ROLE_GRAPHIC]: imageRule,
    399  [Ci.nsIAccessibleRole.ROLE_GROUPING]: formGroupingRule,
    400  [Ci.nsIAccessibleRole.ROLE_HEADING]: headingRule,
    401  [Ci.nsIAccessibleRole.ROLE_IMAGE_MAP]: imageRule,
    402  [Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME]: internalFrameRule,
    403  [Ci.nsIAccessibleRole.ROLE_LINK]: linkRule,
    404  [Ci.nsIAccessibleRole.ROLE_LISTBOX]: formRule,
    405  [Ci.nsIAccessibleRole.ROLE_MATHML_GLYPH]: mathmlGlyphRule,
    406  [Ci.nsIAccessibleRole.ROLE_MENUITEM]: interactiveRule,
    407  [Ci.nsIAccessibleRole.ROLE_OPTION]: interactiveRule,
    408  [Ci.nsIAccessibleRole.ROLE_OUTLINEITEM]: interactiveRule,
    409  [Ci.nsIAccessibleRole.ROLE_PAGETAB]: interactiveRule,
    410  [Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT]: formRule,
    411  [Ci.nsIAccessibleRole.ROLE_PROGRESSBAR]: formRule,
    412  [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON]: interactiveRule,
    413  [Ci.nsIAccessibleRole.ROLE_RADIOBUTTON]: formRule,
    414  [Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM]: interactiveRule,
    415  [Ci.nsIAccessibleRole.ROLE_ROWHEADER]: interactiveRule,
    416  [Ci.nsIAccessibleRole.ROLE_SLIDER]: formRule,
    417  [Ci.nsIAccessibleRole.ROLE_SPINBUTTON]: formRule,
    418  [Ci.nsIAccessibleRole.ROLE_SWITCH]: formRule,
    419  [Ci.nsIAccessibleRole.ROLE_TEXT_CONTAINER]: textContainerRule,
    420  [Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON]: interactiveRule,
    421  [Ci.nsIAccessibleRole.ROLE_TOOLBAR]: toolbarRule,
    422 };
    423 
    424 /**
    425 * Perform audit for WCAG 1.1 criteria related to providing alternative text
    426 * depending on the type of content.
    427 *
    428 * @param {nsIAccessible} accessible
    429 *        Accessible object to be tested to determine if it requires and has
    430 *        an appropriate text alternative.
    431 *
    432 * @return {null | object}
    433 *         Null if accessible does not need or has the right text alternative,
    434 *         audit data otherwise. This data is used in the accessibility panel
    435 *         for its audit filters, audit badges, sidebar checks section and
    436 *         highlighter.
    437 */
    438 function auditTextLabel(accessible) {
    439  const rule = RULES[accessible.role];
    440  return rule ? rule(accessible) : null;
    441 }
    442 
    443 module.exports.auditTextLabel = auditTextLabel;