1 | import _extends from "@babel/runtime/helpers/esm/extends";
|
2 | import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
3 | import * as React from 'react';
|
4 | import * as ReactDOM from 'react-dom';
|
5 | import PropTypes from 'prop-types';
|
6 | import clsx from 'clsx';
|
7 | import { deepmerge, elementAcceptingRef } from '@material-ui/utils';
|
8 | import { alpha } from '../styles/colorManipulator';
|
9 | import withStyles from '../styles/withStyles';
|
10 | import capitalize from '../utils/capitalize';
|
11 | import Grow from '../Grow';
|
12 | import Popper from '../Popper';
|
13 | import useForkRef from '../utils/useForkRef';
|
14 | import useId from '../utils/unstable_useId';
|
15 | import setRef from '../utils/setRef';
|
16 | import useIsFocusVisible from '../utils/useIsFocusVisible';
|
17 | import useControlled from '../utils/useControlled';
|
18 | import useTheme from '../styles/useTheme';
|
19 |
|
20 | function round(value) {
|
21 | return Math.round(value * 1e5) / 1e5;
|
22 | }
|
23 |
|
24 | function arrowGenerator() {
|
25 | return {
|
26 | '&[x-placement*="bottom"] $arrow': {
|
27 | top: 0,
|
28 | left: 0,
|
29 | marginTop: '-0.71em',
|
30 | marginLeft: 4,
|
31 | marginRight: 4,
|
32 | '&::before': {
|
33 | transformOrigin: '0 100%'
|
34 | }
|
35 | },
|
36 | '&[x-placement*="top"] $arrow': {
|
37 | bottom: 0,
|
38 | left: 0,
|
39 | marginBottom: '-0.71em',
|
40 | marginLeft: 4,
|
41 | marginRight: 4,
|
42 | '&::before': {
|
43 | transformOrigin: '100% 0'
|
44 | }
|
45 | },
|
46 | '&[x-placement*="right"] $arrow': {
|
47 | left: 0,
|
48 | marginLeft: '-0.71em',
|
49 | height: '1em',
|
50 | width: '0.71em',
|
51 | marginTop: 4,
|
52 | marginBottom: 4,
|
53 | '&::before': {
|
54 | transformOrigin: '100% 100%'
|
55 | }
|
56 | },
|
57 | '&[x-placement*="left"] $arrow': {
|
58 | right: 0,
|
59 | marginRight: '-0.71em',
|
60 | height: '1em',
|
61 | width: '0.71em',
|
62 | marginTop: 4,
|
63 | marginBottom: 4,
|
64 | '&::before': {
|
65 | transformOrigin: '0 0'
|
66 | }
|
67 | }
|
68 | };
|
69 | }
|
70 |
|
71 | export const styles = theme => ({
|
72 |
|
73 | popper: {
|
74 | zIndex: theme.zIndex.tooltip,
|
75 | pointerEvents: 'none'
|
76 |
|
77 | },
|
78 |
|
79 |
|
80 | popperInteractive: {
|
81 | pointerEvents: 'auto'
|
82 | },
|
83 |
|
84 |
|
85 | popperArrow: arrowGenerator(),
|
86 |
|
87 |
|
88 | tooltip: {
|
89 | backgroundColor: alpha(theme.palette.grey[700], 0.9),
|
90 | borderRadius: theme.shape.borderRadius,
|
91 | color: theme.palette.common.white,
|
92 | fontFamily: theme.typography.fontFamily,
|
93 | padding: '4px 8px',
|
94 | fontSize: theme.typography.pxToRem(10),
|
95 | lineHeight: `${round(14 / 10)}em`,
|
96 | maxWidth: 300,
|
97 | wordWrap: 'break-word',
|
98 | fontWeight: theme.typography.fontWeightMedium
|
99 | },
|
100 |
|
101 |
|
102 | tooltipArrow: {
|
103 | position: 'relative',
|
104 | margin: '0'
|
105 | },
|
106 |
|
107 |
|
108 | arrow: {
|
109 | overflow: 'hidden',
|
110 | position: 'absolute',
|
111 | width: '1em',
|
112 | height: '0.71em'
|
113 |
|
114 | ,
|
115 | boxSizing: 'border-box',
|
116 | color: alpha(theme.palette.grey[700], 0.9),
|
117 | '&::before': {
|
118 | content: '""',
|
119 | margin: 'auto',
|
120 | display: 'block',
|
121 | width: '100%',
|
122 | height: '100%',
|
123 | backgroundColor: 'currentColor',
|
124 | transform: 'rotate(45deg)'
|
125 | }
|
126 | },
|
127 |
|
128 |
|
129 | touch: {
|
130 | padding: '8px 16px',
|
131 | fontSize: theme.typography.pxToRem(14),
|
132 | lineHeight: `${round(16 / 14)}em`,
|
133 | fontWeight: theme.typography.fontWeightRegular
|
134 | },
|
135 |
|
136 |
|
137 | tooltipPlacementLeft: {
|
138 | transformOrigin: 'right center',
|
139 | margin: '0 24px ',
|
140 | [theme.breakpoints.up('sm')]: {
|
141 | margin: '0 14px'
|
142 | }
|
143 | },
|
144 |
|
145 |
|
146 | tooltipPlacementRight: {
|
147 | transformOrigin: 'left center',
|
148 | margin: '0 24px',
|
149 | [theme.breakpoints.up('sm')]: {
|
150 | margin: '0 14px'
|
151 | }
|
152 | },
|
153 |
|
154 |
|
155 | tooltipPlacementTop: {
|
156 | transformOrigin: 'center bottom',
|
157 | margin: '24px 0',
|
158 | [theme.breakpoints.up('sm')]: {
|
159 | margin: '14px 0'
|
160 | }
|
161 | },
|
162 |
|
163 |
|
164 | tooltipPlacementBottom: {
|
165 | transformOrigin: 'center top',
|
166 | margin: '24px 0',
|
167 | [theme.breakpoints.up('sm')]: {
|
168 | margin: '14px 0'
|
169 | }
|
170 | }
|
171 | });
|
172 | let hystersisOpen = false;
|
173 | let hystersisTimer = null;
|
174 | export function testReset() {
|
175 | hystersisOpen = false;
|
176 | clearTimeout(hystersisTimer);
|
177 | }
|
178 | const Tooltip = React.forwardRef(function Tooltip(props, ref) {
|
179 | const {
|
180 | arrow = false,
|
181 | children,
|
182 | classes,
|
183 | disableFocusListener = false,
|
184 | disableHoverListener = false,
|
185 | disableTouchListener = false,
|
186 | enterDelay = 100,
|
187 | enterNextDelay = 0,
|
188 | enterTouchDelay = 700,
|
189 | id: idProp,
|
190 | interactive = false,
|
191 | leaveDelay = 0,
|
192 | leaveTouchDelay = 1500,
|
193 | onClose,
|
194 | onOpen,
|
195 | open: openProp,
|
196 | placement = 'bottom',
|
197 | PopperComponent = Popper,
|
198 | PopperProps,
|
199 | title,
|
200 | TransitionComponent = Grow,
|
201 | TransitionProps
|
202 | } = props,
|
203 | other = _objectWithoutPropertiesLoose(props, ["arrow", "children", "classes", "disableFocusListener", "disableHoverListener", "disableTouchListener", "enterDelay", "enterNextDelay", "enterTouchDelay", "id", "interactive", "leaveDelay", "leaveTouchDelay", "onClose", "onOpen", "open", "placement", "PopperComponent", "PopperProps", "title", "TransitionComponent", "TransitionProps"]);
|
204 |
|
205 | const theme = useTheme();
|
206 | const [childNode, setChildNode] = React.useState();
|
207 | const [arrowRef, setArrowRef] = React.useState(null);
|
208 | const ignoreNonTouchEvents = React.useRef(false);
|
209 | const closeTimer = React.useRef();
|
210 | const enterTimer = React.useRef();
|
211 | const leaveTimer = React.useRef();
|
212 | const touchTimer = React.useRef();
|
213 | const [openState, setOpenState] = useControlled({
|
214 | controlled: openProp,
|
215 | default: false,
|
216 | name: 'Tooltip',
|
217 | state: 'open'
|
218 | });
|
219 | let open = openState;
|
220 |
|
221 | if (process.env.NODE_ENV !== 'production') {
|
222 |
|
223 | const {
|
224 | current: isControlled
|
225 | } = React.useRef(openProp !== undefined);
|
226 |
|
227 | React.useEffect(() => {
|
228 | if (childNode && childNode.disabled && !isControlled && title !== '' && childNode.tagName.toLowerCase() === 'button') {
|
229 | console.error(['Material-UI: You are providing a disabled `button` child to the Tooltip component.', 'A disabled element does not fire events.', "Tooltip needs to listen to the child element's events to display the title.", '', 'Add a simple wrapper element, such as a `span`.'].join('\n'));
|
230 | }
|
231 | }, [title, childNode, isControlled]);
|
232 | }
|
233 |
|
234 | const id = useId(idProp);
|
235 | React.useEffect(() => {
|
236 | return () => {
|
237 | clearTimeout(closeTimer.current);
|
238 | clearTimeout(enterTimer.current);
|
239 | clearTimeout(leaveTimer.current);
|
240 | clearTimeout(touchTimer.current);
|
241 | };
|
242 | }, []);
|
243 |
|
244 | const handleOpen = event => {
|
245 | clearTimeout(hystersisTimer);
|
246 | hystersisOpen = true;
|
247 |
|
248 |
|
249 |
|
250 | setOpenState(true);
|
251 |
|
252 | if (onOpen) {
|
253 | onOpen(event);
|
254 | }
|
255 | };
|
256 |
|
257 | const handleEnter = (forward = true) => event => {
|
258 | const childrenProps = children.props;
|
259 |
|
260 | if (event.type === 'mouseover' && childrenProps.onMouseOver && forward) {
|
261 | childrenProps.onMouseOver(event);
|
262 | }
|
263 |
|
264 | if (ignoreNonTouchEvents.current && event.type !== 'touchstart') {
|
265 | return;
|
266 | }
|
267 |
|
268 |
|
269 |
|
270 |
|
271 | if (childNode) {
|
272 | childNode.removeAttribute('title');
|
273 | }
|
274 |
|
275 | clearTimeout(enterTimer.current);
|
276 | clearTimeout(leaveTimer.current);
|
277 |
|
278 | if (enterDelay || hystersisOpen && enterNextDelay) {
|
279 | event.persist();
|
280 | enterTimer.current = setTimeout(() => {
|
281 | handleOpen(event);
|
282 | }, hystersisOpen ? enterNextDelay : enterDelay);
|
283 | } else {
|
284 | handleOpen(event);
|
285 | }
|
286 | };
|
287 |
|
288 | const {
|
289 | isFocusVisible,
|
290 | onBlurVisible,
|
291 | ref: focusVisibleRef
|
292 | } = useIsFocusVisible();
|
293 | const [childIsFocusVisible, setChildIsFocusVisible] = React.useState(false);
|
294 |
|
295 | const handleBlur = () => {
|
296 | if (childIsFocusVisible) {
|
297 | setChildIsFocusVisible(false);
|
298 | onBlurVisible();
|
299 | }
|
300 | };
|
301 |
|
302 | const handleFocus = (forward = true) => event => {
|
303 |
|
304 |
|
305 |
|
306 | if (!childNode) {
|
307 | setChildNode(event.currentTarget);
|
308 | }
|
309 |
|
310 | if (isFocusVisible(event)) {
|
311 | setChildIsFocusVisible(true);
|
312 | handleEnter()(event);
|
313 | }
|
314 |
|
315 | const childrenProps = children.props;
|
316 |
|
317 | if (childrenProps.onFocus && forward) {
|
318 | childrenProps.onFocus(event);
|
319 | }
|
320 | };
|
321 |
|
322 | const handleClose = event => {
|
323 | clearTimeout(hystersisTimer);
|
324 | hystersisTimer = setTimeout(() => {
|
325 | hystersisOpen = false;
|
326 | }, 800 + leaveDelay);
|
327 | setOpenState(false);
|
328 |
|
329 | if (onClose) {
|
330 | onClose(event);
|
331 | }
|
332 |
|
333 | clearTimeout(closeTimer.current);
|
334 | closeTimer.current = setTimeout(() => {
|
335 | ignoreNonTouchEvents.current = false;
|
336 | }, theme.transitions.duration.shortest);
|
337 | };
|
338 |
|
339 | const handleLeave = (forward = true) => event => {
|
340 | const childrenProps = children.props;
|
341 |
|
342 | if (event.type === 'blur') {
|
343 | if (childrenProps.onBlur && forward) {
|
344 | childrenProps.onBlur(event);
|
345 | }
|
346 |
|
347 | handleBlur();
|
348 | }
|
349 |
|
350 | if (event.type === 'mouseleave' && childrenProps.onMouseLeave && event.currentTarget === childNode) {
|
351 | childrenProps.onMouseLeave(event);
|
352 | }
|
353 |
|
354 | clearTimeout(enterTimer.current);
|
355 | clearTimeout(leaveTimer.current);
|
356 | event.persist();
|
357 | leaveTimer.current = setTimeout(() => {
|
358 | handleClose(event);
|
359 | }, leaveDelay);
|
360 | };
|
361 |
|
362 | const detectTouchStart = event => {
|
363 | ignoreNonTouchEvents.current = true;
|
364 | const childrenProps = children.props;
|
365 |
|
366 | if (childrenProps.onTouchStart) {
|
367 | childrenProps.onTouchStart(event);
|
368 | }
|
369 | };
|
370 |
|
371 | const handleTouchStart = event => {
|
372 | detectTouchStart(event);
|
373 | clearTimeout(leaveTimer.current);
|
374 | clearTimeout(closeTimer.current);
|
375 | clearTimeout(touchTimer.current);
|
376 | event.persist();
|
377 | touchTimer.current = setTimeout(() => {
|
378 | handleEnter()(event);
|
379 | }, enterTouchDelay);
|
380 | };
|
381 |
|
382 | const handleTouchEnd = event => {
|
383 | if (children.props.onTouchEnd) {
|
384 | children.props.onTouchEnd(event);
|
385 | }
|
386 |
|
387 | clearTimeout(touchTimer.current);
|
388 | clearTimeout(leaveTimer.current);
|
389 | event.persist();
|
390 | leaveTimer.current = setTimeout(() => {
|
391 | handleClose(event);
|
392 | }, leaveTouchDelay);
|
393 | };
|
394 |
|
395 | const handleUseRef = useForkRef(setChildNode, ref);
|
396 | const handleFocusRef = useForkRef(focusVisibleRef, handleUseRef);
|
397 |
|
398 | const handleOwnRef = React.useCallback(instance => {
|
399 |
|
400 | setRef(handleFocusRef, ReactDOM.findDOMNode(instance));
|
401 | }, [handleFocusRef]);
|
402 | const handleRef = useForkRef(children.ref, handleOwnRef);
|
403 |
|
404 | if (title === '') {
|
405 | open = false;
|
406 | }
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 | const shouldShowNativeTitle = !open && !disableHoverListener;
|
414 |
|
415 | const childrenProps = _extends({
|
416 | 'aria-describedby': open ? id : null,
|
417 | title: shouldShowNativeTitle && typeof title === 'string' ? title : null
|
418 | }, other, children.props, {
|
419 | className: clsx(other.className, children.props.className),
|
420 | onTouchStart: detectTouchStart,
|
421 | ref: handleRef
|
422 | });
|
423 |
|
424 | const interactiveWrapperListeners = {};
|
425 |
|
426 | if (!disableTouchListener) {
|
427 | childrenProps.onTouchStart = handleTouchStart;
|
428 | childrenProps.onTouchEnd = handleTouchEnd;
|
429 | }
|
430 |
|
431 | if (!disableHoverListener) {
|
432 | childrenProps.onMouseOver = handleEnter();
|
433 | childrenProps.onMouseLeave = handleLeave();
|
434 |
|
435 | if (interactive) {
|
436 | interactiveWrapperListeners.onMouseOver = handleEnter(false);
|
437 | interactiveWrapperListeners.onMouseLeave = handleLeave(false);
|
438 | }
|
439 | }
|
440 |
|
441 | if (!disableFocusListener) {
|
442 | childrenProps.onFocus = handleFocus();
|
443 | childrenProps.onBlur = handleLeave();
|
444 |
|
445 | if (interactive) {
|
446 | interactiveWrapperListeners.onFocus = handleFocus(false);
|
447 | interactiveWrapperListeners.onBlur = handleLeave(false);
|
448 | }
|
449 | }
|
450 |
|
451 | if (process.env.NODE_ENV !== 'production') {
|
452 | if (children.props.title) {
|
453 | console.error(['Material-UI: You have provided a `title` prop to the child of <Tooltip />.', `Remove this title prop \`${children.props.title}\` or the Tooltip component.`].join('\n'));
|
454 | }
|
455 | }
|
456 |
|
457 | const mergedPopperProps = React.useMemo(() => {
|
458 | return deepmerge({
|
459 | popperOptions: {
|
460 | modifiers: {
|
461 | arrow: {
|
462 | enabled: Boolean(arrowRef),
|
463 | element: arrowRef
|
464 | }
|
465 | }
|
466 | }
|
467 | }, PopperProps);
|
468 | }, [arrowRef, PopperProps]);
|
469 | return React.createElement(React.Fragment, null, React.cloneElement(children, childrenProps), React.createElement(PopperComponent, _extends({
|
470 | className: clsx(classes.popper, interactive && classes.popperInteractive, arrow && classes.popperArrow),
|
471 | placement: placement,
|
472 | anchorEl: childNode,
|
473 | open: childNode ? open : false,
|
474 | id: childrenProps['aria-describedby'],
|
475 | transition: true
|
476 | }, interactiveWrapperListeners, mergedPopperProps), ({
|
477 | placement: placementInner,
|
478 | TransitionProps: TransitionPropsInner
|
479 | }) => React.createElement(TransitionComponent, _extends({
|
480 | timeout: theme.transitions.duration.shorter
|
481 | }, TransitionPropsInner, TransitionProps), React.createElement("div", {
|
482 | className: clsx(classes.tooltip, classes[`tooltipPlacement${capitalize(placementInner.split('-')[0])}`], ignoreNonTouchEvents.current && classes.touch, arrow && classes.tooltipArrow)
|
483 | }, title, arrow ? React.createElement("span", {
|
484 | className: classes.arrow,
|
485 | ref: setArrowRef
|
486 | }) : null))));
|
487 | });
|
488 | process.env.NODE_ENV !== "production" ? Tooltip.propTypes = {
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 | |
495 |
|
496 |
|
497 | arrow: PropTypes.bool,
|
498 |
|
499 | |
500 |
|
501 |
|
502 | children: elementAcceptingRef.isRequired,
|
503 |
|
504 | |
505 |
|
506 |
|
507 |
|
508 | classes: PropTypes.object,
|
509 |
|
510 | |
511 |
|
512 |
|
513 | className: PropTypes.string,
|
514 |
|
515 | |
516 |
|
517 |
|
518 | disableFocusListener: PropTypes.bool,
|
519 |
|
520 | |
521 |
|
522 |
|
523 | disableHoverListener: PropTypes.bool,
|
524 |
|
525 | |
526 |
|
527 |
|
528 | disableTouchListener: PropTypes.bool,
|
529 |
|
530 | |
531 |
|
532 |
|
533 |
|
534 | enterDelay: PropTypes.number,
|
535 |
|
536 | |
537 |
|
538 |
|
539 | enterNextDelay: PropTypes.number,
|
540 |
|
541 | |
542 |
|
543 |
|
544 | enterTouchDelay: PropTypes.number,
|
545 |
|
546 | |
547 |
|
548 |
|
549 |
|
550 | id: PropTypes.string,
|
551 |
|
552 | |
553 |
|
554 |
|
555 |
|
556 | interactive: PropTypes.bool,
|
557 |
|
558 | |
559 |
|
560 |
|
561 |
|
562 | leaveDelay: PropTypes.number,
|
563 |
|
564 | |
565 |
|
566 |
|
567 | leaveTouchDelay: PropTypes.number,
|
568 |
|
569 | |
570 |
|
571 |
|
572 |
|
573 |
|
574 | onClose: PropTypes.func,
|
575 |
|
576 | |
577 |
|
578 |
|
579 |
|
580 |
|
581 | onOpen: PropTypes.func,
|
582 |
|
583 | |
584 |
|
585 |
|
586 | open: PropTypes.bool,
|
587 |
|
588 | |
589 |
|
590 |
|
591 | placement: PropTypes.oneOf(['bottom-end', 'bottom-start', 'bottom', 'left-end', 'left-start', 'left', 'right-end', 'right-start', 'right', 'top-end', 'top-start', 'top']),
|
592 |
|
593 | |
594 |
|
595 |
|
596 | PopperComponent: PropTypes.elementType,
|
597 |
|
598 | |
599 |
|
600 |
|
601 | PopperProps: PropTypes.object,
|
602 |
|
603 | |
604 |
|
605 |
|
606 | title: PropTypes
|
607 |
|
608 | .node.isRequired,
|
609 |
|
610 | |
611 |
|
612 |
|
613 |
|
614 | TransitionComponent: PropTypes.elementType,
|
615 |
|
616 | |
617 |
|
618 |
|
619 | TransitionProps: PropTypes.object
|
620 | } : void 0;
|
621 | export default withStyles(styles, {
|
622 | name: 'MuiTooltip',
|
623 | flip: false
|
624 | })(Tooltip); |
\ | No newline at end of file |