diff --git a/js/components/ContextMenu.tsx b/js/components/ContextMenu.tsx index 622b2bd8..24ccd849 100644 --- a/js/components/ContextMenu.tsx +++ b/js/components/ContextMenu.tsx @@ -98,6 +98,7 @@ interface ContextMenuProps { offsetLeft: number; top?: boolean; bottom?: boolean; + // TODO: Remove this. Just conditionally render in the parent. selected: boolean; zIndex: number; } diff --git a/js/components/ContextMenuTarget.tsx b/js/components/ContextMenuTarget.tsx index ef72e624..c5a0c749 100644 --- a/js/components/ContextMenuTarget.tsx +++ b/js/components/ContextMenuTarget.tsx @@ -1,4 +1,5 @@ -import React from "react"; +// @ts-ignore #hook-types +import React, { useState, useRef, useEffect, useMemo } from "react"; import ContextMenu from "./ContextMenu"; type DivProps = React.DetailedHTMLProps< @@ -15,75 +16,68 @@ interface State { selected: boolean; } -export default class ContextMenuTarget extends React.Component { - handleNode?: HTMLDivElement | null; - - constructor(props: Props) { - super(props); - this.state = { selected: false }; +function getNodeOffset(node?: Element) { + if (!node) { + return { top: 0, left: 0 }; } - componentDidMount() { - document.addEventListener("click", this._handleGlobalClick); - } - - componentWillUnmount() { - document.removeEventListener("click", this._handleGlobalClick); - } - - _handleHandleClick = () => { - this.setState({ selected: !this.state.selected }); - }; - - _handleGlobalClick = (e: MouseEvent) => { - if ( - // Typescript does not believe that these click events are always fired on DOM nodes. - e.target instanceof Element && - this.state.selected && - // Not sure how, but it's possible for this to get called when handleNode is null/undefined. - // https://sentry.io/share/issue/2066cd79f21e4f279791319f4d2ea35d/ - this.handleNode && - !this.handleNode.contains(e.target!) - ) { - this.setState({ selected: false }); - } - }; - - _offset() { - if (!this.handleNode) { - return { top: 0, left: 0 }; - } - - const rect = this.handleNode.getBoundingClientRect(); - const scrollLeft = - window.pageXOffset || document.documentElement!.scrollLeft; - const scrollTop = window.pageYOffset || document.documentElement!.scrollTop; - return { top: rect.top + scrollTop, left: rect.left + scrollLeft }; - } - - render() { - const { handle, children, top, bottom, ...passThroughProps } = this.props; - const offset = this._offset(); - return ( -
-
(this.handleNode = node)} - onClick={this._handleHandleClick} - > - {handle} -
- - {children} - -
- ); - } + 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 }; } + +function ContextMenuTarget(props: Props) { + const handleNode = useRef(null); + const [selected, setSelected] = useState(false); + useEffect( + () => { + 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, handleNode.current] + ); + + const offset = useMemo(() => getNodeOffset(handleNode.current), [selected]); + + const { handle, children, top, bottom, ...passThroughProps } = props; + return ( +
+
setSelected(!selected)} + > + {handle} +
+ + {children} + +
+ ); +} + +export default ContextMenuTarget;