react/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js

138 lines
3.5 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) Facebook, Inc. and its 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 React, {
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import {createPortal} from 'react-dom';
import {RegistryContext} from './Contexts';
import styles from './ContextMenu.css';
function respositionToFit(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) {
const {registerMenu} = useContext(RegistryContext);
const [state, setState] = useState(HIDDEN_STATE);
const bodyAccessorRef = useRef(null);
const containerRef = useRef(null);
const menuRef = useRef(null);
useEffect(() => {
const ownerDocument = bodyAccessorRef.current.ownerDocument;
containerRef.current = ownerDocument.createElement('div');
ownerDocument.body.appendChild(containerRef.current);
return () => {
ownerDocument.body.removeChild(containerRef.current);
};
}, []);
useEffect(
() => {
const showMenu = ({data, pageX, pageY}) => {
setState({data, isVisible: true, pageX, pageY});
};
const hideMenu = () => setState(HIDDEN_STATE);
return registerMenu(id, showMenu, hideMenu);
},
[id],
);
useLayoutEffect(
() => {
if (!state.isVisible) {
return;
}
const menu = menuRef.current;
const hideUnlessContains = event => {
if (!menu.contains(event.target)) {
setState(HIDDEN_STATE);
}
};
const hide = event => {
setState(HIDDEN_STATE);
};
const ownerDocument = containerRef.current.ownerDocument;
ownerDocument.addEventListener('mousedown', hideUnlessContains);
ownerDocument.addEventListener('touchstart', hideUnlessContains);
ownerDocument.addEventListener('keydown', hideUnlessContains);
const ownerWindow = ownerDocument.defaultView;
ownerWindow.addEventListener('resize', hide);
respositionToFit(menu, state.pageX, state.pageY);
return () => {
ownerDocument.removeEventListener('mousedown', hideUnlessContains);
ownerDocument.removeEventListener('touchstart', hideUnlessContains);
ownerDocument.removeEventListener('keydown', hideUnlessContains);
ownerWindow.removeEventListener('resize', hide);
};
},
[state],
);
if (!state.isVisible) {
return <div ref={bodyAccessorRef} />;
} else {
return createPortal(
<div ref={menuRef} className={styles.ContextMenu}>
{children(state.data)}
</div>,
containerRef.current,
);
}
}