@syncfusion/react-popups
Version:
A package of Pure React popup components such as Tooltip that is used to display information or messages in separate pop-ups.
1,159 lines • 52.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import * as React from 'react';
import { useRef, useCallback, useEffect, useLayoutEffect, useState, useImperativeHandle, forwardRef } from 'react';
import { Touch, Browser, Animation as AnimationInstance, animationMode } from '@syncfusion/react-base';
import { isNullOrUndefined, getUniqueID, formatUnit } from '@syncfusion/react-base';
import { attributes, closest, preRender, SvgIcon } from '@syncfusion/react-base';
import { Popup } from '../popup/popup';
import { calculatePosition } from '../common/position';
import { isCollide, fit } from '../common/collision';
import { createPortal } from 'react-dom';
const TOUCHEND_HIDE_DELAY = 1500;
const TAP_HOLD_THRESHOLD = 500;
const SHOW_POINTER_TIP_GAP = 0;
const HIDE_POINTER_TIP_GAP = 8;
const MOUSE_TRAIL_GAP = 2;
const POINTER_ADJUST = 2;
const ROOT = 'sf-control sf-tooltip sf-lib';
const DEVICE = 'sf-bigger';
const CLOSE = 'sf-tooltip-close';
const TOOLTIP_WRAP = 'sf-tooltip-wrap';
const CONTENT = 'sf-tip-content';
const ARROW_TIP = 'sf-arrow-tip';
const ARROW_TIP_OUTER = 'sf-arrow-tip-outer';
const ARROW_TIP_INNER = 'sf-arrow-tip-inner';
const TIP_BOTTOM = 'sf-tip-bottom';
const TIP_TOP = 'sf-tip-top';
const TIP_LEFT = 'sf-tip-left';
const TIP_RIGHT = 'sf-tip-right';
const POPUP_ROOT = 'sf-popup';
const POPUP_LIB = 'sf-lib';
const CLOSE_ICON = 'M10.5858 12.0001L2.58575 4.00003L3.99997 2.58582L12 10.5858L20 2.58582L21.4142 4.00003L13.4142 12.0001L21.4142 20L20 21.4142L12 13.4143L4.00003 21.4142L2.58581 20L10.5858 12.0001Z';
const TIP_BOTTOM_ICON = 'M20.7929 7H3.20712C2.76167 7 2.53858 7.53857 2.85356 7.85355L11.6465 16.6464C11.8417 16.8417 12.1583 16.8417 12.3536 16.6464L21.1465 7.85355C21.4614 7.53857 21.2384 7 20.7929 7Z';
const TIP_TOP_ICON = 'M20.7929 17H3.20712C2.76167 17 2.53858 16.4615 2.85356 16.1465L11.6465 7.3536C11.8417 7.15834 12.1583 7.15834 12.3536 7.3536L21.1465 16.1465C21.4614 16.4615 21.2384 17 20.7929 17Z';
const TIP_RIGHT_ICON = 'M7 20.7928L7 3.20706C7 2.76161 7.53857 2.53852 7.85355 2.8535L16.6464 11.6464C16.8417 11.8417 16.8417 12.1582 16.6464 12.3535L7.85355 21.1464C7.53857 21.4614 7 21.2383 7 20.7928Z';
const TIP_LEFT_ICON = 'M17 3.20718L17 20.793C17 21.2384 16.4614 21.4615 16.1464 21.1465L7.35354 12.3536C7.15828 12.1584 7.15828 11.8418 7.35354 11.6465L16.1464 2.85363C16.4614 2.53864 17 2.76173 17 3.20718Z';
/**
* The Tooltip component displays additional information when users hover, click, or focus on an element.
* It supports various positions, animations, and customization options.
*
* ```typescript
* <Tooltip content={<>This is a Tooltip</>}>
* Hover me
* </Tooltip>
* ```
*/
export const Tooltip = forwardRef((props, ref) => {
const { width = 'auto', height = 'auto', position = 'TopCenter', offsetX = 0, offsetY = 0, arrow = true, windowCollision = false, arrowPosition = 'Auto', opensOn = 'Auto', followCursor = false, sticky = false, animation = {
open: { effect: 'FadeIn', duration: 150, delay: 0 },
close: { effect: 'FadeOut', duration: 150, delay: 0 }
}, openDelay = 0, closeDelay = 0, children, target, content, container, className, open, onClose, onOpen, onFilterTarget, ...restProps } = props;
const [isHidden, setIsHidden] = useState(true);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [PopupAnimation, setPopupAnimation] = useState(undefined);
const [openTarget, setOpenTarget] = useState(null);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const [tipClass, setTipClassState] = useState(TIP_BOTTOM);
const [elePos, setElePos] = useState({ top: 0, left: 0 });
const [eventProps, setEventProps] = useState({});
const [arrowInnerTipStyle, setArrowInnerTipStyle] = useState({ top: '', left: '' });
const [TooltipStyle, setTooltipStyle] = useState();
const tooltipEle = useRef(null);
const timers = useRef({ show: null, hide: null, autoClose: null });
const tooltipPosition = useRef({ x: 'Center', y: 'Top' });
const touchModule = useRef(null);
const mouseEventsRef = useRef({ event: null, target: null });
const isBodyContainer = useRef(true);
const targetRef = useRef(null);
const rootElemRef = useRef(null);
const arrowElementRef = useRef(null);
const stickyElementRef = useRef(null);
const originalData = useRef({ event: null, showAnimation: null, hideAnimation: null, hideEvent: null,
hideTarget: null, showTarget: null });
const containerElement = useRef(typeof document !== 'undefined' ? document.body : null);
const initialOpenState = useRef(open);
const scrolled = useRef(false);
if (Browser.isDevice) {
touchModule.current = Touch(rootElemRef);
}
const propsRef = {
width: 'auto',
height: 'auto',
position: 'TopCenter',
offsetX: 0,
offsetY: 0,
arrow: true,
windowCollision: false,
arrowPosition: 'Auto',
opensOn: 'Auto',
followCursor: false,
sticky: false,
animation: {
open: { effect: 'FadeIn', duration: 150, delay: 0 },
close: { effect: 'FadeOut', duration: 150, delay: 0 }
},
openDelay: 0,
closeDelay: 0,
target,
content,
container,
open
};
useEffect(() => {
formatPosition();
setTipClass(position);
if (tooltipEle.current && tooltipEle.current.element && targetRef.current) {
reposition(targetRef.current);
}
}, [position]);
useEffect(() => {
if (tooltipEle.current && tooltipEle.current.element && targetRef.current) {
reposition(targetRef.current);
}
}, [arrowPosition]);
useEffect(() => {
if (tooltipEle.current && tooltipEle.current.element && targetRef.current) {
tooltipBeforeRender(targetRef.current);
tooltipAfterRender(targetRef.current, originalData.current?.event, originalData.current?.showAnimation);
}
}, [isHidden, targetRef.current]);
useEffect(() => {
appendContainer();
wireEvents(opensOn);
return () => {
unWireEvents(opensOn);
};
}, [opensOn, target]);
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
document.addEventListener('wheel', scrollHandler);
document.addEventListener('scroll', scrollHandler);
document.addEventListener('touchend', touchEnd);
document.addEventListener('keydown', keyDown);
window.addEventListener('resize', windowResize);
return () => {
if (typeof document === 'undefined') {
return;
}
document.removeEventListener('wheel', scrollHandler);
document.removeEventListener('scroll', scrollHandler);
document.removeEventListener('touchend', touchEnd);
document.removeEventListener('keydown', keyDown);
window.removeEventListener('resize', windowResize);
};
}, [propsRef]);
useEffect(() => {
if (!open && initialOpenState.current === open) {
return;
}
initialOpenState.current = open;
if (open) {
if (!originalData.current?.showTarget) {
showTooltip(target?.current ? target?.current :
rootElemRef.current, animation.open);
}
else {
beforeRenderCallback(originalData.current?.showTarget, originalData.current.event, originalData.current.showAnimation);
originalData.current = { ...originalData.current, showTarget: null };
}
}
else {
if (!originalData.current?.hideAnimation) {
hideTooltip(animation.close);
}
else {
mouseMoveBeforeRemove();
popupHide(originalData.current?.hideAnimation, originalData.current?.hideTarget, originalData.current?.hideEvent);
originalData.current = { ...originalData.current, hideEvent: null, hideAnimation: null, hideTarget: null };
}
}
}, [open]);
useLayoutEffect(() => {
preRender('tooltip');
initialize();
return () => {
clearTimeout(timers.current.show);
clearTimeout(timers.current.hide);
clearTimeout(timers.current.autoClose);
tooltipEle.current = null;
touchModule.current = null;
originalData.current = { event: null, showAnimation: null, hideAnimation: null, hideEvent: null,
hideTarget: null, showTarget: null };
mouseEventsRef.current = { event: null, target: null };
};
}, []);
useImperativeHandle(ref, () => ({
...propsRef,
animation: animation,
openTooltip: (element, animationSettings) => {
if (isNullOrUndefined(animationSettings)) {
animationSettings = animation?.open;
}
if (isNullOrUndefined(element)) {
element = rootElemRef.current;
}
if (element) {
if (element.style.display === 'none') {
return;
}
showTooltip(element, animationSettings);
}
},
closeTooltip: (animationSettings) => {
if (!animationSettings) {
animationSettings = animation?.close;
}
hideTooltip(animationSettings);
},
refresh: () => {
if (tooltipEle.current && tooltipEle.current.element) {
reposition(targetRef.current ? targetRef.current : rootElemRef.current);
}
if (!isNullOrUndefined(props.target) && !isNullOrUndefined(props.target?.current)) {
unWireEvents(opensOn);
wireEvents(opensOn);
}
},
element: rootElemRef.current
}));
const initialize = useCallback(() => {
formatPosition();
}, [propsRef]);
const formatPosition = () => {
if (!position) {
return;
}
let posX = null;
let posY = null;
if (position.indexOf('Top') === 0 || position.indexOf('Bottom') === 0) {
[posY, posX] = position.split(/(?=[A-Z])/);
}
else {
[posX, posY] = position.split(/(?=[A-Z])/);
}
tooltipPosition.current.x = posX;
tooltipPosition.current.y = posY;
};
const setTipClass = useCallback((position) => {
let newTipClass;
if (isNullOrUndefined(position)) {
return;
}
if (position.indexOf('Right') === 0) {
newTipClass = TIP_LEFT;
}
else if (position.indexOf('Bottom') === 0) {
newTipClass = TIP_TOP;
}
else if (position.indexOf('Left') === 0) {
newTipClass = TIP_RIGHT;
}
else {
newTipClass = TIP_BOTTOM;
}
setTipClassState(newTipClass);
return newTipClass;
}, []);
const renderPopup = (target) => {
if (followCursor && originalData.current?.event) {
onMouseMove(originalData.current?.event);
return;
}
const elePos = getTooltipPosition(target);
setElePos(elePos);
};
const getScalingFactor = (target) => {
if (!target) {
return { x: 1, y: 1 };
}
const scalingFactors = { x: 1, y: 1 };
const elementsWithTransform = target.closest('[style*="transform: scale"]');
if (elementsWithTransform && elementsWithTransform !== tooltipEle.current?.element &&
elementsWithTransform.contains(tooltipEle.current?.element)) {
const computedStyle = window.getComputedStyle(elementsWithTransform);
const transformValue = computedStyle.getPropertyValue('transform');
if (transformValue !== 'none') {
const scaleMatch = transformValue.match(/scale\(([\d., ]+)\)/);
if (scaleMatch) {
const scaleValues = scaleMatch[1].split(',').map(parseFloat);
scalingFactors.x = scaleValues[0];
scalingFactors.y = scaleValues.length > 1 ? scaleValues[1] : scaleValues[0];
}
else {
const matrixMatch = transformValue.match(/matrix\(([^)]+)\)/);
if (matrixMatch) {
const matrixValues = matrixMatch[1].split(',').map(parseFloat);
scalingFactors.x = matrixValues[0];
scalingFactors.y = matrixValues[3];
}
}
}
}
return scalingFactors;
};
const getTooltipPosition = (target) => {
if (tooltipEle.current && tooltipEle.current.element) {
tooltipEle.current.element.style.visibility = 'hidden';
tooltipEle.current.element.style.display = 'block';
}
const parentWithZoomStyle = rootElemRef.current?.closest('[style*="zoom"]');
if (parentWithZoomStyle) {
if (tooltipEle.current && tooltipEle.current.element &&
!parentWithZoomStyle.contains(tooltipEle.current?.element)) {
tooltipEle.current.element.style.zoom = getComputedStyle(parentWithZoomStyle).zoom;
}
}
const pos = calculatePosition(target, tooltipPosition.current.x?.toLowerCase(), tooltipPosition.current.y?.toLowerCase(), isBodyContainer.current ? undefined :
containerElement.current?.getBoundingClientRect());
const scalingFactors = getScalingFactor(target);
const offsetPos = calculateTooltipOffset(position, scalingFactors.x, scalingFactors.y);
const collisionPosition = calculateElementPosition(pos, offsetPos);
const collisionLeft = collisionPosition[0];
const collisionTop = collisionPosition[1];
const elePos = collisionFlipFit(target, collisionLeft, collisionTop);
elePos.left = elePos.left / scalingFactors.x;
elePos.top = elePos.top / scalingFactors.y;
if (!isBodyContainer.current && containerElement.current) {
elePos.left -= (window.scrollX * 2);
elePos.top -= (window.scrollY * 2);
}
if (tooltipEle.current && tooltipEle.current.element) {
tooltipEle.current.element.style.display = '';
if (followCursor) {
tooltipEle.current.element.style.visibility = 'visible';
}
}
return elePos;
};
const windowResize = () => {
reposition(targetRef.current);
};
const reposition = (target) => {
if (tooltipEle.current && tooltipEle.current.element && target) {
const elePos = getTooltipPosition(target);
setElePos(elePos);
tooltipEle.current.element.style.visibility = 'visible';
}
};
const openPopupHandler = () => {
if (!followCursor) {
reposition(targetRef.current);
}
};
const closePopupHandler = () => {
clear();
const currentTooltipEle = React.createRef();
currentTooltipEle.current = tooltipEle.current?.element;
AnimationInstance.stop(currentTooltipEle.current);
scrolled.current = false;
setIsHidden(true);
};
const calculateTooltipOffset = (position, xScalingFactor, yScalingFactor) => {
const pos = {
top: 0,
left: 0
};
let tipWidth;
let tipHeight;
let tooltipEleWidth;
let tooltipEleHeight;
let tipAdjust;
let tipHeightAdjust;
let tipWidthAdjust;
if (xScalingFactor !== 1 || yScalingFactor !== 1) {
const tooltipEleRect = tooltipEle.current?.element?.getBoundingClientRect();
let arrowEleRect;
tooltipEleWidth = Math.round(tooltipEleRect.width);
tooltipEleHeight = Math.round(tooltipEleRect.height);
if (arrowElementRef.current) {
arrowEleRect = arrowElementRef.current.getBoundingClientRect();
}
tipWidth = arrowEleRect ? Math.round(arrowEleRect.width) : 0;
tipHeight = arrowEleRect ? Math.round(arrowEleRect.height) : 0;
tipAdjust = (arrow ? SHOW_POINTER_TIP_GAP : HIDE_POINTER_TIP_GAP);
tipHeightAdjust = (tipHeight / 2) + POINTER_ADJUST +
(tooltipEleHeight - ((tooltipEle.current?.element).clientHeight * yScalingFactor));
tipWidthAdjust = (tipWidth / 2) + POINTER_ADJUST +
(tooltipEleWidth - ((tooltipEle.current?.element).clientWidth * xScalingFactor));
}
else {
tooltipEleWidth = (tooltipEle.current?.element).offsetWidth;
tooltipEleHeight = (tooltipEle.current?.element).offsetHeight;
tipWidth = arrowElementRef.current ? arrowElementRef.current.offsetWidth : 0;
tipHeight = arrowElementRef.current ? arrowElementRef.current.offsetHeight : 0;
tipAdjust = (arrow ? SHOW_POINTER_TIP_GAP : HIDE_POINTER_TIP_GAP);
tipHeightAdjust = (tipHeight / 2) + POINTER_ADJUST +
((tooltipEle.current?.element).offsetHeight - (tooltipEle.current?.element).clientHeight);
tipWidthAdjust = (tipWidth / 2) + POINTER_ADJUST +
((tooltipEle.current?.element).offsetWidth - (tooltipEle.current?.element).clientWidth);
}
if (followCursor) {
tipAdjust += MOUSE_TRAIL_GAP;
}
switch (position) {
case 'RightTop':
pos.left += tipWidth + tipAdjust;
pos.top -= tooltipEleHeight - tipHeightAdjust;
break;
case 'RightCenter':
pos.left += tipWidth + tipAdjust;
pos.top -= (tooltipEleHeight / 2);
break;
case 'RightBottom':
pos.left += tipWidth + tipAdjust;
pos.top -= (tipHeightAdjust);
break;
case 'BottomRight':
pos.top += (tipHeight + tipAdjust);
pos.left -= (tipWidthAdjust);
break;
case 'BottomCenter':
pos.top += (tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth / 2);
break;
case 'BottomLeft':
pos.top += (tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth - tipWidthAdjust);
break;
case 'LeftBottom':
pos.left -= (tipWidth + tooltipEleWidth + tipAdjust);
pos.top -= (tipHeightAdjust);
break;
case 'LeftCenter':
pos.left -= (tipWidth + tooltipEleWidth + tipAdjust);
pos.top -= (tooltipEleHeight / 2);
break;
case 'LeftTop':
pos.left -= (tipWidth + tooltipEleWidth + tipAdjust);
pos.top -= (tooltipEleHeight - tipHeightAdjust);
break;
case 'TopLeft':
pos.top -= (tooltipEleHeight + tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth - tipWidthAdjust);
break;
case 'TopRight':
pos.top -= (tooltipEleHeight + tipHeight + tipAdjust);
pos.left -= (tipWidthAdjust);
break;
default:
pos.top -= (tooltipEleHeight + tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth / 2);
break;
}
pos.left += offsetX;
pos.top += offsetY;
return pos;
};
const adjustArrow = (target, position, tooltipPositionX, tooltipPositionY) => {
if (arrow === false || arrowElementRef.current === null) {
return;
}
const currentTipClass = setTipClass(position);
let leftValue;
let topValue;
const tooltipWidth = (tooltipEle.current?.element).clientWidth;
const tooltipHeight = (tooltipEle.current?.element).clientHeight;
const tipWidth = arrowElementRef.current.offsetWidth;
const tipHeight = arrowElementRef.current.offsetHeight;
if (currentTipClass === TIP_BOTTOM || currentTipClass === TIP_TOP) {
if (currentTipClass === TIP_BOTTOM) {
topValue = '99.9%';
setArrowInnerTipStyle((prevStyle) => ({ ...prevStyle, top: `-${tipHeight - 2}px` }));
}
else {
topValue = -(tipHeight - 1) + 'px';
setArrowInnerTipStyle((prevStyle) => ({ ...prevStyle, top: `-${tipHeight - 6}px` }));
}
if (target) {
const tipPosExclude = tooltipPositionX !== 'Center' || (tooltipWidth > target.offsetWidth) || followCursor;
if ((tipPosExclude && tooltipPositionX === 'Left') || (!tipPosExclude && arrowPosition === 'End')) {
leftValue = (tooltipWidth - tipWidth - POINTER_ADJUST) + 'px';
}
else if ((tipPosExclude && tooltipPositionX === 'Right') || (!tipPosExclude && arrowPosition === 'Start')) {
leftValue = POINTER_ADJUST + 'px';
}
else if ((tipPosExclude) && (arrowPosition === 'End' || arrowPosition === 'Start')) {
leftValue = (arrowPosition === 'End') ? ((target.offsetWidth + (((tooltipEle.current?.element).offsetWidth - target.offsetWidth) / 2)) - (tipWidth / 2)) - POINTER_ADJUST + 'px'
: (((tooltipEle.current?.element).offsetWidth - target.offsetWidth) / 2) - (tipWidth / 2) + POINTER_ADJUST + 'px';
}
else {
leftValue = ((tooltipWidth / 2) - (tipWidth / 2)) + 'px';
}
}
}
else {
if (currentTipClass === TIP_RIGHT) {
leftValue = '99.9%';
setArrowInnerTipStyle((prevStyle) => ({ ...prevStyle, left: `-${tipWidth - 2}px` }));
}
else {
leftValue = -(tipWidth - 1) + 'px';
setArrowInnerTipStyle((prevStyle) => ({ ...prevStyle, left: `${-(tipWidth) + (tipWidth - 2)}px` }));
}
const tipPosExclude = tooltipPositionY !== 'Center' || (tooltipHeight > target.offsetHeight) || followCursor;
if ((tipPosExclude && tooltipPositionY === 'Top') || (!tipPosExclude && arrowPosition === 'End')) {
topValue = (tooltipHeight - tipHeight - POINTER_ADJUST) + 'px';
}
else if ((tipPosExclude && tooltipPositionY === 'Bottom') || (!tipPosExclude && arrowPosition === 'Start')) {
topValue = POINTER_ADJUST + 'px';
}
else {
topValue = ((tooltipHeight / 2) - (tipHeight / 2)) + 'px';
}
}
arrowElementRef.current.style.top = topValue;
arrowElementRef.current.style.left = leftValue;
};
const renderContent = () => {
if (targetRef.current && !isNullOrUndefined(targetRef.current.getAttribute('title'))) {
targetRef.current.setAttribute('data-content', targetRef.current.getAttribute('title'));
targetRef.current.removeAttribute('title');
}
if (React.isValidElement(content)) {
return content;
}
let tooltipContent = '';
if (typeof content === 'function') {
tooltipContent = content();
}
else if (targetRef?.current?.getAttribute('data-content')) {
tooltipContent = targetRef.current.getAttribute('data-content');
}
return tooltipContent;
};
const addDescribedBy = (target, id) => {
const describedby = (target.getAttribute('aria-describedby') || '').split(/\s+/);
if (describedby.indexOf(id) < 0) {
describedby.push(id);
}
attributes(target, { 'aria-describedby': describedby.join(' ').trim(), 'data-tooltip-id': id });
};
const removeDescribedBy = (target) => {
const id = target.getAttribute('data-tooltip-id');
const describedby = (target.getAttribute('aria-describedby') || '').split(/\s+/);
const index = describedby.indexOf(id);
if (index !== -1) {
describedby.splice(index, 1);
}
target.removeAttribute('data-tooltip-id');
const orgDescribedby = describedby.join(' ').trim();
if (orgDescribedby) {
target.setAttribute('aria-describedby', orgDescribedby);
}
else {
target.removeAttribute('aria-describedby');
}
};
const tapHoldHandler = useCallback((evt) => {
if (evt) {
clearTimeout(timers.current.autoClose);
targetHover(evt.originalEvent);
}
}, []);
const touchEndHandler = () => {
if (sticky) {
return;
}
const close = () => {
closeTooltip();
};
timers.current.autoClose = setTimeout(close, TOUCHEND_HIDE_DELAY);
};
const targetClick = (e) => {
let target;
if (props.target?.current) {
target = props.target.current.contains(e.target) ? props.target.current : undefined;
}
else {
target = onFilterTarget ? e.target : rootElemRef.current;
}
if (isNullOrUndefined(target)) {
return;
}
const mouseEvent = e;
if (!tooltipEle.current || (tooltipEle.current && target !== targetRef.current)) {
if (!(mouseEvent.type === 'mousedown' && mouseEvent.button === 2)) {
targetHover(e);
}
}
else if (!sticky) {
hideTooltip(animation.close, e, target);
}
};
const targetHover = (e) => {
let target;
if (onFilterTarget) {
const isTarget = onFilterTarget?.(e.target);
if (!isTarget) {
return;
}
target = e.target;
}
else {
if (props.target?.current) {
target = props.target.current.contains(e.target) ? props.target.current : undefined;
}
else {
target = rootElemRef.current;
}
}
if (isNullOrUndefined(target) || (tooltipEle.current && target === targetRef.current && !followCursor)) {
return;
}
if (tooltipEle.current && tooltipEle.current.element?.getAttribute('sf-animation-id')) {
const delay = closeDelay + (animation?.close?.delay ? animation.close.delay : 0) +
(animation?.close?.duration ? animation.close.duration : 0);
setTimeout(() => {
restoreElement(target);
showTooltip(target, animation.open, e);
}, delay);
return;
}
restoreElement(target);
showTooltip(target, animation.open, e);
};
const mouseMoveBeforeOpen = (e) => {
mouseEventsRef.current.event = e;
};
const mouseMoveBeforeRemove = () => {
if (mouseEventsRef.current.target) {
mouseEventsRef.current.target.removeEventListener('mousemove', mouseMoveBeforeOpen);
mouseEventsRef.current.target.removeEventListener('touchstart', mouseMoveBeforeOpen);
}
};
const showTooltip = (target, showAnimation, e) => {
clearTimeout(timers.current.show);
clearTimeout(timers.current.hide);
if (openDelay && followCursor) {
mouseMoveBeforeRemove();
mouseEventsRef.current.target = target;
mouseEventsRef.current.target.addEventListener('mousemove', mouseMoveBeforeOpen);
mouseEventsRef.current.target.addEventListener('touchstart', mouseMoveBeforeOpen);
}
originalData.current = { ...originalData.current, event: e, showAnimation: showAnimation, showTarget: target };
onOpen?.(e);
if (!isNullOrUndefined(open)) {
if (open) {
beforeRenderCallback(target, e, showAnimation);
}
}
else {
beforeRenderCallback(target, e, showAnimation);
}
};
const beforeRenderCallback = (target, e, showAnimation) => {
targetRef.current = target;
setIsHidden(false);
if (isNullOrUndefined(tooltipEle.current)) {
const ctrlId = rootElemRef.current?.getAttribute('id') ?
getUniqueID(rootElemRef.current?.getAttribute('id')) : getUniqueID('tooltip');
setTooltipStyle((prevStyle) => ({
...prevStyle,
id: ctrlId + '_content'
}));
}
else {
if (target) {
adjustArrow(target, position, tooltipPosition.current.x, tooltipPosition.current.y);
addDescribedBy(target, TooltipStyle?.id);
const currentTooltipEle = React.createRef();
currentTooltipEle.current = tooltipEle.current?.element;
AnimationInstance.stop(currentTooltipEle.current);
reposition(target);
tooltipAfterRender(target, e, showAnimation);
}
}
};
const appendContainer = () => {
if (container?.current instanceof HTMLElement) {
isBodyContainer.current = false;
containerElement.current = container.current;
}
if (!containerElement.current) {
containerElement.current = typeof document !== 'undefined' ? document.body : null;
}
};
const tooltipBeforeRender = (target) => {
if (target) {
appendContainer();
addDescribedBy(target, TooltipStyle?.id);
if (arrow) {
setTipClass(position);
}
renderPopup(target);
adjustArrow(target, position, tooltipPosition.current.x, tooltipPosition.current.y);
const currentTooltipEle = React.createRef();
currentTooltipEle.current = tooltipEle.current?.element;
AnimationInstance.stop(currentTooltipEle.current);
reposition(target);
}
};
const tooltipAfterRender = (target, e, showAnimation) => {
if (target) {
beforeOpenCallback(target, showAnimation, e);
}
};
const beforeOpenCallback = (target, showAnimation, e) => {
const openAnimation = {
name: (showAnimation.effect === 'None' && animationMode === 'Enable') ? 'FadeIn' : animation.open.effect,
duration: showAnimation.duration,
delay: showAnimation.delay,
timingFunction: 'ease-out'
};
if (openDelay > 0) {
const show = () => {
if (followCursor) {
target.addEventListener('mousemove', onMouseMove);
target.addEventListener('touchstart', onMouseMove);
target.addEventListener('mouseenter', onMouseMove);
}
if (tooltipEle.current?.element) {
setOpenTarget(target);
setPopupAnimation((prev) => ({ show: openAnimation, hide: prev?.hide }));
setIsPopupOpen(true);
if (mouseEventsRef.current.event && followCursor) {
onMouseMove(mouseEventsRef.current.event);
}
}
};
timers.current.show = setTimeout(show, openDelay);
}
else {
if (tooltipEle.current) {
setOpenTarget(target);
setPopupAnimation((prev) => ({ show: openAnimation, hide: prev?.hide }));
setIsPopupOpen(true);
}
}
if (e) {
wireMouseEvents(e, target);
}
};
const checkCollision = (x, y) => {
const elePos = {
left: x,
top: y,
position: position,
horizontal: tooltipPosition.current.x,
vertical: tooltipPosition.current.y
};
const collideTarget = checkCollideTarget();
const affectedPos = isCollide(tooltipEle.current?.element, collideTarget ? collideTarget.current : null, sticky && position.indexOf('Right') >= 0 ? stickyElementRef.current.offsetWidth + x : x, y);
if (affectedPos.length > 0) {
elePos.horizontal = affectedPos.indexOf('left') >= 0 ? 'Right' : affectedPos.indexOf('right') >= 0 ? 'Left' :
tooltipPosition.current.x;
elePos.vertical = affectedPos.indexOf('top') >= 0 ? 'Bottom' : affectedPos.indexOf('bottom') >= 0 ? 'Top' :
tooltipPosition.current.y;
}
return elePos;
};
const calculateElementPosition = (pos, offsetPos) => {
return [isBodyContainer.current ? pos.left + offsetPos.left :
(pos.left - containerElement.current.getBoundingClientRect().left) +
offsetPos.left + window.pageXOffset + containerElement.current.scrollLeft,
isBodyContainer.current ? pos.top + offsetPos.top :
(pos.top - containerElement.current.getBoundingClientRect().top) +
offsetPos.top + window.pageYOffset + containerElement.current.scrollTop];
};
const collisionFlipFit = (target, x, y) => {
const elePos = checkCollision(x, y);
let newPos = elePos.position;
if (tooltipPosition.current.y !== elePos.vertical) {
newPos = ((position?.indexOf('Bottom') === 0 || position?.indexOf('Top') === 0) ?
elePos.vertical + tooltipPosition.current.x : tooltipPosition.current.x + elePos.vertical);
}
if (tooltipPosition.current.x !== elePos.horizontal) {
if (newPos.indexOf('Left') === 0) {
elePos.vertical = (newPos === 'LeftTop' || newPos === 'LeftCenter') ? 'Top' : 'Bottom';
newPos = (elePos.vertical + 'Left');
}
if (newPos.indexOf('Right') === 0) {
elePos.vertical = (newPos === 'RightTop' || newPos === 'RightCenter') ? 'Top' : 'Bottom';
newPos = (elePos.vertical + 'Right');
}
elePos.horizontal = tooltipPosition.current.x;
}
const elePosVertical = elePos.vertical;
const elePosHorizontal = elePos.horizontal;
if (elePos.position !== newPos) {
const pos = calculatePosition(target, elePosHorizontal.toLowerCase(), elePosVertical.toLowerCase(), isBodyContainer.current ? undefined :
containerElement.current?.getBoundingClientRect());
adjustArrow(target, newPos, elePosHorizontal, elePosVertical);
const scalingFactors = getScalingFactor(target);
const offsetPos = calculateTooltipOffset(newPos, scalingFactors.x, scalingFactors.y);
if (!isBodyContainer.current) {
offsetPos.top -= getOffSetPosition('TopBottom', newPos, offsetY);
offsetPos.left -= getOffSetPosition('RightLeft', newPos, offsetX);
}
elePos.position = newPos;
const elePosition = calculateElementPosition(pos, offsetPos);
elePos.left = elePosition[0];
elePos.top = elePosition[1];
}
else {
adjustArrow(target, newPos, elePosHorizontal, elePosVertical);
}
const eleOffset = { left: elePos.left, top: elePos.top };
const collideTarget = checkCollideTarget();
const updatedPosition = isBodyContainer.current ?
fit(tooltipEle.current?.element, collideTarget ? collideTarget.current : null, { X: true, Y: windowCollision }, eleOffset) : eleOffset;
if (arrow && arrowElementRef.current != null && (newPos.indexOf('Bottom') === 0 || newPos.indexOf('Top') === 0)) {
let arrowLeft = parseInt(arrowElementRef.current.style.left, 10) - (updatedPosition.left - elePos.left);
if (arrowLeft < 0) {
arrowLeft = 0;
}
else if ((arrowLeft + arrowElementRef.current.offsetWidth) > (tooltipEle.current?.element).clientWidth) {
arrowLeft = (tooltipEle.current?.element).clientWidth - arrowElementRef.current.offsetWidth;
}
arrowElementRef.current.style.left = arrowLeft.toString() + 'px';
}
eleOffset.left = updatedPosition.left;
eleOffset.top = updatedPosition.top;
return eleOffset;
};
const getOffSetPosition = (positionString, newPos, offsetType) => {
return ((positionString.indexOf(position.split(/(?=[A-Z])/)[0]) !== -1) &&
(positionString.indexOf(newPos.split(/(?=[A-Z])/)[0]) !== -1)) ? (2 * offsetType) : 0;
};
const checkCollideTarget = () => {
return !windowCollision && target?.current ? rootElemRef : null;
};
const hideTooltip = (hideAnimation, e, targetElement) => {
if (closeDelay > 0) {
clearTimeout(timers.current.hide);
clearTimeout(timers.current.show);
const hide = () => {
if (closeDelay && tooltipEle.current && tooltipEle.current.element && isTooltipOpen) {
return;
}
tooltipHide(hideAnimation, e, targetElement);
};
timers.current.hide = setTimeout(hide, closeDelay);
}
else {
tooltipHide(hideAnimation, e, targetElement);
}
};
const tooltipHide = (hideAnimation, e, targetElement) => {
let target;
if (e) {
target = props.target?.current || onFilterTarget ? (targetElement || e.target)
: rootElemRef.current;
}
else {
target = targetRef.current;
}
originalData.current = { ...originalData.current, hideEvent: e, hideAnimation: hideAnimation, hideTarget: target };
if (tooltipEle.current && tooltipEle.current.element?.getAttribute('sf-animate')) {
const currentTooltipEle = React.createRef();
currentTooltipEle.current = tooltipEle.current?.element;
AnimationInstance.stop(currentTooltipEle.current, {
end: () => {
handlePopupHide(hideAnimation, target, e);
}
});
}
else {
handlePopupHide(hideAnimation, target, e);
}
};
const handlePopupHide = (hideAnimation, target, e) => {
onClose?.(e);
if (!isNullOrUndefined(open)) {
if (open && !onClose) {
setIsHidden(false);
}
else if (!open) {
mouseMoveBeforeRemove();
popupHide(hideAnimation, target, e);
}
}
else {
mouseMoveBeforeRemove();
popupHide(hideAnimation, target, e);
}
};
const popupHide = (hideAnimation, target, e) => {
if (target && e) {
restoreElement(target);
}
const closeAnimation = {
name: (hideAnimation.effect === 'None' && animationMode === 'Enable') ? 'FadeOut' : animation.close.effect,
duration: hideAnimation.duration,
delay: hideAnimation.delay,
timingFunction: 'ease-in'
};
if (tooltipEle.current) {
setPopupAnimation((prev) => ({ hide: closeAnimation, show: prev?.show }));
setIsPopupOpen(false);
}
};
const restoreElement = (target) => {
unWireMouseEvents(target);
if (!isNullOrUndefined(target.getAttribute('data-content'))) {
target.setAttribute('title', target.getAttribute('data-content'));
target.removeAttribute('data-content');
}
removeDescribedBy(target);
};
const clear = () => {
const target = targetRef.current;
if (target) {
restoreElement(target);
}
if (isHidden) {
setArrowInnerTipStyle({ top: '', left: '' });
tooltipEle.current = null;
}
};
const tooltipHover = () => {
if (tooltipEle.current && tooltipEle.current.element) {
setIsTooltipOpen(true);
}
};
const tooltipMouseOut = (e) => {
setIsTooltipOpen(false);
hideTooltip(animation.close, e, targetRef.current);
};
const onMouseOut = (e) => {
const enteredElement = e.relatedTarget;
if (enteredElement) {
const checkForTooltipElement = closest(enteredElement, `.${TOOLTIP_WRAP}.${POPUP_LIB}.${POPUP_ROOT}`);
if (checkForTooltipElement) {
if (followCursor) {
onMouseMove(e);
}
checkForTooltipElement.addEventListener('mouseleave', tooltipElementMouseOut);
}
else {
hideTooltip(animation.close, e, targetRef.current);
if (closeDelay === 0 && (animation.close.effect === 'None')) {
clear();
}
}
}
else {
hideTooltip(animation.close, e, targetRef.current);
clear();
}
};
const tooltipElementMouseOut = (e) => {
tooltipEle.current?.element?.removeEventListener('mouseleave', tooltipElementMouseOut);
if (!e.relatedTarget || closest(e.relatedTarget, '.sf-tooltip') !== rootElemRef.current) {
hideTooltip(animation.close, e, targetRef.current);
clear();
}
};
const onMouseMove = (event) => {
let eventPageX = 0;
let eventPageY = 0;
if (event.type.indexOf('touch') > -1) {
event.preventDefault();
eventPageX = event.touches[0].pageX;
eventPageY = event.touches[0].pageY;
}
else {
eventPageX = event.pageX;
eventPageY = event.pageY;
}
if (isNullOrUndefined(tooltipEle.current)) {
return;
}
if (tooltipEle.current) {
tooltipEle.current.element.style.display = 'block';
}
const currentTooltipEle = React.createRef();
currentTooltipEle.current = tooltipEle.current?.element;
AnimationInstance.stop(currentTooltipEle.current);
adjustArrow(event.target, position, tooltipPosition.current.x, tooltipPosition.current.y);
const scalingFactors = getScalingFactor(event.target);
const pos = calculateTooltipOffset(position, scalingFactors.x, scalingFactors.y);
const x = eventPageX + pos.left + offsetX;
const y = eventPageY + pos.top + offsetY;
const elePos = checkCollision(x, y);
if (tooltipPosition.current.x !== elePos.horizontal || tooltipPosition.current.y !== elePos.vertical) {
const newPos = (position?.indexOf('Bottom') === 0 || position?.indexOf('Top') === 0) ?
elePos.vertical + elePos.horizontal : elePos.horizontal + elePos.vertical;
elePos.position = newPos;
adjustArrow(event.target, elePos.position, elePos.horizontal, elePos.vertical);
const colPos = calculateTooltipOffset(elePos.position, scalingFactors.x, scalingFactors.y);
elePos.left = eventPageX + colPos.left - offsetX;
elePos.top = eventPageY + colPos.top - offsetY;
}
const popupStyle = {
position: 'absolute',
left: elePos.left + 'px',
top: elePos.top + 'px',
zIndex: 1000,
width: formatUnit(width),
height: formatUnit(height),
display: 'block'
};
setTooltipStyle((prevStyle) => ({
...prevStyle,
style: {
...(prevStyle?.style || {}),
...popupStyle
}
}));
};
const keyDown = (event) => {
if (tooltipEle.current && tooltipEle.current.element && event.keyCode === 27) {
closeTooltip();
}
};
const touchEnd = (e) => {
if (tooltipEle.current && tooltipEle.current.element && closest(e.target, '.sf-tooltip') === null && !sticky && !opensOn.includes('Focus')) {
closeTooltip();
}
};
const scrollHandler = (e) => {
if (tooltipEle.current && tooltipEle.current.element && !sticky && !followCursor && !scrolled.current) {
if (!(closest(e.target, `.${TOOLTIP_WRAP}.${POPUP_LIB}.${POPUP_ROOT}`))) {
setIsPopupOpen((prev) => {
if (!prev) {
setIsHidden(true);
}
return prev;
});
scrolled.current = true;
closeTooltip();
}
}
};
const wireEvents = (trigger) => {
const triggerList = getTriggerList(trigger);
const newEventProps = {};
for (const opensOn of triggerList) {
if (opensOn === 'Custom') {
return;
}
if (opensOn === 'Focus') {
wireFocusEvents();
}
if (opensOn === 'Click') {
newEventProps.onMouseDown = targetClick;
}
if (opensOn === 'Hover') {
if (Browser.isDevice) {
if (touchModule.current) {
touchModule.current.tapHoldThreshold = TAP_HOLD_THRESHOLD;
touchModule.current.tapHold = tapHoldHandler;
}
newEventProps.onTouchEnd = touchEndHandler;
}
else {
newEventProps.onMouseOver = targetHover;
}
}
}
setEventProps((prevProps) => ({
...prevProps,
...newEventProps
}));
};
const getTriggerList = (trigger) => {
if (!trigger) {
return [];
}
if (trigger === 'Auto') {
trigger = (Browser.isDevice) ? 'Hover' : 'Hover Focus';
}
return trigger.split(' ');
};
const wireFocusEvents = () => {
const focusEventProps = {};
if (!isNullOrUndefined(props.target) && !isNullOrUndefined(props.target?.current)) {
props.target?.current.addEventListener('focus', targetHover);
}
else {
focusEventProps.onFocus = targetHover;
}
setEventProps((prevProps) => ({
...prevProps,
...focusEventProps
}));
};
const wireMouseEvents = (e, target) => {
if (tooltipEle.current && tooltipEle.current.element) {
if (!sticky) {
if (e.type === 'focus') {
if (!isNullOrUndefined(props.target) && !isNullOrUndefined(props.target?.current) || onFilterTarget) {
target.addEventListener('blur', onMouseOut);
}
else {
setEventProps((prevProps) => ({
...prevProps,
onBlur: onMouseOut
}));
}
}
if (e.type === 'mouseover') {
target.addEventListener('mouseleave', onMouseOut);
}
if (closeDelay) {
tooltipEle.current.element.addEventListener('mouseenter', tooltipHover);
tooltipEle.current.element.addEventListener('mouseleave', tooltipMouseOut);
}
}
if (followCursor && openDelay === 0) {
target.addEventListener('mousemove', onMouseMove);
target.addEventListener('mouseenter', onMouseMove);
}
}
};
const unWireEvents = (trigger) => {
const triggerList = getTriggerList(trigger);
const newEventProps = {};
for (const opensOn of triggerList) {
if (opensOn === 'Custom') {
return;
}
if (opensOn === 'Focus') {
unWireFocusEvents();
}
if (opensOn === 'Click') {
newEventProps.onMouseDown = undefined;
}
if (opensOn === 'Hover') {
if (touchModule.current && touchModule.current.destroy) {
touchModule.current.destroy();
}
newEventProps.onTouchEnd = undefined;
newEventProps.onMouseOver = undefined;
}
}
setEventProps((prevProps) => {
const updatedProps = { ...prevProps };
Object.keys(newEventProps).forEach((key) => {
delete updatedProps[key];
});
return updatedProps;
});
};
const unWireFocusEvents = () => {
const focusEventProps = {};
if (!isNullOrUndefined(props.target) && !isNullOrUndefined(props.target?.current) && rootElemRef.current) {
props.target?.current.removeEventListener('focus', targetHover);
}
else {
focusEventProps.onFocus = undefined;
}
setEventProps((prevProps) => {
const updatedProps = { ...prevProps };
Object.keys(focusEventProps).forEach((key) => {
delete updatedProps[key];
});
return updatedProps;
});
};
const unWireMouseEvents = (target) => {
if (!sticky) {
const triggerList = getTriggerList(opensOn);
for (const opensOn of triggerList) {
if (opensOn === 'Focus') {
target.removeEventListener('blur', onMouseOut);
setEventProps((prevProps) => {
const update