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

81 lines
2.1 KiB
TypeScript

import React, { ReactNode, useState, useEffect, useCallback } from "react";
import ContextMenu from "./ContextMenu";
interface Props {
renderContents(): ReactNode;
children: ReactNode;
}
// Trigger a context menu at the user's cursor position when the user right
// clicks within this component.
// For a component that triggers relative to a given component when the user
// left-clicks see `<ContextMenuTarget />`.
// TODO: Consider using nested contexts to ensure we don't ever have multiple
// non-nested context menus open at a time.
export default function ContextMenuWraper({
children,
renderContents,
...passThroughProps
}: Props) {
const [openPosition, setOpenPosition] = useState<{
x: number;
y: number;
} | null>(null);
const closeMenu = useCallback(() => {
setOpenPosition(null);
}, []);
const handleGlobalClick = useCallback(
(e: MouseEvent) => {
if (e.button !== 2) {
closeMenu();
}
},
[closeMenu]
);
const handleRightClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const { pageX, pageY } = e;
// TODO: We could do an initial render to see if the menu fits here
// and do a second render if it does not.
setOpenPosition({ x: pageX, y: pageY });
e.preventDefault();
e.stopPropagation();
},
[]
);
// Add click-away listeners when window is open
useEffect(() => {
if (openPosition == null) {
return;
}
document.addEventListener("click", handleGlobalClick);
document.body.addEventListener("contextmenu", closeMenu);
return () => {
document.removeEventListener("click", handleGlobalClick);
document.body.removeEventListener("contextmenu", closeMenu);
};
}, [openPosition, closeMenu, handleGlobalClick]);
return (
<div
onContextMenu={handleRightClick}
style={{ width: "100%", height: "100%" }}
{...passThroughProps}
>
<ContextMenu
selected={openPosition != null}
offsetTop={openPosition?.y ?? 0}
offsetLeft={openPosition?.x ?? 0}
>
{renderContents()}
</ContextMenu>
{children}
</div>
);
}