@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
540 lines (539 loc) • 21.9 kB
JavaScript
"use strict";
"use client";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Popover;
Object.defineProperty(exports, "getRefElement", {
enumerable: true,
get: function () {
return _getRefElement.default;
}
});
var _push = _interopRequireDefault(require("core-js-pure/stable/instance/push.js"));
var _hasOwn = _interopRequireDefault(require("core-js-pure/stable/object/has-own.js"));
var _react = _interopRequireWildcard(require("react"));
var _classnames = _interopRequireDefault(require("classnames"));
var _PopoverCloseButton = _interopRequireDefault(require("./internal/PopoverCloseButton.js"));
var _useId = _interopRequireDefault(require("../../shared/helpers/useId.js"));
var _useTranslation = _interopRequireDefault(require("../../shared/useTranslation.js"));
var _componentHelper = require("../../shared/component-helper.js");
var _getRefElement = _interopRequireDefault(require("../../shared/internal/getRefElement.js"));
var _PopoverPortal = _interopRequireDefault(require("./PopoverPortal.js"));
var _PopoverContainer = _interopRequireDefault(require("./PopoverContainer.js"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function Popover(props) {
var _closeButtonProps$var, _closeButtonProps$ico;
const {
children,
content,
trigger,
triggerAttributes,
triggerClassName,
title,
placement = 'bottom',
open: controlledOpenProp,
openInitially: openInitiallyProp = false,
onOpenChange,
focusOnOpen = true,
focusOnOpenElement,
restoreFocus = true,
preventClose = false,
hideCloseButton = false,
closeButtonProps,
contentClassName,
className,
baseClassName,
theme = 'light',
disableFocusTrap = false,
hideOutline = false,
noInnerSpace = false,
noMaxWidth = false,
keepInDOM = false,
triggerOffset,
targetElement: externalTargetElement,
targetSelector,
targetRefreshKey,
portalRootClass,
showDelay: showDelayProp,
hideDelay: hideDelayProp,
contentRef: externalContentRef,
alignOnTarget = 'center',
horizontalOffset,
autoAlignMode = 'initial',
arrowPosition = 'center',
arrowPositionSelector,
hideArrow = false,
skipPortal = false,
noAnimation = false,
fixedPosition = false,
arrowEdgeOffset,
id: idProp,
omitDescribedBy,
...restAttributes
} = props;
const baseClassNames = (0, _react.useMemo)(() => {
const names = ['dnb-popover'];
if (baseClassName && baseClassName !== 'dnb-popover') {
(0, _push.default)(names).call(names, baseClassName);
}
return names;
}, [baseClassName]);
const tr = (0, _useTranslation.default)().Popover;
const {
isOpen,
setOpenState
} = usePopoverOpenState({
open: controlledOpenProp,
openInitially: openInitiallyProp,
onOpenChange
});
const triggerRef = (0, _react.useRef)(null);
const tooltipRef = (0, _react.useRef)(null);
const contentWrapperRef = (0, _react.useRef)(null);
const previousTargetElementRef = (0, _react.useRef)(null);
const focusRestoreAnimationRef = (0, _react.useRef)(null);
const touchStartTargetRef = (0, _react.useRef)(null);
const touchMovedRef = (0, _react.useRef)(false);
const tooltipId = (0, _useId.default)(idProp);
const descriptionId = `${tooltipId}-description`;
const showDelay = showDelayProp !== null && showDelayProp !== void 0 ? showDelayProp : 0;
const hideDelay = hideDelayProp !== null && hideDelayProp !== void 0 ? hideDelayProp : 0;
const hasExplicitTargetElement = (0, _hasOwn.default)(props, 'targetElement');
const shouldRenderTrigger = !hasExplicitTargetElement && typeof targetSelector !== 'string';
const getCurrentTriggerElement = (0, _react.useCallback)(() => {
if (shouldRenderTrigger) {
return triggerRef.current;
}
if (isPopoverTargetElementObject(externalTargetElement)) {
return resolveTargetNode(externalTargetElement.verticalRef) || resolveTargetNode(externalTargetElement.horizontalRef);
}
if (externalTargetElement) {
return resolveTargetNode(externalTargetElement);
}
if (typeof targetSelector === 'string' && typeof document !== 'undefined') {
return document.querySelector(targetSelector);
}
return null;
}, [externalTargetElement, shouldRenderTrigger, targetSelector]);
const [targetElement, setTargetElement] = (0, _react.useState)(null);
const resolveTargetElementForContainer = (0, _react.useCallback)(() => {
if (isPopoverTargetElementObject(externalTargetElement)) {
const horizontalRef = resolveTargetNode(externalTargetElement.horizontalRef);
const verticalRef = resolveTargetNode(externalTargetElement.verticalRef);
return {
horizontalRef,
verticalRef
};
}
if (externalTargetElement) {
return resolveTargetNode(externalTargetElement);
}
if (typeof targetSelector === 'string' && typeof document !== 'undefined') {
return document.querySelector(targetSelector);
}
if (shouldRenderTrigger) {
return triggerRef.current;
}
return null;
}, [externalTargetElement, shouldRenderTrigger, targetSelector]);
(0, _react.useEffect)(() => {
const resolved = resolveTargetElementForContainer();
const hadPreviousTarget = previousTargetElementRef.current !== null;
const hadValidPreviousTarget = hadPreviousTarget && (previousTargetElementRef.current instanceof HTMLElement || typeof previousTargetElementRef.current === 'object' && previousTargetElementRef.current !== null && ('horizontalRef' in previousTargetElementRef.current || 'verticalRef' in previousTargetElementRef.current));
previousTargetElementRef.current = resolved;
setTargetElement(resolved);
if (!resolved && isOpen && hadValidPreviousTarget) {
setOpenState(false);
}
}, [resolveTargetElementForContainer, isOpen, setOpenState]);
const resolveFocusTarget = (0, _react.useCallback)(() => {
if (!focusOnOpenElement) {
return null;
}
return typeof focusOnOpenElement === 'function' ? focusOnOpenElement() : focusOnOpenElement;
}, [focusOnOpenElement]);
const focusTrigger = (0, _react.useCallback)(() => {
if (!restoreFocus) {
return;
}
const element = getCurrentTriggerElement();
if (!element) {
return;
}
if (focusRestoreAnimationRef.current !== null) {
cancelAnimationFrame(focusRestoreAnimationRef.current);
}
focusRestoreAnimationRef.current = requestAnimationFrame(() => {
element.focus({
preventScroll: true
});
focusRestoreAnimationRef.current = null;
});
}, [getCurrentTriggerElement, restoreFocus]);
(0, _react.useEffect)(() => {
return () => {
if (focusRestoreAnimationRef.current !== null) {
cancelAnimationFrame(focusRestoreAnimationRef.current);
}
};
}, []);
const close = (0, _react.useCallback)(() => {
if (preventClose) {
return;
}
setOpenState(false);
focusTrigger();
}, [focusTrigger, setOpenState, preventClose]);
const openPopover = (0, _react.useCallback)(() => {
setOpenState(true);
}, [setOpenState]);
const toggle = (0, _react.useCallback)(next => {
const value = typeof next === 'boolean' ? next : !isOpen;
if (value) {
openPopover();
} else {
close();
}
}, [close, isOpen, openPopover]);
const runTriggerClick = (0, _react.useCallback)(event => {
if (event && 'preventDefault' in event) {
event.preventDefault();
}
toggle();
}, [toggle]);
(0, _react.useEffect)(() => {
if (!focusOnOpen || !isOpen) {
return;
}
const timers = [];
const focusContent = () => {
var _tooltipRef$current;
const focusTarget = resolveFocusTarget() || contentWrapperRef.current || ((_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.querySelector('.dnb-popover__content'));
if (!(focusTarget instanceof HTMLElement)) {
return false;
}
focusTarget.focus({
preventScroll: true
});
setTimeout(() => {
focusTarget === null || focusTarget === void 0 || focusTarget.focus({
preventScroll: true
});
}, 10);
return true;
};
const scheduleFocusAttempt = (delay, retries) => {
(0, _push.default)(timers).call(timers, setTimeout(() => {
if (!focusContent() && retries > 0) {
scheduleFocusAttempt(delay, retries - 1);
}
}, delay));
};
if (!focusContent()) {
scheduleFocusAttempt(10, 3);
}
return () => timers.forEach(clearTimeout);
}, [focusOnOpen, isOpen, resolveFocusTarget]);
const handleDocumentInteraction = (0, _react.useCallback)((event, overrideTarget) => {
if (preventClose) {
return;
}
const target = overrideTarget !== null && overrideTarget !== void 0 ? overrideTarget : event.target;
if (!(target instanceof Node)) {
return;
}
const insideContent = !!tooltipRef.current && tooltipRef.current.contains(target);
const triggerElement = getCurrentTriggerElement();
const insideTrigger = !!triggerElement && triggerElement.contains(target);
if (!insideContent && !insideTrigger) {
toggle(false);
}
}, [preventClose, getCurrentTriggerElement, toggle]);
const handleDocumentTouchStart = (0, _react.useCallback)(event => {
touchMovedRef.current = false;
touchStartTargetRef.current = event.target;
}, []);
const handleDocumentTouchMove = (0, _react.useCallback)(() => {
touchMovedRef.current = true;
}, []);
const handleDocumentTouchEnd = (0, _react.useCallback)(event => {
const target = touchStartTargetRef.current;
const moved = touchMovedRef.current;
touchMovedRef.current = false;
touchStartTargetRef.current = null;
if (moved) {
return;
}
handleDocumentInteraction(event, target);
}, [handleDocumentInteraction]);
const handleDocumentKeyDown = (0, _react.useCallback)(event => {
if (event.defaultPrevented || preventClose) {
return;
}
if (event.key === 'Escape') {
var _tooltipRef$current2;
const target = event.target || document.activeElement;
const triggerElement = getCurrentTriggerElement();
const insideTrigger = triggerElement === null || triggerElement === void 0 ? void 0 : triggerElement.contains(target);
const insideContent = (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 ? void 0 : _tooltipRef$current2.contains(target);
if (insideContent || insideTrigger) {
var _event$stopImmediateP;
event.preventDefault();
event.stopPropagation();
(_event$stopImmediateP = event.stopImmediatePropagation) === null || _event$stopImmediateP === void 0 || _event$stopImmediateP.call(event);
toggle(false);
}
}
}, [preventClose, getCurrentTriggerElement, toggle]);
(0, _react.useEffect)(() => {
if (!isOpen) {
return;
}
document.documentElement.addEventListener('mousedown', handleDocumentInteraction);
document.documentElement.addEventListener('touchstart', handleDocumentTouchStart, {
passive: true
});
document.documentElement.addEventListener('touchmove', handleDocumentTouchMove, {
passive: true
});
document.documentElement.addEventListener('touchend', handleDocumentTouchEnd, {
passive: true
});
document.documentElement.addEventListener('keyup', handleDocumentInteraction);
document.addEventListener('keydown', handleDocumentKeyDown, true);
return () => {
document.documentElement.removeEventListener('mousedown', handleDocumentInteraction);
document.documentElement.removeEventListener('touchstart', handleDocumentTouchStart);
document.documentElement.removeEventListener('touchmove', handleDocumentTouchMove);
document.documentElement.removeEventListener('touchend', handleDocumentTouchEnd);
document.documentElement.removeEventListener('keyup', handleDocumentInteraction);
document.removeEventListener('keydown', handleDocumentKeyDown, true);
};
}, [handleDocumentInteraction, handleDocumentKeyDown, handleDocumentTouchEnd, handleDocumentTouchMove, handleDocumentTouchStart, isOpen]);
const handleTriggerKeyDown = (0, _react.useCallback)((event, userHandler) => {
userHandler === null || userHandler === void 0 || userHandler(event);
if (event.defaultPrevented) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggle();
}
}, [toggle]);
const mergedTriggerAttributes = triggerAttributes || {};
const {
onClick: triggerOnClick,
onKeyDown: triggerOnKeyDown,
className: triggerAttrClassName,
ref: triggerAttrRef,
role: triggerAttrRole,
tabIndex: triggerAttrTabIndex,
title: triggerAttrTitle,
['aria-describedby']: triggerAttrDescribedBy,
...restTriggerAttrs
} = mergedTriggerAttributes;
const assignTriggerRef = (0, _react.useCallback)(node => {
triggerRef.current = node;
if (!triggerAttrRef) {
return;
}
if (typeof triggerAttrRef === 'function') {
triggerAttrRef(node);
} else if ('current' in triggerAttrRef) {
triggerAttrRef.current = node;
}
}, [triggerAttrRef]);
const statefulTitle = isOpen ? tr.closeTriggerTitle : tr.openTriggerTitle;
const triggerDomProps = {
...restTriggerAttrs,
ref: assignTriggerRef,
className: (0, _classnames.default)('dnb-popover__trigger', triggerAttrClassName, triggerClassName),
title: triggerAttrTitle || statefulTitle,
role: triggerAttrRole || 'button',
tabIndex: typeof triggerAttrTabIndex === 'number' ? triggerAttrTabIndex : 0,
'aria-controls': tooltipId,
'aria-expanded': isOpen,
'aria-describedby': mergeDescribedBy(triggerAttrDescribedBy, descriptionId),
onClick: event => {
triggerOnClick === null || triggerOnClick === void 0 || triggerOnClick(event);
if (event.defaultPrevented) {
return;
}
runTriggerClick(event);
},
onKeyDown: event => handleTriggerKeyDown(event, triggerOnKeyDown)
};
const triggerRenderProps = {
...triggerDomProps
};
Object.defineProperties(triggerRenderProps, {
active: {
value: isOpen,
enumerable: false
},
open: {
value: openPopover,
enumerable: false
},
close: {
value: close,
enumerable: false
},
toggle: {
value: toggle,
enumerable: false
}
});
let triggerMarkup = null;
if (shouldRenderTrigger) {
if (isRenderer(trigger)) {
triggerMarkup = trigger(triggerRenderProps);
} else if ((0, _react.isValidElement)(trigger)) {
triggerMarkup = (0, _react.cloneElement)(trigger, triggerDomProps);
} else if (trigger) {
(0, _componentHelper.warn)('Popover: `trigger` must be a valid React element or render function when not using targetElement/targetSelector.');
} else {
(0, _componentHelper.warn)('Popover: please provide a `trigger` prop or point to an existing element using `targetElement` / `targetSelector`.');
}
}
const hasInternalTrigger = shouldRenderTrigger && Boolean(triggerMarkup);
const contentContext = (0, _react.useMemo)(() => ({
active: isOpen,
open: openPopover,
close,
toggle,
id: tooltipId
}), [close, isOpen, openPopover, toggle, tooltipId]);
const userContent = (0, _react.useMemo)(() => {
const source = typeof content !== 'undefined' ? content : children;
if (isRenderer(source)) {
return source(contentContext);
}
return source;
}, [children, content, contentContext]);
const closeButton = !hideCloseButton && _react.default.createElement(_PopoverCloseButton.default, _extends({
variant: (_closeButtonProps$var = closeButtonProps === null || closeButtonProps === void 0 ? void 0 : closeButtonProps.variant) !== null && _closeButtonProps$var !== void 0 ? _closeButtonProps$var : 'tertiary',
icon: (_closeButtonProps$ico = closeButtonProps === null || closeButtonProps === void 0 ? void 0 : closeButtonProps.icon) !== null && _closeButtonProps$ico !== void 0 ? _closeButtonProps$ico : 'close'
}, closeButtonProps, {
className: (0, _classnames.default)('dnb-popover__close', closeButtonProps === null || closeButtonProps === void 0 ? void 0 : closeButtonProps.className),
title: (closeButtonProps === null || closeButtonProps === void 0 ? void 0 : closeButtonProps.title) || tr.closeButtonTitle,
onClick: event => {
var _closeButtonProps$onC;
closeButtonProps === null || closeButtonProps === void 0 || (_closeButtonProps$onC = closeButtonProps.onClick) === null || _closeButtonProps$onC === void 0 || _closeButtonProps$onC.call(closeButtonProps, event);
if (event !== null && event !== void 0 && event.defaultPrevented) {
return;
}
toggle(false);
}
}));
const popoverClassName = (0, _classnames.default)(baseClassNames, className, theme && baseClassNames.map(name => `${name}--theme-${theme}`), !hideOutline && baseClassNames.map(name => `${name}--show-outline`), noInnerSpace && baseClassNames.map(name => `${name}--no-inner-space`), noMaxWidth && baseClassNames.map(name => `${name}--no-max-width`));
const popoverAttributes = {
...restAttributes,
className: popoverClassName
};
const contentRef = externalContentRef || tooltipRef;
const overlayContent = _react.default.createElement(_react.default.Fragment, null, !disableFocusTrap && _react.default.createElement("button", {
className: "dnb-sr-only",
onFocus: close
}, tr.focusTrapTitle), _react.default.createElement("span", {
className: (0, _classnames.default)("dnb-popover__content dnb-no-focus", contentClassName),
id: tooltipId,
tabIndex: -1,
ref: contentWrapperRef
}, title && _react.default.createElement("span", {
className: "dnb-popover__title"
}, _react.default.createElement("strong", {
className: "dnb-h--basis"
}, title)), _react.default.createElement("span", {
className: "dnb-popover__body"
}, userContent)), closeButton, !disableFocusTrap && _react.default.createElement("button", {
className: "dnb-sr-only",
onFocus: close
}, tr.focusTrapTitle));
if (targetElement === null) {
return triggerMarkup;
}
const PopoverElement = skipPortal ? _PopoverContainer.default : _PopoverPortal.default;
return _react.default.createElement(_react.default.Fragment, null, triggerMarkup, hasInternalTrigger && _react.default.createElement("span", {
className: "dnb-sr-only",
"aria-hidden": "true",
id: descriptionId
}, statefulTitle), _react.default.createElement(PopoverElement, _extends({
baseClassNames: baseClassNames,
active: isOpen,
showDelay: showDelay,
attributes: popoverAttributes,
targetElement: targetElement,
hideDelay: hideDelay,
keepInDOM: keepInDOM,
autoAlignMode: autoAlignMode,
noAnimation: noAnimation,
arrowPosition: arrowPosition,
placement: placement,
alignOnTarget: alignOnTarget,
horizontalOffset: horizontalOffset,
arrowPositionSelector: arrowPositionSelector,
fixedPosition: fixedPosition,
skipPortal: skipPortal
}, !skipPortal && {
portalRootClass
}, {
contentRef: contentRef,
triggerOffset: triggerOffset,
hideArrow: hideArrow,
arrowEdgeOffset: arrowEdgeOffset,
targetRefreshKey: targetRefreshKey
}), overlayContent));
}
function usePopoverOpenState({
open,
openInitially,
onOpenChange
}) {
const isControlled = typeof open === 'boolean';
const [internalOpen, setInternalOpen] = (0, _react.useState)(() => {
if (typeof openInitially === 'boolean') {
return openInitially;
}
return false;
});
const isOpen = isControlled ? open : internalOpen;
const setOpenState = (0, _react.useCallback)(next => {
if (!isControlled) {
setInternalOpen(next);
}
onOpenChange === null || onOpenChange === void 0 || onOpenChange(next);
}, [isControlled, onOpenChange]);
return {
isOpen,
setOpenState
};
}
function isRenderer(value) {
return typeof value === 'function';
}
function mergeDescribedBy(existing, next) {
return (0, _componentHelper.combineDescribedBy)({
'aria-describedby': existing
}, next);
}
function isPopoverTargetElementObject(target) {
return Boolean(target) && typeof target === 'object' && !('getBoundingClientRect' in target) && ('verticalRef' in target || 'horizontalRef' in target);
}
function resolveTargetNode(target) {
if (!target || isPopoverTargetElementObject(target)) {
return null;
}
if (target instanceof HTMLElement) {
return target;
}
if (typeof target === 'object' && 'current' in target) {
return (0, _getRefElement.default)(target);
}
return null;
}
Popover._supportsSpacingProps = true;
//# sourceMappingURL=Popover.js.map