-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
dialog.tsx
155 lines (126 loc) · 3.54 KB
/
dialog.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import "./dialog.scss";
import React from "react";
import { createPortal, findDOMNode } from "react-dom";
import { disposeOnUnmount, observer } from "mobx-react";
import { reaction } from "mobx";
import { Animate } from "../animate";
import { cssNames, noop, stopPropagation } from "../../utils";
import { navigation } from "../../navigation";
// todo: refactor + handle animation-end in props.onClose()?
export interface DialogProps {
className?: string;
isOpen?: boolean;
open?: () => void;
close?: () => void;
onOpen?: () => void;
onClose?: () => void;
modal?: boolean;
pinned?: boolean;
animated?: boolean;
}
interface DialogState {
isOpen: boolean;
}
@observer
export class Dialog extends React.PureComponent<DialogProps, DialogState> {
private contentElem: HTMLElement;
static defaultProps: DialogProps = {
isOpen: false,
open: noop,
close: noop,
onOpen: noop,
onClose: noop,
modal: true,
animated: true,
pinned: false,
};
@disposeOnUnmount
closeOnNavigate = reaction(() => navigation.getPath(), () => this.close());
public state: DialogState = {
isOpen: this.props.isOpen,
};
get elem() {
// eslint-disable-next-line react/no-find-dom-node
return findDOMNode(this) as HTMLElement;
}
get isOpen() {
return this.state.isOpen;
}
componentDidMount() {
if (this.isOpen) this.onOpen();
}
componentDidUpdate(prevProps: DialogProps) {
const { isOpen } = this.props;
if (isOpen !== prevProps.isOpen) {
this.toggle(isOpen);
}
}
componentWillUnmount() {
if (this.isOpen) this.onClose();
}
toggle(isOpen: boolean) {
if (isOpen) this.open();
else this.close();
}
open() {
requestAnimationFrame(this.onOpen); // wait for render(), bind close-event to this.elem
this.setState({ isOpen: true });
this.props.open();
}
close() {
this.onClose(); // must be first to get access to dialog's content from outside
this.setState({ isOpen: false });
this.props.close();
}
onOpen = () => {
this.props.onOpen();
if (!this.props.pinned) {
if (this.elem) this.elem.addEventListener("click", this.onClickOutside);
// Using document.body target to handle keydown event before Drawer does
document.body.addEventListener("keydown", this.onEscapeKey);
}
};
onClose = () => {
this.props.onClose();
if (!this.props.pinned) {
if (this.elem) this.elem.removeEventListener("click", this.onClickOutside);
document.body.removeEventListener("keydown", this.onEscapeKey);
}
};
onEscapeKey = (evt: KeyboardEvent) => {
const escapeKey = evt.code === "Escape";
if (escapeKey) {
this.close();
evt.stopPropagation();
}
};
onClickOutside = (evt: MouseEvent) => {
const target = evt.target as HTMLElement;
if (!this.contentElem.contains(target)) {
this.close();
evt.stopPropagation();
}
};
render() {
const { modal, animated, pinned } = this.props;
let { className } = this.props;
className = cssNames("Dialog flex center", className, { modal, pinned });
let dialog = (
<div className={className} onClick={stopPropagation}>
<div className="box" ref={e => this.contentElem = e}>
{this.props.children}
</div>
</div>
);
if (animated) {
dialog = (
<Animate enter={this.isOpen} name="opacity-scale">
{dialog}
</Animate>
);
} else if (!this.isOpen) {
return null;
}
return createPortal(dialog, document.body) as React.ReactPortal;
}
}