tor-browser

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

accessibility.js (19745B)


      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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
      8 const {
      9  getCurrentZoom,
     10 } = require("resource://devtools/shared/layout/utils.js");
     11 const {
     12  moveInfobar,
     13 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     14 const { truncateString } = require("resource://devtools/shared/string.js");
     15 
     16 const STRINGS_URI = "devtools/shared/locales/accessibility.properties";
     17 loader.lazyRequireGetter(
     18  this,
     19  "LocalizationHelper",
     20  "resource://devtools/shared/l10n.js",
     21  true
     22 );
     23 DevToolsUtils.defineLazyGetter(
     24  this,
     25  "L10N",
     26  () => new LocalizationHelper(STRINGS_URI)
     27 );
     28 
     29 const {
     30  accessibility: {
     31    AUDIT_TYPE,
     32    ISSUE_TYPE: {
     33      [AUDIT_TYPE.KEYBOARD]: {
     34        FOCUSABLE_NO_SEMANTICS,
     35        FOCUSABLE_POSITIVE_TABINDEX,
     36        INTERACTIVE_NO_ACTION,
     37        INTERACTIVE_NOT_FOCUSABLE,
     38        MOUSE_INTERACTIVE_ONLY,
     39        NO_FOCUS_VISIBLE,
     40      },
     41      [AUDIT_TYPE.TEXT_LABEL]: {
     42        AREA_NO_NAME_FROM_ALT,
     43        DIALOG_NO_NAME,
     44        DOCUMENT_NO_TITLE,
     45        EMBED_NO_NAME,
     46        FIGURE_NO_NAME,
     47        FORM_FIELDSET_NO_NAME,
     48        FORM_FIELDSET_NO_NAME_FROM_LEGEND,
     49        FORM_NO_NAME,
     50        FORM_NO_VISIBLE_NAME,
     51        FORM_OPTGROUP_NO_NAME_FROM_LABEL,
     52        FRAME_NO_NAME,
     53        HEADING_NO_CONTENT,
     54        HEADING_NO_NAME,
     55        IFRAME_NO_NAME_FROM_TITLE,
     56        IMAGE_NO_NAME,
     57        INTERACTIVE_NO_NAME,
     58        MATHML_GLYPH_NO_NAME,
     59        TOOLBAR_NO_NAME,
     60      },
     61    },
     62    SCORES,
     63  },
     64 } = require("resource://devtools/shared/constants.js");
     65 
     66 // Max string length for truncating accessible name values.
     67 const MAX_STRING_LENGTH = 50;
     68 
     69 /**
     70 * The AccessibleInfobar is a class responsible for creating the markup for the
     71 * accessible highlighter. It is also reponsible for updating content within the
     72 * infobar such as role and name values.
     73 */
     74 class Infobar {
     75  constructor(highlighter) {
     76    this.highlighter = highlighter;
     77    this.audit = new Audit(this);
     78  }
     79 
     80  get markup() {
     81    return this.highlighter.markup;
     82  }
     83 
     84  get document() {
     85    return this.highlighter.win.document;
     86  }
     87 
     88  get bounds() {
     89    return this.highlighter._bounds;
     90  }
     91 
     92  get options() {
     93    return this.highlighter.options;
     94  }
     95 
     96  get win() {
     97    return this.highlighter.win;
     98  }
     99 
    100  /**
    101   * Move the Infobar to the right place in the highlighter.
    102   *
    103   * @param  {Element} container
    104   *         Container of infobar.
    105   */
    106  _moveInfobar(container) {
    107    // Position the infobar using accessible's bounds
    108    const { left: x, top: y, bottom, width } = this.bounds;
    109    const infobarBounds = { x, y, bottom, width };
    110 
    111    moveInfobar(container, infobarBounds, this.win);
    112  }
    113 
    114  /**
    115   * Build markup for infobar.
    116   *
    117   * @param  {Element} root
    118   *         Root element to build infobar with.
    119   */
    120  buildMarkup(root) {
    121    const container = this.markup.createNode({
    122      parent: root,
    123      attributes: {
    124        class: "accessible-infobar-container",
    125        id: "accessible-infobar-container",
    126        "aria-hidden": "true",
    127        hidden: "true",
    128      },
    129    });
    130 
    131    const infobar = this.markup.createNode({
    132      parent: container,
    133      attributes: {
    134        class: "accessible-infobar",
    135        id: "accessible-infobar",
    136      },
    137    });
    138 
    139    const infobarText = this.markup.createNode({
    140      parent: infobar,
    141      attributes: {
    142        class: "accessible-infobar-text",
    143        id: "accessible-infobar-text",
    144      },
    145    });
    146 
    147    this.markup.createNode({
    148      nodeType: "span",
    149      parent: infobarText,
    150      attributes: {
    151        class: "accessible-infobar-role",
    152        id: "accessible-infobar-role",
    153      },
    154    });
    155 
    156    this.markup.createNode({
    157      nodeType: "span",
    158      parent: infobarText,
    159      attributes: {
    160        class: "accessible-infobar-name",
    161        id: "accessible-infobar-name",
    162      },
    163    });
    164 
    165    this.audit.buildMarkup(infobarText);
    166  }
    167 
    168  /**
    169   * Destroy the Infobar's highlighter.
    170   */
    171  destroy() {
    172    this.highlighter = null;
    173    this.audit.destroy();
    174    this.audit = null;
    175  }
    176 
    177  /**
    178   * Gets the element with the specified ID.
    179   *
    180   * @param  {string} id
    181   *         Element ID.
    182   * @return {Element} The element with specified ID.
    183   */
    184  getElement(id) {
    185    return this.highlighter.getElement(id);
    186  }
    187 
    188  /**
    189   * Gets the text content of element.
    190   *
    191   * @param  {string} id
    192   *          Element ID to retrieve text content from.
    193   * @return {string} The text content of the element.
    194   */
    195  getTextContent(id) {
    196    const anonymousContent = this.markup.content;
    197    return anonymousContent.root.getElementById(id).textContent;
    198  }
    199 
    200  /**
    201   * Hide the accessible infobar.
    202   */
    203  hide() {
    204    const container = this.getElement("accessible-infobar-container");
    205    container.setAttribute("hidden", "true");
    206  }
    207 
    208  /**
    209   * Show the accessible infobar highlighter.
    210   */
    211  show() {
    212    const container = this.getElement("accessible-infobar-container");
    213 
    214    // Remove accessible's infobar "hidden" attribute. We do this first to get the
    215    // computed styles of the infobar container.
    216    container.removeAttribute("hidden");
    217 
    218    // Update the infobar's position and content.
    219    this.update(container);
    220  }
    221 
    222  /**
    223   * Update content of the infobar.
    224   */
    225  update(container) {
    226    const { audit, name, role } = this.options;
    227 
    228    this.updateRole(role, this.getElement("accessible-infobar-role"));
    229    this.updateName(name, this.getElement("accessible-infobar-name"));
    230    this.audit.update(audit);
    231 
    232    // Position the infobar.
    233    this._moveInfobar(container);
    234  }
    235 
    236  /**
    237   * Sets the text content of the specified element.
    238   *
    239   * @param  {Element} el
    240   *         Element to set text content on.
    241   * @param  {string} text
    242   *         Text for content.
    243   */
    244  setTextContent(el, text) {
    245    el.setTextContent(text);
    246  }
    247 
    248  /**
    249   * Show the accessible's name message.
    250   *
    251   * @param  {string} name
    252   *         Accessible's name value.
    253   * @param  {Element} el
    254   *         Element to set text content on.
    255   */
    256  updateName(name, el) {
    257    const nameText = name ? `"${truncateString(name, MAX_STRING_LENGTH)}"` : "";
    258    this.setTextContent(el, nameText);
    259  }
    260 
    261  /**
    262   * Show the accessible's role.
    263   *
    264   * @param  {string} role
    265   *         Accessible's role value.
    266   * @param  {Element} el
    267   *         Element to set text content on.
    268   */
    269  updateRole(role, el) {
    270    this.setTextContent(el, role);
    271  }
    272 }
    273 
    274 /**
    275 * Audit component used within the accessible highlighter infobar. This component is
    276 * responsible for rendering and updating its containing AuditReport components that
    277 * display various audit information such as contrast ratio score.
    278 */
    279 class Audit {
    280  constructor(infobar) {
    281    this.infobar = infobar;
    282 
    283    // A list of audit reports to be shown on the fly when highlighting an accessible
    284    // object.
    285    this.reports = {
    286      [AUDIT_TYPE.CONTRAST]: new ContrastRatio(this),
    287      [AUDIT_TYPE.KEYBOARD]: new Keyboard(this),
    288      [AUDIT_TYPE.TEXT_LABEL]: new TextLabel(this),
    289    };
    290  }
    291 
    292  get markup() {
    293    return this.infobar.markup;
    294  }
    295 
    296  buildMarkup(root) {
    297    const audit = this.markup.createNode({
    298      nodeType: "span",
    299      parent: root,
    300      attributes: {
    301        class: "accessible-infobar-audit",
    302        id: "accessible-infobar-audit",
    303      },
    304    });
    305 
    306    Object.values(this.reports).forEach(report => report.buildMarkup(audit));
    307  }
    308 
    309  update(audit = {}) {
    310    const el = this.getElement("accessible-infobar-audit");
    311    el.setAttribute("hidden", true);
    312 
    313    let updated = false;
    314    Object.values(this.reports).forEach(report => {
    315      if (report.update(audit)) {
    316        updated = true;
    317      }
    318    });
    319 
    320    if (updated) {
    321      el.removeAttribute("hidden");
    322    }
    323  }
    324 
    325  getElement(id) {
    326    return this.infobar.getElement(id);
    327  }
    328 
    329  setTextContent(el, text) {
    330    return this.infobar.setTextContent(el, text);
    331  }
    332 
    333  destroy() {
    334    this.infobar = null;
    335    Object.values(this.reports).forEach(report => report.destroy());
    336    this.reports = null;
    337  }
    338 }
    339 
    340 /**
    341 * A common interface between audit report components used to render accessibility audit
    342 * information for the currently highlighted accessible object.
    343 */
    344 class AuditReport {
    345  constructor(audit) {
    346    this.audit = audit;
    347  }
    348 
    349  get markup() {
    350    return this.audit.markup;
    351  }
    352 
    353  getElement(id) {
    354    return this.audit.getElement(id);
    355  }
    356 
    357  setTextContent(el, text) {
    358    return this.audit.setTextContent(el, text);
    359  }
    360 
    361  destroy() {
    362    this.audit = null;
    363  }
    364 }
    365 
    366 /**
    367 * Contrast ratio audit report that is used to display contrast ratio score as part of the
    368 * inforbar,
    369 */
    370 class ContrastRatio extends AuditReport {
    371  buildMarkup(root) {
    372    this.markup.createNode({
    373      nodeType: "span",
    374      parent: root,
    375      attributes: {
    376        class: "accessible-contrast-ratio-label",
    377        id: "accessible-contrast-ratio-label",
    378      },
    379    });
    380 
    381    this.markup.createNode({
    382      nodeType: "span",
    383      parent: root,
    384      attributes: {
    385        class: "accessible-contrast-ratio-error",
    386        id: "accessible-contrast-ratio-error",
    387      },
    388      text: L10N.getStr("accessibility.contrast.ratio.error"),
    389    });
    390 
    391    this.markup.createNode({
    392      nodeType: "span",
    393      parent: root,
    394      attributes: {
    395        class: "accessible-contrast-ratio",
    396        id: "accessible-contrast-ratio-min",
    397      },
    398    });
    399 
    400    this.markup.createNode({
    401      nodeType: "span",
    402      parent: root,
    403      attributes: {
    404        class: "accessible-contrast-ratio-separator",
    405        id: "accessible-contrast-ratio-separator",
    406      },
    407    });
    408 
    409    this.markup.createNode({
    410      nodeType: "span",
    411      parent: root,
    412      attributes: {
    413        class: "accessible-contrast-ratio",
    414        id: "accessible-contrast-ratio-max",
    415      },
    416    });
    417  }
    418 
    419  _fillAndStyleContrastValue(el, { value, className, color, backgroundColor }) {
    420    value = value.toFixed(2);
    421    this.setTextContent(el, value);
    422    el.classList?.add(className);
    423    el.setAttribute(
    424      "style",
    425      `--accessibility-highlighter-contrast-ratio-color: rgba(${color});` +
    426        `--accessibility-highlighter-contrast-ratio-bg: rgba(${backgroundColor});`
    427    );
    428    el.removeAttribute("hidden");
    429  }
    430 
    431  /**
    432   * Update contrast ratio score infobar markup.
    433   *
    434   * @param  {object}
    435   *         Audit report for a given highlighted accessible.
    436   * @return {boolean}
    437   *         True if the contrast ratio markup was updated correctly and infobar audit
    438   *         block should be visible.
    439   */
    440  update(audit) {
    441    const els = {};
    442    for (const key of ["label", "min", "max", "error", "separator"]) {
    443      const el = (els[key] = this.getElement(
    444        `accessible-contrast-ratio-${key}`
    445      ));
    446      if (["min", "max"].includes(key)) {
    447        Object.values(SCORES).forEach(className =>
    448          el.classList?.remove(className)
    449        );
    450        this.setTextContent(el, "");
    451      }
    452 
    453      el.setAttribute("hidden", true);
    454      el.removeAttribute("style");
    455    }
    456 
    457    if (!audit) {
    458      return false;
    459    }
    460 
    461    const contrastRatio = audit[AUDIT_TYPE.CONTRAST];
    462    if (!contrastRatio) {
    463      return false;
    464    }
    465 
    466    const { isLargeText, error } = contrastRatio;
    467    this.setTextContent(
    468      els.label,
    469      L10N.getStr(
    470        `accessibility.contrast.ratio.label${isLargeText ? ".large" : ""}`
    471      )
    472    );
    473    els.label.removeAttribute("hidden");
    474    if (error) {
    475      els.error.removeAttribute("hidden");
    476      return true;
    477    }
    478 
    479    if (contrastRatio.value) {
    480      const { value, color, score, backgroundColor } = contrastRatio;
    481      this._fillAndStyleContrastValue(els.min, {
    482        value,
    483        className: score,
    484        color,
    485        backgroundColor,
    486      });
    487      return true;
    488    }
    489 
    490    const {
    491      min,
    492      max,
    493      color,
    494      backgroundColorMin,
    495      backgroundColorMax,
    496      scoreMin,
    497      scoreMax,
    498    } = contrastRatio;
    499    this._fillAndStyleContrastValue(els.min, {
    500      value: min,
    501      className: scoreMin,
    502      color,
    503      backgroundColor: backgroundColorMin,
    504    });
    505    els.separator.removeAttribute("hidden");
    506    this._fillAndStyleContrastValue(els.max, {
    507      value: max,
    508      className: scoreMax,
    509      color,
    510      backgroundColor: backgroundColorMax,
    511    });
    512 
    513    return true;
    514  }
    515 }
    516 
    517 /**
    518 * Keyboard audit report that is used to display a problem with keyboard
    519 * accessibility as part of the inforbar.
    520 */
    521 class Keyboard extends AuditReport {
    522  /**
    523   * A map from keyboard issues to annotation component properties.
    524   */
    525  static get ISSUE_TO_INFOBAR_LABEL_MAP() {
    526    return {
    527      [FOCUSABLE_NO_SEMANTICS]: "accessibility.keyboard.issue.semantics",
    528      [FOCUSABLE_POSITIVE_TABINDEX]: "accessibility.keyboard.issue.tabindex",
    529      [INTERACTIVE_NO_ACTION]: "accessibility.keyboard.issue.action",
    530      [INTERACTIVE_NOT_FOCUSABLE]: "accessibility.keyboard.issue.focusable",
    531      [MOUSE_INTERACTIVE_ONLY]: "accessibility.keyboard.issue.mouse.only",
    532      [NO_FOCUS_VISIBLE]: "accessibility.keyboard.issue.focus.visible",
    533    };
    534  }
    535 
    536  buildMarkup(root) {
    537    this.markup.createNode({
    538      nodeType: "span",
    539      parent: root,
    540      attributes: {
    541        class: "accessible-audit",
    542        id: "accessible-keyboard",
    543      },
    544    });
    545  }
    546 
    547  /**
    548   * Update keyboard audit infobar markup.
    549   *
    550   * @param  {object}
    551   *         Audit report for a given highlighted accessible.
    552   * @return {boolean}
    553   *         True if the keyboard markup was updated correctly and infobar audit
    554   *         block should be visible.
    555   */
    556  update(audit) {
    557    const el = this.getElement("accessible-keyboard");
    558    el.setAttribute("hidden", true);
    559    Object.values(SCORES).forEach(className => el.classList?.remove(className));
    560 
    561    if (!audit) {
    562      return false;
    563    }
    564 
    565    const keyboardAudit = audit[AUDIT_TYPE.KEYBOARD];
    566    if (!keyboardAudit) {
    567      return false;
    568    }
    569 
    570    const { issue, score } = keyboardAudit;
    571    this.setTextContent(
    572      el,
    573      L10N.getStr(Keyboard.ISSUE_TO_INFOBAR_LABEL_MAP[issue])
    574    );
    575    el.classList?.add(score);
    576    el.removeAttribute("hidden");
    577 
    578    return true;
    579  }
    580 }
    581 
    582 /**
    583 * Text label audit report that is used to display a problem with text alternatives
    584 * as part of the inforbar.
    585 */
    586 class TextLabel extends AuditReport {
    587  /**
    588   * A map from text label issues to annotation component properties.
    589   */
    590  static get ISSUE_TO_INFOBAR_LABEL_MAP() {
    591    return {
    592      [AREA_NO_NAME_FROM_ALT]: "accessibility.text.label.issue.area",
    593      [DIALOG_NO_NAME]: "accessibility.text.label.issue.dialog",
    594      [DOCUMENT_NO_TITLE]: "accessibility.text.label.issue.document.title",
    595      [EMBED_NO_NAME]: "accessibility.text.label.issue.embed",
    596      [FIGURE_NO_NAME]: "accessibility.text.label.issue.figure",
    597      [FORM_FIELDSET_NO_NAME]: "accessibility.text.label.issue.fieldset",
    598      [FORM_FIELDSET_NO_NAME_FROM_LEGEND]:
    599        "accessibility.text.label.issue.fieldset.legend2",
    600      [FORM_NO_NAME]: "accessibility.text.label.issue.form",
    601      [FORM_NO_VISIBLE_NAME]: "accessibility.text.label.issue.form.visible",
    602      [FORM_OPTGROUP_NO_NAME_FROM_LABEL]:
    603        "accessibility.text.label.issue.optgroup.label2",
    604      [FRAME_NO_NAME]: "accessibility.text.label.issue.frame",
    605      [HEADING_NO_CONTENT]: "accessibility.text.label.issue.heading.content",
    606      [HEADING_NO_NAME]: "accessibility.text.label.issue.heading",
    607      [IFRAME_NO_NAME_FROM_TITLE]: "accessibility.text.label.issue.iframe",
    608      [IMAGE_NO_NAME]: "accessibility.text.label.issue.image",
    609      [INTERACTIVE_NO_NAME]: "accessibility.text.label.issue.interactive",
    610      [MATHML_GLYPH_NO_NAME]: "accessibility.text.label.issue.glyph",
    611      [TOOLBAR_NO_NAME]: "accessibility.text.label.issue.toolbar",
    612    };
    613  }
    614 
    615  buildMarkup(root) {
    616    this.markup.createNode({
    617      nodeType: "span",
    618      parent: root,
    619      attributes: {
    620        class: "accessible-audit",
    621        id: "accessible-text-label",
    622      },
    623    });
    624  }
    625 
    626  /**
    627   * Update text label audit infobar markup.
    628   *
    629   * @param  {object}
    630   *         Audit report for a given highlighted accessible.
    631   * @return {boolean}
    632   *         True if the text label markup was updated correctly and infobar
    633   *         audit block should be visible.
    634   */
    635  update(audit) {
    636    const el = this.getElement("accessible-text-label");
    637    el.setAttribute("hidden", true);
    638    Object.values(SCORES).forEach(className => el.classList?.remove(className));
    639 
    640    if (!audit) {
    641      return false;
    642    }
    643 
    644    const textLabelAudit = audit[AUDIT_TYPE.TEXT_LABEL];
    645    if (!textLabelAudit) {
    646      return false;
    647    }
    648 
    649    const { issue, score } = textLabelAudit;
    650    this.setTextContent(
    651      el,
    652      L10N.getStr(TextLabel.ISSUE_TO_INFOBAR_LABEL_MAP[issue])
    653    );
    654    el.classList?.add(score);
    655    el.removeAttribute("hidden");
    656 
    657    return true;
    658  }
    659 }
    660 
    661 /**
    662 * A helper function that calculate accessible object bounds and positioning to
    663 * be used for highlighting.
    664 *
    665 * @param  {object} win
    666 *         window that contains accessible object.
    667 * @param  {object} options
    668 *         Object used for passing options:
    669 *         - {Number} x
    670 *           x coordinate of the top left corner of the accessible object
    671 *         - {Number} y
    672 *           y coordinate of the top left corner of the accessible object
    673 *         - {Number} w
    674 *           width of the the accessible object
    675 *         - {Number} h
    676 *           height of the the accessible object
    677 * @return {object | null} Returns, if available, positioning and bounds information for
    678 *                 the accessible object.
    679 */
    680 function getBounds(win, { x, y, w, h }) {
    681  const { mozInnerScreenX, mozInnerScreenY, scrollX, scrollY } = win;
    682  const zoom = getCurrentZoom(win);
    683  let left = x;
    684  let right = x + w;
    685  let top = y;
    686  let bottom = y + h;
    687 
    688  left -= mozInnerScreenX - scrollX;
    689  right -= mozInnerScreenX - scrollX;
    690  top -= mozInnerScreenY - scrollY;
    691  bottom -= mozInnerScreenY - scrollY;
    692 
    693  left *= zoom;
    694  right *= zoom;
    695  top *= zoom;
    696  bottom *= zoom;
    697 
    698  const width = right - left;
    699  const height = bottom - top;
    700 
    701  return { left, right, top, bottom, width, height };
    702 }
    703 
    704 /**
    705 * A helper function that calculate accessible object bounds and positioning to
    706 * be used for highlighting in browser toolbox.
    707 *
    708 * @param  {object} win
    709 *         window that contains accessible object.
    710 * @param  {object} options
    711 *         Object used for passing options:
    712 *         - {Number} x
    713 *           x coordinate of the top left corner of the accessible object
    714 *         - {Number} y
    715 *           y coordinate of the top left corner of the accessible object
    716 *         - {Number} w
    717 *           width of the the accessible object
    718 *         - {Number} h
    719 *           height of the the accessible object
    720 *         - {Number} zoom
    721 *           zoom level of the accessible object's parent window
    722 * @return {object | null} Returns, if available, positioning and bounds information for
    723 *                 the accessible object.
    724 */
    725 function getBoundsXUL(win, { x, y, w, h, zoom }) {
    726  const { mozInnerScreenX, mozInnerScreenY } = win;
    727  let left = x;
    728  let right = x + w;
    729  let top = y;
    730  let bottom = y + h;
    731 
    732  left *= zoom;
    733  right *= zoom;
    734  top *= zoom;
    735  bottom *= zoom;
    736 
    737  left -= mozInnerScreenX;
    738  right -= mozInnerScreenX;
    739  top -= mozInnerScreenY;
    740  bottom -= mozInnerScreenY;
    741 
    742  const width = right - left;
    743  const height = bottom - top;
    744 
    745  return { left, right, top, bottom, width, height };
    746 }
    747 
    748 exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
    749 exports.getBounds = getBounds;
    750 exports.getBoundsXUL = getBoundsXUL;
    751 exports.Infobar = Infobar;