@cometchat/chat-uikit-react
Version:
Ready-to-use Chat UI Components for React
558 lines (554 loc) • 18.1 kB
JavaScript
;
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;