@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.
490 lines (489 loc) • 19.6 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import * as React from 'react';
import { useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import { calculatePosition, calculateRelativeBasedPosition } from '../common/position';
import { preRender, useProviderContext } from '@syncfusion/react-base';
import { Animation } from '@syncfusion/react-base';
import { flip, fit, isCollide } from '../common/collision';
/**
* Defines the available collision handling types for popup positioning.
*/
export var CollisionType;
(function (CollisionType) {
/**
* No collision handling - the popup will maintain its original position
* regardless of viewport boundaries.
*/
CollisionType["None"] = "None";
/**
* Flip collision handling - the popup will flip to the opposite side of its
* anchor element when it would otherwise extend beyond viewport boundaries.
*/
CollisionType["Flip"] = "Flip";
/**
* Fit collision handling - the popup will be adjusted to fit within the viewport
* boundaries while maintaining its original side relative to the anchor element.
*/
CollisionType["Fit"] = "Fit";
})(CollisionType || (CollisionType = {}));
/**
* Defines how the popup should behave when scroll events occur in the parent container.
*/
export var ActionOnScrollType;
(function (ActionOnScrollType) {
/**
* The popup will recalculate and update its position to maintain proper alignment
* with the target element when scrolling occurs.
*/
ActionOnScrollType["Reposition"] = "Reposition";
/**
* The popup will be hidden when scrolling occurs in the parent container,
* helping to improve performance or prevent UI clutter during scrolling.
*/
ActionOnScrollType["Hide"] = "Hide";
/**
* The popup will not respond to scroll events and will maintain its absolute
* position on the page regardless of scrolling.
*/
ActionOnScrollType["None"] = "None";
})(ActionOnScrollType || (ActionOnScrollType = {}));
const CLASSNAME_OPEN = 'sf-popup-open';
const CLASSNAME_CLOSE = 'sf-popup-close';
/**
* Popup component for displaying content in a floating container positioned relative to a target element.
*
* ```typescript
* <Popup
* isOpen={true}
* relateTo={elementRef}
* position={{ X: 'left', Y: 'bottom' }}
* >
* <div>Popup content</div>
* </Popup>
* ```
*/
export const Popup = forwardRef((props, ref) => {
const { children, isOpen = false, targetRef, relativeElement = null, position = { X: 'left', Y: 'top' }, offsetX = 0, offsetY = 0, collision = { X: CollisionType.None, Y: CollisionType.None }, animation = {
show: {
name: 'FadeIn',
duration: 0,
timingFunction: 'ease-out'
},
hide: {
name: 'FadeOut',
duration: 0,
timingFunction: 'ease-out'
}
}, relateTo = 'body', viewPortElementRef, zIndex = 1000, width = 'auto', height = 'auto', className = '', actionOnScroll = ActionOnScrollType.Reposition, onOpen, onClose, onTargetExitViewport, style, ...rest } = props;
const popupRef = useRef(null);
const initialOpenState = useRef(isOpen);
const [leftPosition, setLeftPosition] = useState(0);
const [topPosition, setTopPosition] = useState(0);
const [popupClass, setPopupClass] = useState(CLASSNAME_CLOSE);
const [fixedParent, setFixedParent] = useState(false);
const [popupZIndex, setPopupZIndex] = useState(1000);
const { dir } = useProviderContext();
const [currentShowAnimation, setCurrentShowAnimation] = useState(animation.show);
const [currentHideAnimation, setCurrentHideAnimation] = useState(animation.hide);
const [currentRelatedElement, setRelativeElement] = useState(relativeElement);
useImperativeHandle(ref, () => ({
getScrollableParent: (element) => {
return getScrollableParent(element);
},
refreshPosition: (target, collision) => {
refreshPosition(target, collision);
},
element: popupRef.current
}), []);
useEffect(() => {
preRender('popup');
}, []);
useEffect(() => {
updatePosition();
}, [targetRef, position, offsetX, offsetY, viewPortElementRef]);
useEffect(() => {
checkCollision();
}, [collision]);
useEffect(() => {
if (!isEqual(currentShowAnimation, animation.show)) {
setCurrentShowAnimation(animation.show);
}
}, [animation.show]);
useEffect(() => {
if (!isEqual(currentHideAnimation, animation.hide)) {
setCurrentHideAnimation(animation.hide);
}
}, [currentHideAnimation, animation.hide]);
useEffect(() => {
if (!isOpen && initialOpenState.current === isOpen) {
return;
}
initialOpenState.current = isOpen;
if (isOpen) {
show(animation.show, currentRelatedElement);
}
else {
hide(animation.hide);
}
}, [isOpen]);
useEffect(() => {
setPopupZIndex(zIndex);
}, [zIndex]);
useEffect(() => {
setRelativeElement(relativeElement);
}, [relativeElement]);
const isEqual = (previousAnimation, currentAnimation) => {
return JSON.stringify(previousAnimation) === JSON.stringify(currentAnimation);
};
const refreshPosition = (target, collision) => {
if (target) {
checkFixedParent(target);
}
updatePosition();
if (!collision) {
checkCollision();
}
};
const updatePosition = () => {
const element = popupRef.current;
const relateToElement = getRelateToElement();
let pos = { left: 0, top: 0 };
if (!element) {
return;
}
if (typeof position.X === 'number' && typeof position.Y === 'number') {
pos = { left: position.X, top: position.Y };
}
else if ((typeof position.X === 'string' && typeof position.Y === 'number') ||
(typeof position.X === 'number' && typeof position.Y === 'string')) {
const anchorPos = getAnchorPosition(relateToElement, element, position, offsetX, offsetY);
pos = typeof position.X === 'string' ? { left: anchorPos.left, top: position.Y } : { left: position.X, top: anchorPos.top };
}
else if (relateToElement) {
const display = element.style.display;
element.style.display = 'block';
pos = getAnchorPosition(relateToElement, element, position, offsetX, offsetY);
element.style.display = display;
}
if ((pos !== null)) {
setLeftPosition(pos.left);
setTopPosition(pos.top);
}
};
const show = (animationOptions, relativeElement) => {
if (popupRef?.current) {
addScrollListeners();
if (relativeElement || zIndex === 1000) {
const zIndexElement = !relativeElement ? popupRef?.current : relativeElement;
setPopupZIndex(getZindexPartial(zIndexElement));
}
if (collision.X !== CollisionType.None || collision.Y !== CollisionType.None) {
setPopupClass(CLASSNAME_OPEN);
checkCollision();
setPopupClass(CLASSNAME_CLOSE);
}
if (animationOptions) {
animationOptions.begin = () => {
setPopupClass(CLASSNAME_OPEN);
};
animationOptions.end = () => {
onOpen?.();
};
if (Animation) {
const animationInstance = Animation(animationOptions);
if (animationInstance.animate) {
animationInstance.animate(popupRef.current);
}
}
}
}
};
const hide = (animationOptions) => {
if (animationOptions) {
animationOptions.begin = () => {
let duration = animationOptions.duration ? animationOptions.duration - 30 : 0;
duration = duration > 0 ? duration : 0;
setTimeout(() => {
setPopupClass(CLASSNAME_CLOSE);
}, duration);
};
animationOptions.end = () => {
onClose?.();
};
if (Animation) {
const animationInstance = Animation(animationOptions);
if (animationInstance.animate) {
animationInstance.animate(popupRef.current);
}
}
}
removeScrollListeners();
};
const callFit = (param) => {
const element = popupRef.current;
const viewPortElement = viewPortElementRef?.current;
if (!element || !viewPortElement) {
return;
}
if (isCollide(element, viewPortElement).length !== 0) {
let data = { left: 0, top: 0 };
if (!viewPortElement) {
data = fit(element, viewPortElement, param);
}
else {
const elementRect = element.getBoundingClientRect();
const viewPortRect = viewPortElement.getBoundingClientRect();
if (!elementRect || !viewPortRect) {
return;
}
if (param.Y) {
if (viewPortRect.top > elementRect.top) {
element.style.top = '0px';
}
else if (viewPortRect.bottom < elementRect.bottom) {
element.style.top = `${parseInt(element.style.top, 10) - (elementRect.bottom - viewPortRect.bottom)}px`;
}
}
if (param.X) {
if (viewPortRect.right < elementRect.right) {
element.style.left = `${parseInt(element.style.left, 10) - (elementRect.right - viewPortRect.right)}px`;
}
else if (viewPortRect.left > elementRect.left) {
element.style.left = `${parseInt(element.style.left, 10) + (viewPortRect.left - elementRect.left)}px`;
}
}
}
if (param.X) {
element.style.left = `${data.left}px`;
}
if (param.Y) {
element.style.top = `${data.top}px`;
}
}
};
const callFlip = (param) => {
const element = popupRef.current;
const relateToElement = getRelateToElement();
const viewPortElement = viewPortElementRef?.current;
if (!element || !relateToElement) {
return;
}
const flippedPos = flip(element, relateToElement, offsetX, offsetY, typeof position.X === 'string' ? position.X : 'left', typeof position.Y === 'string' ? position.Y : 'top', viewPortElement, param);
if (flippedPos) {
setLeftPosition(flippedPos.left);
setTopPosition(flippedPos.top);
}
};
const checkCollision = () => {
const horz = collision.X;
const vert = collision.Y;
if (horz === CollisionType.None && vert === CollisionType.None) {
return;
}
if (horz === CollisionType.Flip && vert === CollisionType.Flip) {
callFlip({ X: true, Y: true });
}
else if (horz === CollisionType.Fit && vert === CollisionType.Fit) {
callFit({ X: true, Y: true });
}
else {
if (horz === CollisionType.Flip) {
callFlip({ X: true, Y: false });
}
else if (vert === CollisionType.Flip) {
callFlip({ Y: true, X: false });
}
if (horz === CollisionType.Fit) {
callFit({ X: true, Y: false });
}
else if (vert === CollisionType.Fit) {
callFit({ X: false, Y: true });
}
}
};
const getAnchorPosition = (anchorEle, element, position, offsetX, offsetY) => {
const anchorRect = anchorEle.getBoundingClientRect();
const eleRect = element.getBoundingClientRect();
const anchorPos = { left: 0, top: 0 };
const targetTypes = anchorEle.tagName.toUpperCase() === 'BODY' ? 'body' : 'container';
switch (position.X) {
default:
case 'left':
break;
case 'center':
anchorPos.left = targetTypes === 'body'
? window.innerWidth / 2 - eleRect.width / 2
: anchorRect.left + (anchorRect.width / 2 - eleRect.width / 2);
break;
case 'right':
if (targetTypes === 'container') {
const scaleX = 1;
anchorPos.left += ((anchorRect.width - eleRect.width) / scaleX);
}
else {
anchorPos.left += (anchorRect.width);
}
break;
}
switch (position.Y) {
case 'top':
break;
case 'center':
anchorPos.top = targetTypes === 'body'
? window.innerHeight / 2 - eleRect.height / 2
: anchorRect.top + (anchorRect.height / 2 - eleRect.height / 2);
break;
case 'bottom':
anchorPos.top = targetTypes === 'body'
? window.innerHeight - eleRect.height
: anchorRect.top + (anchorRect.height - eleRect.height);
break;
}
anchorPos.left += offsetX;
anchorPos.top += offsetY;
return anchorPos;
};
const addScrollListeners = () => {
if (actionOnScroll !== ActionOnScrollType.None && getRelateToElement()) {
const scrollParents = getScrollableParent(getRelateToElement());
scrollParents.forEach((parent) => {
parent.addEventListener('scroll', handleScroll);
});
}
};
const removeScrollListeners = () => {
if (actionOnScroll !== ActionOnScrollType.None && getRelateToElement()) {
const scrollParents = getScrollableParent(getRelateToElement());
scrollParents.forEach((parent) => {
parent.removeEventListener('scroll', handleScroll);
});
}
};
const getRelateToElement = () => {
const relateToElement = relateTo === '' || relateTo === null || relateTo === 'body' ? document.body : relateTo;
return relateToElement;
};
const handleScroll = () => {
if (actionOnScroll === ActionOnScrollType.Reposition) {
refreshPosition();
}
else if (actionOnScroll === ActionOnScrollType.Hide) {
hide();
onClose?.();
}
if (targetRef?.current && !isElementOnViewport(targetRef?.current)) {
onTargetExitViewport?.();
}
};
const getScrollableParent = (element) => {
checkFixedParent(element);
return getFixedScrollableParent(element, fixedParent);
};
const checkFixedParent = (element) => {
let parent = element.parentElement;
while (parent && parent.tagName !== 'HTML') {
const { position } = getComputedStyle(parent);
if (popupRef?.current) {
const popupElement = popupRef.current;
const popupElementStyle = getComputedStyle(popupElement);
if (!popupElement?.offsetParent && position === 'fixed' && popupElementStyle && popupElementStyle.position === 'fixed') {
setFixedParent(true);
}
parent = parent.parentElement;
}
}
};
const isElementOnViewport = (element) => {
const rect = element.getBoundingClientRect();
return (rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth);
};
const popupStyle = {
position: 'absolute',
left: `${leftPosition}px`,
top: `${topPosition}px`,
zIndex: popupZIndex,
width: width,
height: height,
...style
};
const popupClasses = [
'sf-popup sf-control sf-lib',
(dir === 'rtl') ? 'sf-rtl' : '',
popupClass,
className
]
.filter(Boolean)
.join(' ');
return (_jsx("div", { ref: popupRef, className: popupClasses, style: popupStyle, ...rest, children: children }));
});
export default React.memo(Popup);
export { calculatePosition, calculateRelativeBasedPosition, flip, fit, isCollide, getZindexPartial, getFixedScrollableParent };
const getZindexPartial = (element) => {
let parent = element.parentElement;
const parentZindex = [];
while (parent) {
if (parent.tagName !== 'BODY') {
const computedStyle = window.getComputedStyle(parent);
const index = computedStyle.zIndex;
const position = computedStyle.position;
if (index !== 'auto' && position !== 'static') {
parentZindex.push(index);
}
parent = parent.parentElement;
}
else {
break;
}
}
const childrenZindex = [];
for (let i = 0; i < document.body.children.length; i++) {
const child = document.body.children[i];
if (!element.isEqualNode(child) && child instanceof HTMLElement) {
const computedStyle = window.getComputedStyle(child);
const index = computedStyle.zIndex;
const position = computedStyle.position;
if (index !== 'auto' && position !== 'static') {
childrenZindex.push(index);
}
}
}
childrenZindex.push('999');
const siblingsZindex = [];
if (element.parentElement && element.parentElement.tagName !== 'BODY') {
const childNodes = Array.from(element.parentElement.children);
for (let i = 0; i < childNodes.length; i++) {
const child = childNodes[i];
if (!element.isEqualNode(child) && child instanceof HTMLElement) {
const computedStyle = window.getComputedStyle(child);
const index = computedStyle.zIndex;
const position = computedStyle.position;
if (index !== 'auto' && position !== 'static') {
siblingsZindex.push(index);
}
}
}
}
const finalValue = parentZindex.concat(childrenZindex, siblingsZindex);
const currentZindexValue = Math.max(...finalValue.map(Number)) + 1;
return currentZindexValue > 2147483647 ? 2147483647 : currentZindexValue;
};
const getFixedScrollableParent = (element, fixedParent = false) => {
const scrollParents = [];
const overflowRegex = /(auto|scroll)/;
let parent = element.parentElement;
while (parent && parent.tagName !== 'HTML') {
const { position, overflow, overflowY, overflowX } = getComputedStyle(parent);
if (!(getComputedStyle(element).position === 'absolute' && position === 'static')
&& overflowRegex.test(`${overflow} ${overflowY} ${overflowX}`)) {
scrollParents.push(parent);
}
parent = parent.parentElement;
}
if (!fixedParent) {
scrollParents.push(document.documentElement);
}
return scrollParents;
};