/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @flow*/import * as React from 'react';
import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {RegistryContext} from './Contexts';
import styles from './ContextMenu.css';
import type {RegistryContextType} from './Contexts';
function repositionToFit(element: HTMLElement, pageX: number, pageY: number) {
const ownerWindow = element.ownerDocument.defaultView;
if (element !== null) {
if (pageY + element.offsetHeight >= ownerWindow.innerHeight) {
if (pageY - element.offsetHeight > 0) {
element.style.top = `${pageY - element.offsetHeight}px`;
} else {
element.style.top = '0px';
}} else {
element.style.top = `${pageY}px`;
}if (pageX + element.offsetWidth >= ownerWindow.innerWidth) {
if (pageX - element.offsetWidth > 0) {
element.style.left = `${pageX - element.offsetWidth}px`;
} else {
element.style.left = '0px';
}} else {
element.style.left = `${pageX}px`;
}}}const HIDDEN_STATE = {
data: null,
isVisible: false,
pageX: 0,
pageY: 0,
};type Props = {
children: (data: Object) => React$Node,
id: string,
};export default function ContextMenu({children, id}: Props): React.Node {
const {hideMenu, registerMenu} =
useContext<RegistryContextType>(RegistryContext);
const [state, setState] = useState(HIDDEN_STATE);
const bodyAccessorRef = useRef(null);
const containerRef = useRef(null);
const menuRef = useRef(null);
useEffect(() => {
const element = bodyAccessorRef.current;
if (element !== null) {
const ownerDocument = element.ownerDocument;
containerRef.current = ownerDocument.querySelector(
'[data-react-devtools-portal-root]',
);if (containerRef.current == null) {
console.warn(
'DevTools tooltip root node not found; context menus will be disabled.',
);}}}, []);useEffect(() => {
const showMenuFn = ({
data,
pageX,
pageY,
}: {data: any,
pageX: number,
pageY: number,
}) => {
setState({data, isVisible: true, pageX, pageY});
};const hideMenuFn = () => setState(HIDDEN_STATE);
return registerMenu(id, showMenuFn, hideMenuFn);
}, [id]);
useLayoutEffect(() => {
if (!state.isVisible) {
return;
}const menu = ((menuRef.current: any): HTMLElement);
const container = containerRef.current;
if (container !== null) {
// $FlowFixMe[missing-local-annot]
const hideUnlessContains = event => {
if (!menu.contains(event.target)) {
hideMenu();
}};const ownerDocument = container.ownerDocument;
ownerDocument.addEventListener('mousedown', hideUnlessContains);
ownerDocument.addEventListener('touchstart', hideUnlessContains);
ownerDocument.addEventListener('keydown', hideUnlessContains);
const ownerWindow = ownerDocument.defaultView;
ownerWindow.addEventListener('resize', hideMenu);
repositionToFit(menu, state.pageX, state.pageY);
return () => {
ownerDocument.removeEventListener('mousedown', hideUnlessContains);
ownerDocument.removeEventListener('touchstart', hideUnlessContains);
ownerDocument.removeEventListener('keydown', hideUnlessContains);
ownerWindow.removeEventListener('resize', hideMenu);
};}}, [state]);
if (!state.isVisible) {
return <div ref={bodyAccessorRef} />;
} else {
const container = containerRef.current;
if (container !== null) {
return createPortal(
<div ref={menuRef} className={styles.ContextMenu}>
{children(state.data)}
</div>,container,
);} else {
return null;}}}