From aa299437ec3ae35b0de553e79b20d5f252b8aa56 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 17 Apr 2026 13:57:19 -0300 Subject: [PATCH 01/42] patch useOverlayPosition --- ...-aria-overlays-npm-3.25.0-2628866e6e.patch | 354 ++++++++++++++++++ package.json | 1 + yarn.lock | 24 +- 3 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 .yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch diff --git a/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch new file mode 100644 index 0000000000000..a59b74468509d --- /dev/null +++ b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch @@ -0,0 +1,354 @@ +diff --git a/dist/useOverlayPosition.main.js b/dist/useOverlayPosition.main.js +index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec30218b6da4d2 100644 +--- a/dist/useOverlayPosition.main.js ++++ b/dist/useOverlayPosition.main.js +@@ -25,12 +25,23 @@ $parcel$export(module.exports, "useOverlayPosition", () => $cd94b4896dd97759$exp + + + +-let $cd94b4896dd97759$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $cd94b4896dd97759$var$getWindowAndVisualViewport = (targetNode)=>{ ++ var _window; ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = ((_window = window) === null || _window === void 0 ? void 0 : _window.visualViewport) || null; ++ return [ ++ actualWindow, ++ visualViewport ++ ]; ++}; + function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + let { direction: direction } = (0, $6TXnl$reactariai18n.useLocale)(); + let { arrowSize: arrowSize = 0, targetRef: targetRef, overlayRef: overlayRef, scrollRef: scrollRef = overlayRef, placement: placement = 'bottom', containerPadding: containerPadding = 12, shouldFlip: shouldFlip = true, boundaryElement: boundaryElement = typeof document !== 'undefined' ? document.body : null, offset: offset = 0, crossOffset: crossOffset = 0, shouldUpdatePosition: shouldUpdatePosition = true, isOpen: isOpen = true, onClose: onClose, maxHeight: maxHeight, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = props; + let [position, setPosition] = (0, $6TXnl$react.useState)(null); ++ let [actualWindow, visualViewport] = $cd94b4896dd97759$var$getWindowAndVisualViewport(targetRef.current); + let deps = [ ++ visualViewport, ++ actualWindow, + shouldUpdatePosition, + placement, + overlayRef.current, +@@ -50,15 +61,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might + // just be a non-realistic use case + // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles +- let lastScale = (0, $6TXnl$react.useRef)($cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.scale); ++ let lastScale = (0, $6TXnl$react.useRef)(visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale); + (0, $6TXnl$react.useEffect)(()=>{ +- if (isOpen) lastScale.current = $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.scale; ++ if (isOpen) lastScale.current = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale; + }, [ + isOpen + ]); + let updatePosition = (0, $6TXnl$react.useCallback)(()=>{ + if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) return; +- if (($cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.scale) !== lastScale.current) return; ++ if ((visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== lastScale.current) return; + // Determine a scroll anchor based on the focused element. + // This stores the offset of the anchor element from the scroll container + // so it can be restored after repositioning. This way if the overlay height +@@ -85,11 +96,10 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. + let overlay = overlayRef.current; + if (!maxHeight && overlayRef.current) { +- var _window_visualViewport; + overlay.style.top = '0px'; + overlay.style.bottom = ''; +- var _window_visualViewport_height; +- overlay.style.maxHeight = ((_window_visualViewport_height = (_window_visualViewport = window.visualViewport) === null || _window_visualViewport === void 0 ? void 0 : _window_visualViewport.height) !== null && _window_visualViewport_height !== void 0 ? _window_visualViewport_height : window.innerHeight) + 'px'; ++ var _visualViewport_height; ++ overlay.style.maxHeight = ((_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : actualWindow.innerHeight) + 'px'; + } + let position = (0, $5935ba4d7da2c103$exports.calculatePosition)({ + placement: $cd94b4896dd97759$var$translateRTL(placement, direction), +@@ -129,7 +139,7 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, $6TXnl$reactariautils.useLayoutEffect)(updatePosition, deps); + // Update position on window resize +- $cd94b4896dd97759$var$useResize(updatePosition); ++ $cd94b4896dd97759$var$useResize(updatePosition, actualWindow); + // Update position when the overlay changes size (might need to flip). + (0, $6TXnl$reactariautils.useResizeObserver)({ + ref: overlayRef, +@@ -158,14 +168,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + let onScroll = ()=>{ + if (isResizing.current) onResize(); + }; +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.addEventListener('resize', onResize); +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.addEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('scroll', onScroll); + return ()=>{ +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.removeEventListener('resize', onResize); +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.removeEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('scroll', onScroll); + }; + }, [ +- updatePosition ++ updatePosition, ++ visualViewport + ]); + let close = (0, $6TXnl$react.useCallback)(()=>{ + if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); +@@ -202,14 +213,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + updatePosition: updatePosition + }; + } +-function $cd94b4896dd97759$var$useResize(onResize) { ++function $cd94b4896dd97759$var$useResize(onResize, actualWindow = window) { + (0, $6TXnl$reactariautils.useLayoutEffect)(()=>{ +- window.addEventListener('resize', onResize, false); ++ actualWindow.addEventListener('resize', onResize, false); + return ()=>{ +- window.removeEventListener('resize', onResize, false); ++ actualWindow.removeEventListener('resize', onResize, false); + }; + }, [ +- onResize ++ onResize, ++ actualWindow + ]); + } + function $cd94b4896dd97759$var$translateRTL(position, direction) { +diff --git a/dist/useOverlayPosition.main.js.map b/dist/useOverlayPosition.main.js.map +index 05aa0e8aaf07ec8b4946a29a6276ac77519813c0..39b2c1a12dca8a591cfb50aefd561902d1f7fc2d 100644 +--- a/dist/useOverlayPosition.main.js.map ++++ b/dist/useOverlayPosition.main.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n\n let deps = [\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize) {\n useLayoutEffect(() => {\n window.addEventListener('resize', onResize, false);\n return () => {\n window.removeEventListener('resize', onResize, false);\n };\n }, [onResize]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window & typeof globalThis, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = window?.visualViewport || null\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} +\ No newline at end of file +diff --git a/dist/useOverlayPosition.mjs b/dist/useOverlayPosition.mjs +index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de5060ba03214 100644 +--- a/dist/useOverlayPosition.mjs ++++ b/dist/useOverlayPosition.mjs +@@ -19,12 +19,23 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; + + + +-let $2a41e45df1593e64$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $2a41e45df1593e64$var$getWindowAndVisualViewport = (targetNode)=>{ ++ var _window; ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = ((_window = window) === null || _window === void 0 ? void 0 : _window.visualViewport) || null; ++ return [ ++ actualWindow, ++ visualViewport ++ ]; ++}; + function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let { direction: direction } = (0, $39EOa$useLocale)(); + let { arrowSize: arrowSize = 0, targetRef: targetRef, overlayRef: overlayRef, scrollRef: scrollRef = overlayRef, placement: placement = 'bottom', containerPadding: containerPadding = 12, shouldFlip: shouldFlip = true, boundaryElement: boundaryElement = typeof document !== 'undefined' ? document.body : null, offset: offset = 0, crossOffset: crossOffset = 0, shouldUpdatePosition: shouldUpdatePosition = true, isOpen: isOpen = true, onClose: onClose, maxHeight: maxHeight, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = props; + let [position, setPosition] = (0, $39EOa$useState)(null); ++ let [actualWindow, visualViewport] = $2a41e45df1593e64$var$getWindowAndVisualViewport(targetRef.current); + let deps = [ ++ visualViewport, ++ actualWindow, + shouldUpdatePosition, + placement, + overlayRef.current, +@@ -44,15 +55,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might + // just be a non-realistic use case + // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles +- let lastScale = (0, $39EOa$useRef)($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale); ++ let lastScale = (0, $39EOa$useRef)(visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale); + (0, $39EOa$useEffect)(()=>{ +- if (isOpen) lastScale.current = $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale; ++ if (isOpen) lastScale.current = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale; + }, [ + isOpen + ]); + let updatePosition = (0, $39EOa$useCallback)(()=>{ + if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) return; +- if (($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale) !== lastScale.current) return; ++ if ((visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== lastScale.current) return; + // Determine a scroll anchor based on the focused element. + // This stores the offset of the anchor element from the scroll container + // so it can be restored after repositioning. This way if the overlay height +@@ -79,11 +90,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. + let overlay = overlayRef.current; + if (!maxHeight && overlayRef.current) { +- var _window_visualViewport; + overlay.style.top = '0px'; + overlay.style.bottom = ''; +- var _window_visualViewport_height; +- overlay.style.maxHeight = ((_window_visualViewport_height = (_window_visualViewport = window.visualViewport) === null || _window_visualViewport === void 0 ? void 0 : _window_visualViewport.height) !== null && _window_visualViewport_height !== void 0 ? _window_visualViewport_height : window.innerHeight) + 'px'; ++ var _visualViewport_height; ++ overlay.style.maxHeight = ((_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : actualWindow.innerHeight) + 'px'; + } + let position = (0, $edcf132a9284368a$export$b3ceb0cbf1056d98)({ + placement: $2a41e45df1593e64$var$translateRTL(placement, direction), +@@ -123,7 +133,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, $39EOa$useLayoutEffect)(updatePosition, deps); + // Update position on window resize +- $2a41e45df1593e64$var$useResize(updatePosition); ++ $2a41e45df1593e64$var$useResize(updatePosition, actualWindow); + // Update position when the overlay changes size (might need to flip). + (0, $39EOa$useResizeObserver)({ + ref: overlayRef, +@@ -152,14 +162,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let onScroll = ()=>{ + if (isResizing.current) onResize(); + }; +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('scroll', onScroll); + return ()=>{ +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('scroll', onScroll); + }; + }, [ +- updatePosition ++ updatePosition, ++ visualViewport + ]); + let close = (0, $39EOa$useCallback)(()=>{ + if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); +@@ -196,14 +207,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + updatePosition: updatePosition + }; + } +-function $2a41e45df1593e64$var$useResize(onResize) { ++function $2a41e45df1593e64$var$useResize(onResize, actualWindow = window) { + (0, $39EOa$useLayoutEffect)(()=>{ +- window.addEventListener('resize', onResize, false); ++ actualWindow.addEventListener('resize', onResize, false); + return ()=>{ +- window.removeEventListener('resize', onResize, false); ++ actualWindow.removeEventListener('resize', onResize, false); + }; + }, [ +- onResize ++ onResize, ++ actualWindow + ]); + } + function $2a41e45df1593e64$var$translateRTL(position, direction) { +diff --git a/dist/useOverlayPosition.module.js b/dist/useOverlayPosition.module.js +index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280db36a865 100644 +--- a/dist/useOverlayPosition.module.js ++++ b/dist/useOverlayPosition.module.js +@@ -19,12 +19,23 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; + + + +-let $2a41e45df1593e64$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $2a41e45df1593e64$var$getWindowAndVisualViewport = (targetNode)=>{ ++ var _window; ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = ((_window = window) === null || _window === void 0 ? void 0 : _window.visualViewport) || null; ++ return [ ++ actualWindow, ++ visualViewport ++ ]; ++}; + function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let { direction: direction } = (0, $39EOa$useLocale)(); + let { arrowSize: arrowSize = 0, targetRef: targetRef, overlayRef: overlayRef, scrollRef: scrollRef = overlayRef, placement: placement = 'bottom', containerPadding: containerPadding = 12, shouldFlip: shouldFlip = true, boundaryElement: boundaryElement = typeof document !== 'undefined' ? document.body : null, offset: offset = 0, crossOffset: crossOffset = 0, shouldUpdatePosition: shouldUpdatePosition = true, isOpen: isOpen = true, onClose: onClose, maxHeight: maxHeight, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = props; + let [position, setPosition] = (0, $39EOa$useState)(null); ++ let [actualWindow, visualViewport] = $2a41e45df1593e64$var$getWindowAndVisualViewport(targetRef.current); + let deps = [ ++ visualViewport, ++ actualWindow, + shouldUpdatePosition, + placement, + overlayRef.current, +@@ -44,15 +55,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might + // just be a non-realistic use case + // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles +- let lastScale = (0, $39EOa$useRef)($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale); ++ let lastScale = (0, $39EOa$useRef)(visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale); + (0, $39EOa$useEffect)(()=>{ +- if (isOpen) lastScale.current = $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale; ++ if (isOpen) lastScale.current = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale; + }, [ + isOpen + ]); + let updatePosition = (0, $39EOa$useCallback)(()=>{ + if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) return; +- if (($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale) !== lastScale.current) return; ++ if ((visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== lastScale.current) return; + // Determine a scroll anchor based on the focused element. + // This stores the offset of the anchor element from the scroll container + // so it can be restored after repositioning. This way if the overlay height +@@ -79,11 +90,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. + let overlay = overlayRef.current; + if (!maxHeight && overlayRef.current) { +- var _window_visualViewport; + overlay.style.top = '0px'; + overlay.style.bottom = ''; +- var _window_visualViewport_height; +- overlay.style.maxHeight = ((_window_visualViewport_height = (_window_visualViewport = window.visualViewport) === null || _window_visualViewport === void 0 ? void 0 : _window_visualViewport.height) !== null && _window_visualViewport_height !== void 0 ? _window_visualViewport_height : window.innerHeight) + 'px'; ++ var _visualViewport_height; ++ overlay.style.maxHeight = ((_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : actualWindow.innerHeight) + 'px'; + } + let position = (0, $edcf132a9284368a$export$b3ceb0cbf1056d98)({ + placement: $2a41e45df1593e64$var$translateRTL(placement, direction), +@@ -123,7 +133,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, $39EOa$useLayoutEffect)(updatePosition, deps); + // Update position on window resize +- $2a41e45df1593e64$var$useResize(updatePosition); ++ $2a41e45df1593e64$var$useResize(updatePosition, actualWindow); + // Update position when the overlay changes size (might need to flip). + (0, $39EOa$useResizeObserver)({ + ref: overlayRef, +@@ -152,14 +162,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let onScroll = ()=>{ + if (isResizing.current) onResize(); + }; +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('scroll', onScroll); + return ()=>{ +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('scroll', onScroll); + }; + }, [ +- updatePosition ++ updatePosition, ++ visualViewport + ]); + let close = (0, $39EOa$useCallback)(()=>{ + if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); +@@ -196,14 +207,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + updatePosition: updatePosition + }; + } +-function $2a41e45df1593e64$var$useResize(onResize) { ++function $2a41e45df1593e64$var$useResize(onResize, actualWindow = window) { + (0, $39EOa$useLayoutEffect)(()=>{ +- window.addEventListener('resize', onResize, false); ++ actualWindow.addEventListener('resize', onResize, false); + return ()=>{ +- window.removeEventListener('resize', onResize, false); ++ actualWindow.removeEventListener('resize', onResize, false); + }; + }, [ +- onResize ++ onResize, ++ actualWindow + ]); + } + function $2a41e45df1593e64$var$translateRTL(position, direction) { +diff --git a/dist/useOverlayPosition.module.js.map b/dist/useOverlayPosition.module.js.map +index fc91cdbf9437c324ff1264e12f9f9bbb84f5bd04..474289d82f87ec910abe4b871bad36be382a0c97 100644 +--- a/dist/useOverlayPosition.module.js.map ++++ b/dist/useOverlayPosition.module.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n\n let deps = [\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize) {\n useLayoutEffect(() => {\n window.addEventListener('resize', onResize, false);\n return () => {\n window.removeEventListener('resize', onResize, false);\n };\n }, [onResize]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window & typeof globalThis, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = window?.visualViewport || null\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} +\ No newline at end of file diff --git a/package.json b/package.json index 2d34e6b121f74..be2b8749eae8e 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "@react-aria/i18n@npm:^3.12.5": "patch:@react-aria/i18n@npm%3A3.12.5#~/.yarn/patches/@react-aria-i18n-npm-3.12.5-435edff786.patch", "@react-aria/toolbar@npm:^3.0.0-nightly.5042": "3.0.0-nightly-fb28ab3b4-241024", "xml-crypto/@xmldom/xmldom": "0.8.13", + "@react-aria/overlays@npm:^3.25.0": "patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch", "xml-encryption/@xmldom/xmldom": "0.8.13", "form-data@npm:^2.5.0": "2.5.5", "form-data@npm:^4.0.0": "4.0.5", diff --git a/yarn.lock b/yarn.lock index 63d36d8332850..c8cfa1f86ca27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7268,7 +7268,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/overlays@npm:^3.25.0": +"@react-aria/overlays@npm:3.25.0": version: 3.25.0 resolution: "@react-aria/overlays@npm:3.25.0" dependencies: @@ -7290,6 +7290,28 @@ __metadata: languageName: node linkType: hard +"@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch": + version: 3.25.0 + resolution: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch::version=3.25.0&hash=5cb6f6" + dependencies: + "@react-aria/focus": "npm:^3.19.1" + "@react-aria/i18n": "npm:^3.12.5" + "@react-aria/interactions": "npm:^3.23.0" + "@react-aria/ssr": "npm:^3.9.7" + "@react-aria/utils": "npm:^3.27.0" + "@react-aria/visually-hidden": "npm:^3.8.19" + "@react-stately/overlays": "npm:^3.6.13" + "@react-types/button": "npm:^3.10.2" + "@react-types/overlays": "npm:^3.8.12" + "@react-types/shared": "npm:^3.27.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/dd5b420d0cda39dbd9c60a5257e2783d22196b77ec98b3982a2176abb25d117f3593f49da7ee2e6df8799e0a9dc43b7d88b503d419f3d57310b041bb3f781f7b + languageName: node + linkType: hard + "@react-aria/progress@npm:^3.4.19": version: 3.4.19 resolution: "@react-aria/progress@npm:3.4.19" From 6bf4f7af7a66bcfc87e39fb90144fd3871ea08df Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 23 Apr 2026 16:51:44 -0300 Subject: [PATCH 02/42] Fix patch --- ...-aria-overlays-npm-3.25.0-2628866e6e.patch | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch index a59b74468509d..e6b13c5de83ea 100644 --- a/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch +++ b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch @@ -1,16 +1,15 @@ diff --git a/dist/useOverlayPosition.main.js b/dist/useOverlayPosition.main.js -index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec30218b6da4d2 100644 +index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef85b7a4d93 100644 --- a/dist/useOverlayPosition.main.js +++ b/dist/useOverlayPosition.main.js -@@ -25,12 +25,23 @@ $parcel$export(module.exports, "useOverlayPosition", () => $cd94b4896dd97759$exp +@@ -25,12 +25,22 @@ $parcel$export(module.exports, "useOverlayPosition", () => $cd94b4896dd97759$exp -let $cd94b4896dd97759$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; +let $cd94b4896dd97759$var$getWindowAndVisualViewport = (targetNode)=>{ -+ var _window; + let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; -+ let visualViewport = ((_window = window) === null || _window === void 0 ? void 0 : _window.visualViewport) || null; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.visualViewport) || null; + return [ + actualWindow, + visualViewport @@ -27,7 +26,7 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec3021 shouldUpdatePosition, placement, overlayRef.current, -@@ -50,15 +61,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { +@@ -50,15 +60,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might // just be a non-realistic use case // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles @@ -46,7 +45,7 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec3021 // Determine a scroll anchor based on the focused element. // This stores the offset of the anchor element from the scroll container // so it can be restored after repositioning. This way if the overlay height -@@ -85,11 +96,10 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { +@@ -85,11 +95,10 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. let overlay = overlayRef.current; if (!maxHeight && overlayRef.current) { @@ -60,7 +59,7 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec3021 } let position = (0, $5935ba4d7da2c103$exports.calculatePosition)({ placement: $cd94b4896dd97759$var$translateRTL(placement, direction), -@@ -129,7 +139,7 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { +@@ -129,7 +138,7 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { // eslint-disable-next-line react-hooks/exhaustive-deps (0, $6TXnl$reactariautils.useLayoutEffect)(updatePosition, deps); // Update position on window resize @@ -69,7 +68,7 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec3021 // Update position when the overlay changes size (might need to flip). (0, $6TXnl$reactariautils.useResizeObserver)({ ref: overlayRef, -@@ -158,14 +168,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { +@@ -158,14 +167,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { let onScroll = ()=>{ if (isResizing.current) onResize(); }; @@ -90,7 +89,7 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec3021 ]); let close = (0, $6TXnl$react.useCallback)(()=>{ if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); -@@ -202,14 +213,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { +@@ -202,14 +212,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { updatePosition: updatePosition }; } @@ -111,27 +110,26 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..46b3201a0d314a4ebfbe581721ec3021 } function $cd94b4896dd97759$var$translateRTL(position, direction) { diff --git a/dist/useOverlayPosition.main.js.map b/dist/useOverlayPosition.main.js.map -index 05aa0e8aaf07ec8b4946a29a6276ac77519813c0..39b2c1a12dca8a591cfb50aefd561902d1f7fc2d 100644 +index 05aa0e8aaf07ec8b4946a29a6276ac77519813c0..a2caed5c64bd2f1dfd4f3654c8c8f71927be50eb 100644 --- a/dist/useOverlayPosition.main.js.map +++ b/dist/useOverlayPosition.main.js.map @@ -1 +1 @@ -{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n\n let deps = [\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize) {\n useLayoutEffect(() => {\n window.addEventListener('resize', onResize, false);\n return () => {\n window.removeEventListener('resize', onResize, false);\n };\n }, [onResize]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} \ No newline at end of file -+{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window & typeof globalThis, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = window?.visualViewport || null\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = actualWindow?.visualViewport || null;\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow: Window = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} \ No newline at end of file diff --git a/dist/useOverlayPosition.mjs b/dist/useOverlayPosition.mjs -index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de5060ba03214 100644 +index 1a1371765ece0839f2ddfdcac60731e2c904e93f..f6e02859a5d60ce9503a93173b453b8553f76a70 100644 --- a/dist/useOverlayPosition.mjs +++ b/dist/useOverlayPosition.mjs -@@ -19,12 +19,23 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; +@@ -19,12 +19,22 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; -let $2a41e45df1593e64$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; +let $2a41e45df1593e64$var$getWindowAndVisualViewport = (targetNode)=>{ -+ var _window; + let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; -+ let visualViewport = ((_window = window) === null || _window === void 0 ? void 0 : _window.visualViewport) || null; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.visualViewport) || null; + return [ + actualWindow, + visualViewport @@ -148,7 +146,7 @@ index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de506 shouldUpdatePosition, placement, overlayRef.current, -@@ -44,15 +55,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -44,15 +54,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might // just be a non-realistic use case // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles @@ -167,7 +165,7 @@ index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de506 // Determine a scroll anchor based on the focused element. // This stores the offset of the anchor element from the scroll container // so it can be restored after repositioning. This way if the overlay height -@@ -79,11 +90,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -79,11 +89,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. let overlay = overlayRef.current; if (!maxHeight && overlayRef.current) { @@ -181,7 +179,7 @@ index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de506 } let position = (0, $edcf132a9284368a$export$b3ceb0cbf1056d98)({ placement: $2a41e45df1593e64$var$translateRTL(placement, direction), -@@ -123,7 +133,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -123,7 +132,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { // eslint-disable-next-line react-hooks/exhaustive-deps (0, $39EOa$useLayoutEffect)(updatePosition, deps); // Update position on window resize @@ -190,7 +188,7 @@ index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de506 // Update position when the overlay changes size (might need to flip). (0, $39EOa$useResizeObserver)({ ref: overlayRef, -@@ -152,14 +162,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -152,14 +161,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { let onScroll = ()=>{ if (isResizing.current) onResize(); }; @@ -211,7 +209,7 @@ index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de506 ]); let close = (0, $39EOa$useCallback)(()=>{ if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); -@@ -196,14 +207,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -196,14 +206,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { updatePosition: updatePosition }; } @@ -232,18 +230,17 @@ index 1a1371765ece0839f2ddfdcac60731e2c904e93f..141c66d3b4fc9a23c17e93dc511de506 } function $2a41e45df1593e64$var$translateRTL(position, direction) { diff --git a/dist/useOverlayPosition.module.js b/dist/useOverlayPosition.module.js -index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280db36a865 100644 +index c87314995a34cb98128a28c4c0acf23dbefc9359..e7a834e44579e23558be7ba178da2150717192f4 100644 --- a/dist/useOverlayPosition.module.js +++ b/dist/useOverlayPosition.module.js -@@ -19,12 +19,23 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; +@@ -19,12 +19,22 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; -let $2a41e45df1593e64$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; +let $2a41e45df1593e64$var$getWindowAndVisualViewport = (targetNode)=>{ -+ var _window; + let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; -+ let visualViewport = ((_window = window) === null || _window === void 0 ? void 0 : _window.visualViewport) || null; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.visualViewport) || null; + return [ + actualWindow, + visualViewport @@ -260,7 +257,7 @@ index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280 shouldUpdatePosition, placement, overlayRef.current, -@@ -44,15 +55,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -44,15 +54,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might // just be a non-realistic use case // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles @@ -279,7 +276,7 @@ index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280 // Determine a scroll anchor based on the focused element. // This stores the offset of the anchor element from the scroll container // so it can be restored after repositioning. This way if the overlay height -@@ -79,11 +90,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -79,11 +89,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. let overlay = overlayRef.current; if (!maxHeight && overlayRef.current) { @@ -293,7 +290,7 @@ index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280 } let position = (0, $edcf132a9284368a$export$b3ceb0cbf1056d98)({ placement: $2a41e45df1593e64$var$translateRTL(placement, direction), -@@ -123,7 +133,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -123,7 +132,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { // eslint-disable-next-line react-hooks/exhaustive-deps (0, $39EOa$useLayoutEffect)(updatePosition, deps); // Update position on window resize @@ -302,7 +299,7 @@ index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280 // Update position when the overlay changes size (might need to flip). (0, $39EOa$useResizeObserver)({ ref: overlayRef, -@@ -152,14 +162,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -152,14 +161,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { let onScroll = ()=>{ if (isResizing.current) onResize(); }; @@ -323,7 +320,7 @@ index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280 ]); let close = (0, $39EOa$useCallback)(()=>{ if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); -@@ -196,14 +207,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { +@@ -196,14 +206,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { updatePosition: updatePosition }; } @@ -344,11 +341,11 @@ index c87314995a34cb98128a28c4c0acf23dbefc9359..2757d4b57d8863ad5e0ec2260e4a9280 } function $2a41e45df1593e64$var$translateRTL(position, direction) { diff --git a/dist/useOverlayPosition.module.js.map b/dist/useOverlayPosition.module.js.map -index fc91cdbf9437c324ff1264e12f9f9bbb84f5bd04..474289d82f87ec910abe4b871bad36be382a0c97 100644 +index fc91cdbf9437c324ff1264e12f9f9bbb84f5bd04..e1d295495ed2b34b7ad6f6b30851c92258b9708b 100644 --- a/dist/useOverlayPosition.module.js.map +++ b/dist/useOverlayPosition.module.js.map @@ -1 +1 @@ -{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n\n let deps = [\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize) {\n useLayoutEffect(() => {\n window.addEventListener('resize', onResize, false);\n return () => {\n window.removeEventListener('resize', onResize, false);\n };\n }, [onResize]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} \ No newline at end of file -+{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window & typeof globalThis, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = window?.visualViewport || null\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = actualWindow?.visualViewport || null;\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow: Window = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} \ No newline at end of file From 88bcbca593de85e0cbf06a3325d623b5eca0a91a Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 14 Apr 2026 12:06:56 -0300 Subject: [PATCH 03/42] feat: Open call in a popup window (WIP) --- .../src/views/MediaCallPopoutWindow.tsx | 93 +++++++++++++++++++ .../MediaCallRoomActivity.tsx | 45 ++++++--- .../MediaCallRoomSection.tsx | 11 ++- 3 files changed, 132 insertions(+), 17 deletions(-) create mode 100644 packages/ui-voip/src/views/MediaCallPopoutWindow.tsx diff --git a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx new file mode 100644 index 0000000000000..5c54457bfaf25 --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx @@ -0,0 +1,93 @@ +import { StyleOptions as FuselageStyleOptions } from '@rocket.chat/fuselage'; +import { useStableCallback } from '@rocket.chat/fuselage-hooks'; +import { StyledOptions } from '@rocket.chat/styled'; +import type { ReactNode } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +type MediaCallPopoutWindowProps = { + children: ReactNode; + onClose: () => void; +}; + +const MediaCallPopoutWindow = ({ children, onClose }: MediaCallPopoutWindowProps) => { + const [container, setContainer] = useState<{ root: HTMLDivElement; externalWindow: Window } | null>(null); + const timeoutRef = useRef(null); + const triggerClose = useStableCallback(onClose); + + useEffect(() => { + if (container?.externalWindow.closed === false) { + return; + } + + let externalWindow: Window | null; + try { + // TODO: use peer name instead of 'Call with Peer X' + externalWindow = window.open('', 'Call with Peer X', 'width=800,height=500,popup'); + } catch (error) { + console.error('Failed to open external window', error); + triggerClose(); + return; + } + + if (!externalWindow) { + triggerClose(); + return; + } + + const win = externalWindow; + + const root = win.document.createElement('div'); + win.document.body.appendChild(root); + root.style.width = '100%'; + root.style.height = '100%'; + + Array.from(document.styleSheets).forEach((stylesheet) => { + if (stylesheet.href) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = stylesheet.href; + win.document.head.appendChild(link); + } else if (stylesheet?.cssRules?.length > 0) { + const style = document.createElement('style'); + Array.from(stylesheet.cssRules).forEach((rule) => { + style.appendChild(document.createTextNode(rule.cssText)); + }); + win.document.head.appendChild(style); + } + }); + + setContainer({ root, externalWindow: win }); + }, [triggerClose, container?.externalWindow.closed]); + + useEffect(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + const handleUnload = () => triggerClose(); + container?.externalWindow.addEventListener('beforeunload', handleUnload); + + return () => { + timeoutRef.current = setTimeout(() => { + container?.externalWindow.removeEventListener('beforeunload', handleUnload); + container?.externalWindow.close(); + triggerClose(); + }, 400); + }; + }, [container?.externalWindow, triggerClose]); + + const contextValue = useMemo(() => ({ document: container?.externalWindow.document }), [container?.externalWindow.document]); + + if (!container) { + return null; + } + + return ( + + {createPortal(children, container.root)} + + ); +}; + +export default MediaCallPopoutWindow; diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomActivity.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomActivity.tsx index b525c6b0a5d79..8db8c16e0cbc0 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomActivity.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomActivity.tsx @@ -7,6 +7,7 @@ import { useMemo, useState } from 'react'; import MediaCallRoomSection from './MediaCallRoomSection'; import MediaCallViewProvider from '../../providers/MediaCallViewProvider'; +import MediaCallPopoutWindow from '../MediaCallPopoutWindow'; type MediaCallRoomActivityProps = { children: ReactNode; @@ -14,8 +15,9 @@ type MediaCallRoomActivityProps = { const MediaCallRoomActivity = ({ children }: MediaCallRoomActivityProps) => { const [showChat, setShowChat] = useState(true); - const user = useUser(); + const [isPopout, setIsPopout] = useState(false); + const user = useUser(); const displayName = useUserDisplayName({ name: user?.name, username: user?.username }); const getUserAvatarPath = useUserAvatarPath(); @@ -28,20 +30,37 @@ const MediaCallRoomActivity = ({ children }: MediaCallRoomActivityProps) => { }; }, [displayName, getUserAvatarPath, user?._id]); - const onClickToggleChat = () => { - setShowChat((prev) => !prev); - }; + const mediaCallRoomSection = ( + + setShowChat((prev) => !prev)} + user={ownUser} + containerHeight={borderBoxSize?.blockSize || 0} + onPopout={() => setIsPopout((prev) => !prev)} + isPopout={isPopout} + key={isPopout ? 'popout' : 'normal'} + /> + + ); + + // TODO: this shouldn't be inside here, since the popout has to be always rendered. + if (isPopout) { + return ( + <> + setIsPopout(false)}> + + {mediaCallRoomSection} + + + {children} + + ); + } + return ( - - - - + {mediaCallRoomSection} {showChat && ( {children} diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index fdf221acd8e4e..ec73af5b71e15 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx @@ -28,10 +28,12 @@ type MediaCallRoomSectionProps = { avatarUrl: string; }; containerHeight: number; + onPopout: () => void; + isPopout?: boolean; }; -const getSplitStyles = (showChat?: boolean) => { - if (showChat) { +const getSplitStyles = (showChat?: boolean, isPopout?: boolean) => { + if (showChat && !isPopout) { return { maxHeight: `${CARD_LIST_SECTION_MAX_HEIGHT}vh`, }; @@ -44,7 +46,7 @@ const getSplitStyles = (showChat?: boolean) => { }; }; -const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: MediaCallRoomSectionProps) => { +const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight, onPopout, isPopout }: MediaCallRoomSectionProps) => { const { t } = useTranslation(); const [focusedCard, setFocusedCard] = useState<'remote' | 'local' | null>('remote'); @@ -114,7 +116,7 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: overflow='hidden' display='flex' flexDirection='column' - {...getSplitStyles(showChat)} + {...getSplitStyles(showChat, isPopout)} > @@ -152,6 +154,7 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: pressed={localScreen?.active ?? false} onToggle={onToggleScreenSharing} /> + From 0807561716a0ef73fc90ca016833b33d51371cbd Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 14 Apr 2026 12:59:37 -0300 Subject: [PATCH 04/42] Prevent multiple roots from being added --- .../src/views/MediaCallPopoutWindow.tsx | 91 +++++++++++-------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx index 5c54457bfaf25..6612eecca8b6c 100644 --- a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx +++ b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx @@ -10,55 +10,68 @@ type MediaCallPopoutWindowProps = { onClose: () => void; }; +const createRootElement = (externalWindow: Window) => { + const newRoot = externalWindow.document.createElement('div'); + newRoot.style.width = '100%'; + newRoot.style.height = '100%'; + externalWindow.document.body.appendChild(newRoot); + return newRoot; +}; + +const copyStylesheets = (externalWindow: Window) => { + Array.from(document.styleSheets).forEach((stylesheet) => { + if (stylesheet.href) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = stylesheet.href; + externalWindow.document.head.appendChild(link); + } else if (stylesheet?.cssRules?.length > 0) { + const style = document.createElement('style'); + Array.from(stylesheet.cssRules).forEach((rule) => { + style.appendChild(document.createTextNode(rule.cssText)); + }); + externalWindow.document.head.appendChild(style); + } + }); +}; + +const openExternalWindow = (title: string) => { + try { + const externalWindow = window.open('', title, 'width=800,height=500,popup'); + if (!externalWindow) { + throw new Error('No window was opened'); + } + copyStylesheets(externalWindow); + return externalWindow; + } catch (error) { + console.error('Failed to open external window', error); + return null; + } +}; + const MediaCallPopoutWindow = ({ children, onClose }: MediaCallPopoutWindowProps) => { const [container, setContainer] = useState<{ root: HTMLDivElement; externalWindow: Window } | null>(null); const timeoutRef = useRef(null); const triggerClose = useStableCallback(onClose); useEffect(() => { - if (container?.externalWindow.closed === false) { - return; - } - - let externalWindow: Window | null; - try { - // TODO: use peer name instead of 'Call with Peer X' - externalWindow = window.open('', 'Call with Peer X', 'width=800,height=500,popup'); - } catch (error) { - console.error('Failed to open external window', error); - triggerClose(); - return; - } + setContainer((prev) => { + if (prev?.externalWindow && prev?.root) { + return prev; + } - if (!externalWindow) { - triggerClose(); - return; - } + const externalWindow = !prev?.externalWindow ? openExternalWindow('Call with Peer X') : prev?.externalWindow; - const win = externalWindow; - - const root = win.document.createElement('div'); - win.document.body.appendChild(root); - root.style.width = '100%'; - root.style.height = '100%'; - - Array.from(document.styleSheets).forEach((stylesheet) => { - if (stylesheet.href) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = stylesheet.href; - win.document.head.appendChild(link); - } else if (stylesheet?.cssRules?.length > 0) { - const style = document.createElement('style'); - Array.from(stylesheet.cssRules).forEach((rule) => { - style.appendChild(document.createTextNode(rule.cssText)); - }); - win.document.head.appendChild(style); + if (!externalWindow) { + triggerClose(); + return null; } - }); - setContainer({ root, externalWindow: win }); - }, [triggerClose, container?.externalWindow.closed]); + const root = createRootElement(externalWindow); + + return { root, externalWindow }; + }); + }, [triggerClose]); useEffect(() => { if (timeoutRef.current) { From eab26c94843eaee329ab54d5164f93009cf0ccd0 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 14 Apr 2026 13:48:18 -0300 Subject: [PATCH 05/42] fix stream stops playing when switching between popup and regular view --- .../MediaCallRoomSection.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index ec73af5b71e15..fefa21755f22a 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx @@ -86,7 +86,14 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight, o const remoteStreamCard = remoteScreen?.active ? ( - @@ -100,7 +107,14 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight, o focused={focusedCard === 'local'} showStopSharingOnHover > - @@ -182,7 +171,7 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: titles={[t('Popout'), t('Close_popout')]} icons={['arrow-expand', 'arrow-collapse']} pressed={isPopout} - onToggle={togglePopout} + onToggle={isPopout ? onClosePopout : onOpenPopout} danger={false} /> diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx index f3ffb917fe3d4..a108ff31aa43f 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx @@ -1,5 +1,4 @@ import { Box, ButtonGroup, Callout } from '@rocket.chat/fuselage'; -import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -19,7 +18,6 @@ import { StreamCard, } from '../../components'; import { useMediaCallInstance } from '../../context'; -import type { AvailableViews } from '../../context/MediaCallInstanceContext'; import { useMediaCallView } from '../../context/MediaCallViewContext'; import { usePlayMediaStream } from '../../providers/usePlayMediaStream'; @@ -36,21 +34,12 @@ const OngoingCall = () => { streams, onToggleScreenSharing, widgetPositionTracker, + onClosePopout, } = useMediaCallView(); const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; - const { currentViews, setCurrentViews } = useMediaCallInstance(); + const { currentViews } = useMediaCallInstance(); const isPopout = currentViews.has('popout'); - const closePopout = useCallback(() => { - setCurrentViews((prev) => { - if (!prev.has('popout')) { - return prev; - } - prev.delete('popout'); - return new Set(prev); - }); - }, [setCurrentViews]); - const { localScreen, remoteScreen } = streams; const [remoteStreamRefCallback] = usePlayMediaStream(remoteScreen?.stream ?? null); @@ -75,7 +64,7 @@ const OngoingCall = () => { )} {/* TODO: translation and icon */} - {isPopout && } + {isPopout && } From 3445035911a1174339853dd0b724b8708bed3734 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 5 May 2026 12:17:56 -0300 Subject: [PATCH 18/42] feat: Translation strings --- packages/i18n/src/locales/en.i18n.json | 3 +++ packages/ui-voip/src/views/MediaCallPopoutView.tsx | 6 +----- .../MediaCallRoomSection/MediaCallRoomSection.tsx | 5 +++-- .../MediaCallWidget/OngoingCallWithScreen.tsx | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index da25834c623c0..fbd3ac4c07c26 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2155,6 +2155,7 @@ "Exclude_pinned": "Exclude pinned messages", "Execute_Synchronization_Now": "Execute Synchronization Now", "Exit_Full_Screen": "Exit Full Screen", + "Exit_fullscreen": "Exit fullscreen", "Expand": "Expand", "Expand_group": "Expand {{group}}", "Expand_all": "Expand all", @@ -4064,6 +4065,7 @@ "Open_Days": "Open days", "Open_Dialpad": "Open Dialpad", "Open_dialpad": "Open dialpad", + "Open_in_new_window": "Open in new window", "Open_Livechats": "Chats in progress", "Open_Outlook": "Open Outlook", "Open_call": "Open call", @@ -4609,6 +4611,7 @@ "Retrying": "Retrying", "Retry_Count": "Retry Count", "Return_to_home": "Return to home", + "Return_to_main_window": "Return to main window", "Return_to_previous_page": "Return to previous page", "Return_to_the_queue": "Return back to the Queue", "Review": "Review", diff --git a/packages/ui-voip/src/views/MediaCallPopoutView.tsx b/packages/ui-voip/src/views/MediaCallPopoutView.tsx index fd8e4f57f9771..6e2fdd4e21afe 100644 --- a/packages/ui-voip/src/views/MediaCallPopoutView.tsx +++ b/packages/ui-voip/src/views/MediaCallPopoutView.tsx @@ -126,13 +126,9 @@ const MediaCallPopoutView = ({ user, onClickClosePopout, onClickFullscreen, full } rightSlot={ - {/* TODO: trigger focus back to window */} {/* TODO: new icon */} - {/* TODO: trigger focus back to window */} - {/* TODO: translation string */} - - {/* Todo: translation string */} + { onForward, onEndCall, onClickDirectMessage, + onOpenPopout, streams, onToggleScreenSharing, widgetPositionTracker, @@ -63,8 +64,17 @@ const OngoingCall = () => { {onClickDirectMessage && ( )} - {/* TODO: translation and icon */} - {isPopout && } + + + From 811cf018341e6d697cc5f948fd8323db643b5277 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 7 May 2026 16:33:37 -0300 Subject: [PATCH 19/42] fix: Update "document" contexts import --- packages/ui-voip/src/views/MediaCallPopoutWindow.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx index 4248e13127461..a9a489a061959 100644 --- a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx +++ b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx @@ -1,5 +1,5 @@ -import { Box, TargetDocument as FuselageTargetDocument } from '@rocket.chat/fuselage'; -import { TargetDocument as StyledTargetDocument } from '@rocket.chat/styled'; +import { Box, OwnerDocument as FuselageOwnerDocument } from '@rocket.chat/fuselage'; +import { OwnerDocument as StyledOwnerDocument } from '@rocket.chat/styled'; import { useUserDisplayName } from '@rocket.chat/ui-client'; import { useUser, useUserAvatarPath } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; @@ -142,8 +142,8 @@ const MediaCallPopoutWindow = () => { } return ( - - + + {createPortal( { , container.root, )} - - + + ); }; From 53cc2d22c3ebbd22407062af833976a5eb6e1ea5 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 7 May 2026 16:34:20 -0300 Subject: [PATCH 20/42] Update Icons --- packages/ui-voip/src/views/MediaCallPopoutView.tsx | 3 +-- .../src/views/MediaCallRoomSection/MediaCallRoomSection.tsx | 5 ++--- .../src/views/MediaCallWidget/OngoingCallWithScreen.tsx | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallPopoutView.tsx b/packages/ui-voip/src/views/MediaCallPopoutView.tsx index 6e2fdd4e21afe..57c774a17bbdd 100644 --- a/packages/ui-voip/src/views/MediaCallPopoutView.tsx +++ b/packages/ui-voip/src/views/MediaCallPopoutView.tsx @@ -127,8 +127,7 @@ const MediaCallPopoutView = ({ user, onClickClosePopout, onClickFullscreen, full rightSlot={ - {/* TODO: new icon */} - + {/* TODO: new icon missing */} - @@ -169,8 +169,7 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: { Date: Thu, 7 May 2026 16:43:27 -0300 Subject: [PATCH 21/42] chore: Remove "go to dm" button from popout view --- packages/ui-voip/src/views/MediaCallPopoutView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallPopoutView.tsx b/packages/ui-voip/src/views/MediaCallPopoutView.tsx index 57c774a17bbdd..4779c702cb79e 100644 --- a/packages/ui-voip/src/views/MediaCallPopoutView.tsx +++ b/packages/ui-voip/src/views/MediaCallPopoutView.tsx @@ -42,7 +42,6 @@ const MediaCallPopoutView = ({ user, onClickClosePopout, onClickFullscreen, full onForward, onEndCall, onToggleScreenSharing, - onClickDirectMessage, streams: { remoteScreen, localScreen }, } = useMediaCallView(); @@ -126,7 +125,6 @@ const MediaCallPopoutView = ({ user, onClickClosePopout, onClickFullscreen, full } rightSlot={ - Date: Thu, 7 May 2026 17:19:31 -0300 Subject: [PATCH 22/42] fix: Widget "Show call here" buttons styles --- .../ui-voip/src/components/ToggleButton.tsx | 20 +++++++++++++++---- .../MediaCallWidget/OngoingCallWithScreen.tsx | 10 ++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/ui-voip/src/components/ToggleButton.tsx b/packages/ui-voip/src/components/ToggleButton.tsx index d25f951808e04..03ea1e2362e7b 100644 --- a/packages/ui-voip/src/components/ToggleButton.tsx +++ b/packages/ui-voip/src/components/ToggleButton.tsx @@ -11,20 +11,32 @@ type ToggleButtonProps = { onToggle?: () => void; } & Omit, 'icon' | 'title' | 'aria-label' | 'disabled' | 'onClick'>; -const ToggleButton = ({ disabled, label, pressed, icons, titles, onToggle, danger = true, ...props }: ToggleButtonProps) => { +const ToggleButton = ({ + disabled, + label, + pressed, + icons, + titles, + onToggle, + danger = true, + secondary = true, + tiny = false, + ...props +}: ToggleButtonProps) => { const iconName = icons[pressed ? 1 : 0]; const title = titles[pressed ? 1 : 0]; const iconColor = pressed && danger ? 'font-danger' : undefined; + const size = tiny ? { tiny: true } : { medium: true }; + return ( } title={title} - pressed={pressed} aria-label={label} disabled={disabled} onClick={onToggle} diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx index c429a1b4f3308..2e20c04a8b15a 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx @@ -1,4 +1,4 @@ -import { Box, ButtonGroup, Callout } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useTranslation } from 'react-i18next'; import { @@ -72,6 +72,8 @@ const OngoingCall = () => { pressed={isPopout} onToggle={isPopout ? onClosePopout : onOpenPopout} danger={false} + secondary={false} + tiny /> @@ -80,7 +82,11 @@ const OngoingCall = () => { - {isPopout && {t('Call_open_separate_window')}} + {isPopout && ( + + )} {!isPopout && ( <> From 20e4d6110df5ebd48914ab9ed570030f91c1cea3 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 7 May 2026 17:19:38 -0300 Subject: [PATCH 23/42] Remove comments --- .../src/views/MediaCallRoomSection/MediaCallRoomSection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index 805c38b536695..f5a5e54de2cd9 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx @@ -142,7 +142,6 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: {t('Call_open_separate_window')} - {/* TODO: new icon missing */} From 5800c64e3feb3885213d0b7c2fe0a31b635e441a Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Mon, 11 May 2026 18:35:04 -0300 Subject: [PATCH 24/42] refactor: Improve available views tracking logic and reliability --- .../src/context/MediaCallInstanceContext.ts | 13 +-- .../ui-voip/src/context/useRegisterView.ts | 40 ++------- .../providers/MediaCallInstanceProvider.tsx | 10 ++- .../src/providers/MediaCallViewProvider.tsx | 26 ++---- .../src/providers/MockedMediaCallProvider.tsx | 5 +- .../src/providers/useAvailableViewTracker.ts | 85 +++++++++++++++++++ .../MediaCallRoomSection.tsx | 2 +- .../MediaCallWidget/OngoingCallWithScreen.tsx | 2 +- 8 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 packages/ui-voip/src/providers/useAvailableViewTracker.ts diff --git a/packages/ui-voip/src/context/MediaCallInstanceContext.ts b/packages/ui-voip/src/context/MediaCallInstanceContext.ts index f4e5823700c2c..72eced82fa6b7 100644 --- a/packages/ui-voip/src/context/MediaCallInstanceContext.ts +++ b/packages/ui-voip/src/context/MediaCallInstanceContext.ts @@ -12,17 +12,19 @@ export type Signals = { export type AvailableViews = 'room' | 'popout' | 'widget'; -type SetCurrentViews = (set: (views: Set) => Set) => void; +type RegisterView = (view: AvailableViews) => void; +type UnregisterView = (view: AvailableViews) => void; type MediaCallInstanceContextValue = { instance: MediaSignalingSession | undefined; signalEmitter: Emitter; audioElement: RefObject | undefined; openRoomId: string | undefined; - currentViews: Set; + currentViews: AvailableViews[]; setOpenRoomId: (openRoomId: string | undefined) => void; getAutocompleteOptions: (filter: string) => Promise; - setCurrentViews: SetCurrentViews; + registerView: RegisterView; + unregisterView: UnregisterView; }; export const MediaCallInstanceContext = createContext({ @@ -32,8 +34,9 @@ export const MediaCallInstanceContext = createContext undefined, getAutocompleteOptions: () => Promise.resolve([]), - currentViews: new Set(), - setCurrentViews: () => undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, }); export const useMediaCallInstance = (): MediaCallInstanceContextValue => useContext(MediaCallInstanceContext); diff --git a/packages/ui-voip/src/context/useRegisterView.ts b/packages/ui-voip/src/context/useRegisterView.ts index cafc22e7284b6..d0059fa1b1c27 100644 --- a/packages/ui-voip/src/context/useRegisterView.ts +++ b/packages/ui-voip/src/context/useRegisterView.ts @@ -1,44 +1,16 @@ -import { useLayoutEffect, useMemo } from 'react'; +import { useLayoutEffect } from 'react'; import { useMediaCallInstance } from '.'; import type { AvailableViews } from './MediaCallInstanceContext'; -const useRegisterView = (view: AvailableViews, filter?: (view: Set) => boolean): Set => { - const { currentViews, setCurrentViews } = useMediaCallInstance(); - - const shouldAddToSet = useMemo(() => { - if (!filter) { - return true; - } - - return filter(currentViews); - }, [currentViews, filter]); +const useRegisterView = (view: AvailableViews): AvailableViews[] => { + const { currentViews, registerView, unregisterView } = useMediaCallInstance(); useLayoutEffect(() => { - if (!shouldAddToSet) { - return; - } - - setCurrentViews((prev) => { - if (prev.has(view)) { - return prev; - } - - prev.add(view); - return new Set(prev); - }); - - return () => { - setCurrentViews((prev) => { - if (!prev.has(view)) { - return prev; - } + registerView(view); - prev.delete(view); - return new Set(prev); - }); - }; - }, [setCurrentViews, view, shouldAddToSet]); + return () => unregisterView(view); + }, [view, registerView, unregisterView]); return currentViews; }; diff --git a/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx b/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx index ff5d77eb896f3..f1b13a5e6dc5a 100644 --- a/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx +++ b/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx @@ -4,10 +4,11 @@ import { useMemo, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { useAudioStream } from './useAudioStream'; +import useAvailableViewTracker from './useAvailableViewTracker'; import { useGetAutocompleteOptions } from './useGetAutocompleteOptions'; import { useMediaSessionInstance } from './useMediaSessionInstance'; import { MediaCallInstanceContext } from '../context/MediaCallInstanceContext'; -import type { Signals, AvailableViews } from '../context/MediaCallInstanceContext'; +import type { Signals } from '../context/MediaCallInstanceContext'; type MediaCallInstanceProviderProps = { children: ReactNode; @@ -15,7 +16,7 @@ type MediaCallInstanceProviderProps = { const MediaCallInstanceProvider = ({ children }: MediaCallInstanceProviderProps) => { const [openRoomId, setOpenRoomId] = useState(undefined); - const [currentViews, setCurrentViews] = useState(() => new Set()); + const { currentViews, registerView, unregisterView } = useAvailableViewTracker(); const user = useUser(); const instance = useMediaSessionInstance(user?._id); const [signalEmitter] = useState(() => new Emitter()); @@ -33,9 +34,10 @@ const MediaCallInstanceProvider = ({ children }: MediaCallInstanceProviderProps) setOpenRoomId, getAutocompleteOptions, currentViews, - setCurrentViews, + registerView, + unregisterView, }), - [instance, signalEmitter, audioElement, openRoomId, setOpenRoomId, getAutocompleteOptions, currentViews, setCurrentViews], + [instance, signalEmitter, audioElement, openRoomId, setOpenRoomId, getAutocompleteOptions, currentViews, registerView, unregisterView], ); return ( diff --git a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx index 6b9b8ec02cdb5..32a31eaaca2a9 100644 --- a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx +++ b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx @@ -19,7 +19,7 @@ import { useScreenShareStreams } from './useScreenShareStreams'; import { useWidgetExternalControlSignalListener } from './useWidgetExternalControlSignalListener'; import useWidgetPositionTracker from './useWidgetPositionTracker'; import { useMediaCallInstance } from '../context/MediaCallInstanceContext'; -import type { AvailableViews } from '../context/MediaCallInstanceContext'; +// import type { AvailableViews } from '../context/MediaCallInstanceContext'; import MediaCallViewContext from '../context/MediaCallViewContext'; import type { PeerInfo } from '../context/definitions'; import { stopTracks, useDevicePermissionPrompt2, PermissionRequestCancelledCallRejectedError } from '../hooks/useDevicePermissionPrompt'; @@ -36,7 +36,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { const setModal = useSetModal(); - const { instance, audioElement, openRoomId, setCurrentViews } = useMediaCallInstance(); + const { instance, audioElement, openRoomId, registerView, unregisterView } = useMediaCallInstance(); const { sessionState, toggleWidget, selectPeer } = useMediaSession(instance); const controls = useMediaSessionControls(instance); @@ -210,26 +210,12 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { }; const onOpenPopout = useCallback(() => { - setCurrentViews((prev) => { - if (prev.has('popout')) { - return prev; - } - const next = new Set(prev); - next.add('popout'); - return next; - }); - }, [setCurrentViews]); + registerView('popout'); + }, [registerView]); const onClosePopout = useCallback(() => { - setCurrentViews((prev) => { - if (!prev.has('popout')) { - return prev; - } - const next = new Set(prev); - next.delete('popout'); - return next; - }); - }, [setCurrentViews]); + unregisterView('popout'); + }, [unregisterView]); const streams = useScreenShareStreams(instance); diff --git a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx index 3b60c2025f0c7..4afa10b9e9136 100644 --- a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx +++ b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx @@ -168,8 +168,9 @@ const MockedMediaCallProvider = ({ getState: () => null, on: () => undefined, } as unknown as MediaSignalingSession, - currentViews: new Set(), - setCurrentViews: () => undefined, + currentViews: [] as AvailableViews[], + registerView: (_view: AvailableViews) => undefined, + unregisterView: (_view: AvailableViews) => undefined, signalEmitter: new Emitter(), audioElement: undefined, openRoomId: undefined, diff --git a/packages/ui-voip/src/providers/useAvailableViewTracker.ts b/packages/ui-voip/src/providers/useAvailableViewTracker.ts new file mode 100644 index 0000000000000..0bb273a6cdf18 --- /dev/null +++ b/packages/ui-voip/src/providers/useAvailableViewTracker.ts @@ -0,0 +1,85 @@ +import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react'; + +import type { AvailableViews } from '../context/MediaCallInstanceContext'; + +const filter = (view: AvailableViews, _index: number, array: AvailableViews[]) => { + switch (view) { + case 'widget': + return !array.includes('room'); + case 'popout': + case 'room': + default: + return true; + } +}; + +const FLUSH_DELAY = 100; + +const useAvailableViewTracker = () => { + const viewsRef = useRef>(new Set()); + const filteredViewsRef = useRef([]); + + const [registerView, unregisterView, subscribeToViews] = useMemo(() => { + let timeout: NodeJS.Timeout | undefined; + + let sub: (() => void) | undefined = undefined; + const subscribeToViews = (onStoreChange: () => void) => { + sub = onStoreChange; + return () => { + sub = undefined; + }; + }; + + // TODO maybe we don't need to debounce this + // It is used to prevent an useEffect from unregistering the view too early + // Specially when the view will be re-registered when the effect runs again + // meaning it should not have unregistered at all + const flushDebounced = () => { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + timeout = setTimeout(() => { + const viewsArray = [...viewsRef.current].filter(filter); + if (viewsArray.length === filteredViewsRef.current.length && viewsArray.every((view) => filteredViewsRef.current.includes(view))) { + return; + } + filteredViewsRef.current = [...viewsRef.current].filter(filter); + return sub?.(); + }, FLUSH_DELAY); + }; + + const unregisterView = (view: AvailableViews) => { + if (!viewsRef.current.has(view)) { + return; + } + viewsRef.current.delete(view); + flushDebounced(); + }; + + const registerView = (view: AvailableViews) => { + if (viewsRef.current.has(view)) { + return; + } + viewsRef.current.add(view); + flushDebounced(); + }; + + return [registerView, unregisterView, subscribeToViews]; + }, []); + + const currentViews = useSyncExternalStore( + subscribeToViews, + useCallback(() => filteredViewsRef.current, []), + useCallback(() => filteredViewsRef.current, []), + ); + + return { + currentViews, + registerView, + unregisterView, + }; +}; + +export default useAvailableViewTracker; diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index f5a5e54de2cd9..103c092c26bf6 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx @@ -62,7 +62,7 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: } = useMediaCallView(); const { currentViews } = useMediaCallInstance(); - const isPopout = currentViews.has('popout'); + const isPopout = currentViews.includes('popout'); const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx index 2e20c04a8b15a..a90fe4efeb5a1 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx @@ -39,7 +39,7 @@ const OngoingCall = () => { } = useMediaCallView(); const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; const { currentViews } = useMediaCallInstance(); - const isPopout = currentViews.has('popout'); + const isPopout = currentViews.includes('popout'); const { localScreen, remoteScreen } = streams; From 8762c53e66bc0f2c01026d5388b3d2e6e349409d Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Mon, 11 May 2026 18:38:00 -0300 Subject: [PATCH 25/42] refactor: Improve popout window opening/closing logic --- .../ui-voip/src/views/MediaCallPopout.tsx | 22 +++- .../src/views/MediaCallPopoutWindow.tsx | 121 ++---------------- .../views/MediaCallWidget/MediaCallWidget.tsx | 10 +- packages/ui-voip/src/views/usePopoutWindow.ts | 100 +++++++++++++++ 4 files changed, 134 insertions(+), 119 deletions(-) create mode 100644 packages/ui-voip/src/views/usePopoutWindow.ts diff --git a/packages/ui-voip/src/views/MediaCallPopout.tsx b/packages/ui-voip/src/views/MediaCallPopout.tsx index 3664b53167189..15446423662b2 100644 --- a/packages/ui-voip/src/views/MediaCallPopout.tsx +++ b/packages/ui-voip/src/views/MediaCallPopout.tsx @@ -1,11 +1,18 @@ -import { useLayoutEffect } from 'react'; +import { useCallback, useEffect, useLayoutEffect } from 'react'; import { useMediaCallInstance, useMediaCallView } from '../context'; import MediaCallPopoutWindow from './MediaCallPopoutWindow'; +import { usePopoutWindow } from './usePopoutWindow'; const MediaCallPopout = () => { const { currentViews } = useMediaCallInstance(); const { sessionState, onClosePopout } = useMediaCallView(); + const { container, closePopoutWindow, openPopoutWindow } = usePopoutWindow(); + + const onClosePopoutAndWindow = useCallback(() => { + onClosePopout(); + closePopoutWindow(); + }, [onClosePopout, closePopoutWindow]); useLayoutEffect(() => { if (sessionState.state !== 'ongoing') { @@ -13,11 +20,20 @@ const MediaCallPopout = () => { } }, [sessionState.state, onClosePopout]); - if (!currentViews.has('popout')) { + useEffect(() => { + if (currentViews.includes('popout')) { + // TODO: Fix this title + openPopoutWindow('Call with Peer X', onClosePopout); + return; + } + closePopoutWindow(); + }, [currentViews, openPopoutWindow, closePopoutWindow, onClosePopout]); + + if (!container) { return null; } - return ; + return ; }; export default MediaCallPopout; diff --git a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx index a9a489a061959..d4934370991d0 100644 --- a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx +++ b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx @@ -2,66 +2,18 @@ import { Box, OwnerDocument as FuselageOwnerDocument } from '@rocket.chat/fusela import { OwnerDocument as StyledOwnerDocument } from '@rocket.chat/styled'; import { useUserDisplayName } from '@rocket.chat/ui-client'; import { useUser, useUserAvatarPath } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; -import { useMediaCallInstance, useMediaCallView } from '../context'; import MediaCallPopoutView from './MediaCallPopoutView'; +import type { PopoutContainer } from './usePopoutWindow'; -const createRootElement = (externalWindow: Window) => { - const newRoot = externalWindow.document.createElement('div'); - newRoot.style.width = '100%'; - newRoot.style.height = '100%'; - externalWindow.document.body.appendChild(newRoot); - return newRoot; +type MediaCallPopoutWindowProps = { + container: PopoutContainer; + onClosePopout: () => void; }; - -const copyStylesheets = (externalWindow: Window) => { - Array.from(document.styleSheets).forEach((stylesheet) => { - if (stylesheet.href) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = stylesheet.href; - externalWindow.document.head.appendChild(link); - } else if (stylesheet?.cssRules?.length > 0) { - const style = document.createElement('style'); - Array.from(stylesheet.cssRules).forEach((rule) => { - style.appendChild(document.createTextNode(rule.cssText)); - }); - externalWindow.document.head.appendChild(style); - } - }); -}; - -const openExternalWindow = (title: string) => { - try { - const externalWindow = window.open('', title, 'width=800,height=500,popup'); - if (!externalWindow) { - throw new Error('No window was opened'); - } - copyStylesheets(externalWindow); - return externalWindow; - } catch (error) { - console.error('Failed to open external window', error); - return null; - } -}; - -const MediaCallPopoutWindow = () => { - const [container, setContainer] = useState<{ root: HTMLDivElement; externalWindow: Window } | null>(null); +const MediaCallPopoutWindow = ({ container, onClosePopout }: MediaCallPopoutWindowProps) => { const [fullscreen, setFullscreen] = useState(false); - const { setCurrentViews } = useMediaCallInstance(); - const { onClosePopout } = useMediaCallView(); - - const closePopout = useCallback( - (windowClosed = false) => { - onClosePopout(); - if (!windowClosed) { - container?.externalWindow.close(); - } - }, - [container?.externalWindow, onClosePopout], - ); const user = useUser(); const displayName = useUserDisplayName({ name: user?.name, username: user?.username }); @@ -73,59 +25,16 @@ const MediaCallPopoutWindow = () => { }; }, [displayName, getUserAvatarPath, user?._id]); - useLayoutEffect(() => { - setContainer((prev) => { - if (prev?.externalWindow && prev?.root) { - return prev; - } - - const externalWindow = !prev?.externalWindow ? openExternalWindow('Call with Peer X') : prev?.externalWindow; - - if (!externalWindow) { - closePopout(true); - return null; - } - - const root = createRootElement(externalWindow); - - return { root, externalWindow }; - }); - }, [closePopout]); - - useEffect(() => { - const handleBeforeUnload = () => closePopout(true); - container?.externalWindow.addEventListener('beforeunload', handleBeforeUnload); - window.addEventListener('beforeunload', handleBeforeUnload); - - return () => { - container?.externalWindow.removeEventListener('beforeunload', handleBeforeUnload); - window.removeEventListener('beforeunload', handleBeforeUnload); - }; - }, [container?.externalWindow, closePopout]); - - useLayoutEffect(() => { - return () => { - // Since this happens during cleanup, we need the most up-to-date state of the current views - // to avoid closing the window prematurely - // so we use the setter functions "previous value", as it should contain the newest state. - // If we were to use the current state, it would always be outdated on the cleanup. - setCurrentViews((prev) => { - if (!prev.has('popout')) { - closePopout(false); - } - return prev; - }); - }; - }, [closePopout, setCurrentViews]); + const { root, ownerDocument } = container; const onClickFullscreen = useCallback(() => { const requestFullScreen = async () => { try { if (!fullscreen) { - await container?.externalWindow.document.documentElement.requestFullscreen(); + await ownerDocument.documentElement.requestFullscreen(); setFullscreen(true); } else { - await container?.externalWindow.document.exitFullscreen(); + await ownerDocument.exitFullscreen(); setFullscreen(false); } } catch (error) { @@ -133,13 +42,9 @@ const MediaCallPopoutWindow = () => { } }; void requestFullScreen(); - }, [container?.externalWindow, fullscreen]); - - const contextValue = useMemo(() => ({ document: container?.externalWindow.document || document }), [container?.externalWindow.document]); + }, [ownerDocument, fullscreen]); - if (!container) { - return null; - } + const contextValue = useMemo(() => ({ document: ownerDocument || document }), [ownerDocument]); return ( @@ -148,12 +53,12 @@ const MediaCallPopoutWindow = () => { closePopout(false)} + onClickClosePopout={onClosePopout} onClickFullscreen={onClickFullscreen} fullscreen={fullscreen} /> , - container.root, + root, )} diff --git a/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx b/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx index e0ae84af7aaae..ca4551c7c8bd8 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx @@ -1,21 +1,15 @@ -import { useCallback } from 'react'; - import { OngoingCall, NewCall, IncomingCall, OutgoingCall, IncomingCallTransfer, OutgoingCallTransfer } from '..'; import OngoingCallWithScreen from './OngoingCallWithScreen'; -import type { AvailableViews } from '../../context/MediaCallInstanceContext'; import { useMediaCallView } from '../../context/MediaCallViewContext'; import useRegisterView from '../../context/useRegisterView'; const MediaCallWidget = () => { - const currentViews = useRegisterView( - 'widget', - useCallback((views: Set) => !views.has('room'), []), - ); + const currentViews = useRegisterView('widget'); const { sessionState: { state, hidden, transferredBy, peerInfo, supportedFeatures }, } = useMediaCallView(); - if (hidden || !currentViews.has('widget')) { + if (hidden || !currentViews.includes('widget')) { return null; } diff --git a/packages/ui-voip/src/views/usePopoutWindow.ts b/packages/ui-voip/src/views/usePopoutWindow.ts new file mode 100644 index 0000000000000..f1d939804d103 --- /dev/null +++ b/packages/ui-voip/src/views/usePopoutWindow.ts @@ -0,0 +1,100 @@ +import { useCallback, useRef, useState } from 'react'; + +const createRootElement = (externalWindow: Window) => { + const newRoot = externalWindow.document.createElement('div'); + newRoot.style.width = '100%'; + newRoot.style.height = '100%'; + externalWindow.document.body.appendChild(newRoot); + return newRoot; +}; + +const copyStylesheets = (externalWindow: Window) => { + Array.from(document.styleSheets).forEach((stylesheet) => { + if (stylesheet.href) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = stylesheet.href; + externalWindow.document.head.appendChild(link); + } else if (stylesheet?.cssRules?.length > 0) { + const style = document.createElement('style'); + Array.from(stylesheet.cssRules).forEach((rule) => { + style.appendChild(document.createTextNode(rule.cssText)); + }); + externalWindow.document.head.appendChild(style); + } + }); +}; + +const attachEventListeners = (externalWindow: Window, onBeforeUnload: () => void) => { + externalWindow.addEventListener('beforeunload', onBeforeUnload); + window.addEventListener('beforeunload', onBeforeUnload); +}; + +const openExternalWindow = (title: string, onBeforeUnload: () => void) => { + try { + const externalWindow = window.open('', title, 'width=800,height=500,popup'); + if (!externalWindow) { + throw new Error('No window was opened'); + } + copyStylesheets(externalWindow); + attachEventListeners(externalWindow, onBeforeUnload); + const root = createRootElement(externalWindow); + return { root, externalWindow }; + } catch (error) { + // This should maybe throw instead of returning null + console.error('Failed to open external window', error); + return null; + } +}; + +export type PopoutContainer = { root: HTMLDivElement; ownerDocument: Document }; +type PopoutRef = { root: HTMLDivElement; externalWindow: Window; closing: boolean }; +type OpenPopoutWindow = (title: string, onCloseOrFail: () => void) => void; +type ClosePopoutWindow = () => void; + +type UsePopoutWindowReturn = { + container: PopoutContainer | null; + openPopoutWindow: OpenPopoutWindow; + closePopoutWindow: ClosePopoutWindow; +}; + +export const usePopoutWindow = (): UsePopoutWindowReturn => { + const popoutRef = useRef(null); + const [container, setContainer] = useState(null); + + const openPopoutWindow = useCallback((title: string, onCloseOrFail: () => void) => { + if (!!popoutRef.current && popoutRef.current.externalWindow?.closed === false) { + return; + } + + const container = openExternalWindow(title, () => { + if (popoutRef.current) { + popoutRef.current.closing = true; + } + onCloseOrFail(); + popoutRef.current = null; + }); + + if (container) { + const { root, externalWindow } = container; + popoutRef.current = { root, externalWindow, closing: false }; + setContainer({ root, ownerDocument: externalWindow.document }); + return; + } + onCloseOrFail(); + }, []); + + const closePopoutWindow = useCallback(() => { + if (popoutRef.current !== null && popoutRef.current?.externalWindow?.closed !== true && !popoutRef.current.closing) { + popoutRef.current.externalWindow.close(); + popoutRef.current = null; + setContainer(null); + } + }, []); + + return { + container, + openPopoutWindow, + closePopoutWindow, + }; +}; From 3fe34c7eb28f3f5cd7682a4d8ac92ee48fa0979f Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 12 May 2026 15:54:45 -0300 Subject: [PATCH 26/42] fix: useAvailableViewTracker filter being applied twice --- packages/ui-voip/src/providers/useAvailableViewTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-voip/src/providers/useAvailableViewTracker.ts b/packages/ui-voip/src/providers/useAvailableViewTracker.ts index 0bb273a6cdf18..cde6b2afd139d 100644 --- a/packages/ui-voip/src/providers/useAvailableViewTracker.ts +++ b/packages/ui-voip/src/providers/useAvailableViewTracker.ts @@ -45,7 +45,7 @@ const useAvailableViewTracker = () => { if (viewsArray.length === filteredViewsRef.current.length && viewsArray.every((view) => filteredViewsRef.current.includes(view))) { return; } - filteredViewsRef.current = [...viewsRef.current].filter(filter); + filteredViewsRef.current = viewsArray; return sub?.(); }, FLUSH_DELAY); }; From 50323433b8b8323014068147054abe25611dbcae Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 12 May 2026 15:55:13 -0300 Subject: [PATCH 27/42] fix: MediaCallProvider (core) unauthorized context value Co-authored-by: Copilot --- apps/meteor/client/providers/MediaCallProvider.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/providers/MediaCallProvider.tsx b/apps/meteor/client/providers/MediaCallProvider.tsx index b1665ba68a1c7..a9a89d0f066af 100644 --- a/apps/meteor/client/providers/MediaCallProvider.tsx +++ b/apps/meteor/client/providers/MediaCallProvider.tsx @@ -14,8 +14,9 @@ const MediaCallProvider = ({ children }: { children: ReactNode }) => { const unauthorizedContextValue = useMemo( () => ({ - currentViews: new Set(), - setCurrentViews: () => undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, instance: undefined, signalEmitter: new Emitter(), audioElement: undefined, From 15114394273db48183e70b778986ad7090810c08 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 12 May 2026 15:56:02 -0300 Subject: [PATCH 28/42] chore: Remove old "inRoomView" logic from mocked provider --- packages/ui-voip/src/providers/MockedMediaCallProvider.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx index 4afa10b9e9136..8a971fb8ff5c0 100644 --- a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx +++ b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx @@ -176,8 +176,6 @@ const MockedMediaCallProvider = ({ openRoomId: undefined, setOpenRoomId: () => undefined, getAutocompleteOptions, - inRoomView: false, - setInRoomView: () => undefined, }; return ( From 3594937848ab942ad5511fad8a768740c4e794e5 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 12 May 2026 19:46:35 -0300 Subject: [PATCH 29/42] refactor: Accept onBeforeUnload through usePopoutWindow hook parameter and register via useEffect --- .../ui-voip/src/views/MediaCallPopout.tsx | 6 +- packages/ui-voip/src/views/usePopoutWindow.ts | 70 +++++++++++-------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallPopout.tsx b/packages/ui-voip/src/views/MediaCallPopout.tsx index 15446423662b2..3b6b17a54e140 100644 --- a/packages/ui-voip/src/views/MediaCallPopout.tsx +++ b/packages/ui-voip/src/views/MediaCallPopout.tsx @@ -7,7 +7,7 @@ import { usePopoutWindow } from './usePopoutWindow'; const MediaCallPopout = () => { const { currentViews } = useMediaCallInstance(); const { sessionState, onClosePopout } = useMediaCallView(); - const { container, closePopoutWindow, openPopoutWindow } = usePopoutWindow(); + const { container, closePopoutWindow, openPopoutWindow } = usePopoutWindow(onClosePopout); const onClosePopoutAndWindow = useCallback(() => { onClosePopout(); @@ -23,11 +23,11 @@ const MediaCallPopout = () => { useEffect(() => { if (currentViews.includes('popout')) { // TODO: Fix this title - openPopoutWindow('Call with Peer X', onClosePopout); + openPopoutWindow('Call with Peer X'); return; } closePopoutWindow(); - }, [currentViews, openPopoutWindow, closePopoutWindow, onClosePopout]); + }, [currentViews, openPopoutWindow, closePopoutWindow]); if (!container) { return null; diff --git a/packages/ui-voip/src/views/usePopoutWindow.ts b/packages/ui-voip/src/views/usePopoutWindow.ts index f1d939804d103..2cb6f860f4850 100644 --- a/packages/ui-voip/src/views/usePopoutWindow.ts +++ b/packages/ui-voip/src/views/usePopoutWindow.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; const createRootElement = (externalWindow: Window) => { const newRoot = externalWindow.document.createElement('div'); @@ -25,19 +25,13 @@ const copyStylesheets = (externalWindow: Window) => { }); }; -const attachEventListeners = (externalWindow: Window, onBeforeUnload: () => void) => { - externalWindow.addEventListener('beforeunload', onBeforeUnload); - window.addEventListener('beforeunload', onBeforeUnload); -}; - -const openExternalWindow = (title: string, onBeforeUnload: () => void) => { +const openExternalWindow = (title: string) => { try { const externalWindow = window.open('', title, 'width=800,height=500,popup'); if (!externalWindow) { throw new Error('No window was opened'); } copyStylesheets(externalWindow); - attachEventListeners(externalWindow, onBeforeUnload); const root = createRootElement(externalWindow); return { root, externalWindow }; } catch (error) { @@ -49,7 +43,7 @@ const openExternalWindow = (title: string, onBeforeUnload: () => void) => { export type PopoutContainer = { root: HTMLDivElement; ownerDocument: Document }; type PopoutRef = { root: HTMLDivElement; externalWindow: Window; closing: boolean }; -type OpenPopoutWindow = (title: string, onCloseOrFail: () => void) => void; +type OpenPopoutWindow = (title: string) => void; type ClosePopoutWindow = () => void; type UsePopoutWindowReturn = { @@ -58,31 +52,28 @@ type UsePopoutWindowReturn = { closePopoutWindow: ClosePopoutWindow; }; -export const usePopoutWindow = (): UsePopoutWindowReturn => { +export const usePopoutWindow = (onBeforeUnload: () => void): UsePopoutWindowReturn => { const popoutRef = useRef(null); const [container, setContainer] = useState(null); - const openPopoutWindow = useCallback((title: string, onCloseOrFail: () => void) => { - if (!!popoutRef.current && popoutRef.current.externalWindow?.closed === false) { - return; - } - - const container = openExternalWindow(title, () => { - if (popoutRef.current) { - popoutRef.current.closing = true; + const openPopoutWindow = useCallback( + (title: string) => { + if (!!popoutRef.current && popoutRef.current.externalWindow?.closed === false) { + return; } - onCloseOrFail(); - popoutRef.current = null; - }); - if (container) { - const { root, externalWindow } = container; - popoutRef.current = { root, externalWindow, closing: false }; - setContainer({ root, ownerDocument: externalWindow.document }); - return; - } - onCloseOrFail(); - }, []); + const result = openExternalWindow(title); + + if (result) { + const { root, externalWindow } = result; + popoutRef.current = { root, externalWindow, closing: false }; + setContainer({ root, ownerDocument: externalWindow.document }); + return; + } + onBeforeUnload(); + }, + [onBeforeUnload], + ); const closePopoutWindow = useCallback(() => { if (popoutRef.current !== null && popoutRef.current?.externalWindow?.closed !== true && !popoutRef.current.closing) { @@ -92,6 +83,27 @@ export const usePopoutWindow = (): UsePopoutWindowReturn => { } }, []); + useEffect(() => { + const externalWindow = popoutRef.current?.externalWindow; + if (!externalWindow || !container) return; + + const handleBeforeUnload = () => { + if (popoutRef.current) { + popoutRef.current.closing = true; + } + onBeforeUnload(); + popoutRef.current = null; + }; + + externalWindow.addEventListener('beforeunload', handleBeforeUnload); + window.addEventListener('beforeunload', closePopoutWindow); + + return () => { + externalWindow.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener('beforeunload', closePopoutWindow); + }; + }, [container, onBeforeUnload, closePopoutWindow]); + return { container, openPopoutWindow, From d0dbe1cfc775e2223bf5c6de772a3a922ccf2212 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 2 Jun 2026 14:17:09 -0300 Subject: [PATCH 30/42] update icons --- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/storybook-config/package.json | 2 +- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-contexts/package.json | 2 +- packages/ui-kit/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/ui-voip/package.json | 2 +- packages/web-ui-registration/package.json | 2 +- yarn.lock | 40 ++++++++++----------- 17 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 77b26bef19a64..3d11b597dc610 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -47,7 +47,7 @@ "ws": "~8.19.0" }, "devDependencies": { - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@types/cookie": "^0.5.4", "@types/cookie-parser": "^1.4.10", "@types/ejson": "^2.2.2", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 074b8f0169d67..4dd789ede58e2 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -116,7 +116,7 @@ "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/i18n": "workspace:^", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "^0.34.2", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 2fd47ac6ab815..fd1b54851a162 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -23,7 +23,7 @@ "@rocket.chat/fuselage-toastbar": "^0.35.2", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/fuselage-ui-kit": "workspace:~", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/logo": "^0.32.5", "@rocket.chat/styled": "^0.33.0", "@rocket.chat/ui-avatar": "workspace:^", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 30f4f74e1e5a7..e0246b3283dac 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -20,7 +20,7 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/federation-sdk": "0.6.3", "@rocket.chat/http-router": "workspace:^", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/media-signaling": "workspace:^", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index fc809f6655081..87027787b95fb 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -18,7 +18,7 @@ "test": "echo \"no tests\" && exit 1" }, "dependencies": { - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", "typia": "patch:typia@npm%3A9.7.2#~/.yarn/patches/typia-npm-9.7.2-5c5d9c80b4.patch", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index e188f1323d041..e162615f615a1 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -49,7 +49,7 @@ "@rocket.chat/fuselage": "^0.78.0", "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/styled": "^0.33.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index a481b8f6cc6ff..7b32166a0f855 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -34,7 +34,7 @@ "@rocket.chat/fuselage": "^0.78.0", "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/styled": "^0.33.0", diff --git a/packages/storybook-config/package.json b/packages/storybook-config/package.json index 63c9740ddde9a..db0c1dff46776 100644 --- a/packages/storybook-config/package.json +++ b/packages/storybook-config/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@rocket.chat/fuselage": "^0.78.0", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/tsconfig": "workspace:*", "@storybook/react": "^8.6.18", "eslint": "~9.39.4", diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index f51bebce21e6f..0e33d6610decd 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -20,7 +20,7 @@ "@rocket.chat/fuselage": "^0.78.0", "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~18.3.28", "@types/react-dom": "~18.3.7", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 5fa764dbdc268..a884c2cdda133 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -29,7 +29,7 @@ "@rocket.chat/fuselage": "^0.78.0", "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/layout": "^0.34.2", "@rocket.chat/logo": "^0.32.5", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index fbb0a7f86100b..d9190e6f9439f 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -25,7 +25,7 @@ "@rocket.chat/fuselage": "^0.78.0", "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@rocket.chat/ui-client": "workspace:~", diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index c098a3b68c1d5..84bbf945d3de3 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -27,7 +27,7 @@ "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/i18n": "workspace:~", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/tools": "workspace:~", diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 102f8932d1984..b60bcad091ad5 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -34,7 +34,7 @@ "typia": "patch:typia@npm%3A9.7.2#~/.yarn/patches/typia-npm-9.7.2-5c5d9c80b4.patch" }, "devDependencies": { - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 83c0d3ebea85b..757fa29579b87 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -25,7 +25,7 @@ "@rocket.chat/fuselage": "^0.78.0", "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/styled": "^0.33.0", "@rocket.chat/tsconfig": "workspace:*", diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index ea61c625ff897..ea5e4b3dcc402 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -34,7 +34,7 @@ "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/fuselage-ui-kit": "workspace:^", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:~", "@rocket.chat/styled": "^0.33.0", diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 697a12d7852bf..c4565e01d77b2 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -27,7 +27,7 @@ "@rocket.chat/fuselage-hooks": "^0.41.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/i18n": "workspace:~", - "@rocket.chat/icons": "~0.47.0", + "@rocket.chat/icons": "^0.48.0", "@rocket.chat/layout": "^0.34.2", "@rocket.chat/mock-providers": "workspace:~", "@rocket.chat/rest-typings": "workspace:~", diff --git a/yarn.lock b/yarn.lock index 63d36d8332850..67e1a8ef2d7d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9105,7 +9105,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/federation-sdk": "npm:0.6.3" "@rocket.chat/http-router": "workspace:^" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/media-signaling": "workspace:^" "@rocket.chat/message-parser": "workspace:^" @@ -9128,7 +9128,7 @@ __metadata: resolution: "@rocket.chat/core-typings@workspace:packages/core-typings" dependencies: "@rocket.chat/apps-engine": "workspace:^" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/express": "npm:^4.17.25" @@ -9423,7 +9423,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/styled": "npm:^0.33.0" @@ -9511,7 +9511,7 @@ __metadata: "@rocket.chat/fuselage": "npm:^0.78.0" "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/styled": "npm:^0.33.0" @@ -9607,10 +9607,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/icons@npm:~0.47.0": - version: 0.47.0 - resolution: "@rocket.chat/icons@npm:0.47.0" - checksum: 10/1a7a99a4bd7227f81225dc79bd966057dae1200c461320e7627ebfe2e2cee3a4d62e2577fb0892a9c1d79d57cb38cb69bd523ea1741587203a063c68b7f508d4 +"@rocket.chat/icons@npm:^0.48.0": + version: 0.48.0 + resolution: "@rocket.chat/icons@npm:0.48.0" + checksum: 10/29a8367a541c2dc0ee4d17b6601336aff4a22b84ccd1c743fd85a3d59185695d98bda1703271383142eec966af981b611fdf6f302cc2ba5040c431eeeb7a9536 languageName: node linkType: hard @@ -9967,7 +9967,7 @@ __metadata: "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/i18n": "workspace:^" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jwt": "workspace:^" @@ -10847,7 +10847,7 @@ __metadata: "@rocket.chat/fuselage": "npm:^0.78.0" "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/tsconfig": "workspace:*" "@storybook/addon-a11y": "npm:^8.6.18" "@storybook/addon-essentials": "npm:^8.6.18" @@ -10945,7 +10945,7 @@ __metadata: "@rocket.chat/fuselage": "npm:^0.78.0" "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/ui-contexts": "workspace:^" "@types/react": "npm:~18.3.28" "@types/react-dom": "npm:~18.3.7" @@ -10972,7 +10972,7 @@ __metadata: "@rocket.chat/fuselage": "npm:^0.78.0" "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/layout": "npm:^0.34.2" "@rocket.chat/logo": "npm:^0.32.5" @@ -11030,7 +11030,7 @@ __metadata: "@rocket.chat/fuselage": "npm:^0.78.0" "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@rocket.chat/ui-client": "workspace:~" @@ -11074,7 +11074,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/i18n": "workspace:~" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" @@ -11105,7 +11105,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-kit@workspace:packages/ui-kit" dependencies: - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" @@ -11132,7 +11132,7 @@ __metadata: "@rocket.chat/fuselage": "npm:^0.78.0" "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/styled": "npm:^0.33.0" "@rocket.chat/tsconfig": "workspace:*" @@ -11186,7 +11186,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/fuselage-ui-kit": "workspace:^" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/media-signaling": "workspace:~" "@rocket.chat/mock-providers": "workspace:~" @@ -11258,7 +11258,7 @@ __metadata: "@rocket.chat/fuselage-toastbar": "npm:^0.35.2" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/fuselage-ui-kit": "workspace:~" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/logo": "npm:^0.32.5" "@rocket.chat/styled": "npm:^0.33.0" "@rocket.chat/tsconfig": "workspace:*" @@ -11295,7 +11295,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "npm:^0.41.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/i18n": "workspace:~" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/layout": "npm:^0.34.2" "@rocket.chat/mock-providers": "workspace:~" "@rocket.chat/rest-typings": "workspace:~" @@ -33335,7 +33335,7 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/icons": "npm:~0.47.0" + "@rocket.chat/icons": "npm:^0.48.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" From fc639467f7d79d70cacb9e2cd8b9281a37418fcd Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 14:06:02 -0300 Subject: [PATCH 31/42] chore: Generate static landing page for popout --- packages/ui-voip/package.json | 5 +- .../ui-voip/src/generate-landing-view.tsx | 136 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 packages/ui-voip/src/generate-landing-view.tsx diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index ea5e4b3dcc402..61a6c63a08a72 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -8,7 +8,8 @@ "/dist" ], "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.build.json", + "build": "rm -rf dist && tsc -p tsconfig.build.json && yarn build:post", + "build:post": "node --no-warnings dist/generate-landing-view.js && rm -f dist/generate-landing-view.*", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint .", "lint:fix": "eslint --fix .", @@ -56,11 +57,13 @@ "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", "@testing-library/user-event": "~14.6.1", + "@types/node": "~22.19.17", "@types/jest": "~30.0.0", "@types/jest-axe": "~3.5.9", "@types/react": "~18.3.28", "@types/react-dom": "~18.3.7", "date-fns": "~4.1.0", + "jsdom": "~26.1.0", "eslint": "~9.39.4", "i18next": "~23.4.9", "jest": "~30.2.0", diff --git a/packages/ui-voip/src/generate-landing-view.tsx b/packages/ui-voip/src/generate-landing-view.tsx new file mode 100644 index 0000000000000..22ff5256374cd --- /dev/null +++ b/packages/ui-voip/src/generate-landing-view.tsx @@ -0,0 +1,136 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { createRequire } from 'module'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +import type { CSSProperties } from 'react'; + +const require = createRequire(import.meta.url); +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// 1. Set up a jsdom DOM environment before loading fuselage. +// Fuselage's webpack bundle reads window/document/document.body at module init time. +// renderToStaticMarkup never calls effects, so the DOM is never actually used. +const { JSDOM } = require('jsdom') as { JSDOM: new (html: string, options?: { url?: string }) => { window: Window & typeof globalThis } }; + +const { window: jsdomWindow } = new JSDOM('', { url: 'http://localhost' }); + +const g = globalThis as Record; +g.self = jsdomWindow; +g.window = jsdomWindow; +g.document = jsdomWindow.document; + +// 2. Redirect all React-family CJS requires to this repo's copy. +// When @rocket.chat/fuselage is symlinked from a separate dev repo it has its own +// node_modules/react, which creates a second React instance and breaks hook calls. +// Module._resolveFilename runs before the module cache lookup, so this forces every +// require('react') — including fuselage's internal ones — to resolve to the same file. +const NodeModule = require('module') as { _resolveFilename: (...args: unknown[]) => string }; + +const origResolveFilename = NodeModule._resolveFilename.bind(NodeModule); +const reactRedirects: Record = { + 'react': require.resolve('react'), + 'react/jsx-runtime': require.resolve('react/jsx-runtime'), + 'react-dom': require.resolve('react-dom'), + 'react-dom/server': require.resolve('react-dom/server'), +}; +NodeModule._resolveFilename = (...args: unknown[]) => { + const request = args[0] as string; + return reactRedirects[request] ?? origResolveFilename(...args); +}; + +// 3. Load fuselage and react-dom/server after all redirects are in place. +const { renderToStaticMarkup } = require('react-dom/server') as typeof import('react-dom/server'); +const { flushSync } = require('react-dom') as typeof import('react-dom'); +const { createRoot } = require('react-dom/client') as typeof import('react-dom/client'); +const { States, StatesIcon, StatesSubtitle, StatesTitle, PaletteStyleTag } = + require('@rocket.chat/fuselage') as typeof import('@rocket.chat/fuselage'); + +const fuselageCssPath = require.resolve('@rocket.chat/fuselage/dist/fuselage.css'); + +// 4. Embed the RocketChat icon font as a base64 data URI. +// fuselage.css sets font-family:RocketChat on .rcx-icon but does not include the +// @font-face declaration — that lives in @rocket.chat/icons. Inlining the woff2 +// makes the HTML fully self-contained with no external font file dependencies. +const iconFontPath = require.resolve('@rocket.chat/icons/dist/font/rocketchat.woff2'); +const iconFontBase64 = readFileSync(iconFontPath).toString('base64'); +const iconFontCss = `@font-face { + font-family: 'RocketChat'; + font-weight: 400; + font-style: normal; + font-display: auto; + src: url('data:font/woff2;base64,${iconFontBase64}') format('woff2'); +}`; + +// 5. Render PaletteStyleTag for all three themes using the real DOM renderer so its +// createPortal calls work. Each instance injects a + + + + + +
${body}
+ +`; + +writeFileSync(`${__dirname}/voice-call-popup.html`, html, 'utf-8'); +console.log('Generated dist/landing-view.html'); From 64d80ae04cee1c7bd2243a19eac723b25f10bf3b Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 16:30:40 -0300 Subject: [PATCH 32/42] chore: Add symbolic link for popup landing page to `public` folder --- apps/meteor/packages/meteor-inject-initial/lib/inject-core.js | 2 ++ apps/meteor/public/voice-call-popup.html | 1 + 2 files changed, 3 insertions(+) create mode 120000 apps/meteor/public/voice-call-popup.html diff --git a/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js b/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js index 6f3a23a66c962..06e67ee0814f2 100644 --- a/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js +++ b/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js @@ -45,6 +45,8 @@ Inject.appUrl = function (url) { // Avoid serving app HTML for declared routes such as /sockjs/. if (typeof RoutePolicy !== 'undefined' && RoutePolicy.classify(url)) return false; + if (url === '/voice-call-popup.html') return false; + // we currently return app HTML on all URLs by default return true; }; diff --git a/apps/meteor/public/voice-call-popup.html b/apps/meteor/public/voice-call-popup.html new file mode 120000 index 0000000000000..b800ef444c8d1 --- /dev/null +++ b/apps/meteor/public/voice-call-popup.html @@ -0,0 +1 @@ +../node_modules/@rocket.chat/ui-voip/dist/voice-call-popup.html \ No newline at end of file From e1a446bd85b19826a02f92047c7e347f1625b101 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 16:31:08 -0300 Subject: [PATCH 33/42] chore: Missing translation for landing page --- packages/i18n/src/locales/en.i18n.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index fbd3ac4c07c26..91348eb2eb813 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6030,6 +6030,7 @@ "You_are_not_authorized_to_access_this_feature": "You are not authorized to access this feature.", "You_are_sharing_your_screen": "You are sharing your screen", "You_can_change_a_different_avatar_too": "You can override the avatar used to post from this integration.", + "You_can_close_this_window": "You can close this window", "You_can_close_this_window_now": "You can close this window now.", "You_can_do_from_account_preferences": "You can do this later from your account preferences", "You_can_search_using_RegExp_eg": "You can search using Regular Expression. e.g. /^text$/i", @@ -7272,4 +7273,4 @@ "Avatar_preview_updated": "Avatar preview updated", "Select_message_from_user": "Select message from {{username}}", "Select_message_from_user_with_preview": "Select message from {{username}}: {{message}}" -} \ No newline at end of file +} From 99b76e6c9479222901668919c2fd036e09b5da3b Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 16:32:05 -0300 Subject: [PATCH 34/42] chore: Update `usePopoutWindow` to open in landing page --- .../ui-voip/src/views/MediaCallPopout.tsx | 2 +- packages/ui-voip/src/views/usePopoutWindow.ts | 77 +++++++++++++------ 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallPopout.tsx b/packages/ui-voip/src/views/MediaCallPopout.tsx index 3b6b17a54e140..f7c169b3dca9a 100644 --- a/packages/ui-voip/src/views/MediaCallPopout.tsx +++ b/packages/ui-voip/src/views/MediaCallPopout.tsx @@ -23,7 +23,7 @@ const MediaCallPopout = () => { useEffect(() => { if (currentViews.includes('popout')) { // TODO: Fix this title - openPopoutWindow('Call with Peer X'); + void openPopoutWindow('Call with Peer X'); return; } closePopoutWindow(); diff --git a/packages/ui-voip/src/views/usePopoutWindow.ts b/packages/ui-voip/src/views/usePopoutWindow.ts index 2cb6f860f4850..d70e4d9a1f66b 100644 --- a/packages/ui-voip/src/views/usePopoutWindow.ts +++ b/packages/ui-voip/src/views/usePopoutWindow.ts @@ -1,38 +1,50 @@ +import { useThemeMode } from '@rocket.chat/ui-client'; +import type { TFunction } from 'i18next'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; -const createRootElement = (externalWindow: Window) => { +const createRootElement = async (externalWindow: Window) => { const newRoot = externalWindow.document.createElement('div'); newRoot.style.width = '100%'; newRoot.style.height = '100%'; - externalWindow.document.body.appendChild(newRoot); + + // await for page to load so that the root exists + await new Promise((resolve) => { + externalWindow.onload = resolve; + }); + + const landingPageRoot = externalWindow.document.getElementById('root'); + if (!landingPageRoot) { + throw new Error('usePopoutWindow - createRootElement - landingPageRoot not found'); + } + + landingPageRoot.appendChild(newRoot); + return newRoot; }; -const copyStylesheets = (externalWindow: Window) => { - Array.from(document.styleSheets).forEach((stylesheet) => { - if (stylesheet.href) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = stylesheet.href; - externalWindow.document.head.appendChild(link); - } else if (stylesheet?.cssRules?.length > 0) { - const style = document.createElement('style'); - Array.from(stylesheet.cssRules).forEach((rule) => { - style.appendChild(document.createTextNode(rule.cssText)); - }); - externalWindow.document.head.appendChild(style); - } +const replaceWithTranslationString = (t: TFunction, externalDocument: Document) => { + externalDocument.querySelectorAll('[data-i18n]').forEach((el) => { + el.textContent = t(el.getAttribute('data-i18n') as string); }); }; -const openExternalWindow = (title: string) => { +const changeTheme = (ownerDocument: Document, theme?: string) => { + if (theme) { + ownerDocument.documentElement.setAttribute('data-color-scheme', theme); + } else { + ownerDocument.documentElement.removeAttribute('data-color-scheme'); + } +}; + +const openExternalWindow = async (title: string, theme: string) => { try { - const externalWindow = window.open('', title, 'width=800,height=500,popup'); + const externalWindow = window.open(`/voice-call-popup.html${theme ? `?theme=${theme}` : ''}`, title, 'width=800,height=500,popup'); if (!externalWindow) { throw new Error('No window was opened'); } - copyStylesheets(externalWindow); - const root = createRootElement(externalWindow); + + const root = await createRootElement(externalWindow); return { root, externalWindow }; } catch (error) { // This should maybe throw instead of returning null @@ -43,7 +55,7 @@ const openExternalWindow = (title: string) => { export type PopoutContainer = { root: HTMLDivElement; ownerDocument: Document }; type PopoutRef = { root: HTMLDivElement; externalWindow: Window; closing: boolean }; -type OpenPopoutWindow = (title: string) => void; +type OpenPopoutWindow = (title: string) => Promise; type ClosePopoutWindow = () => void; type UsePopoutWindowReturn = { @@ -55,14 +67,17 @@ type UsePopoutWindowReturn = { export const usePopoutWindow = (onBeforeUnload: () => void): UsePopoutWindowReturn => { const popoutRef = useRef(null); const [container, setContainer] = useState(null); + const { t } = useTranslation(); + + const [, , theme] = useThemeMode(); const openPopoutWindow = useCallback( - (title: string) => { + async (title: string) => { if (!!popoutRef.current && popoutRef.current.externalWindow?.closed === false) { return; } - const result = openExternalWindow(title); + const result = await openExternalWindow(title, theme); if (result) { const { root, externalWindow } = result; @@ -72,7 +87,7 @@ export const usePopoutWindow = (onBeforeUnload: () => void): UsePopoutWindowRetu } onBeforeUnload(); }, - [onBeforeUnload], + [onBeforeUnload, theme], ); const closePopoutWindow = useCallback(() => { @@ -104,6 +119,20 @@ export const usePopoutWindow = (onBeforeUnload: () => void): UsePopoutWindowRetu }; }, [container, onBeforeUnload, closePopoutWindow]); + useEffect(() => { + if (!container) { + return; + } + replaceWithTranslationString(t, container.ownerDocument); + }, [container, t]); + + useEffect(() => { + if (!container) { + return; + } + changeTheme(container.ownerDocument, theme); + }, [container, theme]); + return { container, openPopoutWindow, From 7630ad74b7d1b62b27b7827048d4e5dc65be9fba Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 16:34:14 -0300 Subject: [PATCH 35/42] chore: Fix landing view generation message --- packages/ui-voip/src/generate-landing-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-voip/src/generate-landing-view.tsx b/packages/ui-voip/src/generate-landing-view.tsx index 22ff5256374cd..fc87f0f6142b7 100644 --- a/packages/ui-voip/src/generate-landing-view.tsx +++ b/packages/ui-voip/src/generate-landing-view.tsx @@ -133,4 +133,4 @@ const html = ` `; writeFileSync(`${__dirname}/voice-call-popup.html`, html, 'utf-8'); -console.log('Generated dist/landing-view.html'); +console.log('Generated dist/voice-call-popup.html'); From 9ac673415a30d3f62eb10ddfe782baf4001633df Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 16:38:36 -0300 Subject: [PATCH 36/42] fix: Packages versions --- packages/ui-voip/package.json | 4 ++-- yarn.lock | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 61a6c63a08a72..ee6480eabe742 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -57,17 +57,17 @@ "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", "@testing-library/user-event": "~14.6.1", - "@types/node": "~22.19.17", "@types/jest": "~30.0.0", "@types/jest-axe": "~3.5.9", + "@types/node": "~22.19.17", "@types/react": "~18.3.28", "@types/react-dom": "~18.3.7", "date-fns": "~4.1.0", - "jsdom": "~26.1.0", "eslint": "~9.39.4", "i18next": "~23.4.9", "jest": "~30.2.0", "jest-axe": "~10.0.0", + "jsdom": "^26.1.0", "react": "~18.3.1", "react-dom": "~18.3.1", "react-virtuoso": "~4.12.8", diff --git a/yarn.lock b/yarn.lock index 67e1a8ef2d7d7..55fc8b2cd4a73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11212,6 +11212,7 @@ __metadata: "@testing-library/user-event": "npm:~14.6.1" "@types/jest": "npm:~30.0.0" "@types/jest-axe": "npm:~3.5.9" + "@types/node": "npm:~22.19.17" "@types/react": "npm:~18.3.28" "@types/react-dom": "npm:~18.3.7" date-fns: "npm:~4.1.0" @@ -11219,6 +11220,7 @@ __metadata: i18next: "npm:~23.4.9" jest: "npm:~30.2.0" jest-axe: "npm:~10.0.0" + jsdom: "npm:^26.1.0" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-i18next: "npm:~13.2.2" From 14eff1f4df74bfb30ed8aa59bbad8af644d7a3d6 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 17:13:19 -0300 Subject: [PATCH 37/42] fix: Mocked provider types --- .../ui-voip/src/context/usePeekMediaSessionPeerInfo.spec.tsx | 5 +++-- .../ui-voip/src/context/usePeekMediaSessionState.spec.tsx | 5 +++-- packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx | 5 +++-- .../ui-voip/src/hooks/useMediaCallOpenRoomTracker.spec.tsx | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ui-voip/src/context/usePeekMediaSessionPeerInfo.spec.tsx b/packages/ui-voip/src/context/usePeekMediaSessionPeerInfo.spec.tsx index 464daafc926ae..7b2eab5e020e0 100644 --- a/packages/ui-voip/src/context/usePeekMediaSessionPeerInfo.spec.tsx +++ b/packages/ui-voip/src/context/usePeekMediaSessionPeerInfo.spec.tsx @@ -16,8 +16,9 @@ const createWrapper = (instance: MockInstance | undefined) => { const wrapper = ({ children }: { children?: ReactNode }) => ( undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, instance: instance as any, signalEmitter: new Emitter(), audioElement: undefined, diff --git a/packages/ui-voip/src/context/usePeekMediaSessionState.spec.tsx b/packages/ui-voip/src/context/usePeekMediaSessionState.spec.tsx index d493ff77d466d..a6d31f6392948 100644 --- a/packages/ui-voip/src/context/usePeekMediaSessionState.spec.tsx +++ b/packages/ui-voip/src/context/usePeekMediaSessionState.spec.tsx @@ -16,8 +16,9 @@ const createWrapper = (instance: MockInstance | undefined) => { const wrapper = ({ children }: { children?: ReactNode }) => ( undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, instance: instance as any, signalEmitter: new Emitter(), audioElement: undefined, diff --git a/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx b/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx index 6153410802d72..1382b150ae007 100644 --- a/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx +++ b/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx @@ -24,8 +24,9 @@ const appRoot = () => .wrap((children) => ( undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, instance: undefined, signalEmitter: new Emitter(), audioElement: undefined, diff --git a/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.spec.tsx b/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.spec.tsx index 3fd13b28c919e..543d43e4dc345 100644 --- a/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.spec.tsx +++ b/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.spec.tsx @@ -12,8 +12,9 @@ const createWrapper = () => { const wrapper = ({ children }: { children?: ReactNode }) => ( undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, instance: undefined, signalEmitter: new Emitter(), audioElement: undefined, From e0b53bd0946067a5dd0123cd2dd9843c5be2d26b Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 5 Jun 2026 17:17:24 -0300 Subject: [PATCH 38/42] fix: Widget unit tests --- .../src/providers/MockedMediaCallProvider.tsx | 2 +- .../__snapshots__/MediaCallWidget.spec.tsx.snap | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx index 8a971fb8ff5c0..9f232c65fbe15 100644 --- a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx +++ b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx @@ -168,7 +168,7 @@ const MockedMediaCallProvider = ({ getState: () => null, on: () => undefined, } as unknown as MediaSignalingSession, - currentViews: [] as AvailableViews[], + currentViews: ['widget'] as AvailableViews[], registerView: (_view: AvailableViews) => undefined, unregisterView: (_view: AvailableViews) => undefined, signalEmitter: new Emitter(), diff --git a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap index d5423e552f3e5..1b98ceccc4d50 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap +++ b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap @@ -594,6 +594,20 @@ exports[`renders OngoingCall without crashing 1`] = `
+ + + + {localScreen?.active && ( + + )} + )} {!isPopout && ( From 76a2a7169d5d0d8837e312be812943f32c800062 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 11 Jun 2026 18:15:29 -0300 Subject: [PATCH 41/42] fix: Theme not applied on page open --- packages/ui-voip/src/views/usePopoutWindow.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui-voip/src/views/usePopoutWindow.ts b/packages/ui-voip/src/views/usePopoutWindow.ts index d70e4d9a1f66b..45045694d3455 100644 --- a/packages/ui-voip/src/views/usePopoutWindow.ts +++ b/packages/ui-voip/src/views/usePopoutWindow.ts @@ -39,11 +39,13 @@ const changeTheme = (ownerDocument: Document, theme?: string) => { const openExternalWindow = async (title: string, theme: string) => { try { - const externalWindow = window.open(`/voice-call-popup.html${theme ? `?theme=${theme}` : ''}`, title, 'width=800,height=500,popup'); + const externalWindow = window.open(`/voice-call-popup.html`, title, 'width=800,height=500,popup'); if (!externalWindow) { throw new Error('No window was opened'); } + changeTheme(externalWindow.document, theme); + const root = await createRootElement(externalWindow); return { root, externalWindow }; } catch (error) { From 784357cb3789e8f1568437fd19b948aeaa566b27 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 12 Jun 2026 09:29:41 -0300 Subject: [PATCH 42/42] chore: yarn lock --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5ef66da980b77..8fae300af7b53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7292,7 +7292,7 @@ __metadata: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch": version: 3.25.0 - resolution: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch::version=3.25.0&hash=5cb6f6" + resolution: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch::version=3.25.0&hash=8f84db" dependencies: "@react-aria/focus": "npm:^3.19.1" "@react-aria/i18n": "npm:^3.12.5" @@ -7308,7 +7308,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/dd5b420d0cda39dbd9c60a5257e2783d22196b77ec98b3982a2176abb25d117f3593f49da7ee2e6df8799e0a9dc43b7d88b503d419f3d57310b041bb3f781f7b + checksum: 10/0a13ca74216d854e8fb7d3e055398eabfe8cb972f8ccc12fbd5dc59716a2dac8eb16897cecb03b936a60f2b79c64f27e23fa563059ef181f753f751890c63230 languageName: node linkType: hard