@blueprintjs/core
Version:
Core styles & components
275 lines • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PopoverNext = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
/* !
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*/
const react_1 = require("react");
const common_1 = require("../../common");
const Errors = tslib_1.__importStar(require("../../common/errors"));
const useValidateProps_1 = require("../../hooks/useValidateProps");
const popoverArrow_1 = require("../popover/popoverArrow");
const popoverProps_1 = require("../popover/popoverProps");
const floatingUtils_1 = require("./floatingUtils");
const popoverPopup_1 = require("./popoverPopup");
const popoverTarget_1 = require("./popoverTarget");
const usePopover_1 = require("./usePopover");
exports.PopoverNext = (0, react_1.forwardRef)((props, ref) => {
const { animation = "scale", arrow = true, boundary = "clippingAncestors", canEscapeKeyClose = true, children, content, defaultIsOpen = false, disabled = false, hasBackdrop = false, hoverCloseDelay = 300, hoverOpenDelay = 150, interactionKind = popoverProps_1.PopoverInteractionKind.CLICK, isOpen, matchTargetWidth = false, middleware: middlewareOverrides, onClose, onInteraction, openOnTargetFocus = true, placement, positioningStrategy = "absolute", renderTarget, rootBoundary, shouldReturnFocusOnClose = true, targetProps, usePortal = true, } = props;
(0, useValidateProps_1.useValidateProps)(() => {
if (isOpen == null && onInteraction != null) {
console.warn(Errors.POPOVER_WARN_UNCONTROLLED_ONINTERACTION);
}
if (hasBackdrop && !usePortal) {
console.warn(Errors.POPOVER_WARN_HAS_BACKDROP_INLINE);
}
if (hasBackdrop && interactionKind !== popoverProps_1.PopoverInteractionKind.CLICK) {
console.warn(Errors.POPOVER_HAS_BACKDROP_INTERACTION);
}
const childrenCount = react_1.Children.count(children);
const hasRenderTargetProp = renderTarget !== undefined;
const hasTargetPropsProp = targetProps !== undefined;
if (childrenCount === 0 && !hasRenderTargetProp) {
console.warn(Errors.POPOVER_REQUIRES_TARGET);
}
if (childrenCount > 1) {
console.warn(Errors.POPOVER_WARN_TOO_MANY_CHILDREN);
}
if (childrenCount > 0 && hasRenderTargetProp) {
console.warn(Errors.POPOVER_WARN_DOUBLE_TARGET);
}
if (hasRenderTargetProp && hasTargetPropsProp) {
console.warn(Errors.POPOVER_WARN_TARGET_PROPS_WITH_RENDER_TARGET);
}
}, [
arrow,
children,
hasBackdrop,
interactionKind,
isOpen,
onInteraction,
placement,
renderTarget,
targetProps,
usePortal,
]);
const [hasDarkParent, setHasDarkParent] = (0, react_1.useState)(false);
const [isClosingViaEscapeKeypress, setIsClosingViaEscapeKeypress] = (0, react_1.useState)(false);
const arrowRef = (0, react_1.useRef)(null);
const cancelOpenTimeout = (0, react_1.useRef)(undefined);
const isMouseInTargetOrPopover = (0, react_1.useRef)(false);
const lostFocusOnSamePage = (0, react_1.useRef)(true);
const targetRef = (0, react_1.useRef)(null);
const timeoutIds = (0, react_1.useRef)([]);
const isControlled = isOpen !== undefined;
const isContentEmpty = content == null || common_1.Utils.isEmptyString(content);
const computedIsOpen = disabled ? false : (isOpen ?? defaultIsOpen);
const middleware = (0, react_1.useMemo)(() => {
const defaultMiddleware = {
...(placement === undefined
? { autoPlacement: { boundary, rootBoundary } }
: { flip: { boundary, rootBoundary } }),
...(arrow
? {
arrow: { element: arrowRef },
offset: { mainAxis: popoverArrow_1.POPOVER_ARROW_SVG_SIZE / 2 },
}
: {}),
shift: { boundary, rootBoundary },
size: matchTargetWidth
? {
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
});
},
}
: undefined,
};
const mergedMiddleware = { ...defaultMiddleware, ...middlewareOverrides };
return (0, floatingUtils_1.convertMiddlewareConfigToArray)(mergedMiddleware);
}, [placement, boundary, rootBoundary, arrow, arrowRef, matchTargetWidth, middlewareOverrides]);
const floatingData = (0, usePopover_1.usePopover)({
canEscapeKeyClose,
disabled,
hasBackdrop,
interactionKind,
isControlled,
isOpen: computedIsOpen,
middleware,
onOpenChange: (nextOpen, event) => {
// Use setOpenState logic which handles both controlled and uncontrolled components
setOpenState(nextOpen, event);
},
placement,
positioningStrategy,
});
(0, react_1.useImperativeHandle)(ref, () => ({
reposition: () => {
floatingData.update();
},
}), [floatingData]);
const popoverElement = floatingData.refs.floating.current;
const isHoverInteractionKind = interactionKind === popoverProps_1.PopoverInteractionKind.HOVER ||
interactionKind === popoverProps_1.PopoverInteractionKind.HOVER_TARGET_ONLY;
const getPopoverElement = (0, react_1.useCallback)(() => {
return popoverElement?.querySelector(`.${common_1.Classes.POPOVER}`);
}, [popoverElement]);
const isElementInPopover = (0, react_1.useCallback)((element) => {
return getPopoverElement()?.contains(element) ?? false;
}, [getPopoverElement]);
const setTimeout = (0, react_1.useCallback)((callback, timeout) => {
const handle = window.setTimeout(callback, timeout);
timeoutIds.current.push(handle);
return () => window.clearTimeout(handle);
}, []);
// a wrapper around setIsOpen that will call onInteraction instead when in controlled mode.
// starts a timeout to delay changing the state if a non-zero duration is provided.
const setOpenState = (0, react_1.useCallback)((isOpenState, event, timeout) => {
// cancel any existing timeout because we have new state
cancelOpenTimeout.current?.();
if (timeout !== undefined && timeout > 0) {
// Persist the react event since it will be used in a later macrotask.
event?.persist();
cancelOpenTimeout.current = setTimeout(() => {
setOpenState(isOpenState, event);
}, timeout);
}
else {
if (isOpen == null) {
// For uncontrolled popovers, update the usePopover state directly
floatingData.setIsOpen(isOpenState);
}
else {
onInteraction?.(isOpenState, event);
}
if (!isOpenState) {
// non-null assertion because the only time `e` is undefined is when in controlled mode
// or the rare special case in uncontrolled mode when the `disabled` flag is toggled true
onClose?.(event);
setIsClosingViaEscapeKeypress(isEscapeKeypressEvent(event?.nativeEvent));
}
}
}, [floatingData, isOpen, onInteraction, onClose, setTimeout]);
const handleTargetContextMenu = (0, react_1.useCallback)((event) => {
// we assume that when someone prevents the default interaction on this event (a browser native context menu),
// they are showing a custom context menu (as ContextMenu2 does); in this case, we should close this popover/tooltip
if (event.defaultPrevented) {
setOpenState(false, event);
}
}, [setOpenState]);
const handleMouseLeave = (0, react_1.useCallback)((event) => {
isMouseInTargetOrPopover.current = false;
event.persist();
setTimeout(() => {
if (isMouseInTargetOrPopover.current) {
return;
}
setOpenState(false, event, hoverCloseDelay);
});
}, [hoverCloseDelay, setOpenState, setTimeout]);
const handleMouseEnter = (0, react_1.useCallback)((event) => {
isMouseInTargetOrPopover.current = true;
// if we're entering the popover, and the mode is set to be HOVER_TARGET_ONLY, we want to manually
// trigger the mouse leave event, as hovering over the popover shouldn't count.
if (!usePortal &&
isElementInPopover(event.target) &&
interactionKind === popoverProps_1.PopoverInteractionKind.HOVER_TARGET_ONLY &&
!openOnTargetFocus) {
handleMouseLeave(event);
}
else if (!disabled) {
// only begin opening popover when it is enabled
setOpenState(true, event, hoverOpenDelay);
}
}, [
disabled,
handleMouseLeave,
hoverOpenDelay,
interactionKind,
isElementInPopover,
openOnTargetFocus,
setOpenState,
usePortal,
]);
const handleTargetFocus = (0, react_1.useCallback)((event) => {
if (openOnTargetFocus && isHoverInteractionKind) {
if (event.relatedTarget == null && !lostFocusOnSamePage.current) {
// ignore this focus event -- the target was already focused but the page itself
// lost focus (e.g. due to switching tabs).
return;
}
handleMouseEnter(event);
}
}, [handleMouseEnter, isHoverInteractionKind, openOnTargetFocus]);
const handleTargetBlur = (0, react_1.useCallback)((event) => {
if (openOnTargetFocus && isHoverInteractionKind) {
if (event.relatedTarget != null) {
// if the next element to receive focus is within the popover, we'll want to leave the
// popover open.
if (event.relatedTarget !== popoverElement &&
!isElementInPopover(event.relatedTarget)) {
handleMouseLeave(event);
}
}
else {
handleMouseLeave(event);
}
}
lostFocusOnSamePage.current = event.relatedTarget != null;
}, [handleMouseLeave, isElementInPopover, isHoverInteractionKind, openOnTargetFocus, popoverElement]);
const handlePopoverClick = (0, react_1.useCallback)((event) => {
const eventTarget = event.target;
const eventPopover = eventTarget.closest(`.${common_1.Classes.POPOVER}`);
const isEventFromSelf = eventPopover === getPopoverElement();
const isEventPopoverCapturing = eventPopover?.classList.contains(common_1.Classes.POPOVER_CAPTURING_DISMISS) ?? false;
// an OVERRIDE inside a DISMISS does not dismiss, and a DISMISS inside an OVERRIDE will dismiss.
const dismissElement = eventTarget.closest(`.${common_1.Classes.POPOVER_DISMISS}, .${common_1.Classes.POPOVER_DISMISS_OVERRIDE}`);
const shouldDismiss = dismissElement?.classList.contains(common_1.Classes.POPOVER_DISMISS) ?? false;
const isDisabled = eventTarget.closest(`:disabled, .${common_1.Classes.DISABLED}`) != null;
if (shouldDismiss && !isDisabled && (!isEventPopoverCapturing || isEventFromSelf)) {
setOpenState(false, event);
}
}, [getPopoverElement, setOpenState]);
const handleOverlayClose = (0, react_1.useCallback)((event) => {
if (targetRef.current == null || event === undefined) {
return;
}
const nativeEvent = (event.nativeEvent ?? event);
const eventTarget = (nativeEvent.composed ? nativeEvent.composedPath()[0] : nativeEvent.target);
// if click was in target, target event listener will handle things, so don't close
if (!common_1.Utils.elementIsOrContains(targetRef.current, eventTarget) ||
event.nativeEvent instanceof KeyboardEvent) {
setOpenState(false, event);
}
}, [setOpenState, targetRef]);
const updateDarkParent = (0, react_1.useCallback)(() => {
if (usePortal && floatingData.isOpen) {
setHasDarkParent(targetRef.current?.closest(`.${common_1.Classes.DARK}`) != null);
}
}, [floatingData.isOpen, targetRef, usePortal]);
(0, react_1.useEffect)(() => {
updateDarkParent();
if (isOpen != null && computedIsOpen !== floatingData.isOpen) {
setOpenState(computedIsOpen);
}
else if (disabled && floatingData.isOpen && isOpen == null) {
// special case: close an uncontrolled popover when disabled is set to true
setOpenState(false);
}
// Warn about empty content when trying to open (needs to be in effect to access current state)
if (isContentEmpty) {
if (!disabled && computedIsOpen !== false && !common_1.Utils.isNodeEnv("production")) {
console.warn(Errors.POPOVER_WARN_EMPTY_CONTENT);
}
}
}, [computedIsOpen, disabled, floatingData, isContentEmpty, isOpen, setOpenState, updateDarkParent]);
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(popoverTarget_1.PopoverTarget, { floatingData: floatingData, handleMouseEnter: handleMouseEnter, handleMouseLeave: handleMouseLeave, handleTargetBlur: handleTargetBlur, handleTargetContextMenu: handleTargetContextMenu, handleTargetFocus: handleTargetFocus, isContentEmpty: isContentEmpty, isControlled: isControlled, isHoverInteractionKind: isHoverInteractionKind, openOnTargetFocus: openOnTargetFocus, ref: targetRef, ...props, children: children }), !isContentEmpty && ((0, jsx_runtime_1.jsx)(popoverPopup_1.PopoverPopup, { animation: animation, arrowRef: arrowRef, floatingData: floatingData, handleMouseEnter: handleMouseEnter, handleMouseLeave: handleMouseLeave, handleOverlayClose: handleOverlayClose, handlePopoverClick: handlePopoverClick, hasDarkParent: hasDarkParent, isClosingViaEscapeKeypress: isClosingViaEscapeKeypress, isHoverInteractionKind: isHoverInteractionKind, shouldReturnFocusOnClose: shouldReturnFocusOnClose, ...props }))] }));
});
exports.PopoverNext.displayName = `${common_1.DISPLAYNAME_PREFIX}.PopoverNext`;
function isEscapeKeypressEvent(e) {
return e instanceof KeyboardEvent && e.key === "Escape";
}
//# sourceMappingURL=popoverNext.js.map