tor-browser

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

MenuList.js (4530B)


      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 // A list of menu items.
      8 //
      9 // This component provides keyboard navigation amongst any focusable
     10 // children.
     11 
     12 const {
     13  Children,
     14  PureComponent,
     15 } = require("resource://devtools/client/shared/vendor/react.mjs");
     16 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     17 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     18 const { div } = dom;
     19 
     20 const lazy = {};
     21 ChromeUtils.defineESModuleGetters(lazy, {
     22  focusableSelector: "resource://devtools/client/shared/focus.mjs",
     23 });
     24 
     25 class MenuList extends PureComponent {
     26  static get propTypes() {
     27    return {
     28      // ID to assign to the list container.
     29      id: PropTypes.string,
     30 
     31      // Children of the list.
     32      children: PropTypes.any,
     33 
     34      // Called whenever there is a change to the hovered or selected child.
     35      // The callback is passed the ID of the highlighted child or null if no
     36      // child is highlighted.
     37      onHighlightedChildChange: PropTypes.func,
     38    };
     39  }
     40 
     41  constructor(props) {
     42    super(props);
     43 
     44    this.onKeyDown = this.onKeyDown.bind(this);
     45    this.onMouseOverOrFocus = this.onMouseOverOrFocus.bind(this);
     46    this.onMouseOutOrBlur = this.onMouseOutOrBlur.bind(this);
     47    this.notifyHighlightedChildChange =
     48      this.notifyHighlightedChildChange.bind(this);
     49 
     50    this.setWrapperRef = element => {
     51      this.wrapperRef = element;
     52    };
     53  }
     54 
     55  onMouseOverOrFocus(e) {
     56    this.notifyHighlightedChildChange(e.target.id);
     57  }
     58 
     59  onMouseOutOrBlur() {
     60    const hoveredElem = this.wrapperRef.querySelector(":hover");
     61    if (!hoveredElem) {
     62      this.notifyHighlightedChildChange(null);
     63    }
     64  }
     65 
     66  notifyHighlightedChildChange(id) {
     67    if (this.props.onHighlightedChildChange) {
     68      this.props.onHighlightedChildChange(id);
     69    }
     70  }
     71 
     72  onKeyDown(e) {
     73    // Check if the focus is in the list.
     74    if (
     75      !this.wrapperRef ||
     76      !this.wrapperRef.contains(e.target.ownerDocument.activeElement)
     77    ) {
     78      return;
     79    }
     80 
     81    const getTabList = () =>
     82      Array.from(this.wrapperRef.querySelectorAll(lazy.focusableSelector));
     83 
     84    switch (e.key) {
     85      case "Tab":
     86      case "ArrowUp":
     87      case "ArrowDown":
     88        {
     89          const tabList = getTabList();
     90          const currentElement = e.target.ownerDocument.activeElement;
     91          const currentIndex = tabList.indexOf(currentElement);
     92          if (currentIndex !== -1) {
     93            let nextIndex;
     94            if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
     95              nextIndex =
     96                currentIndex === tabList.length - 1 ? 0 : currentIndex + 1;
     97            } else {
     98              nextIndex =
     99                currentIndex === 0 ? tabList.length - 1 : currentIndex - 1;
    100            }
    101            tabList[nextIndex].focus();
    102            e.preventDefault();
    103          }
    104        }
    105        break;
    106 
    107      case "Home":
    108        {
    109          const firstItem = this.wrapperRef.querySelector(
    110            lazy.focusableSelector
    111          );
    112          if (firstItem) {
    113            firstItem.focus();
    114            e.preventDefault();
    115          }
    116        }
    117        break;
    118 
    119      case "End":
    120        {
    121          const tabList = getTabList();
    122          if (tabList.length) {
    123            tabList[tabList.length - 1].focus();
    124            e.preventDefault();
    125          }
    126        }
    127        break;
    128    }
    129  }
    130 
    131  render() {
    132    const attr = {
    133      role: "menu",
    134      ref: this.setWrapperRef,
    135      onKeyDown: this.onKeyDown,
    136      onMouseOver: this.onMouseOverOrFocus,
    137      onMouseOut: this.onMouseOutOrBlur,
    138      onFocus: this.onMouseOverOrFocus,
    139      onBlur: this.onMouseOutOrBlur,
    140      className: "menu-standard-padding",
    141    };
    142 
    143    if (this.props.id) {
    144      attr.id = this.props.id;
    145    }
    146 
    147    // Add padding for checkbox image if necessary.
    148    let hasCheckbox = false;
    149    Children.forEach(this.props.children, (child, i) => {
    150      if (child == null || typeof child == "undefined") {
    151        console.warn("MenuList children at index", i, "is", child);
    152        return;
    153      }
    154 
    155      if (typeof child?.props?.checked !== "undefined") {
    156        hasCheckbox = true;
    157      }
    158    });
    159    if (hasCheckbox) {
    160      attr.className = "checkbox-container menu-standard-padding";
    161    }
    162 
    163    return div(attr, this.props.children);
    164  }
    165 }
    166 
    167 module.exports = MenuList;