Convert ContextMenu and ContextMenuTarget to typescriptThis is the first component to use a connected component

This commit is contained in:
Jordan Eldredge 2018-09-17 08:32:52 -07:00
parent f20830c247
commit 1c05bd7b7a
4 changed files with 77 additions and 35 deletions

View file

@ -1,35 +1,47 @@
import invariant from "invariant";
import React from "react";
import { createPortal } from "react-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import classnames from "classnames";
import "../../css/context-menu.css";
import { AppState } from "../types";
interface PortalProps {
zIndex: number;
top: number;
left: number;
}
class Portal extends React.Component<PortalProps> {
_node?: HTMLDivElement;
class Portal extends React.Component {
componentWillMount() {
this._node = document.createElement("div");
this._node.id = "webamp-context-menu";
this._node.style.position = "absolute";
this._node.style.top = 0;
this._node.style.left = 0;
this._node.style.zIndex = this.props.zIndex + 1;
this._node.style.top = "0";
this._node.style.left = "0";
this._node.style.zIndex = String(this.props.zIndex + 1);
document.body.appendChild(this._node);
}
componentWillUnmount() {
document.body.removeChild(this._node);
if (this._node) {
document.body.removeChild(this._node);
}
}
render() {
const style = {
position: "absolute",
top: this.props.top,
left: this.props.left
top: String(this.props.top),
left: String(this.props.left),
// WTF Typescript. There's got to be a better way.
position: "absolute" as "absolute"
};
return createPortal(
<div style={style}>{this.props.children}</div>,
this._node
this._node!
);
}
}
@ -40,25 +52,36 @@ export const Hr = () => (
</li>
);
export const Parent = ({ children, label }) => (
interface ParentProps {
label: string;
children: React.ReactNode;
}
export const Parent = ({ children, label }: ParentProps) => (
<li className="parent">
<ul>{children}</ul>
{label}
</li>
);
export const LinkNode = props => (
interface LinkNodeProps {
label: string;
href: string;
}
export const LinkNode = (props: LinkNodeProps) => (
<li>
<a {...props}>{props.label}</a>
</li>
);
LinkNode.propTypes = {
label: PropTypes.string.isRequired,
href: PropTypes.string.isRequired
};
interface NodeProps {
label: string;
checked: boolean;
className?: string;
}
export const Node = props => {
export const Node = (props: NodeProps) => {
const { label, checked, className = "", ...passThroughProps } = props;
return (
<li className={classnames(className, { checked })} {...passThroughProps}>
@ -67,12 +90,17 @@ export const Node = props => {
);
};
Node.propTypes = {
label: PropTypes.string.isRequired,
hotkey: PropTypes.string
};
interface ContextMenuProps {
children: React.ReactNode;
offsetTop: number;
offsetLeft: number;
top: number;
bottom: number;
selected: boolean;
zIndex: number;
}
class ContextMenu extends React.Component {
class ContextMenu extends React.Component<ContextMenuProps> {
render() {
const {
children,
@ -95,7 +123,7 @@ class ContextMenu extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: AppState) => ({
zIndex: state.display.zIndex
});

View file

@ -1,9 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import ContextMenu from "./ContextMenu";
export default class ContextMenuTarget extends React.Component {
constructor(props) {
interface Props {
handle: React.ReactNode;
children: React.ReactNode;
top: number;
bottom: number;
}
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 };
}
@ -20,13 +31,15 @@ export default class ContextMenuTarget extends React.Component {
this.setState({ selected: !this.state.selected });
};
_handleGlobalClick = e => {
_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.handleNode.contains(e.target!)
) {
this.setState({ selected: false });
}
@ -70,10 +83,3 @@ export default class ContextMenuTarget extends React.Component {
);
}
}
ContextMenuTarget.propTypes = {
children: PropTypes.any.isRequired,
handle: PropTypes.any.isRequired,
top: PropTypes.bool,
bottom: PropTypes.bool
};

View file

@ -61,6 +61,7 @@
"@types/classnames": "^2.2.6",
"@types/invariant": "^2.2.29",
"@types/react": "^16.4.14",
"@types/react-dom": "^16.0.7",
"@types/react-redux": "^6.0.8",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^9.0.0-beta.3",

View file

@ -739,6 +739,13 @@
dependencies:
"@types/react" "*"
"@types/react-dom@^16.0.7":
version "16.0.7"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.7.tgz#54d0f867a76b90597e8432030d297982f25c20ba"
dependencies:
"@types/node" "*"
"@types/react" "*"
"@types/react-redux@^6.0.8":
version "6.0.8"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.8.tgz#29c884174516f0cfab6638d2236fd7d53f45640e"