Use hooks for ContextMenuTarget

This commit is contained in:
Jordan Eldredge 2018-10-28 21:16:04 -07:00
parent 3df608a665
commit 3dc8dc5af6
2 changed files with 65 additions and 70 deletions

View file

@ -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;
}

View file

@ -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<Props, State> {
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 (
<div {...passThroughProps}>
<div
className="handle"
style={{ width: "100%", height: "100%" }}
ref={node => (this.handleNode = node)}
onClick={this._handleHandleClick}
>
{handle}
</div>
<ContextMenu
selected={this.state.selected}
offsetTop={offset.top}
offsetLeft={offset.left}
top={top}
bottom={bottom}
>
{children}
</ContextMenu>
</div>
);
}
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 (
<div {...passThroughProps}>
<div
className="handle"
style={{ width: "100%", height: "100%" }}
ref={handleNode}
onClick={() => setSelected(!selected)}
>
{handle}
</div>
<ContextMenu
selected={selected}
offsetTop={offset.top}
offsetLeft={offset.left}
top={top}
bottom={bottom}
>
{children}
</ContextMenu>
</div>
);
}
export default ContextMenuTarget;