tor-browser

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

Accessible.js (15126B)


      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 "use strict";
      5 
      6 /* global EVENTS */
      7 
      8 // React & Redux
      9 const {
     10  createFactory,
     11  Component,
     12 } = require("resource://devtools/client/shared/vendor/react.mjs");
     13 const {
     14  div,
     15  span,
     16 } = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     17 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     18 const {
     19  findDOMNode,
     20 } = require("resource://devtools/client/shared/vendor/react-dom.mjs");
     21 const {
     22  connect,
     23 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     24 
     25 const {
     26  TREE_ROW_HEIGHT,
     27  ORDERED_PROPS,
     28  ACCESSIBLE_EVENTS,
     29  VALUE_FLASHING_DURATION,
     30 } = require("resource://devtools/client/accessibility/constants.js");
     31 const {
     32  L10N,
     33 } = require("resource://devtools/client/accessibility/utils/l10n.js");
     34 const {
     35  flashElementOn,
     36  flashElementOff,
     37 } = require("resource://devtools/client/inspector/markup/utils.js");
     38 const {
     39  updateDetails,
     40 } = require("resource://devtools/client/accessibility/actions/details.js");
     41 const {
     42  select,
     43  unhighlight,
     44 } = require("resource://devtools/client/accessibility/actions/accessibles.js");
     45 
     46 const VirtualizedTree = createFactory(
     47  require("resource://devtools/client/shared/components/VirtualizedTree.js")
     48 );
     49 // Reps
     50 const { REPS, MODE } = ChromeUtils.importESModule(
     51  "resource://devtools/client/shared/components/reps/index.mjs"
     52 );
     53 const { Rep, ElementNode, Accessible: AccessibleRep, Obj } = REPS;
     54 
     55 const {
     56  translateNodeFrontToGrip,
     57 } = require("resource://devtools/client/inspector/shared/utils.js");
     58 
     59 loader.lazyRequireGetter(
     60  this,
     61  "openContentLink",
     62  "resource://devtools/client/shared/link.js",
     63  true
     64 );
     65 
     66 const TREE_DEPTH_PADDING_INCREMENT = 20;
     67 
     68 class AccessiblePropertyClass extends Component {
     69  static get propTypes() {
     70    return {
     71      accessibleFrontActorID: PropTypes.string,
     72      object: PropTypes.any,
     73      focused: PropTypes.bool,
     74      children: PropTypes.func,
     75    };
     76  }
     77 
     78  componentDidUpdate({
     79    object: prevObject,
     80    accessibleFrontActorID: prevAccessibleFrontActorID,
     81  }) {
     82    const { accessibleFrontActorID, object, focused } = this.props;
     83    // Fast check if row is focused or if the value did not update.
     84    if (
     85      focused ||
     86      accessibleFrontActorID !== prevAccessibleFrontActorID ||
     87      prevObject === object ||
     88      (object && prevObject && typeof object === "object")
     89    ) {
     90      return;
     91    }
     92 
     93    this.flashRow();
     94  }
     95 
     96  flashRow() {
     97    const row = findDOMNode(this);
     98    flashElementOn(row);
     99    if (this._flashMutationTimer) {
    100      clearTimeout(this._flashMutationTimer);
    101      this._flashMutationTimer = null;
    102    }
    103    this._flashMutationTimer = setTimeout(() => {
    104      flashElementOff(row);
    105    }, VALUE_FLASHING_DURATION);
    106  }
    107 
    108  render() {
    109    return this.props.children();
    110  }
    111 }
    112 
    113 const AccessibleProperty = createFactory(AccessiblePropertyClass);
    114 
    115 class Accessible extends Component {
    116  static get propTypes() {
    117    return {
    118      accessibleFront: PropTypes.object,
    119      dispatch: PropTypes.func.isRequired,
    120      nodeFront: PropTypes.object,
    121      items: PropTypes.array,
    122      labelledby: PropTypes.string.isRequired,
    123      parents: PropTypes.object,
    124      relations: PropTypes.object,
    125      toolbox: PropTypes.object.isRequired,
    126      toolboxHighlighter: PropTypes.object.isRequired,
    127      highlightAccessible: PropTypes.func.isRequired,
    128      unhighlightAccessible: PropTypes.func.isRequired,
    129    };
    130  }
    131 
    132  constructor(props) {
    133    super(props);
    134 
    135    this.state = {
    136      expanded: new Set(),
    137      active: null,
    138      focused: null,
    139    };
    140 
    141    this.onAccessibleInspected = this.onAccessibleInspected.bind(this);
    142    this.renderItem = this.renderItem.bind(this);
    143    this.update = this.update.bind(this);
    144  }
    145 
    146  componentDidMount() {
    147    window.on(
    148      EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
    149      this.onAccessibleInspected
    150    );
    151  }
    152 
    153  componentDidUpdate(prevProps) {
    154    const oldAccessibleFront = prevProps.accessibleFront;
    155    const { accessibleFront } = this.props;
    156 
    157    if (
    158      accessibleFront &&
    159      !accessibleFront.isDestroyed() &&
    160      accessibleFront !== oldAccessibleFront
    161    ) {
    162      window.emit(EVENTS.PROPERTIES_UPDATED);
    163    }
    164 
    165    if (oldAccessibleFront) {
    166      if (
    167        accessibleFront &&
    168        accessibleFront.actorID === oldAccessibleFront.actorID
    169      ) {
    170        return;
    171      }
    172      ACCESSIBLE_EVENTS.forEach(event =>
    173        oldAccessibleFront.off(event, this.update)
    174      );
    175    }
    176 
    177    if (accessibleFront) {
    178      ACCESSIBLE_EVENTS.forEach(event =>
    179        accessibleFront.on(event, this.update)
    180      );
    181    }
    182  }
    183 
    184  componentWillUnmount() {
    185    window.off(
    186      EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
    187      this.onAccessibleInspected
    188    );
    189 
    190    const { accessibleFront } = this.props;
    191    if (accessibleFront) {
    192      ACCESSIBLE_EVENTS.forEach(event =>
    193        accessibleFront.off(event, this.update)
    194      );
    195    }
    196  }
    197 
    198  onAccessibleInspected() {
    199    const { props } = this.refs;
    200    if (props) {
    201      props.refs.tree.focus();
    202    }
    203  }
    204 
    205  update() {
    206    const { dispatch, accessibleFront } = this.props;
    207    if (accessibleFront.isDestroyed()) {
    208      return;
    209    }
    210 
    211    dispatch(updateDetails(accessibleFront));
    212  }
    213 
    214  setExpanded(item, isExpanded) {
    215    const { expanded } = this.state;
    216 
    217    if (isExpanded) {
    218      expanded.add(item.path);
    219    } else {
    220      expanded.delete(item.path);
    221    }
    222 
    223    this.setState({ expanded });
    224  }
    225 
    226  async showHighlighter(nodeFront) {
    227    if (!this.props.toolboxHighlighter) {
    228      return;
    229    }
    230 
    231    await this.props.toolboxHighlighter.highlight(nodeFront);
    232  }
    233 
    234  async hideHighlighter() {
    235    if (!this.props.toolboxHighlighter) {
    236      return;
    237    }
    238 
    239    await this.props.toolboxHighlighter.unhighlight();
    240  }
    241 
    242  showAccessibleHighlighter(accessibleFront) {
    243    this.props.dispatch(unhighlight());
    244    this.props.highlightAccessible(accessibleFront);
    245  }
    246 
    247  hideAccessibleHighlighter(accessibleFront) {
    248    this.props.dispatch(unhighlight());
    249    this.props.unhighlightAccessible(accessibleFront);
    250  }
    251 
    252  async selectNode(nodeFront, reason = "accessibility") {
    253    Glean.devtoolsAccessibility.nodeInspectedCount.add(1);
    254 
    255    if (!this.props.toolbox) {
    256      return;
    257    }
    258 
    259    const inspector = await this.props.toolbox.selectTool("inspector");
    260    inspector.selection.setNodeFront(nodeFront, reason);
    261  }
    262 
    263  async selectAccessible(accessibleFront) {
    264    if (!accessibleFront) {
    265      return;
    266    }
    267 
    268    await this.props.dispatch(select(accessibleFront));
    269 
    270    const { props } = this.refs;
    271    if (props) {
    272      props.refs.tree.blur();
    273    }
    274    await this.setState({ active: null, focused: null });
    275 
    276    window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED);
    277  }
    278 
    279  openLink(link) {
    280    openContentLink(link);
    281  }
    282 
    283  renderItem(item, depth, focused, arrow, expanded) {
    284    const object = item.contents;
    285    const valueProps = {
    286      object,
    287      mode: MODE.TINY,
    288      title: "Object",
    289      openLink: this.openLink,
    290    };
    291 
    292    if (isNodeFront(object)) {
    293      valueProps.defaultRep = ElementNode;
    294      valueProps.onDOMNodeMouseOut = () => this.hideHighlighter();
    295      valueProps.onDOMNodeMouseOver = () =>
    296        this.showHighlighter(this.props.nodeFront);
    297 
    298      valueProps.inspectIconTitle = L10N.getStr(
    299        "accessibility.accessible.selectNodeInInspector.title"
    300      );
    301      valueProps.onInspectIconClick = () =>
    302        this.selectNode(this.props.nodeFront);
    303    } else if (isAccessibleFront(object)) {
    304      const target = findAccessibleTarget(this.props.relations, object.actor);
    305      valueProps.defaultRep = AccessibleRep;
    306      valueProps.onAccessibleMouseOut = () =>
    307        this.hideAccessibleHighlighter(target);
    308      valueProps.onAccessibleMouseOver = () =>
    309        this.showAccessibleHighlighter(target);
    310      valueProps.inspectIconTitle = L10N.getStr(
    311        "accessibility.accessible.selectElement.title"
    312      );
    313      valueProps.onInspectIconClick = (obj, e) => {
    314        e.stopPropagation();
    315        this.selectAccessible(target);
    316      };
    317      valueProps.separatorText = "";
    318    } else if (item.name === "relations") {
    319      valueProps.defaultRep = Obj;
    320    } else {
    321      valueProps.noGrip = true;
    322    }
    323 
    324    const classList = ["node", "object-node"];
    325    if (focused) {
    326      classList.push("focused");
    327    }
    328 
    329    const depthPadding = depth * TREE_DEPTH_PADDING_INCREMENT;
    330 
    331    return AccessibleProperty(
    332      {
    333        object,
    334        focused,
    335        accessibleFrontActorID: this.props.accessibleFront.actorID,
    336      },
    337      () =>
    338        div(
    339          {
    340            className: classList.join(" "),
    341            style: {
    342              paddingInlineStart: depthPadding,
    343              inlineSize: `calc(var(--accessibility-properties-item-width) - ${depthPadding}px)`,
    344            },
    345            onClick: e => {
    346              if (e.target.classList.contains("theme-twisty")) {
    347                this.setExpanded(item, !expanded);
    348              }
    349            },
    350          },
    351          arrow,
    352          span({ className: "object-label" }, item.name),
    353          span({ className: "object-delimiter" }, ":"),
    354          span({ className: "object-value" }, Rep(valueProps) || "")
    355        )
    356    );
    357  }
    358 
    359  render() {
    360    const { expanded, active, focused } = this.state;
    361    const { items, parents, accessibleFront, labelledby } = this.props;
    362 
    363    if (accessibleFront) {
    364      return VirtualizedTree({
    365        ref: "props",
    366        key: "accessible-properties",
    367        itemHeight: TREE_ROW_HEIGHT,
    368        getRoots: () => items,
    369        getKey: item => item.path,
    370        getParent: item => parents.get(item),
    371        getChildren: item => item.children,
    372        isExpanded: item => expanded.has(item.path),
    373        onExpand: item => this.setExpanded(item, true),
    374        onCollapse: item => this.setExpanded(item, false),
    375        onFocus: item => {
    376          if (this.state.focused !== item.path) {
    377            this.setState({ focused: item.path });
    378          }
    379        },
    380        onActivate: item => {
    381          if (item == null) {
    382            this.setState({ active: null });
    383          } else if (this.state.active !== item.path) {
    384            this.setState({ active: item.path });
    385          }
    386        },
    387        focused: findByPath(focused, items),
    388        active: findByPath(active, items),
    389        renderItem: this.renderItem,
    390        labelledby,
    391      });
    392    }
    393 
    394    return div(
    395      { className: "info" },
    396      L10N.getStr("accessibility.accessible.notAvailable")
    397    );
    398  }
    399 }
    400 
    401 /**
    402 * Match accessibility object from relations targets to the grip that's being activated.
    403 *
    404 * @param  {object} relations  Object containing relations grouped by type and targets.
    405 * @param  {string} actorID    Actor ID to match to the relation target.
    406 * @return {object}            Accessible front that matches the relation target.
    407 */
    408 const findAccessibleTarget = (relations, actorID) => {
    409  for (const relationType in relations) {
    410    let targets = relations[relationType];
    411    targets = Array.isArray(targets) ? targets : [targets];
    412    for (const target of targets) {
    413      if (target.actorID === actorID) {
    414        return target;
    415      }
    416    }
    417  }
    418 
    419  return null;
    420 };
    421 
    422 /**
    423 * Find an item based on a given path.
    424 *
    425 * @param  {string} path
    426 *         Key of the item to be looked up.
    427 * @param  {Array}  items
    428 *         Accessibility properties array.
    429 * @return {object?}
    430 *         Possibly found item.
    431 */
    432 const findByPath = (path, items) => {
    433  for (const item of items) {
    434    if (item.path === path) {
    435      return item;
    436    }
    437 
    438    const found = findByPath(path, item.children);
    439    if (found) {
    440      return found;
    441    }
    442  }
    443  return null;
    444 };
    445 
    446 /**
    447 * Check if a given property is a DOMNode front.
    448 *
    449 * @param  {object?} value A property to check for being a DOMNode.
    450 * @return {boolean}       A flag that indicates whether a property is a DOMNode.
    451 */
    452 const isNodeFront = value => value && value.typeName === "domnode";
    453 
    454 /**
    455 * Check if a given property is an Accessible front.
    456 *
    457 * @param  {object?} value A property to check for being an Accessible.
    458 * @return {boolean}       A flag that indicates whether a property is an Accessible.
    459 */
    460 const isAccessibleFront = value => value && value.typeName === "accessible";
    461 
    462 /**
    463 * While waiting for a reps fix in https://github.com/firefox-devtools/reps/issues/92,
    464 * translate accessibleFront to a grip-like object that can be used with an Accessible
    465 * rep.
    466 *
    467 * @param {AccessibleFront} accessibleFront
    468 *          The AccessibleFront for which we want to create a grip-like object.
    469 * @returns {object} a grip-like object that can be used with Reps.
    470 */
    471 const translateAccessibleFrontToGrip = accessibleFront => ({
    472  actor: accessibleFront.actorID,
    473  typeName: accessibleFront.typeName,
    474  preview: {
    475    name: accessibleFront.name,
    476    role: accessibleFront.role,
    477    // All the grid containers are assumed to be in the Accessibility tree.
    478    isConnected: true,
    479  },
    480 });
    481 
    482 const translateNodeFrontToGripWrapper = nodeFront => ({
    483  ...translateNodeFrontToGrip(nodeFront),
    484  typeName: nodeFront.typeName,
    485 });
    486 
    487 /**
    488 * Build props ingestible by VirtualizedTree component.
    489 *
    490 * @param  {object} props      Component properties to be processed.
    491 * @param  {string} parentPath Unique path that is used to identify a Tree Node.
    492 * @return {object}            Processed properties.
    493 */
    494 const makeItemsForDetails = (props, parentPath) =>
    495  Object.getOwnPropertyNames(props).map(name => {
    496    let children = [];
    497    const path = `${parentPath}/${name}`;
    498    let contents = props[name];
    499 
    500    if (contents) {
    501      if (isNodeFront(contents)) {
    502        contents = translateNodeFrontToGripWrapper(contents);
    503        name = "DOMNode";
    504      } else if (isAccessibleFront(contents)) {
    505        contents = translateAccessibleFrontToGrip(contents);
    506      } else if (Array.isArray(contents) || typeof contents === "object") {
    507        children = makeItemsForDetails(contents, path);
    508      }
    509    }
    510 
    511    return { name, path, contents, children };
    512  });
    513 
    514 const makeParentMap = items => {
    515  const map = new WeakMap();
    516 
    517  function _traverse(item) {
    518    if (item.children.length) {
    519      for (const child of item.children) {
    520        map.set(child, item);
    521        _traverse(child);
    522      }
    523    }
    524  }
    525 
    526  items.forEach(_traverse);
    527  return map;
    528 };
    529 
    530 const mapStateToProps = ({ details }) => {
    531  const {
    532    accessible: accessibleFront,
    533    DOMNode: nodeFront,
    534    relations,
    535  } = details;
    536  if (!accessibleFront || !nodeFront) {
    537    return {};
    538  }
    539 
    540  const items = makeItemsForDetails(
    541    ORDERED_PROPS.reduce((props, key) => {
    542      if (key === "DOMNode") {
    543        props.nodeFront = nodeFront;
    544      } else if (key === "relations") {
    545        props.relations = relations;
    546      } else {
    547        props[key] = accessibleFront[key];
    548      }
    549 
    550      return props;
    551    }, {}),
    552    ""
    553  );
    554  const parents = makeParentMap(items);
    555 
    556  return { accessibleFront, nodeFront, items, parents, relations };
    557 };
    558 
    559 module.exports = connect(mapStateToProps)(Accessible);