@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
546 lines (545 loc) • 24.3 kB
JavaScript
"use client";
import _extends from "@babel/runtime-corejs3/helpers/esm/extends";
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classnames from 'classnames';
import { getOffsetLeft, getOffsetTop } from "../../shared/helpers.js";
import { getClosestScrollViewElement } from "../../shared/component-helper.js";
import { useIsomorphicLayoutEffect as useLayoutEffect } from "../../shared/helpers/useIsomorphicLayoutEffect.js";
const isResolvedTargetRefsObject = target => Boolean(target) && typeof target === 'object' && !('getBoundingClientRect' in target) && ('verticalRef' in target || 'horizontalRef' in target);
function PopoverContainer(props) {
const {
baseClassNames = ['dnb-popover'],
active,
showDelay = 0,
attributes,
arrowPosition = 'center',
placement = 'bottom',
alignOnTarget,
horizontalOffset = 0,
arrowPositionSelector,
hideDelay = 0,
fixedPosition,
keepInDOM: keepInDOMProp,
noAnimation,
skipPortal,
contentRef,
children,
targetElement,
triggerOffset: triggerOffsetProp = 0,
autoAlignMode = 'initial',
hideArrow = false,
arrowEdgeOffset,
targetRefreshKey
} = props;
const [style, setStyle] = useState(null);
const [arrowStyle, setArrowStyle] = useState(null);
const [resolvedPlacement, setResolvedPlacement] = useState(placement);
const [wasActive, setWasActive] = useState(active);
const [delayedActive, setDelayedActive] = useState(false);
const showDelayTimeout = useRef();
const isActive = delayedActive;
const [isInDOM, setIsInDOM] = useState(() => keepInDOMProp || active);
const offset = useRef(triggerOffsetProp);
const domTimeout = useRef();
const clearDomTimeout = () => {
clearTimeout(domTimeout.current);
};
useEffect(() => {
offset.current = triggerOffsetProp;
}, [triggerOffsetProp]);
const debounceTimeout = useRef();
const resizeObserver = useRef(null);
const tmpRef = useRef(null);
const elementRef = contentRef && 'current' in contentRef ? contentRef : tmpRef;
const scrollViewElementRef = useRef(null);
const resolvedTargetRef = useRef(null);
const autoAlignInitialUsedRef = useRef(false);
const prevIsActiveRef = useRef(false);
const clearTimers = () => {
clearTimeout(debounceTimeout.current);
};
const clearShowDelay = () => {
clearTimeout(showDelayTimeout.current);
};
useEffect(() => clearShowDelay, []);
useEffect(() => {
if (active) {
const run = () => {
setDelayedActive(true);
setWasActive(true);
};
if (noAnimation || globalThis.IS_TEST) {
clearShowDelay();
run();
return;
}
const delay = Math.max(0, parseFloat(String(showDelay)) || 0);
if (delay === 0) {
clearShowDelay();
run();
return;
}
clearShowDelay();
showDelayTimeout.current = setTimeout(run, delay);
return;
}
clearShowDelay();
setDelayedActive(false);
}, [active, noAnimation, showDelay]);
useEffect(() => {
clearDomTimeout();
if (active) {
setIsInDOM(true);
return clearDomTimeout;
}
if (keepInDOMProp) {
return clearDomTimeout;
}
if (noAnimation || globalThis.IS_TEST) {
setIsInDOM(false);
return clearDomTimeout;
}
const delay = Math.max(0, parseFloat(String(hideDelay)) || 0);
domTimeout.current = setTimeout(() => {
setIsInDOM(false);
}, delay + 300);
return clearDomTimeout;
}, [active, hideDelay, keepInDOMProp, noAnimation]);
const getBodySize = useCallback(() => {
if (!isActive || typeof document === 'undefined') {
return 1;
}
const {
width,
height
} = document.body.getBoundingClientRect();
return width + height;
}, [isActive]);
const [renewStyles, triggerRecalculation] = useState(getBodySize);
const requestRecalculation = useCallback(() => {
triggerRecalculation(value => value + 1);
}, [triggerRecalculation]);
const handleViewportResize = useCallback(() => {
triggerRecalculation(getBodySize());
}, [getBodySize]);
const addPositionObserver = useCallback(() => {
if (resizeObserver.current || typeof document === 'undefined') {
return;
}
try {
resizeObserver.current = new ResizeObserver(() => {
clearTimers();
debounceTimeout.current = setTimeout(() => triggerRecalculation(getBodySize()), 100);
});
resizeObserver.current.observe(document.body);
} catch (e) {}
}, [getBodySize]);
useLayoutEffect(() => {
const removePositionObserver = () => {
var _resizeObserver$curre;
clearTimers();
(_resizeObserver$curre = resizeObserver.current) === null || _resizeObserver$curre === void 0 || _resizeObserver$curre.disconnect();
};
const hasJustActivated = isActive && !prevIsActiveRef.current;
prevIsActiveRef.current = isActive;
if (hasJustActivated) {
autoAlignInitialUsedRef.current = false;
}
if (isActive) {
setWasActive(true);
addPositionObserver();
} else if (wasActive) {
removePositionObserver();
}
return removePositionObserver;
}, [addPositionObserver, isActive, wasActive]);
const offsetLeft = useRef(0);
const offsetTop = useRef(0);
useEffect(() => {
setResolvedPlacement(placement);
}, [placement]);
useLayoutEffect(() => {
if (typeof window === 'undefined') {
return;
}
window.addEventListener('resize', handleViewportResize);
return () => window.removeEventListener('resize', handleViewportResize);
}, [handleViewportResize]);
useLayoutEffect(() => {
if (typeof document === 'undefined') {
return;
}
if (!isActive) {
return;
}
const handleScroll = event => {
const targetNode = event.target;
const scrollViewElement = scrollViewElementRef.current;
const resolvedTarget = resolvedTargetRef.current;
if (scrollViewElement && resolvedTarget && typeof resolvedTarget.getBoundingClientRect === 'function' && scrollViewElement.contains(targetNode)) {
const scrollRect = scrollViewElement.getBoundingClientRect();
const targetRect = resolvedTarget.getBoundingClientRect();
const isVisible = targetRect.bottom >= scrollRect.top && targetRect.top <= scrollRect.bottom;
if (!isVisible) {
return;
}
}
requestRecalculation();
};
document.addEventListener('scroll', handleScroll, true);
return () => {
document.removeEventListener('scroll', handleScroll, true);
};
}, [isActive, requestRecalculation]);
useLayoutEffect(() => {
var _ref, _resolvedRefs$horizon, _ref2, _resolvedRefs$vertica, _ref3, _ref4, _ref5;
if (!isActive && renewStyles) {
if (wasActive) {
clearTimers();
debounceTimeout.current = setTimeout(() => {
setStyle(null);
}, hideDelay + 200);
}
return;
}
const element = elementRef === null || elementRef === void 0 ? void 0 : elementRef.current;
if (typeof window === 'undefined' || !element) {
return;
}
const resolvedRefs = isResolvedTargetRefsObject(targetElement) ? targetElement : null;
const horizontalTarget = (_ref = (_resolvedRefs$horizon = resolvedRefs === null || resolvedRefs === void 0 ? void 0 : resolvedRefs.horizontalRef) !== null && _resolvedRefs$horizon !== void 0 ? _resolvedRefs$horizon : resolvedRefs === null || resolvedRefs === void 0 ? void 0 : resolvedRefs.verticalRef) !== null && _ref !== void 0 ? _ref : targetElement;
const verticalTarget = (_ref2 = (_resolvedRefs$vertica = resolvedRefs === null || resolvedRefs === void 0 ? void 0 : resolvedRefs.verticalRef) !== null && _resolvedRefs$vertica !== void 0 ? _resolvedRefs$vertica : resolvedRefs === null || resolvedRefs === void 0 ? void 0 : resolvedRefs.horizontalRef) !== null && _ref2 !== void 0 ? _ref2 : targetElement;
const effectiveHorizontalTarget = (_ref3 = horizontalTarget !== null && horizontalTarget !== void 0 ? horizontalTarget : verticalTarget) !== null && _ref3 !== void 0 ? _ref3 : null;
const effectiveVerticalTarget = (_ref4 = verticalTarget !== null && verticalTarget !== void 0 ? verticalTarget : horizontalTarget) !== null && _ref4 !== void 0 ? _ref4 : null;
const primaryTarget = (_ref5 = effectiveHorizontalTarget !== null && effectiveHorizontalTarget !== void 0 ? effectiveHorizontalTarget : effectiveVerticalTarget) !== null && _ref5 !== void 0 ? _ref5 : null;
scrollViewElementRef.current = primaryTarget ? getClosestScrollViewElement(primaryTarget) : null;
resolvedTargetRef.current = primaryTarget;
if (!(effectiveHorizontalTarget !== null && effectiveHorizontalTarget !== void 0 && effectiveHorizontalTarget.getBoundingClientRect) || !(effectiveVerticalTarget !== null && effectiveVerticalTarget !== void 0 && effectiveVerticalTarget.getBoundingClientRect)) {
return;
}
const elementWidth = element.offsetWidth;
const elementHeight = element.offsetHeight;
const horizontalRect = effectiveHorizontalTarget.getBoundingClientRect();
const verticalRect = effectiveVerticalTarget.getBoundingClientRect();
const scrollViewElement = scrollViewElementRef.current;
const scrollViewRect = scrollViewElement && typeof scrollViewElement.getBoundingClientRect === 'function' ? scrollViewElement.getBoundingClientRect() : null;
const horizontalTargetSize = {
width: horizontalRect.width || effectiveHorizontalTarget.offsetWidth,
height: horizontalRect.height || effectiveHorizontalTarget.offsetHeight
};
const verticalTargetSize = {
width: verticalRect.width || effectiveVerticalTarget.offsetWidth,
height: verticalRect.height || effectiveVerticalTarget.offsetHeight
};
const targetBodySize = {
width: horizontalTargetSize.width || verticalTargetSize.width,
height: verticalTargetSize.height || horizontalTargetSize.height
};
if (skipPortal && (!offsetLeft.current || !offsetTop.current)) {
offsetLeft.current = getOffsetLeft(element) - offset.current;
offsetTop.current = getOffsetTop(element) - offset.current;
}
const containerRect = skipPortal && element.offsetParent instanceof HTMLElement ? element.offsetParent.getBoundingClientRect() : null;
const relativeVerticalTop = containerRect ? verticalRect.top - containerRect.top : 0;
const relativeHorizontalLeft = containerRect ? horizontalRect.left - containerRect.left : 0;
const scrollY = window.scrollY !== undefined ? window.scrollY : window.pageYOffset;
const scrollX = window.scrollX !== undefined ? window.scrollX : window.pageXOffset;
const scrollYOffset = fixedPosition ? 0 : scrollY;
const top = skipPortal ? relativeVerticalTop : scrollYOffset + verticalRect.top - offsetTop.current;
const widthBased = skipPortal ? relativeHorizontalLeft : scrollX + horizontalRect.left;
const left = skipPortal ? widthBased : widthBased - offsetLeft.current;
const computedStyle = {};
const arrowStyle = {
top: null,
left: null
};
const viewportMargin = 16;
const shouldReuseResolved = autoAlignMode === 'initial' && autoAlignInitialUsedRef.current;
let placementKey = shouldReuseResolved ? resolvedPlacement : placement;
const centerX = left + targetBodySize.width / 2;
let anchorY = top + targetBodySize.height / 2;
const isInitialVertical = placementKey === 'top' || placementKey === 'bottom';
const alignOffset = alignOnTarget === 'left' ? -targetBodySize.width / 2 : alignOnTarget === 'right' ? targetBodySize.width / 2 : 0;
let anchorX = centerX + (isInitialVertical ? alignOffset : 0);
if (arrowPositionSelector) {
var _alignmentRoot$ownerD;
const matchesSelector = element => {
if (!element || typeof element.matches !== 'function') {
return false;
}
try {
return element.matches(arrowPositionSelector);
} catch (_error) {
return false;
}
};
const queryWithin = root => {
if (!root || typeof root.querySelector !== 'function') {
return null;
}
try {
return root.querySelector(arrowPositionSelector);
} catch (_error) {
return null;
}
};
const alignmentRoot = isInitialVertical ? effectiveHorizontalTarget : effectiveVerticalTarget;
const ownerDocument = (_alignmentRoot$ownerD = alignmentRoot === null || alignmentRoot === void 0 ? void 0 : alignmentRoot.ownerDocument) !== null && _alignmentRoot$ownerD !== void 0 ? _alignmentRoot$ownerD : typeof document !== 'undefined' ? document : null;
const alignmentElement = (matchesSelector(alignmentRoot) ? alignmentRoot : null) || queryWithin(alignmentRoot) || queryWithin(ownerDocument);
if (alignmentElement !== null && alignmentElement !== void 0 && alignmentElement.getBoundingClientRect) {
const alignmentRect = alignmentElement.getBoundingClientRect();
if (isInitialVertical) {
anchorX = scrollX + alignmentRect.left + alignmentRect.width / 2 - offsetLeft.current;
} else {
anchorY = scrollYOffset + alignmentRect.top + alignmentRect.height / 2 - offsetTop.current;
}
}
}
const placements = {
left: () => ({
left: left - elementWidth - offset.current + horizontalOffset,
top: anchorY - elementHeight / 2
}),
right: () => ({
left: left + targetBodySize.width + offset.current + horizontalOffset,
top: anchorY - elementHeight / 2
}),
top: () => ({
left: anchorX - elementWidth / 2 + horizontalOffset,
top: top - elementHeight - offset.current
}),
bottom: () => ({
left: anchorX - elementWidth / 2 + horizontalOffset,
top: top + targetBodySize.height + offset.current
})
};
const getPlacement = key => {
const resolver = placements[key] || placements.bottom;
return resolver();
};
const topPlacement = getPlacement('top');
const bottomPlacement = getPlacement('bottom');
let {
left: nextLeft,
top: nextTop
} = getPlacement(placementKey);
const initialAutoAlignAllowed = autoAlignMode === 'initial' && !autoAlignInitialUsedRef.current;
const allowAutoAlign = autoAlignMode === 'never' ? false : autoAlignMode === 'initial' ? initialAutoAlignAllowed : true;
if (initialAutoAlignAllowed) {
autoAlignInitialUsedRef.current = true;
}
if (typeof window !== 'undefined' && allowAutoAlign && (placementKey === 'top' || placementKey === 'bottom')) {
const viewportTopEdge = scrollYOffset + viewportMargin;
const viewportBottomEdge = scrollYOffset + window.innerHeight - viewportMargin;
const fitsTop = topPlacement.top >= viewportTopEdge;
const fitsBottom = bottomPlacement.top + elementHeight <= viewportBottomEdge;
if (placementKey === 'bottom' && !fitsBottom && fitsTop) {
placementKey = 'top';
nextLeft = topPlacement.left;
nextTop = topPlacement.top;
} else if (placementKey === 'top' && !fitsTop && fitsBottom) {
placementKey = 'bottom';
nextLeft = bottomPlacement.left;
nextTop = bottomPlacement.top;
} else if (!fitsTop && !fitsBottom) {
const topInvalid = topPlacement.top < 0;
if (topInvalid) {
placementKey = 'bottom';
nextLeft = bottomPlacement.left;
nextTop = bottomPlacement.top;
} else {
const getVisibleHeight = y => {
const top = y;
const bottom = y + elementHeight;
const visibleTop = Math.max(top, viewportTopEdge);
const visibleBottom = Math.min(bottom, viewportBottomEdge);
return Math.max(0, visibleBottom - visibleTop);
};
const topVisible = getVisibleHeight(topPlacement.top);
const bottomVisible = getVisibleHeight(bottomPlacement.top);
if (topVisible > bottomVisible) {
placementKey = 'top';
nextLeft = topPlacement.left;
nextTop = topPlacement.top;
} else {
placementKey = 'bottom';
nextLeft = bottomPlacement.left;
nextTop = bottomPlacement.top;
}
}
}
}
const isVerticalPlacement = placementKey === 'top' || placementKey === 'bottom';
const arrowOffset = arrowEdgeOffset !== null && arrowEdgeOffset !== void 0 ? arrowEdgeOffset : 16;
const arrowPositions = {
left: () => ({
left: centerX - offset.current + alignOffset + horizontalOffset - arrowOffset
}),
right: () => ({
left: centerX - elementWidth + offset.current + alignOffset + horizontalOffset + arrowOffset
}),
top: () => ({
top: anchorY - offset.current - arrowOffset
}),
bottom: () => ({
top: anchorY - elementHeight + offset.current + arrowOffset
})
};
const shouldOverrideHorizontal = isVerticalPlacement && (arrowPosition === 'left' || arrowPosition === 'right');
const shouldOverrideVertical = !isVerticalPlacement && (arrowPosition === 'top' || arrowPosition === 'bottom');
const arrowOverride = (shouldOverrideHorizontal || shouldOverrideVertical) && arrowPositions[arrowPosition];
if (arrowOverride) {
const overrides = arrowOverride();
if ('left' in overrides) {
nextLeft = overrides.left;
}
if ('top' in overrides) {
nextTop = overrides.top;
}
} else {
nextLeft += horizontalOffset;
}
const edgeSpacing = arrowEdgeOffset !== null && arrowEdgeOffset !== void 0 ? arrowEdgeOffset : 8;
const arrowHugsEdge = isVerticalPlacement && (arrowPosition === 'left' || arrowPosition === 'right');
if (arrowHugsEdge) {
nextLeft += arrowPosition === 'left' ? -edgeSpacing : edgeSpacing;
}
const clampTopWithinScrollView = value => {
if (!scrollViewRect) {
return value;
}
const minTop = scrollViewRect.top + scrollYOffset - elementHeight;
const maxTop = scrollViewRect.bottom + scrollYOffset;
if (maxTop < minTop) {
return minTop;
}
return Math.min(Math.max(value, minTop), maxTop);
};
nextTop = clampTopWithinScrollView(nextTop);
const lacksLayout = !elementWidth && !elementHeight && !targetBodySize.width && !targetBodySize.height;
if (lacksLayout) {
if (typeof computedStyle.left === 'undefined') {
computedStyle.left = 0;
}
if (typeof computedStyle.top === 'undefined') {
computedStyle.top = 0;
}
setStyle(computedStyle);
setArrowStyle(arrowStyle);
return;
}
const computedElementStyle = typeof window !== 'undefined' && element ? window.getComputedStyle(element) : null;
const marginLeft = computedElementStyle ? parseFloat(computedElementStyle.marginLeft) || 0 : 0;
const marginRight = computedElementStyle ? parseFloat(computedElementStyle.marginRight) || 0 : 0;
let actualLeft = nextLeft;
if (!skipPortal) {
const elementWidthWithMargins = elementWidth + marginRight;
const rightBoundary = window.innerWidth - viewportMargin;
const actualRight = actualLeft + elementWidthWithMargins;
if (actualRight > rightBoundary) {
const allowedLeft = rightBoundary - elementWidthWithMargins;
actualLeft = Math.max(viewportMargin, allowedLeft);
}
if (actualLeft < viewportMargin) {
actualLeft = viewportMargin;
}
computedStyle.left = actualLeft - marginLeft;
} else {
computedStyle.left = nextLeft - marginLeft;
}
computedStyle.top = nextTop;
const actualTop = nextTop;
if (isVerticalPlacement) {
const arrowWidth = 16;
const arrowBoundary = arrowEdgeOffset !== null && arrowEdgeOffset !== void 0 ? arrowEdgeOffset : 8;
const maxLeft = Math.max(0, elementWidth - arrowWidth);
const arrowLeft = anchorX - actualLeft - arrowWidth / 2;
const arrowMin = Math.min(maxLeft, arrowBoundary);
const arrowMax = Math.max(arrowMin, Math.max(0, maxLeft - arrowBoundary));
let arrowClampMin = arrowMin;
let arrowClampMax = arrowMax;
if (scrollViewRect) {
const scrollMin = scrollViewRect.left + scrollX - actualLeft;
const scrollMax = scrollViewRect.right + scrollX - actualLeft - arrowWidth;
arrowClampMin = Math.max(arrowClampMin, scrollMin);
arrowClampMax = Math.min(arrowClampMax, scrollMax);
}
if (arrowClampMax < arrowClampMin) {
arrowClampMax = arrowClampMin;
}
let nextArrowLeft = arrowLeft;
if (nextArrowLeft < arrowClampMin) {
nextArrowLeft = arrowClampMin;
} else if (nextArrowLeft > arrowClampMax) {
nextArrowLeft = arrowClampMax;
}
arrowStyle.left = nextArrowLeft;
} else {
const arrowHeight = 16;
const arrowBoundary = arrowEdgeOffset !== null && arrowEdgeOffset !== void 0 ? arrowEdgeOffset : 8;
const maxTop = Math.max(0, elementHeight - arrowHeight);
const arrowTop = anchorY - actualTop - arrowHeight / 2;
const arrowMin = Math.min(maxTop, arrowBoundary);
const arrowMax = Math.max(arrowMin, Math.max(0, maxTop - arrowBoundary));
let arrowClampMin = arrowMin;
let arrowClampMax = arrowMax;
if (scrollViewRect) {
const scrollMin = scrollViewRect.top + scrollYOffset - actualTop;
const scrollMax = scrollViewRect.bottom + scrollYOffset - actualTop - arrowHeight;
arrowClampMin = Math.max(arrowClampMin, scrollMin);
arrowClampMax = Math.min(arrowClampMax, scrollMax);
}
if (arrowClampMax < arrowClampMin) {
arrowClampMax = arrowClampMin;
}
let nextArrowTop = arrowTop;
if (nextArrowTop < arrowClampMin) {
nextArrowTop = arrowClampMin;
} else if (nextArrowTop > arrowClampMax) {
nextArrowTop = arrowClampMax;
}
arrowStyle.top = nextArrowTop;
}
if (resolvedPlacement !== placementKey) {
setResolvedPlacement(placementKey);
}
setStyle(computedStyle);
setArrowStyle(arrowStyle);
}, [alignOnTarget, autoAlignMode, arrowPosition, horizontalOffset, arrowEdgeOffset, elementRef, fixedPosition, hideDelay, isActive, triggerOffsetProp, placement, renewStyles, skipPortal, targetElement, targetRefreshKey, wasActive, arrowPositionSelector, resolvedPlacement]);
const handlePropagation = useCallback(event => {
event.stopPropagation();
}, []);
const shouldRender = isInDOM || keepInDOMProp;
const mergedStyle = {
...(style || {}),
...((attributes === null || attributes === void 0 ? void 0 : attributes.style) || {})
};
const hasPlacement = Boolean(style) && typeof style.left !== 'undefined' && typeof style.top !== 'undefined';
const containerStyle = !hasPlacement && (active || isActive) && typeof mergedStyle.visibility === 'undefined' ? {
...mergedStyle,
visibility: 'hidden'
} : mergedStyle;
if (!shouldRender) {
return null;
}
const noAnimationClasses = noAnimation ? baseClassNames.map(base => `${base}--no-animation`) : null;
const fixedClasses = fixedPosition ? baseClassNames.map(base => `${base}--fixed`) : null;
const activeClasses = isActive ? baseClassNames.map(base => `${base}--active`) : null;
const hideClasses = !isActive && wasActive ? baseClassNames.map(base => `${base}--hide`) : null;
return React.createElement("span", _extends({
ref: elementRef
}, attributes, {
onMouseMove: handlePropagation,
onMouseDown: handlePropagation,
onTouchStart: handlePropagation,
className: classnames(attributes === null || attributes === void 0 ? void 0 : attributes.className, noAnimationClasses, fixedClasses, activeClasses, hideClasses),
style: containerStyle
}), !hideArrow && React.createElement("span", {
className: classnames(baseClassNames.map(base => `${base}__arrow`), baseClassNames.map(base => `${base}__arrow__arrow--${arrowPosition}`), baseClassNames.map(base => `${base}__arrow__placement--${resolvedPlacement}`)),
style: {
...arrowStyle
}
}), children);
}
export default PopoverContainer;
//# sourceMappingURL=PopoverContainer.js.map