@firefox-devtools/react-contextmenu
Version:
Context Menu implemented in React
261 lines (258 loc) • 8.21 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireDefault(require("react"));
var _reactDom = _interopRequireDefault(require("react-dom"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _classnames = _interopRequireDefault(require("classnames"));
var _objectAssign = _interopRequireDefault(require("object-assign"));
var _globalEventListener = _interopRequireDefault(require("./globalEventListener"));
var _AbstractMenu = _interopRequireDefault(require("./AbstractMenu"));
var _SubMenu = _interopRequireDefault(require("./SubMenu"));
var _actions = require("./actions");
var _helpers = require("./helpers");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/* This is a backward-compatible shim for React < 18 */
function flushSync(callback) {
if (_reactDom.default.flushSync) {
_reactDom.default.flushSync(callback);
} else {
callback();
}
}
class ContextMenu extends _AbstractMenu.default {
static propTypes = {
id: _propTypes.default.string.isRequired,
children: _propTypes.default.node.isRequired,
data: _propTypes.default.object,
className: _propTypes.default.string,
hideOnLeave: _propTypes.default.bool,
rtl: _propTypes.default.bool,
onHide: _propTypes.default.func,
onMouseLeave: _propTypes.default.func,
onShow: _propTypes.default.func,
preventHideOnContextMenu: _propTypes.default.bool,
preventHideOnResize: _propTypes.default.bool,
preventHideOnScroll: _propTypes.default.bool,
style: _propTypes.default.object
};
static defaultProps = {
className: '',
data: {},
hideOnLeave: false,
rtl: false,
onHide() {
return null;
},
onMouseLeave() {
return null;
},
onShow() {
return null;
},
preventHideOnContextMenu: false,
preventHideOnResize: false,
preventHideOnScroll: false,
style: {}
};
constructor(props) {
super(props);
this.state = (0, _objectAssign.default)({}, this.state, {
x: 0,
y: 0,
isVisible: false
});
}
getSubMenuType() {
// eslint-disable-line class-methods-use-this
return _SubMenu.default;
}
componentDidMount() {
this.listenId = _globalEventListener.default.register(this.handleShow, this.handleHide);
}
componentDidUpdate() {
const wrapper = window.requestAnimationFrame || setTimeout;
if (this.state.isVisible) {
wrapper(() => {
const {
x,
y
} = this.state;
const {
top,
left
} = this.props.rtl ? this.getRTLMenuPosition(x, y) : this.getMenuPosition(x, y);
wrapper(() => {
if (!this.menu || !this.state.isVisible) return;
this.menu.style.top = `${top}px`;
this.menu.style.left = `${left}px`;
this.menu.style.opacity = 1;
this.menu.style.pointerEvents = 'auto';
});
});
} else {
wrapper(() => {
if (!this.menu) return;
this.menu.style.opacity = 0;
this.menu.style.pointerEvents = 'none';
});
}
}
componentWillUnmount() {
if (this.listenId) {
_globalEventListener.default.unregister(this.listenId);
}
this.unregisterHandlers();
}
registerHandlers = () => {
document.addEventListener('mousedown', this.handleOutsideClick);
document.addEventListener('touchstart', this.handleOutsideClick);
if (!this.props.preventHideOnScroll) document.addEventListener('scroll', this.handleHide);
if (!this.props.preventHideOnContextMenu) document.addEventListener('contextmenu', this.handleHide);
document.addEventListener('keydown', this.handleKeyNavigation);
if (!this.props.preventHideOnResize) window.addEventListener('resize', this.handleHide);
};
unregisterHandlers = () => {
document.removeEventListener('mousedown', this.handleOutsideClick);
document.removeEventListener('touchstart', this.handleOutsideClick);
document.removeEventListener('scroll', this.handleHide);
document.removeEventListener('contextmenu', this.handleHide);
document.removeEventListener('keydown', this.handleKeyNavigation);
window.removeEventListener('resize', this.handleHide);
};
handleShow = e => {
if (e.detail.id !== this.props.id || this.state.isVisible) return;
const {
x,
y
} = e.detail.position;
this.setState({
isVisible: true,
x,
y
});
this.registerHandlers();
(0, _helpers.callIfExists)(this.props.onShow, e);
};
handleHide = e => {
if (this.state.isVisible && (!e.detail || !e.detail.id || e.detail.id === this.props.id)) {
this.unregisterHandlers();
flushSync(() => {
/* We rely on being able to read this state change in handleShow,
* so let's force a synchronous update in React 18. */
this.setState({
isVisible: false,
selectedItem: null,
forceSubMenuOpen: false
});
});
(0, _helpers.callIfExists)(this.props.onHide, e);
}
};
handleOutsideClick = e => {
if (!this.menu.contains(e.target)) (0, _actions.hideMenu)();
};
handleMouseLeave = event => {
event.preventDefault();
(0, _helpers.callIfExists)(this.props.onMouseLeave, event, (0, _objectAssign.default)({}, this.props.data, _helpers.store.data), _helpers.store.target);
if (this.props.hideOnLeave) (0, _actions.hideMenu)();
};
handleContextMenu = e => {
if (process.env.NODE_ENV === 'production') {
e.preventDefault();
}
this.handleHide(e);
};
// Disabling this rule for more consistency.
/* eslint-disable-next-line class-methods-use-this */
hideMenu = e => {
if (e.key === 'Escape' || e.key === 'Esc' || e.key === 'Enter') {
(0, _actions.hideMenu)();
}
};
getMenuPosition = (x = 0, y = 0) => {
let menuStyles = {
top: y,
left: x
};
if (!this.menu) return menuStyles;
const {
innerWidth,
innerHeight
} = window;
const rect = this.menu.getBoundingClientRect();
if (y + rect.height > innerHeight) {
menuStyles.top -= rect.height;
}
if (x + rect.width > innerWidth) {
menuStyles.left -= rect.width;
}
if (menuStyles.top < 0) {
menuStyles.top = rect.height < innerHeight ? (innerHeight - rect.height) / 2 : 0;
}
if (menuStyles.left < 0) {
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
}
return menuStyles;
};
getRTLMenuPosition = (x = 0, y = 0) => {
let menuStyles = {
top: y,
left: x
};
if (!this.menu) return menuStyles;
const {
innerWidth,
innerHeight
} = window;
const rect = this.menu.getBoundingClientRect();
// Try to position the menu on the left side of the cursor
menuStyles.left = x - rect.width;
if (y + rect.height > innerHeight) {
menuStyles.top -= rect.height;
}
if (menuStyles.left < 0) {
menuStyles.left += rect.width;
}
if (menuStyles.top < 0) {
menuStyles.top = rect.height < innerHeight ? (innerHeight - rect.height) / 2 : 0;
}
if (menuStyles.left + rect.width > innerWidth) {
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
}
return menuStyles;
};
menuRef = c => {
this.menu = c;
};
render() {
const {
children,
className,
style
} = this.props;
const {
isVisible
} = this.state;
const inlineStyle = (0, _objectAssign.default)({}, style, {
position: 'fixed',
opacity: 0,
pointerEvents: 'none'
});
const menuClassnames = (0, _classnames.default)(_helpers.cssClasses.menu, className, {
[_helpers.cssClasses.menuVisible]: isVisible
});
return /*#__PURE__*/_react.default.createElement("nav", {
role: "menu",
tabIndex: "-1",
ref: this.menuRef,
style: inlineStyle,
className: menuClassnames,
onContextMenu: this.handleContextMenu,
onMouseLeave: this.handleMouseLeave
}, this.renderChildren(children));
}
}
exports.default = ContextMenu;