1 | import React, { Component, cloneElement } from 'react';
|
2 | import ReactDOM from 'react-dom';
|
3 | import createChainedFunction from 'tinper-bee-core/lib/createChainedFunction';
|
4 | import splitComponentProps from 'tinper-bee-core/lib/splitComponent';
|
5 | import PropTypes from 'prop-types';
|
6 | import Overlay from 'bee-overlay/build/Overlay';
|
7 | import Portal from 'bee-overlay/build/Portal';
|
8 | import Content from './Content';
|
9 | import contains from 'dom-helpers/query/contains';
|
10 |
|
11 |
|
12 |
|
13 | const isReact16 = ReactDOM.createPortal !== undefined;
|
14 |
|
15 | const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']);
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | function isOneOf(one, of) {
|
25 | if (Array.isArray(of)) {
|
26 | return of.indexOf(one) >= 0;
|
27 | }
|
28 | return one === of;
|
29 | }
|
30 |
|
31 | const propTypes = {
|
32 | ...Overlay.propTypes,
|
33 |
|
34 |
|
35 |
|
36 | |
37 |
|
38 |
|
39 | defaultOverlayShown: PropTypes.bool,
|
40 |
|
41 | |
42 |
|
43 |
|
44 | content: PropTypes.node.isRequired,
|
45 | |
46 |
|
47 |
|
48 | delay: PropTypes.number,
|
49 | |
50 |
|
51 |
|
52 | delayShow: PropTypes.number,
|
53 | |
54 |
|
55 |
|
56 | delayHide: PropTypes.number,
|
57 |
|
58 | |
59 |
|
60 |
|
61 | onClick: PropTypes.func,
|
62 | onClose: PropTypes.func,
|
63 | onCancel: PropTypes.func,
|
64 |
|
65 |
|
66 |
|
67 | |
68 |
|
69 |
|
70 | target: PropTypes.oneOf([null]),
|
71 | |
72 |
|
73 |
|
74 | onHide: PropTypes.oneOf([null]),
|
75 | |
76 |
|
77 |
|
78 | show: PropTypes.bool,
|
79 |
|
80 | trigger: PropTypes.oneOfType([
|
81 | triggerType, PropTypes.arrayOf(triggerType),
|
82 | ]),
|
83 | |
84 |
|
85 |
|
86 | onBlur: PropTypes.func,
|
87 | |
88 |
|
89 |
|
90 | onFocus: PropTypes.func,
|
91 | |
92 |
|
93 |
|
94 | onMouseOut: PropTypes.func,
|
95 | |
96 |
|
97 |
|
98 | onMouseOver: PropTypes.func,
|
99 | };
|
100 |
|
101 |
|
102 | const defaultProps = {
|
103 | placement: 'right',
|
104 | clsPrefix: 'u-popover',
|
105 | rootClose: true,
|
106 | defaultOverlayShown: false,
|
107 | };
|
108 |
|
109 | class Popover extends Component{
|
110 | constructor(props, context) {
|
111 | super(props, context);
|
112 |
|
113 | this._mountNode = null;
|
114 |
|
115 | this.state = {
|
116 | show: props.defaultOverlayShown,
|
117 | };
|
118 |
|
119 | this.handleMouseOver = e => (
|
120 | this.handleMouseOverOut(this.handleDelayedShow, e)
|
121 | );
|
122 | this.handleMouseOut = e => (
|
123 | this.handleMouseOverOut(this.handleDelayedHide, e)
|
124 | );
|
125 | }
|
126 |
|
127 | componentDidMount() {
|
128 | this._mountNode = document.createElement('div');
|
129 | !isReact16 && this.renderOverlay();
|
130 | }
|
131 |
|
132 | componentWillReceiveProps(nextProps) {
|
133 | if(nextProps.hasOwnProperty('show')){
|
134 | if(nextProps.show){
|
135 | this.handleShow();
|
136 | }else{
|
137 | this.handleHide();
|
138 | }
|
139 | }
|
140 | }
|
141 |
|
142 | componentDidUpdate() {
|
143 | !isReact16 && this.renderOverlay();
|
144 | }
|
145 |
|
146 | componentWillUnmount() {
|
147 | !isReact16 && ReactDOM.unmountComponentAtNode(this._mountNode);
|
148 | this._mountNode = null;
|
149 |
|
150 | }
|
151 |
|
152 | handleToggle = () => {
|
153 | if (!this.state.show) {
|
154 | this.show();
|
155 | }else{
|
156 | this.hide();
|
157 | }
|
158 | }
|
159 |
|
160 | handleDelayedShow = () => {
|
161 | if (this._hoverHideDelay != null) {
|
162 | clearTimeout(this._hoverHideDelay);
|
163 | this._hoverHideDelay = null;
|
164 | return;
|
165 | }
|
166 |
|
167 | if (this.state.show || this._hoverShowDelay != null) {
|
168 | return;
|
169 | }
|
170 |
|
171 | const delay = this.props.delayShow != null ?
|
172 | this.props.delayShow : this.props.delay;
|
173 |
|
174 | if (!delay) {
|
175 | this.show();
|
176 | return;
|
177 | }
|
178 |
|
179 | this._hoverShowDelay = setTimeout(() => {
|
180 | this._hoverShowDelay = null;
|
181 | this.show();
|
182 | }, delay);
|
183 | }
|
184 |
|
185 | handleDelayedHide = () => {
|
186 | if (this._hoverShowDelay != null) {
|
187 | clearTimeout(this._hoverShowDelay);
|
188 | this._hoverShowDelay = null;
|
189 | return;
|
190 | }
|
191 |
|
192 | if (!this.state.show || this._hoverHideDelay != null) {
|
193 | return;
|
194 | }
|
195 |
|
196 | const delay = this.props.delayHide != null ?
|
197 | this.props.delayHide : this.props.delay;
|
198 |
|
199 | if (!delay) {
|
200 | this.hide();
|
201 | return;
|
202 | }
|
203 |
|
204 | this._hoverHideDelay = setTimeout(() => {
|
205 | this._hoverHideDelay = null;
|
206 | this.hide();
|
207 | }, delay);
|
208 | }
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 | handleMouseOverOut = (handler, e) => {
|
215 | const target = e.currentTarget;
|
216 | const related = e.relatedTarget || e.nativeEvent.toElement;
|
217 |
|
218 | if (!related || related !== target && !contains(target, related)) {
|
219 | handler(e);
|
220 | }
|
221 | }
|
222 |
|
223 |
|
224 | handleHide = () => {
|
225 | if(this.state.show){
|
226 | this.hide();
|
227 | }
|
228 | }
|
229 |
|
230 | handleShow = () => {
|
231 | if(!this.state.show){
|
232 | this.show();
|
233 | }
|
234 | }
|
235 |
|
236 | show = () => {
|
237 | this.setState({ show: true });
|
238 | }
|
239 |
|
240 | hide = () => {
|
241 | let { onHide } = this.props;
|
242 | onHide && onHide()
|
243 | this.setState({ show: false });
|
244 | }
|
245 |
|
246 | makeOverlay = (overlay, props) => {
|
247 | return (
|
248 | <Overlay
|
249 | {...props}
|
250 | show={this.state.show}
|
251 | onHide={this.handleHide}
|
252 | target={this}
|
253 | >
|
254 | {overlay}
|
255 | </Overlay>
|
256 | );
|
257 | }
|
258 |
|
259 | renderOverlay = () => {
|
260 | ReactDOM.unstable_renderSubtreeIntoContainer(
|
261 | this, this._overlay, this._mountNode
|
262 | );
|
263 | }
|
264 |
|
265 | render() {
|
266 | const {
|
267 | content,
|
268 | children,
|
269 | onClick,
|
270 | trigger,
|
271 | onBlur,
|
272 | onFocus,
|
273 | onMouseOut,
|
274 | onMouseOver,
|
275 | ...props
|
276 | } = this.props;
|
277 |
|
278 | delete props.delay;
|
279 | delete props.delayShow;
|
280 | delete props.delayHide;
|
281 | delete props.defaultOverlayShown;
|
282 |
|
283 | const [overlayProps, confirmProps] = splitComponentProps(props, Overlay);
|
284 |
|
285 | const child = React.Children.only(children);
|
286 | const childProps = child.props;
|
287 |
|
288 | let overlay = (
|
289 | <Content placement={ props.placement } {...confirmProps} >
|
290 | {content}
|
291 | </Content>
|
292 | );
|
293 |
|
294 | const triggerProps = {
|
295 | 'aria-describedby': overlay.props.id
|
296 | };
|
297 |
|
298 |
|
299 |
|
300 | triggerProps.onClick = createChainedFunction(childProps.onClick, onClick);
|
301 |
|
302 | if (isOneOf('click', trigger)) {
|
303 | triggerProps.onClick = createChainedFunction(
|
304 | triggerProps.onClick, this.handleToggle
|
305 | );
|
306 | }
|
307 |
|
308 | if (isOneOf('hover', trigger)) {
|
309 |
|
310 | triggerProps.onMouseOver = createChainedFunction(
|
311 | childProps.onMouseOver, onMouseOver, this.handleMouseOver
|
312 | );
|
313 | triggerProps.onMouseOut = createChainedFunction(
|
314 | childProps.onMouseOut, onMouseOut, this.handleMouseOut
|
315 | );
|
316 | }
|
317 |
|
318 | if (isOneOf('focus', trigger)) {
|
319 | triggerProps.onFocus = createChainedFunction(
|
320 | childProps.onFocus, onFocus, this.handleDelayedShow
|
321 | );
|
322 | triggerProps.onBlur = createChainedFunction(
|
323 | childProps.onBlur, onBlur, this.handleDelayedHide
|
324 | );
|
325 | }
|
326 |
|
327 |
|
328 | this._overlay = this.makeOverlay(overlay, overlayProps);
|
329 |
|
330 | if (!isReact16) {
|
331 | return cloneElement(child, triggerProps);
|
332 | }
|
333 | triggerProps.key = 'overlay';
|
334 |
|
335 | let portal = (
|
336 | <Portal
|
337 | key="portal"
|
338 | container={props.container}>
|
339 | { this._overlay }
|
340 | </Portal>
|
341 | )
|
342 |
|
343 |
|
344 | return [
|
345 | cloneElement(child, triggerProps),
|
346 | portal
|
347 | ]
|
348 | }
|
349 |
|
350 | }
|
351 |
|
352 | Popover.propTypes = propTypes;
|
353 | Popover.defaultProps = defaultProps;
|
354 |
|
355 | export default Popover;
|