UNPKG

@blueprintjs/core

Version:
275 lines 14.6 kB
"use strict"; 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