UNPKG

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