/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; import { connect } from "react-redux"; export class ContextMenu extends React.PureComponent { constructor(props) { super(props); this.hideContext = this.hideContext.bind(this); this.onShow = this.onShow.bind(this); this.onClick = this.onClick.bind(this); } hideContext() { this.props.onUpdate(false); } onShow() { if (this.props.onShow) { this.props.onShow(); } } componentDidMount() { this.onShow(); setTimeout(() => { global.addEventListener("click", this.hideContext); }, 0); } componentWillUnmount() { global.removeEventListener("click", this.hideContext); } onClick(event) { // Eat all clicks on the context menu so they don't bubble up to window. // This prevents the context menu from closing when clicking disabled items // or the separators. event.stopPropagation(); } render() { // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper. return ( // eslint-disable-next-line jsx-a11y/interactive-supports-focus ); } } export class _ContextMenuItem extends React.PureComponent { constructor(props) { super(props); this.onClick = this.onClick.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.focusFirst = this.focusFirst.bind(this); } onClick(event) { this.props.hideContext(); this.props.option.onClick(event); } // Focus the first menu item if the menu was accessed via the keyboard. focusFirst(button) { if (this.props.keyboardAccess && button) { button.focus(); } } // This selects the correct node based on the key pressed focusSibling(target, key) { const parent = target.parentNode; const closestSiblingSelector = key === "ArrowUp" ? "previousSibling" : "nextSibling"; if (!parent[closestSiblingSelector]) { return; } if (parent[closestSiblingSelector].firstElementChild) { parent[closestSiblingSelector].firstElementChild.focus(); } else { parent[closestSiblingSelector][ closestSiblingSelector ].firstElementChild.focus(); } } onKeyDown(event) { const { option } = this.props; switch (event.key) { case "Tab": // tab goes down in context menu, shift + tab goes up in context menu // if we're on the last item, one more tab will close the context menu // similarly, if we're on the first item, one more shift + tab will close it if ( (event.shiftKey && option.first) || (!event.shiftKey && option.last) ) { this.props.hideContext(); } break; case "ArrowUp": case "ArrowDown": event.preventDefault(); this.focusSibling(event.target, event.key); break; case "Enter": case " ": event.preventDefault(); this.props.hideContext(); option.onClick(); break; case "Escape": this.props.hideContext(); break; } } // Prevents the default behavior of spacebar // scrolling the page & auto-triggering buttons. onKeyUp(event) { if (event.key === " ") { event.preventDefault(); } } render() { const { option } = this.props; return (
  • ); } } export const ContextMenuItem = connect(state => ({ Prefs: state.Prefs, }))(_ContextMenuItem);