UNPKG

@cometchat/chat-uikit-react

Version:

Ready-to-use Chat UI Components for React

558 lines (554 loc) 18.1 kB
'use strict'; var chunkC7J4AZRB_cjs = require('./chunk-C7J4AZRB.cjs'); var react = require('react'); var jsxRuntime = require('react/jsx-runtime'); require('./index.css'); var CometChatPopoverContext = react.createContext(null); function useCometChatPopoverContext() { const ctx = react.useContext(CometChatPopoverContext); if (!ctx) { throw new Error("useCometChatPopoverContext must be used within <CometChatPopover.Root>"); } return ctx; } var VIEWPORT_MARGIN = 10; var TRIGGER_GAP = 10; var ARROW_GAP = 5; function getAvailablePlacement(triggerRect, contentHeight, contentWidth, preferred, currentWindow) { const spaceAbove = triggerRect.top; const spaceBelow = currentWindow.innerHeight - triggerRect.bottom; const spaceLeft = triggerRect.left; const spaceRight = currentWindow.innerWidth - triggerRect.right; const fits = (placement) => { switch (placement) { case "top": return spaceAbove >= contentHeight + VIEWPORT_MARGIN; case "bottom": return spaceBelow >= contentHeight + VIEWPORT_MARGIN; case "left": return spaceLeft >= contentWidth + VIEWPORT_MARGIN; case "right": return spaceRight >= contentWidth + VIEWPORT_MARGIN; } }; if (fits(preferred)) return preferred; const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" }; const remaining = ["top", "bottom", "left", "right"].filter( (p) => p !== preferred && p !== opposite[preferred] ); if (fits(opposite[preferred])) return opposite[preferred]; for (const p of remaining) { if (fits(p)) return p; } return preferred; } function computePosition(triggerRect, contentWidth, contentHeight, placement, showArrow, currentWindow) { const gap = showArrow ? ARROW_GAP : TRIGGER_GAP; const viewportWidth = currentWindow.innerWidth; const viewportHeight = currentWindow.innerHeight; const style = {}; if (placement === "top" || placement === "bottom") { const topVal = placement === "top" ? triggerRect.top - contentHeight - gap : triggerRect.bottom + gap; style.top = `${String(topVal)}px`; let left = triggerRect.left; left = Math.max( VIEWPORT_MARGIN, Math.min(left, viewportWidth - contentWidth - VIEWPORT_MARGIN) ); style.left = `${String(left)}px`; } else { const leftVal = placement === "left" ? triggerRect.left - contentWidth - gap : triggerRect.right + gap; style.left = `${String(leftVal)}px`; let top = triggerRect.top + triggerRect.height / 2 - contentHeight / 2; top = Math.max( VIEWPORT_MARGIN, Math.min(top, viewportHeight - contentHeight - VIEWPORT_MARGIN) ); style.top = `${String(top)}px`; } return style; } var CometChatPopoverRoot = ({ isOpen: isOpenProp, onClose, onOpen, placement = "bottom", closeOnOutsideClick = true, showOnHover = false, debounceOnHover = 500, showArrow = false, trapFocus = false, ariaLabel, ariaLabelledBy, ariaDescribedBy, children, className }) => { const triggerRef = react.useRef(null); const popoverRef = react.useRef(null); const previousFocusRef = react.useRef(null); const resizeTimeoutRef = react.useRef(null); const resizeObserverRef = react.useRef(null); const IframeContext = chunkC7J4AZRB_cjs.useCometChatFrameContext(); const getCurrentDocument = react.useCallback(() => { return IframeContext.iframeDocument ?? document; }, [IframeContext.iframeDocument]); const getCurrentWindow = react.useCallback(() => { return IframeContext.iframeWindow ?? window; }, [IframeContext.iframeWindow]); const reactId = react.useId(); const popoverId = `cometchat-popover-${reactId.replace(/:/g, "")}`; const [internalOpen, setInternalOpen] = react.useState(false); const isControlled = isOpenProp !== void 0; const isOpen = isControlled ? isOpenProp : internalOpen; const [computedPlacement, setComputedPlacement] = react.useState(placement); const [positionStyle, setPositionStyle] = react.useState({}); const [isPositioned, setIsPositioned] = react.useState(false); const handleOpen = react.useCallback(() => { if (isOpen) return; previousFocusRef.current = getCurrentDocument().activeElement; if (!isControlled) { setInternalOpen(true); } if (onOpen) { onOpen(); } }, [isOpen, isControlled, onOpen, getCurrentDocument]); const handleClose = react.useCallback(() => { if (!isOpen) return; if (!isControlled) { setInternalOpen(false); } if (onClose) { onClose(); } }, [isOpen, isControlled, onClose]); const handleToggle = react.useCallback(() => { if (isOpen) { handleClose(); } else { handleOpen(); } }, [isOpen, handleOpen, handleClose]); const updatePosition = react.useCallback(() => { if (!triggerRef.current || !popoverRef.current) return; const triggerRect = triggerRef.current.getBoundingClientRect(); const contentEl = popoverRef.current; contentEl.style.visibility = "hidden"; contentEl.style.display = "block"; const contentWidth = contentEl.scrollWidth || contentEl.offsetWidth; const contentHeight = contentEl.scrollHeight || contentEl.offsetHeight; contentEl.style.visibility = ""; contentEl.style.display = ""; const resolved = getAvailablePlacement( triggerRect, contentHeight, contentWidth, placement, getCurrentWindow() ); const style = computePosition( triggerRect, contentWidth, contentHeight, resolved, showArrow, getCurrentWindow() ); setComputedPlacement(resolved); setPositionStyle(style); setIsPositioned(true); }, [placement, showArrow, getCurrentWindow]); react.useEffect(() => { if (isOpen) { setIsPositioned(false); const id = requestAnimationFrame(() => { updatePosition(); }); return () => { cancelAnimationFrame(id); }; } else { setIsPositioned(false); setPositionStyle({}); } return void 0; }, [isOpen, updatePosition]); react.useEffect(() => { if (isOpen && trapFocus && popoverRef.current) { const id = requestAnimationFrame(() => { const focusable = popoverRef.current?.querySelectorAll( 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ); if (focusable && focusable.length > 0) { focusable[0].focus(); } else { popoverRef.current?.focus(); } }); return () => { cancelAnimationFrame(id); }; } else if (!isOpen && previousFocusRef.current) { previousFocusRef.current.focus(); previousFocusRef.current = null; } return void 0; }, [isOpen, trapFocus]); react.useEffect(() => { if (!isOpen || !closeOnOutsideClick) return; const handleMouseDown = (event) => { const target = event.target; const trigger = triggerRef.current; const popover = popoverRef.current; if (trigger && !trigger.contains(target) && popover && !popover.contains(target)) { handleClose(); } }; getCurrentDocument().addEventListener("mousedown", handleMouseDown); return () => { getCurrentDocument().removeEventListener("mousedown", handleMouseDown); }; }, [isOpen, closeOnOutsideClick, handleClose, getCurrentDocument]); react.useEffect(() => { if (!isOpen) return; const handleKeyDown = (event) => { if (event.key === "Escape") { event.preventDefault(); handleClose(); } }; getCurrentDocument().addEventListener("keydown", handleKeyDown); return () => { getCurrentDocument().removeEventListener("keydown", handleKeyDown); }; }, [isOpen, handleClose, getCurrentDocument]); react.useEffect(() => { if (!isOpen) return; const handleScroll = () => { if (triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect(); if (rect.bottom < 0 || rect.top > getCurrentWindow().innerHeight || rect.right < 0 || rect.left > getCurrentWindow().innerWidth) { handleClose(); return; } } requestAnimationFrame(updatePosition); }; const handleResize = () => { if (resizeTimeoutRef.current) { clearTimeout(resizeTimeoutRef.current); } resizeTimeoutRef.current = setTimeout(() => { requestAnimationFrame(updatePosition); }, 0); }; getCurrentWindow().addEventListener("scroll", handleScroll, true); getCurrentWindow().addEventListener("resize", handleResize); return () => { getCurrentWindow().removeEventListener("scroll", handleScroll, true); getCurrentWindow().removeEventListener("resize", handleResize); if (resizeTimeoutRef.current) { clearTimeout(resizeTimeoutRef.current); } }; }, [isOpen, updatePosition, handleClose, getCurrentWindow]); react.useEffect(() => { if (!isOpen || !popoverRef.current || typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(() => { requestAnimationFrame(updatePosition); }); observer.observe(popoverRef.current); resizeObserverRef.current = observer; return () => { observer.disconnect(); resizeObserverRef.current = null; }; }, [isOpen, updatePosition]); const ctxValue = react.useMemo( () => ({ isOpen, open: handleOpen, close: handleClose, toggle: handleToggle, showOnHover, debounceOnHover, showArrow, trapFocus, triggerRef, popoverRef, popoverId, ariaLabel, ariaLabelledBy, ariaDescribedBy, computedPlacement, positionStyle, isPositioned }), [ isOpen, handleOpen, handleClose, handleToggle, showOnHover, debounceOnHover, showArrow, trapFocus, popoverId, ariaLabel, ariaLabelledBy, ariaDescribedBy, computedPlacement, positionStyle, isPositioned ] ); const rootClass = ["cometchat-popover", className].filter(Boolean).join(" "); return /* @__PURE__ */ jsxRuntime.jsx(CometChatPopoverContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: rootClass, children }) }); }; CometChatPopoverRoot.displayName = "CometChatPopoverRoot"; var CometChatPopoverTrigger = ({ children, className }) => { const { isOpen, toggle, open, close, showOnHover, debounceOnHover, showArrow, trapFocus, triggerRef, popoverId } = useCometChatPopoverContext(); const IframeContext = chunkC7J4AZRB_cjs.useCometChatFrameContext(); const getCurrentDocument = react.useCallback(() => { return IframeContext.iframeDocument ?? document; }, [IframeContext.iframeDocument]); const hoverTimeoutRef = react.useRef(null); const clearHoverTimeout = react.useCallback(() => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; } }, []); const handleClick = react.useCallback( (event) => { event.stopPropagation(); if (!showOnHover) { toggle(); } }, [showOnHover, toggle] ); const handleKeyDown = react.useCallback( (event) => { if (showOnHover) return; const target = event.target; const currentTarget = event.currentTarget; if (target !== currentTarget) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); event.stopPropagation(); toggle(); } }, [showOnHover, toggle] ); const handleMouseEnter = react.useCallback(() => { if (!showOnHover) return; clearHoverTimeout(); if (!isOpen) { hoverTimeoutRef.current = setTimeout(() => { open(); }, debounceOnHover); } }, [showOnHover, isOpen, open, debounceOnHover, clearHoverTimeout]); const handleMouseLeave = react.useCallback( (event) => { if (!showOnHover) return; clearHoverTimeout(); const relatedTarget = event.relatedTarget; const popoverEl = getCurrentDocument().getElementById(popoverId); if (relatedTarget && popoverEl) { try { if (popoverEl.contains(relatedTarget)) { return; } } catch { } } if (isOpen) { hoverTimeoutRef.current = setTimeout(() => { close(); }, debounceOnHover); } }, [showOnHover, isOpen, close, debounceOnHover, clearHoverTimeout, popoverId, getCurrentDocument] ); const isTooltipMode = showArrow && !trapFocus; const ariaHasPopup = isTooltipMode ? void 0 : "dialog"; const triggerClass = ["cometchat-popover__trigger", className].filter(Boolean).join(" "); return /* @__PURE__ */ jsxRuntime.jsx( "div", { ref: triggerRef, className: triggerClass, onClick: handleClick, onKeyDown: handleKeyDown, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, "aria-expanded": isOpen, "aria-haspopup": ariaHasPopup, "aria-controls": isOpen ? popoverId : void 0, children } ); }; CometChatPopoverTrigger.displayName = "CometChatPopoverTrigger"; var CometChatPopoverContent = ({ children, className }) => { const { isOpen, close, trapFocus, showOnHover, debounceOnHover, showArrow, triggerRef, popoverRef, popoverId, ariaLabel, ariaLabelledBy, ariaDescribedBy, computedPlacement, positionStyle, isPositioned } = useCometChatPopoverContext(); const IframeContext = chunkC7J4AZRB_cjs.useCometChatFrameContext(); const getCurrentDocument = react.useCallback(() => { return IframeContext.iframeDocument ?? document; }, [IframeContext.iframeDocument]); const hoverTimeoutRef = react.useRef(null); const clearHoverTimeout = react.useCallback(() => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; } }, []); const handleContentMouseEnter = react.useCallback(() => { if (!showOnHover) return; clearHoverTimeout(); }, [showOnHover, clearHoverTimeout]); const handleContentMouseLeave = react.useCallback( (event) => { if (!showOnHover) return; clearHoverTimeout(); const relatedTarget = event.relatedTarget; if (relatedTarget && triggerRef.current?.contains(relatedTarget)) { return; } hoverTimeoutRef.current = setTimeout(() => { close(); }, debounceOnHover); }, [showOnHover, close, debounceOnHover, clearHoverTimeout, triggerRef] ); const handleKeyDown = react.useCallback( (event) => { if (!trapFocus) return; if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) { event.stopPropagation(); } if (event.key === "Tab") { const container = popoverRef.current; if (!container) return; const focusable = Array.from( container.querySelectorAll( 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ) ); if (focusable.length === 0) return; const currentIdx = focusable.indexOf(getCurrentDocument().activeElement); let nextIdx; if (event.shiftKey) { nextIdx = currentIdx <= 0 ? focusable.length - 1 : currentIdx - 1; } else { nextIdx = currentIdx >= focusable.length - 1 ? 0 : currentIdx + 1; } event.preventDefault(); focusable[nextIdx]?.focus(); } }, [trapFocus, popoverRef, getCurrentDocument] ); if (!isOpen) return null; const isTooltipMode = showArrow && !trapFocus; const role = isTooltipMode ? "tooltip" : "dialog"; const contentClasses = [ "cometchat-popover__content", `cometchat-popover__content--${computedPlacement}`, isPositioned ? "cometchat-popover__content--positioned" : "", className ].filter(Boolean).join(" "); const arrow = showArrow ? /* @__PURE__ */ jsxRuntime.jsx( "div", { className: ["cometchat-popover__arrow", `cometchat-popover__arrow--${computedPlacement}`].filter(Boolean).join(" "), "aria-hidden": "true" } ) : null; const arrowBefore = computedPlacement === "bottom" || computedPlacement === "right"; const arrowAfter = computedPlacement === "top" || computedPlacement === "left"; return /* @__PURE__ */ jsxRuntime.jsxs( "div", { ref: popoverRef, id: popoverId, className: contentClasses, style: positionStyle, role, "aria-modal": !isTooltipMode && trapFocus ? "true" : void 0, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, tabIndex: trapFocus ? -1 : void 0, onKeyDown: trapFocus ? handleKeyDown : void 0, onMouseEnter: showOnHover ? handleContentMouseEnter : void 0, onMouseLeave: showOnHover ? handleContentMouseLeave : void 0, children: [ arrowBefore && arrow, children, arrowAfter && arrow ] } ); }; CometChatPopoverContent.displayName = "CometChatPopoverContent"; var CometChatPopoverComponent = ({ trigger, content, ...rootProps }) => { return /* @__PURE__ */ jsxRuntime.jsxs(CometChatPopoverRoot, { ...rootProps, children: [ /* @__PURE__ */ jsxRuntime.jsx(CometChatPopoverTrigger, { children: trigger }), /* @__PURE__ */ jsxRuntime.jsx(CometChatPopoverContent, { children: content }) ] }); }; CometChatPopoverComponent.displayName = "CometChatPopover"; var CometChatPopover = Object.assign(CometChatPopoverComponent, { Root: CometChatPopoverRoot, Trigger: CometChatPopoverTrigger, Content: CometChatPopoverContent }); exports.CometChatPopover = CometChatPopover; exports.useCometChatPopoverContext = useCometChatPopoverContext;