webamp/js/components/ContextMenuTarget.tsx
2019-12-19 06:48:55 -08:00

85 lines
2.5 KiB
TypeScript

import React, { useState, useRef, useEffect, useMemo } from "react";
import ContextMenu from "./ContextMenu";
interface Props extends React.HTMLAttributes<HTMLDivElement> {
renderMenu: () => React.ReactNode;
top?: boolean;
bottom?: boolean;
}
function getNodeOffset(node: HTMLDivElement | null) {
if (node == null) {
return { top: 0, left: 0 };
}
const rect = node.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement!.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement!.scrollTop;
return { top: rect.top + scrollTop, left: rect.left + scrollLeft };
}
// Trigger a context menu relative to the child element when the user
// left-clicks on the child.
//
// For a component that triggers relative to the user's cursor on right-click
// see `<ContextMenuWrapper />`.
function ContextMenuTarget(props: Props) {
const handleNode = useRef<HTMLDivElement>(null);
const [selected, setSelected] = useState(false);
useEffect(() => {
if (!selected) {
return;
}
function handleGlobalClick(e: MouseEvent) {
if (
selected &&
// Typescript does not believe that these click events are always fired on DOM nodes.
e.target instanceof Element &&
selected &&
// Not sure how, but it's possible for this to get called when handleNode is null/undefined.
// https://sentry.io/share/issue/2066cd79f21e4f279791319f4d2ea35d/
handleNode.current &&
!handleNode.current.contains(e.target!)
) {
setSelected(false);
}
}
document.addEventListener("click", handleGlobalClick);
return () => {
document.removeEventListener("click", handleGlobalClick);
};
}, [selected]);
const offset = useMemo(() => {
return selected
? getNodeOffset(handleNode.current)
: // Kinda awkward. This is a nonsense return value since we only use
//this value when we are selected.
{ top: 0, left: 0 };
}, [selected]);
const { renderMenu, children, top, bottom, ...passThroughProps } = props;
return (
<div {...passThroughProps}>
<div
className="handle"
style={{ width: "100%", height: "100%" }}
ref={handleNode}
onClick={() => setSelected(!selected)}
>
{children}
</div>
<ContextMenu
selected={selected}
offsetTop={offset.top}
offsetLeft={offset.left}
top={top}
bottom={bottom}
>
{renderMenu()}
</ContextMenu>
</div>
);
}
export default ContextMenuTarget;