UNPKG

10.2 kBJavaScriptView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import PopperContent from './PopperContent';
4import {
5 getTarget,
6 targetPropType,
7 omit,
8 PopperPlacements,
9 mapToCssModules,
10 DOMElement
11} from './utils';
12
13export const propTypes = {
14 children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
15 placement: PropTypes.oneOf(PopperPlacements),
16 target: targetPropType.isRequired,
17 container: targetPropType,
18 isOpen: PropTypes.bool,
19 disabled: PropTypes.bool,
20 hideArrow: PropTypes.bool,
21 boundariesElement: PropTypes.oneOfType([PropTypes.string, DOMElement]),
22 className: PropTypes.string,
23 innerClassName: PropTypes.string,
24 arrowClassName: PropTypes.string,
25 popperClassName: PropTypes.string,
26 cssModule: PropTypes.object,
27 toggle: PropTypes.func,
28 autohide: PropTypes.bool,
29 placementPrefix: PropTypes.string,
30 delay: PropTypes.oneOfType([
31 PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
32 PropTypes.number
33 ]),
34 modifiers: PropTypes.object,
35 offset: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
36 innerRef: PropTypes.oneOfType([
37 PropTypes.func,
38 PropTypes.string,
39 PropTypes.object
40 ]),
41 trigger: PropTypes.string,
42 fade: PropTypes.bool,
43 flip: PropTypes.bool,
44};
45
46const DEFAULT_DELAYS = {
47 show: 0,
48 hide: 50
49};
50
51const defaultProps = {
52 isOpen: false,
53 hideArrow: false,
54 autohide: false,
55 delay: DEFAULT_DELAYS,
56 toggle: function () {},
57 trigger: 'click',
58 fade: true,
59};
60
61function isInDOMSubtree(element, subtreeRoot) {
62 return subtreeRoot && (element === subtreeRoot || subtreeRoot.contains(element));
63}
64
65function isInDOMSubtrees(element, subtreeRoots = []) {
66 return subtreeRoots && subtreeRoots.length && subtreeRoots.filter(subTreeRoot=> isInDOMSubtree(element, subTreeRoot))[0];
67}
68
69class TooltipPopoverWrapper extends React.Component {
70 constructor(props) {
71 super(props);
72
73 this._targets = [];
74 this.currentTargetElement = null;
75 this.addTargetEvents = this.addTargetEvents.bind(this);
76 this.handleDocumentClick = this.handleDocumentClick.bind(this);
77 this.removeTargetEvents = this.removeTargetEvents.bind(this);
78 this.toggle = this.toggle.bind(this);
79 this.showWithDelay = this.showWithDelay.bind(this);
80 this.hideWithDelay = this.hideWithDelay.bind(this);
81 this.onMouseOverTooltipContent = this.onMouseOverTooltipContent.bind(this);
82 this.onMouseLeaveTooltipContent = this.onMouseLeaveTooltipContent.bind(
83 this
84 );
85 this.show = this.show.bind(this);
86 this.hide = this.hide.bind(this);
87 this.onEscKeyDown = this.onEscKeyDown.bind(this);
88 this.getRef = this.getRef.bind(this);
89 this.state = { isOpen: props.isOpen };
90 this._isMounted = false;
91 }
92
93 componentDidMount() {
94 this._isMounted = true;
95 this.updateTarget();
96 }
97
98 componentWillUnmount() {
99 this._isMounted = false;
100 this.removeTargetEvents();
101 this._targets = null;
102 this.clearShowTimeout();
103 this.clearHideTimeout();
104 }
105
106 static getDerivedStateFromProps(props, state) {
107 if (props.isOpen && !state.isOpen) {
108 return { isOpen: props.isOpen };
109 }
110 else return null;
111 }
112
113 onMouseOverTooltipContent() {
114 if (this.props.trigger.indexOf('hover') > -1 && !this.props.autohide) {
115 if (this._hideTimeout) {
116 this.clearHideTimeout();
117 }
118 if (this.state.isOpen && !this.props.isOpen) {
119 this.toggle();
120 }
121 }
122 }
123
124 onMouseLeaveTooltipContent(e) {
125 if (this.props.trigger.indexOf('hover') > -1 && !this.props.autohide) {
126 if (this._showTimeout) {
127 this.clearShowTimeout();
128 }
129 e.persist();
130 this._hideTimeout = setTimeout(
131 this.hide.bind(this, e),
132 this.getDelay('hide')
133 );
134 }
135 }
136
137 onEscKeyDown(e) {
138 if (e.key === 'Escape') {
139 this.hide(e);
140 }
141 }
142
143 getRef(ref) {
144 const { innerRef } = this.props;
145 if (innerRef) {
146 if (typeof innerRef === 'function') {
147 innerRef(ref);
148 } else if (typeof innerRef === 'object') {
149 innerRef.current = ref;
150 }
151 }
152 this._popover = ref;
153 }
154
155 getDelay(key) {
156 const { delay } = this.props;
157 if (typeof delay === 'object') {
158 return isNaN(delay[key]) ? DEFAULT_DELAYS[key] : delay[key];
159 }
160 return delay;
161 }
162
163 show(e) {
164 if (!this.props.isOpen) {
165 this.clearShowTimeout();
166 this.currentTargetElement = e ? e.currentTarget || e.target : null;
167 if (e && e.composedPath && typeof e.composedPath === 'function') {
168 const path = e.composedPath();
169 this.currentTargetElement = path && path[0] || this.currentTargetElement;
170 }
171 this.toggle(e);
172 }
173 }
174
175 showWithDelay(e) {
176 if (this._hideTimeout) {
177 this.clearHideTimeout();
178 }
179 this._showTimeout = setTimeout(
180 this.show.bind(this, e),
181 this.getDelay('show')
182 );
183 }
184 hide(e) {
185 if (this.props.isOpen) {
186 this.clearHideTimeout();
187 this.currentTargetElement = null;
188 this.toggle(e);
189 }
190 }
191
192 hideWithDelay(e) {
193 if (this._showTimeout) {
194 this.clearShowTimeout();
195 }
196 this._hideTimeout = setTimeout(
197 this.hide.bind(this, e),
198 this.getDelay('hide')
199 );
200 }
201
202
203 clearShowTimeout() {
204 clearTimeout(this._showTimeout);
205 this._showTimeout = undefined;
206 }
207
208 clearHideTimeout() {
209 clearTimeout(this._hideTimeout);
210 this._hideTimeout = undefined;
211 }
212
213 handleDocumentClick(e) {
214 const triggers = this.props.trigger.split(' ');
215
216 if (triggers.indexOf('legacy') > -1 && (this.props.isOpen || isInDOMSubtrees(e.target, this._targets))) {
217 if (this._hideTimeout) {
218 this.clearHideTimeout();
219 }
220 if (this.props.isOpen && !isInDOMSubtree(e.target, this._popover)) {
221 this.hideWithDelay(e);
222 } else if (!this.props.isOpen) {
223 this.showWithDelay(e);
224 }
225 } else if (triggers.indexOf('click') > -1 && isInDOMSubtrees(e.target, this._targets)) {
226 if (this._hideTimeout) {
227 this.clearHideTimeout();
228 }
229
230 if (!this.props.isOpen) {
231 this.showWithDelay(e);
232 } else {
233 this.hideWithDelay(e);
234 }
235 }
236 }
237
238 addEventOnTargets(type, handler, isBubble) {
239 this._targets.forEach(target=> {
240 target.addEventListener(type, handler, isBubble);
241 });
242 }
243
244 removeEventOnTargets(type, handler, isBubble) {
245 this._targets.forEach(target=> {
246 target.removeEventListener(type, handler, isBubble);
247 });
248 }
249
250 addTargetEvents() {
251 if (this.props.trigger) {
252 let triggers = this.props.trigger.split(' ');
253 if (triggers.indexOf('manual') === -1) {
254 if (triggers.indexOf('click') > -1 || triggers.indexOf('legacy') > -1) {
255 document.addEventListener('click', this.handleDocumentClick, true);
256 }
257
258 if (this._targets && this._targets.length) {
259 if (triggers.indexOf('hover') > -1) {
260 this.addEventOnTargets(
261 'mouseover',
262 this.showWithDelay,
263 true
264 );
265 this.addEventOnTargets(
266 'mouseout',
267 this.hideWithDelay,
268 true
269 );
270 }
271 if (triggers.indexOf('focus') > -1) {
272 this.addEventOnTargets('focusin', this.show, true);
273 this.addEventOnTargets('focusout', this.hide, true);
274 }
275 this.addEventOnTargets('keydown', this.onEscKeyDown, true);
276 }
277 }
278 }
279 }
280
281 removeTargetEvents() {
282 if (this._targets) {
283 this.removeEventOnTargets(
284 'mouseover',
285 this.showWithDelay,
286 true
287 );
288 this.removeEventOnTargets(
289 'mouseout',
290 this.hideWithDelay,
291 true
292 );
293 this.removeEventOnTargets('keydown', this.onEscKeyDown, true);
294 this.removeEventOnTargets('focusin', this.show, true);
295 this.removeEventOnTargets('focusout', this.hide, true);
296 }
297
298 document.removeEventListener('click', this.handleDocumentClick, true)
299 }
300
301 updateTarget() {
302 const newTarget = getTarget(this.props.target, true);
303 if (newTarget !== this._targets) {
304 this.removeTargetEvents();
305 this._targets = newTarget ? Array.from(newTarget) : [];
306 this.currentTargetElement = this.currentTargetElement || this._targets[0];
307 this.addTargetEvents();
308 }
309 }
310
311 toggle(e) {
312 if (this.props.disabled || !this._isMounted) {
313 return e && e.preventDefault();
314 }
315
316 return this.props.toggle(e);
317 }
318
319 render() {
320 if (!this.props.isOpen) {
321 return null;
322 }
323
324 this.updateTarget();
325
326 const {
327 className,
328 cssModule,
329 innerClassName,
330 isOpen,
331 hideArrow,
332 boundariesElement,
333 placement,
334 placementPrefix,
335 arrowClassName,
336 popperClassName,
337 container,
338 modifiers,
339 offset,
340 fade,
341 flip,
342 children
343 } = this.props;
344
345 const attributes = omit(this.props, Object.keys(propTypes));
346
347 const popperClasses = mapToCssModules(popperClassName, cssModule);
348
349 const classes = mapToCssModules(innerClassName, cssModule);
350
351 return (
352 <PopperContent
353 className={className}
354 target={this.currentTargetElement || this._targets[0]}
355 isOpen={isOpen}
356 hideArrow={hideArrow}
357 boundariesElement={boundariesElement}
358 placement={placement}
359 placementPrefix={placementPrefix}
360 arrowClassName={arrowClassName}
361 popperClassName={popperClasses}
362 container={container}
363 modifiers={modifiers}
364 offset={offset}
365 cssModule={cssModule}
366 fade={fade}
367 flip={flip}
368 >
369 {({ scheduleUpdate }) => (
370 <div
371 {...attributes}
372 ref={this.getRef}
373 className={classes}
374 role="tooltip"
375 onMouseOver={this.onMouseOverTooltipContent}
376 onMouseLeave={this.onMouseLeaveTooltipContent}
377 onKeyDown={this.onEscKeyDown}
378 >
379 {typeof children === 'function' ? children({ scheduleUpdate }) : children}
380 </div>
381 )}
382
383 </PopperContent>
384 );
385 }
386}
387
388TooltipPopoverWrapper.propTypes = propTypes;
389TooltipPopoverWrapper.defaultProps = defaultProps;
390
391export default TooltipPopoverWrapper;