tor-browser

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

ContextMenu.jsx (4985B)


      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 import React from "react";
      6 import { connect } from "react-redux";
      7 
      8 export class ContextMenu extends React.PureComponent {
      9  constructor(props) {
     10    super(props);
     11    this.hideContext = this.hideContext.bind(this);
     12    this.onShow = this.onShow.bind(this);
     13    this.onClick = this.onClick.bind(this);
     14  }
     15 
     16  hideContext() {
     17    this.props.onUpdate(false);
     18  }
     19 
     20  onShow() {
     21    if (this.props.onShow) {
     22      this.props.onShow();
     23    }
     24  }
     25 
     26  componentDidMount() {
     27    this.onShow();
     28    setTimeout(() => {
     29      globalThis.addEventListener("click", this.hideContext);
     30    }, 0);
     31  }
     32 
     33  componentWillUnmount() {
     34    globalThis.removeEventListener("click", this.hideContext);
     35  }
     36 
     37  onClick(event) {
     38    // Eat all clicks on the context menu so they don't bubble up to window.
     39    // This prevents the context menu from closing when clicking disabled items
     40    // or the separators.
     41    event.stopPropagation();
     42  }
     43 
     44  render() {
     45    // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
     46    return (
     47      // eslint-disable-next-line jsx-a11y/interactive-supports-focus
     48      <span className="context-menu">
     49        <ul
     50          role="menu"
     51          onClick={this.onClick}
     52          onKeyDown={this.onClick}
     53          className="context-menu-list"
     54        >
     55          {this.props.options.map((option, i) =>
     56            option.type === "separator" ? (
     57              <li key={i} className="separator" role="separator" />
     58            ) : (
     59              option.type !== "empty" && (
     60                <ContextMenuItem
     61                  key={i}
     62                  option={option}
     63                  hideContext={this.hideContext}
     64                  keyboardAccess={this.props.keyboardAccess}
     65                />
     66              )
     67            )
     68          )}
     69        </ul>
     70      </span>
     71    );
     72  }
     73 }
     74 
     75 export class _ContextMenuItem extends React.PureComponent {
     76  constructor(props) {
     77    super(props);
     78    this.onClick = this.onClick.bind(this);
     79    this.onKeyDown = this.onKeyDown.bind(this);
     80    this.onKeyUp = this.onKeyUp.bind(this);
     81    this.focusFirst = this.focusFirst.bind(this);
     82  }
     83 
     84  onClick(event) {
     85    this.props.hideContext();
     86    this.props.option.onClick(event);
     87  }
     88 
     89  // Focus the first menu item if the menu was accessed via the keyboard.
     90  focusFirst(button) {
     91    if (this.props.keyboardAccess && button) {
     92      button.focus();
     93    }
     94  }
     95 
     96  // This selects the correct node based on the key pressed
     97  focusSibling(target, key) {
     98    const { parentNode } = target;
     99    const closestSiblingSelector =
    100      key === "ArrowUp" ? "previousSibling" : "nextSibling";
    101    if (!parentNode[closestSiblingSelector]) {
    102      return;
    103    }
    104    if (parentNode[closestSiblingSelector].firstElementChild) {
    105      parentNode[closestSiblingSelector].firstElementChild.focus();
    106    } else {
    107      parentNode[closestSiblingSelector][
    108        closestSiblingSelector
    109      ].firstElementChild.focus();
    110    }
    111  }
    112 
    113  onKeyDown(event) {
    114    const { option } = this.props;
    115    switch (event.key) {
    116      case "Tab":
    117        // tab goes down in context menu, shift + tab goes up in context menu
    118        // if we're on the last item, one more tab will close the context menu
    119        // similarly, if we're on the first item, one more shift + tab will close it
    120        if (
    121          (event.shiftKey && option.first) ||
    122          (!event.shiftKey && option.last)
    123        ) {
    124          this.props.hideContext();
    125        }
    126        break;
    127      case "ArrowUp":
    128      case "ArrowDown":
    129        event.preventDefault();
    130        this.focusSibling(event.target, event.key);
    131        break;
    132      case "Enter":
    133      case " ":
    134        event.preventDefault();
    135        this.props.hideContext();
    136        option.onClick();
    137        break;
    138      case "Escape":
    139        this.props.hideContext();
    140        break;
    141    }
    142  }
    143 
    144  // Prevents the default behavior of spacebar
    145  // scrolling the page & auto-triggering buttons.
    146  onKeyUp(event) {
    147    if (event.key === " ") {
    148      event.preventDefault();
    149    }
    150  }
    151 
    152  render() {
    153    const { option } = this.props;
    154    const className = [option.disabled ? "disabled" : ""].join(" ");
    155    return (
    156      <li role="presentation" className="context-menu-item">
    157        <button
    158          role="menuitem"
    159          className={className}
    160          onClick={this.onClick}
    161          onKeyDown={this.onKeyDown}
    162          onKeyUp={this.onKeyUp}
    163          ref={option.first ? this.focusFirst : null}
    164          aria-haspopup={
    165            option.id === "newtab-menu-edit-topsites" ? "dialog" : null
    166          }
    167        >
    168          <span data-l10n-id={option.string_id || option.id} />
    169        </button>
    170      </li>
    171    );
    172  }
    173 }
    174 
    175 export const ContextMenuItem = connect(state => ({
    176  Prefs: state.Prefs,
    177 }))(_ContextMenuItem);