diff --git a/js/components/ContextMenu.js b/js/components/ContextMenu.tsx similarity index 53% rename from js/components/ContextMenu.js rename to js/components/ContextMenu.tsx index 8800106a..23947a2a 100644 --- a/js/components/ContextMenu.js +++ b/js/components/ContextMenu.tsx @@ -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 { + _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(
{this.props.children}
, - this._node + this._node! ); } } @@ -40,25 +52,36 @@ export const Hr = () => ( ); -export const Parent = ({ children, label }) => ( +interface ParentProps { + label: string; + children: React.ReactNode; +} + +export const Parent = ({ children, label }: ParentProps) => (
  • {label}
  • ); -export const LinkNode = props => ( +interface LinkNodeProps { + label: string; + href: string; +} + +export const LinkNode = (props: LinkNodeProps) => (
  • {props.label}
  • ); -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 (
  • @@ -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 { render() { const { children, @@ -95,7 +123,7 @@ class ContextMenu extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: AppState) => ({ zIndex: state.display.zIndex }); diff --git a/js/components/ContextMenuTarget.js b/js/components/ContextMenuTarget.tsx similarity index 77% rename from js/components/ContextMenuTarget.js rename to js/components/ContextMenuTarget.tsx index 8ef17c3a..7346208d 100644 --- a/js/components/ContextMenuTarget.js +++ b/js/components/ContextMenuTarget.tsx @@ -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 { + 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 -}; diff --git a/package.json b/package.json index a4bcce2d..94c0186c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 43cae93c..147f7bc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"