UNPKG

21.2 kBJavaScriptView Raw
1import React, { useEffect, useLayoutEffect, forwardRef, useState, useRef, useImperativeHandle } from 'react';
2import ReactDOM from 'react-dom';
3
4function _extends() {
5 _extends = Object.assign || function (target) {
6 for (var i = 1; i < arguments.length; i++) {
7 var source = arguments[i];
8
9 for (var key in source) {
10 if (Object.prototype.hasOwnProperty.call(source, key)) {
11 target[key] = source[key];
12 }
13 }
14 }
15
16 return target;
17 };
18
19 return _extends.apply(this, arguments);
20}
21
22var useOnEscape = function useOnEscape(handler, active) {
23 if (active === void 0) {
24 active = true;
25 }
26
27 useEffect(function () {
28 if (!active) return;
29
30 var listener = function listener(event) {
31 // check if key is an Escape
32 if (event.key === 'Escape') handler(event);
33 };
34
35 document.addEventListener('keyup', listener);
36 return function () {
37 if (!active) return;
38 document.removeEventListener('keyup', listener);
39 };
40 }, [handler, active]);
41};
42var useRepositionOnResize = function useRepositionOnResize(handler, active) {
43 if (active === void 0) {
44 active = true;
45 }
46
47 useEffect(function () {
48 if (!active) return;
49
50 var listener = function listener() {
51 handler();
52 };
53
54 window.addEventListener('resize', listener);
55 return function () {
56 if (!active) return;
57 window.removeEventListener('resize', listener);
58 };
59 }, [handler, active]);
60};
61var useOnClickOutside = function useOnClickOutside(ref, handler, active) {
62 if (active === void 0) {
63 active = true;
64 }
65
66 useEffect(function () {
67 if (!active) return;
68
69 var listener = function listener(event) {
70 // Do nothing if clicking ref's element or descendent elements
71 var refs = Array.isArray(ref) ? ref : [ref];
72 var contains = false;
73 refs.forEach(function (r) {
74 if (!r.current || r.current.contains(event.target)) {
75 contains = true;
76 return;
77 }
78 });
79 event.stopPropagation();
80 if (!contains) handler(event);
81 };
82
83 document.addEventListener('mousedown', listener);
84 document.addEventListener('touchstart', listener);
85 return function () {
86 if (!active) return;
87 document.removeEventListener('mousedown', listener);
88 document.removeEventListener('touchstart', listener);
89 };
90 }, [ref, handler, active]);
91}; // Make sure that user is not able TAB out of the Modal content on Open
92
93var useTabbing = function useTabbing(contentRef, active) {
94 if (active === void 0) {
95 active = true;
96 }
97
98 useEffect(function () {
99 if (!active) return;
100
101 var listener = function listener(event) {
102 // check if key is an Tab
103 if (event.keyCode === 9) {
104 var _contentRef$current;
105
106 var els = contentRef === null || contentRef === void 0 ? void 0 : (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 ? void 0 : _contentRef$current.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
107 var focusableEls = Array.prototype.slice.call(els);
108
109 if (focusableEls.length === 1) {
110 event.preventDefault();
111 return;
112 }
113
114 var firstFocusableEl = focusableEls[0];
115 var lastFocusableEl = focusableEls[focusableEls.length - 1];
116
117 if (event.shiftKey && document.activeElement === firstFocusableEl) {
118 event.preventDefault();
119 lastFocusableEl.focus();
120 } else if (document.activeElement === lastFocusableEl) {
121 event.preventDefault();
122 firstFocusableEl.focus();
123 }
124 }
125 };
126
127 document.addEventListener('keydown', listener);
128 return function () {
129 if (!active) return;
130 document.removeEventListener('keydown', listener);
131 };
132 }, [contentRef, active]);
133};
134var useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
135
136var Style = {
137 popupContent: {
138 tooltip: {
139 position: 'absolute',
140 zIndex: 999
141 },
142 modal: {
143 position: 'relative',
144 margin: 'auto'
145 }
146 },
147 popupArrow: {
148 height: '8px',
149 width: '16px',
150 position: 'absolute',
151 background: 'transparent',
152 color: '#FFF',
153 zIndex: -1
154 },
155 overlay: {
156 tooltip: {
157 position: 'fixed',
158 top: '0',
159 bottom: '0',
160 left: '0',
161 right: '0',
162 zIndex: 999
163 },
164 modal: {
165 position: 'fixed',
166 top: '0',
167 bottom: '0',
168 left: '0',
169 right: '0',
170 display: 'flex',
171 zIndex: 999
172 }
173 }
174};
175
176var POSITION_TYPES = ['top left', 'top center', 'top right', 'right top', 'right center', 'right bottom', 'bottom left', 'bottom center', 'bottom right', 'left top', 'left center', 'left bottom'];
177
178var getCoordinatesForPosition = function getCoordinatesForPosition(triggerBounding, ContentBounding, position, //PopupPosition | PopupPosition[],
179arrow, _ref) {
180 var offsetX = _ref.offsetX,
181 offsetY = _ref.offsetY;
182 var margin = arrow ? 8 : 0;
183 var args = position.split(' '); // the step N 1 : center the popup content => ok
184
185 var CenterTop = triggerBounding.top + triggerBounding.height / 2;
186 var CenterLeft = triggerBounding.left + triggerBounding.width / 2;
187 var height = ContentBounding.height,
188 width = ContentBounding.width;
189 var top = CenterTop - height / 2;
190 var left = CenterLeft - width / 2;
191 var transform = '';
192 var arrowTop = '0%';
193 var arrowLeft = '0%'; // the step N 2 : => ok
194
195 switch (args[0]) {
196 case 'top':
197 top -= height / 2 + triggerBounding.height / 2 + margin;
198 transform = "rotate(180deg) translateX(50%)";
199 arrowTop = '100%';
200 arrowLeft = '50%';
201 break;
202
203 case 'bottom':
204 top += height / 2 + triggerBounding.height / 2 + margin;
205 transform = "rotate(0deg) translateY(-100%) translateX(-50%)";
206 arrowLeft = '50%';
207 break;
208
209 case 'left':
210 left -= width / 2 + triggerBounding.width / 2 + margin;
211 transform = " rotate(90deg) translateY(50%) translateX(-25%)";
212 arrowLeft = '100%';
213 arrowTop = '50%';
214 break;
215
216 case 'right':
217 left += width / 2 + triggerBounding.width / 2 + margin;
218 transform = "rotate(-90deg) translateY(-150%) translateX(25%)";
219 arrowTop = '50%';
220 break;
221 }
222
223 switch (args[1]) {
224 case 'top':
225 top = triggerBounding.top;
226 arrowTop = triggerBounding.height / 2 + "px";
227 break;
228
229 case 'bottom':
230 top = triggerBounding.top - height + triggerBounding.height;
231 arrowTop = height - triggerBounding.height / 2 + "px";
232 break;
233
234 case 'left':
235 left = triggerBounding.left;
236 arrowLeft = triggerBounding.width / 2 + "px";
237 break;
238
239 case 'right':
240 left = triggerBounding.left - width + triggerBounding.width;
241 arrowLeft = width - triggerBounding.width / 2 + "px";
242 break;
243 }
244
245 top = args[0] === 'top' ? top - offsetY : top + offsetY;
246 left = args[0] === 'left' ? left - offsetX : left + offsetX;
247 return {
248 top: top,
249 left: left,
250 transform: transform,
251 arrowLeft: arrowLeft,
252 arrowTop: arrowTop
253 };
254};
255
256var getTooltipBoundary = function getTooltipBoundary(keepTooltipInside) {
257 // add viewport
258 var boundingBox = {
259 top: 0,
260 left: 0,
261
262 /* eslint-disable-next-line no-undef */
263 width: window.innerWidth,
264
265 /* eslint-disable-next-line no-undef */
266 height: window.innerHeight
267 };
268
269 if (typeof keepTooltipInside === 'string') {
270 /* eslint-disable-next-line no-undef */
271 var selector = document.querySelector(keepTooltipInside);
272
273 if (process.env.NODE_ENV !== 'production') {
274 if (selector === null) throw new Error(keepTooltipInside + " selector does not exist : keepTooltipInside must be a valid html selector 'class' or 'Id' or a boolean value");
275 }
276
277 if (selector !== null) boundingBox = selector.getBoundingClientRect();
278 }
279
280 return boundingBox;
281};
282
283var calculatePosition = function calculatePosition(triggerBounding, ContentBounding, position, arrow, _ref2, keepTooltipInside) {
284 var offsetX = _ref2.offsetX,
285 offsetY = _ref2.offsetY;
286 var bestCoords = {
287 arrowLeft: '0%',
288 arrowTop: '0%',
289 left: 0,
290 top: 0,
291 transform: 'rotate(135deg)'
292 };
293 var i = 0;
294 var wrapperBox = getTooltipBoundary(keepTooltipInside);
295 var positions = Array.isArray(position) ? position : [position]; // keepTooltipInside would be activated if the keepTooltipInside exist or the position is Array
296
297 if (keepTooltipInside || Array.isArray(position)) positions = [].concat(positions, POSITION_TYPES); // add viewPort for WarpperBox
298 // wrapperBox.top = wrapperBox.top + window.scrollY;
299 // wrapperBox.left = wrapperBox.left + window.scrollX;
300
301 while (i < positions.length) {
302 bestCoords = getCoordinatesForPosition(triggerBounding, ContentBounding, positions[i], arrow, {
303 offsetX: offsetX,
304 offsetY: offsetY
305 });
306 var contentBox = {
307 top: bestCoords.top,
308 left: bestCoords.left,
309 width: ContentBounding.width,
310 height: ContentBounding.height
311 };
312
313 if (contentBox.top <= wrapperBox.top || contentBox.left <= wrapperBox.left || contentBox.top + contentBox.height >= wrapperBox.top + wrapperBox.height || contentBox.left + contentBox.width >= wrapperBox.left + wrapperBox.width) {
314 i++;
315 } else {
316 break;
317 }
318 }
319
320 return bestCoords;
321};
322
323var popupIdCounter = 0;
324
325var getRootPopup = function getRootPopup() {
326 var PopupRoot = document.getElementById('popup-root');
327
328 if (PopupRoot === null) {
329 PopupRoot = document.createElement('div');
330 PopupRoot.setAttribute('id', 'popup-root');
331 document.body.appendChild(PopupRoot);
332 }
333
334 return PopupRoot;
335};
336
337var Popup = /*#__PURE__*/forwardRef(function (_ref, ref) {
338 var _ref$trigger = _ref.trigger,
339 trigger = _ref$trigger === void 0 ? null : _ref$trigger,
340 _ref$onOpen = _ref.onOpen,
341 onOpen = _ref$onOpen === void 0 ? function () {} : _ref$onOpen,
342 _ref$onClose = _ref.onClose,
343 onClose = _ref$onClose === void 0 ? function () {} : _ref$onClose,
344 _ref$defaultOpen = _ref.defaultOpen,
345 defaultOpen = _ref$defaultOpen === void 0 ? false : _ref$defaultOpen,
346 _ref$open = _ref.open,
347 open = _ref$open === void 0 ? undefined : _ref$open,
348 _ref$disabled = _ref.disabled,
349 disabled = _ref$disabled === void 0 ? false : _ref$disabled,
350 _ref$nested = _ref.nested,
351 nested = _ref$nested === void 0 ? false : _ref$nested,
352 _ref$closeOnDocumentC = _ref.closeOnDocumentClick,
353 closeOnDocumentClick = _ref$closeOnDocumentC === void 0 ? true : _ref$closeOnDocumentC,
354 _ref$repositionOnResi = _ref.repositionOnResize,
355 repositionOnResize = _ref$repositionOnResi === void 0 ? true : _ref$repositionOnResi,
356 _ref$closeOnEscape = _ref.closeOnEscape,
357 closeOnEscape = _ref$closeOnEscape === void 0 ? true : _ref$closeOnEscape,
358 _ref$on = _ref.on,
359 on = _ref$on === void 0 ? ['click'] : _ref$on,
360 _ref$contentStyle = _ref.contentStyle,
361 contentStyle = _ref$contentStyle === void 0 ? {} : _ref$contentStyle,
362 _ref$arrowStyle = _ref.arrowStyle,
363 arrowStyle = _ref$arrowStyle === void 0 ? {} : _ref$arrowStyle,
364 _ref$overlayStyle = _ref.overlayStyle,
365 overlayStyle = _ref$overlayStyle === void 0 ? {} : _ref$overlayStyle,
366 _ref$className = _ref.className,
367 className = _ref$className === void 0 ? '' : _ref$className,
368 _ref$position = _ref.position,
369 position = _ref$position === void 0 ? 'bottom center' : _ref$position,
370 _ref$modal = _ref.modal,
371 modal = _ref$modal === void 0 ? false : _ref$modal,
372 _ref$lockScroll = _ref.lockScroll,
373 lockScroll = _ref$lockScroll === void 0 ? false : _ref$lockScroll,
374 _ref$arrow = _ref.arrow,
375 arrow = _ref$arrow === void 0 ? true : _ref$arrow,
376 _ref$offsetX = _ref.offsetX,
377 offsetX = _ref$offsetX === void 0 ? 0 : _ref$offsetX,
378 _ref$offsetY = _ref.offsetY,
379 offsetY = _ref$offsetY === void 0 ? 0 : _ref$offsetY,
380 _ref$mouseEnterDelay = _ref.mouseEnterDelay,
381 mouseEnterDelay = _ref$mouseEnterDelay === void 0 ? 100 : _ref$mouseEnterDelay,
382 _ref$mouseLeaveDelay = _ref.mouseLeaveDelay,
383 mouseLeaveDelay = _ref$mouseLeaveDelay === void 0 ? 100 : _ref$mouseLeaveDelay,
384 _ref$keepTooltipInsid = _ref.keepTooltipInside,
385 keepTooltipInside = _ref$keepTooltipInsid === void 0 ? false : _ref$keepTooltipInsid,
386 children = _ref.children;
387
388 var _useState = useState(open || defaultOpen),
389 isOpen = _useState[0],
390 setIsOpen = _useState[1];
391
392 var triggerRef = useRef(null);
393 var contentRef = useRef(null);
394 var arrowRef = useRef(null);
395 var focusedElBeforeOpen = useRef(null);
396 var popupId = useRef("popup-" + ++popupIdCounter);
397 var isModal = modal ? true : !trigger;
398 var timeOut = useRef(0);
399 useIsomorphicLayoutEffect(function () {
400 if (isOpen) {
401 focusedElBeforeOpen.current = document.activeElement;
402 setPosition();
403 focusContentOnOpen(); // for accessibility
404
405 lockScrolll();
406 } else {
407 resetScroll();
408 }
409
410 return function () {
411 clearTimeout(timeOut.current);
412 };
413 }, [isOpen]); // for uncontrolled popup we need to sync isOpen with open prop
414
415 useEffect(function () {
416 if (typeof open === 'boolean') {
417 if (open) openPopup();else closePopup();
418 }
419 }, [open, disabled]);
420
421 var openPopup = function openPopup(event) {
422 if (isOpen || disabled) return;
423 setIsOpen(true);
424 setTimeout(function () {
425 return onOpen(event);
426 }, 0);
427 };
428
429 var closePopup = function closePopup(event) {
430 var _focusedElBeforeOpen$;
431
432 if (!isOpen || disabled) return;
433 setIsOpen(false);
434 if (isModal) (_focusedElBeforeOpen$ = focusedElBeforeOpen.current) === null || _focusedElBeforeOpen$ === void 0 ? void 0 : _focusedElBeforeOpen$.focus();
435 setTimeout(function () {
436 return onClose(event);
437 }, 0);
438 };
439
440 var togglePopup = function togglePopup(event) {
441 event === null || event === void 0 ? void 0 : event.stopPropagation();
442 if (!isOpen) openPopup(event);else closePopup(event);
443 };
444
445 var onMouseEnter = function onMouseEnter(event) {
446 clearTimeout(timeOut.current);
447 timeOut.current = setTimeout(function () {
448 return openPopup(event);
449 }, mouseEnterDelay);
450 };
451
452 var onContextMenu = function onContextMenu(event) {
453 event === null || event === void 0 ? void 0 : event.preventDefault();
454 togglePopup();
455 };
456
457 var onMouseLeave = function onMouseLeave(event) {
458 clearTimeout(timeOut.current);
459 timeOut.current = setTimeout(function () {
460 return closePopup(event);
461 }, mouseLeaveDelay);
462 };
463
464 var lockScrolll = function lockScrolll() {
465 if (isModal && lockScroll) document.getElementsByTagName('body')[0].style.overflow = 'hidden'; // migrate to document.body
466 };
467
468 var resetScroll = function resetScroll() {
469 if (isModal && lockScroll) document.getElementsByTagName('body')[0].style.overflow = 'auto';
470 };
471
472 var focusContentOnOpen = function focusContentOnOpen() {
473 var _contentRef$current;
474
475 var focusableEls = contentRef === null || contentRef === void 0 ? void 0 : (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 ? void 0 : _contentRef$current.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
476 var firstEl = Array.prototype.slice.call(focusableEls)[0];
477 firstEl === null || firstEl === void 0 ? void 0 : firstEl.focus();
478 };
479
480 useImperativeHandle(ref, function () {
481 return {
482 open: function open() {
483 openPopup();
484 },
485 close: function close() {
486 closePopup();
487 },
488 toggle: function toggle() {
489 togglePopup();
490 }
491 };
492 }); // set Position
493
494 var setPosition = function setPosition() {
495 if (isModal || !isOpen) return;
496 if (!(triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current) || !(triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current) || !(contentRef === null || contentRef === void 0 ? void 0 : contentRef.current)) return; /// show error as one of ref is undefined
497
498 var trigger = triggerRef.current.getBoundingClientRect();
499 var content = contentRef.current.getBoundingClientRect();
500 var cords = calculatePosition(trigger, content, position, arrow, {
501 offsetX: offsetX,
502 offsetY: offsetY
503 }, keepTooltipInside);
504 contentRef.current.style.top = cords.top + window.scrollY + "px";
505 contentRef.current.style.left = cords.left + window.scrollX + "px";
506
507 if (arrow && !!arrowRef.current) {
508 var _arrowStyle$top, _arrowStyle$left;
509
510 arrowRef.current.style.transform = cords.transform;
511 arrowRef.current.style.setProperty('-ms-transform', cords.transform);
512 arrowRef.current.style.setProperty('-webkit-transform', cords.transform);
513 arrowRef.current.style.top = ((_arrowStyle$top = arrowStyle.top) === null || _arrowStyle$top === void 0 ? void 0 : _arrowStyle$top.toString()) || cords.arrowTop;
514 arrowRef.current.style.left = ((_arrowStyle$left = arrowStyle.left) === null || _arrowStyle$left === void 0 ? void 0 : _arrowStyle$left.toString()) || cords.arrowLeft;
515 }
516 }; // hooks
517
518
519 useOnEscape(closePopup, closeOnEscape); // can be optimized if we disabled for hover
520
521 useTabbing(contentRef, isOpen && isModal);
522 useRepositionOnResize(setPosition, repositionOnResize);
523 useOnClickOutside(!!trigger ? [contentRef, triggerRef] : [contentRef], closePopup, closeOnDocumentClick && !nested); // we need to add a ne
524 // render the trigger element and add events
525
526 var renderTrigger = function renderTrigger() {
527 var triggerProps = {
528 key: 'T',
529 ref: triggerRef,
530 'aria-describedby': popupId.current
531 };
532 var onAsArray = Array.isArray(on) ? on : [on];
533
534 for (var i = 0, len = onAsArray.length; i < len; i++) {
535 switch (onAsArray[i]) {
536 case 'click':
537 triggerProps.onClick = togglePopup;
538 break;
539
540 case 'right-click':
541 triggerProps.onContextMenu = onContextMenu;
542 break;
543
544 case 'hover':
545 triggerProps.onMouseEnter = onMouseEnter;
546 triggerProps.onMouseLeave = onMouseLeave;
547 break;
548
549 case 'focus':
550 triggerProps.onFocus = onMouseEnter;
551 triggerProps.onBlur = onMouseLeave;
552 break;
553 }
554 }
555
556 if (typeof trigger === 'function') {
557 var comp = trigger(isOpen);
558 return !!trigger && React.cloneElement(comp, triggerProps);
559 }
560
561 return !!trigger && React.cloneElement(trigger, triggerProps);
562 };
563
564 var addWarperAction = function addWarperAction() {
565 var popupContentStyle = isModal ? Style.popupContent.modal : Style.popupContent.tooltip;
566 var childrenElementProps = {
567 className: "popup-content " + (className !== '' ? className.split(' ').map(function (c) {
568 return c + "-content";
569 }).join(' ') : ''),
570 style: _extends({}, popupContentStyle, contentStyle, {
571 pointerEvents: 'auto'
572 }),
573 ref: contentRef,
574 onClick: function onClick(e) {
575 e.stopPropagation();
576 }
577 };
578
579 if (!modal && on.indexOf('hover') >= 0) {
580 childrenElementProps.onMouseEnter = onMouseEnter;
581 childrenElementProps.onMouseLeave = onMouseLeave;
582 }
583
584 return childrenElementProps;
585 };
586
587 var renderContent = function renderContent() {
588 return React.createElement("div", Object.assign({}, addWarperAction(), {
589 key: "C",
590 role: isModal ? 'dialog' : 'tooltip',
591 id: popupId.current
592 }), arrow && !isModal && React.createElement("div", {
593 ref: arrowRef,
594 style: Style.popupArrow
595 }, React.createElement("svg", {
596 "data-testid": "arrow",
597 className: "popup-arrow " + (className !== '' ? className.split(' ').map(function (c) {
598 return c + "-arrow";
599 }).join(' ') : ''),
600 viewBox: "0 0 32 16",
601 style: _extends({
602 position: 'absolute'
603 }, arrowStyle)
604 }, React.createElement("path", {
605 d: "M16 0l16 16H0z",
606 fill: "currentcolor"
607 }))), children && typeof children === 'function' ? children(closePopup, isOpen) : children);
608 };
609
610 var overlay = !(on.indexOf('hover') >= 0);
611 var ovStyle = isModal ? Style.overlay.modal : Style.overlay.tooltip;
612 var content = [overlay && React.createElement("div", {
613 key: "O",
614 "data-testid": "overlay",
615 "data-popup": isModal ? 'modal' : 'tooltip',
616 className: "popup-overlay " + (className !== '' ? className.split(' ').map(function (c) {
617 return c + "-overlay";
618 }).join(' ') : ''),
619 style: _extends({}, ovStyle, overlayStyle, {
620 pointerEvents: closeOnDocumentClick && nested || isModal ? 'auto' : 'none'
621 }),
622 onClick: closeOnDocumentClick && nested ? closePopup : undefined,
623 tabIndex: -1
624 }, isModal && renderContent()), !isModal && renderContent()];
625 return React.createElement(React.Fragment, null, renderTrigger(), isOpen && ReactDOM.createPortal(content, getRootPopup()));
626});
627
628export default Popup;
629export { Popup };
630//# sourceMappingURL=reactjs-popup.esm.js.map