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..e6b13c5de83ea --- /dev/null +++ b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch @@ -0,0 +1,351 @@ +diff --git a/dist/useOverlayPosition.main.js b/dist/useOverlayPosition.main.js +index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef85b7a4d93 100644 +--- a/dist/useOverlayPosition.main.js ++++ b/dist/useOverlayPosition.main.js +@@ -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)=>{ ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.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 +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 +- 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 +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) { +- 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 +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 +- $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 +167,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 +212,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..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, 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..f6e02859a5d60ce9503a93173b453b8553f76a70 100644 +--- a/dist/useOverlayPosition.mjs ++++ b/dist/useOverlayPosition.mjs +@@ -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)=>{ ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.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 +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 +- 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 +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) { +- 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 +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 +- $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 +161,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 +206,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..e7a834e44579e23558be7ba178da2150717192f4 100644 +--- a/dist/useOverlayPosition.module.js ++++ b/dist/useOverlayPosition.module.js +@@ -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)=>{ ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.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 +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 +- 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 +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) { +- 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 +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 +- $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 +161,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 +206,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..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, 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 diff --git a/apps/meteor/client/providers/MediaCallProvider.tsx b/apps/meteor/client/providers/MediaCallProvider.tsx index e741e76d51025..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( () => ({ - inRoomView: false, - setInRoomView: () => undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, instance: undefined, signalEmitter: new Emitter(), audioElement: undefined, 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/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 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/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/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/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 6d06dfd8b6ad8..91348eb2eb813 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1052,6 +1052,7 @@ "Call_not_found": "Call not found", "Call_not_found_error": "This could happen when the call URL is not valid, or you're having connection issues. Please check with the source of the call URL and try again, or talk to your workspace administrator if the problem persists", "Call_ongoing": "Call ongoing", + "Call_open_separate_window": "Call open in a separate window", "Call_provider": "Call Provider", "Call_ringer_volume": "Call ringer volume", "Call_ringer_volume_hint": "For all incoming voice and video call notifications", @@ -2154,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", @@ -4063,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", @@ -4608,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", @@ -4978,6 +4982,7 @@ "Show_agent_email": "Show agent email", "Show_agent_info": "Show agent information", "Show_all": "Show All", + "Show_call_here": "Show call here", "Show_counter": "Mark as unread", "Show_default_content": "Show default content", "Show_email_field": "Show email field", @@ -6025,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", @@ -7267,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 +} 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..ee6480eabe742 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 .", @@ -34,7 +35,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", @@ -58,6 +59,7 @@ "@testing-library/user-event": "~14.6.1", "@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", @@ -65,6 +67,7 @@ "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/packages/ui-voip/src/components/ToggleButton.tsx b/packages/ui-voip/src/components/ToggleButton.tsx index 86f6a72852d4a..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, ...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 ? 'font-danger' : undefined; + 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/context/MediaCallInstanceContext.ts b/packages/ui-voip/src/context/MediaCallInstanceContext.ts index f520079ba5488..72eced82fa6b7 100644 --- a/packages/ui-voip/src/context/MediaCallInstanceContext.ts +++ b/packages/ui-voip/src/context/MediaCallInstanceContext.ts @@ -10,15 +10,21 @@ export type Signals = { toggleWidget: { peerInfo?: PeerInfo }; }; +export type AvailableViews = 'room' | 'popout' | 'widget'; + +type RegisterView = (view: AvailableViews) => void; +type UnregisterView = (view: AvailableViews) => void; + type MediaCallInstanceContextValue = { instance: MediaSignalingSession | undefined; signalEmitter: Emitter; audioElement: RefObject | undefined; openRoomId: string | undefined; - inRoomView: boolean; + currentViews: AvailableViews[]; setOpenRoomId: (openRoomId: string | undefined) => void; getAutocompleteOptions: (filter: string) => Promise; - setInRoomView: (inRoomView: boolean) => void; + registerView: RegisterView; + unregisterView: UnregisterView; }; export const MediaCallInstanceContext = createContext({ @@ -28,8 +34,9 @@ export const MediaCallInstanceContext = createContext undefined, getAutocompleteOptions: () => Promise.resolve([]), - inRoomView: false, - setInRoomView: () => undefined, + currentViews: [], + registerView: () => undefined, + unregisterView: () => undefined, }); export const useMediaCallInstance = (): MediaCallInstanceContextValue => useContext(MediaCallInstanceContext); diff --git a/packages/ui-voip/src/context/MediaCallViewContext.ts b/packages/ui-voip/src/context/MediaCallViewContext.ts index c10f93fbfda7f..29ebe544c1324 100644 --- a/packages/ui-voip/src/context/MediaCallViewContext.ts +++ b/packages/ui-voip/src/context/MediaCallViewContext.ts @@ -23,6 +23,8 @@ type MediaCallViewContextValue = { onAccept: () => Promise; onSelectPeer: (peerInfo: PeerInfo) => void; onToggleScreenSharing: () => void; + onOpenPopout: () => void; + onClosePopout: () => void; streams: MediaCallStreams; widgetPositionTracker?: { onChangePosition: (position: LastKnownPosition | null) => void; @@ -56,6 +58,8 @@ export const defaultMediaCallContextValue: MediaCallViewContextValue = { onAccept: () => Promise.resolve(undefined), onSelectPeer: () => undefined, onToggleScreenSharing: () => undefined, + onOpenPopout: () => undefined, + onClosePopout: () => undefined, streams: {}, }; 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/context/useRegisterView.ts b/packages/ui-voip/src/context/useRegisterView.ts new file mode 100644 index 0000000000000..d0059fa1b1c27 --- /dev/null +++ b/packages/ui-voip/src/context/useRegisterView.ts @@ -0,0 +1,18 @@ +import { useLayoutEffect } from 'react'; + +import { useMediaCallInstance } from '.'; +import type { AvailableViews } from './MediaCallInstanceContext'; + +const useRegisterView = (view: AvailableViews): AvailableViews[] => { + const { currentViews, registerView, unregisterView } = useMediaCallInstance(); + + useLayoutEffect(() => { + registerView(view); + + return () => unregisterView(view); + }, [view, registerView, unregisterView]); + + return currentViews; +}; + +export default useRegisterView; diff --git a/packages/ui-voip/src/context/useRoomView.ts b/packages/ui-voip/src/context/useRoomView.ts deleted file mode 100644 index c877a726d4d8a..0000000000000 --- a/packages/ui-voip/src/context/useRoomView.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useLayoutEffect } from 'react'; - -import { useMediaCallInstance } from '.'; - -const useRoomView = () => { - const { setInRoomView } = useMediaCallInstance(); - - useLayoutEffect(() => { - setInRoomView(true); - return () => { - setInRoomView(false); - }; - }, [setInRoomView]); -}; - -export default useRoomView; 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..fc87f0f6142b7 --- /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/voice-call-popup.html'); 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, diff --git a/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx b/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx index 8f4e1bc2a8f40..f1b13a5e6dc5a 100644 --- a/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx +++ b/packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx @@ -4,6 +4,7 @@ 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'; @@ -15,7 +16,7 @@ type MediaCallInstanceProviderProps = { const MediaCallInstanceProvider = ({ children }: MediaCallInstanceProviderProps) => { const [openRoomId, setOpenRoomId] = useState(undefined); - const [inRoomView, setInRoomView] = useState(false); + const { currentViews, registerView, unregisterView } = useAvailableViewTracker(); const user = useUser(); const instance = useMediaSessionInstance(user?._id); const [signalEmitter] = useState(() => new Emitter()); @@ -25,8 +26,18 @@ const MediaCallInstanceProvider = ({ children }: MediaCallInstanceProviderProps) const getAutocompleteOptions = useGetAutocompleteOptions(instance); const value = useMemo( - () => ({ instance, signalEmitter, audioElement, openRoomId, setOpenRoomId, getAutocompleteOptions, inRoomView, setInRoomView }), - [instance, signalEmitter, audioElement, openRoomId, setOpenRoomId, getAutocompleteOptions, inRoomView], + () => ({ + instance, + signalEmitter, + audioElement, + openRoomId, + setOpenRoomId, + getAutocompleteOptions, + currentViews, + registerView, + unregisterView, + }), + [instance, signalEmitter, audioElement, openRoomId, setOpenRoomId, getAutocompleteOptions, currentViews, registerView, unregisterView], ); return ( diff --git a/packages/ui-voip/src/providers/MediaCallProvider.tsx b/packages/ui-voip/src/providers/MediaCallProvider.tsx index 3940882d7ef7a..f3e4ff0faec85 100644 --- a/packages/ui-voip/src/providers/MediaCallProvider.tsx +++ b/packages/ui-voip/src/providers/MediaCallProvider.tsx @@ -1,7 +1,10 @@ +import { AnchorPortal } from '@rocket.chat/ui-client'; import type { ReactNode } from 'react'; import MediaCallInstanceProvider from './MediaCallInstanceProvider'; import MediaCallViewProvider from './MediaCallViewProvider'; +import { MediaCallWidget } from '../views'; +import MediaCallPopout from '../views/MediaCallPopout'; type MediaCallProviderProps = { children: ReactNode; @@ -10,7 +13,12 @@ type MediaCallProviderProps = { const MediaCallProvider = ({ children }: MediaCallProviderProps) => { return ( - + + + + + + {children} ); diff --git a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx index bbf30e8a9fdea..32a31eaaca2a9 100644 --- a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx +++ b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx @@ -1,4 +1,4 @@ -import { AnchorPortal, useGoToDirectMessage } from '@rocket.chat/ui-client'; +import { useGoToDirectMessage } from '@rocket.chat/ui-client'; import type { Device } from '@rocket.chat/ui-contexts'; import { useSetOutputMediaDevice, @@ -19,11 +19,11 @@ import { useScreenShareStreams } from './useScreenShareStreams'; import { useWidgetExternalControlSignalListener } from './useWidgetExternalControlSignalListener'; import useWidgetPositionTracker from './useWidgetPositionTracker'; import { useMediaCallInstance } 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'; import { isValidTone, useTonePlayer } from '../hooks/useTonePlayer'; -import { MediaCallWidget } from '../views'; import TransferModal from '../views/TransferModal'; type MediaCallViewProviderProps = { @@ -36,7 +36,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { const setModal = useSetModal(); - const { instance, audioElement, openRoomId } = useMediaCallInstance(); + const { instance, audioElement, openRoomId, registerView, unregisterView } = useMediaCallInstance(); const { sessionState, toggleWidget, selectPeer } = useMediaSession(instance); const controls = useMediaSessionControls(instance); @@ -209,6 +209,14 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { controls.toggleScreenSharing(); }; + const onOpenPopout = useCallback(() => { + registerView('popout'); + }, [registerView]); + + const onClosePopout = useCallback(() => { + unregisterView('popout'); + }, [unregisterView]); + const streams = useScreenShareStreams(instance); useWidgetExternalControlSignalListener( @@ -242,6 +250,8 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { onAccept, onSelectPeer, onToggleScreenSharing, + onOpenPopout, + onClosePopout, streams, widgetPositionTracker: { onChangePosition, @@ -249,14 +259,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { }, }; - return ( - - - - - {children} - - ); + return {children}; }; export default MediaCallViewProvider; diff --git a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx index ec13c9f75113b..9f232c65fbe15 100644 --- a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx +++ b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx @@ -4,7 +4,8 @@ import type { MediaSignalingSession } from '@rocket.chat/media-signaling'; import type { ReactNode } from 'react'; import { useState } from 'react'; -import { MediaCallInstanceContext, type Signals } from '../context/MediaCallInstanceContext'; +import { MediaCallInstanceContext } from '../context/MediaCallInstanceContext'; +import type { AvailableViews, Signals } from '../context/MediaCallInstanceContext'; import MediaCallViewContext from '../context/MediaCallViewContext'; import type { State, PeerInfo, SessionState } from '../context/definitions'; @@ -158,6 +159,8 @@ const MockedMediaCallProvider = ({ onSelectPeer, streams: {}, onToggleScreenSharing: () => undefined, + onOpenPopout: () => undefined, + onClosePopout: () => undefined, }; const instanceContextValue = { @@ -165,13 +168,14 @@ const MockedMediaCallProvider = ({ getState: () => null, on: () => undefined, } as unknown as MediaSignalingSession, + currentViews: ['widget'] as AvailableViews[], + registerView: (_view: AvailableViews) => undefined, + unregisterView: (_view: AvailableViews) => undefined, signalEmitter: new Emitter(), audioElement: undefined, openRoomId: undefined, setOpenRoomId: () => undefined, getAutocompleteOptions, - inRoomView: false, - setInRoomView: () => undefined, }; return ( diff --git a/packages/ui-voip/src/providers/useAvailableViewTracker.ts b/packages/ui-voip/src/providers/useAvailableViewTracker.ts new file mode 100644 index 0000000000000..cde6b2afd139d --- /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 = viewsArray; + 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/MediaCallPopout.tsx b/packages/ui-voip/src/views/MediaCallPopout.tsx new file mode 100644 index 0000000000000..f7c169b3dca9a --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopout.tsx @@ -0,0 +1,39 @@ +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(onClosePopout); + + const onClosePopoutAndWindow = useCallback(() => { + onClosePopout(); + closePopoutWindow(); + }, [onClosePopout, closePopoutWindow]); + + useLayoutEffect(() => { + if (sessionState.state !== 'ongoing') { + onClosePopout(); + } + }, [sessionState.state, onClosePopout]); + + useEffect(() => { + if (currentViews.includes('popout')) { + // TODO: Fix this title + void openPopoutWindow('Call with Peer X'); + return; + } + closePopoutWindow(); + }, [currentViews, openPopoutWindow, closePopoutWindow]); + + if (!container) { + return null; + } + + return ; +}; + +export default MediaCallPopout; diff --git a/packages/ui-voip/src/views/MediaCallPopoutView.tsx b/packages/ui-voip/src/views/MediaCallPopoutView.tsx new file mode 100644 index 0000000000000..4779c702cb79e --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopoutView.tsx @@ -0,0 +1,163 @@ +import { Box, ButtonGroup } from '@rocket.chat/fuselage'; +import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + ToggleButton, + Timer, + DevicePicker, + ActionButton, + CardListContainer, + CardListSection, + PeerCard, + StreamCard, + useShouldWrapCards, + // CARD_LIST_SECTION_MAX_HEIGHT, + ActionStrip, + // ActionToggleChat, +} from '../components'; +import { useMediaCallView } from '../context/MediaCallViewContext'; +// import useRegisterView from '../context/useRegisterView'; +import { usePlayMediaStream } from '../providers/usePlayMediaStream'; + +type MediaCallPopoutViewProps = { + user: { + displayName: string; + avatarUrl: string; + }; + onClickClosePopout: () => void; + onClickFullscreen: () => void; + fullscreen: boolean; +}; + +const MediaCallPopoutView = ({ user, onClickClosePopout, onClickFullscreen, fullscreen }: MediaCallPopoutViewProps) => { + const { t } = useTranslation(); + + const [focusedCard, setFocusedCard] = useState<'remote' | 'local' | null>('remote'); + const { + sessionState, + onMute, + onHold, + onForward, + onEndCall, + onToggleScreenSharing, + streams: { remoteScreen, localScreen }, + } = useMediaCallView(); + + const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; + + const { ref, borderBoxSize } = useResizeObserver(); + + const shouldWrapCards = useShouldWrapCards(false, borderBoxSize?.blockSize || 0); + + const connecting = connectionState === 'CONNECTING'; + const reconnecting = connectionState === 'RECONNECTING'; + + const [remoteStreamRefCallback] = usePlayMediaStream(remoteScreen?.stream ?? null); + const [localStreamRefCallback] = usePlayMediaStream(localScreen?.stream ?? null); + + const onClickFocusRemoteCard = () => { + setFocusedCard((prev) => (prev === 'remote' ? null : 'remote')); + }; + + const onClickFocusLocalCard = () => { + setFocusedCard((prev) => (prev === 'local' ? null : 'local')); + }; + + if (!peerInfo || 'number' in peerInfo) { + return null; + } + + const remoteStreamCard = remoteScreen?.active ? ( + + + + ) : null; + + const localStreamCard = localScreen?.active ? ( + + + + ) : null; + + const focusedCardElement = focusedCard === 'remote' ? remoteStreamCard : localStreamCard; + + return ( + + + + + + {focusedCard !== 'remote' && remoteStreamCard} + {focusedCard !== 'local' && localStreamCard} + + + + + + } + rightSlot={ + + + + + + } + > + + + + + + + + ); +}; + +export default memo(MediaCallPopoutView); diff --git a/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx new file mode 100644 index 0000000000000..83e30428a3255 --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx @@ -0,0 +1,70 @@ +import { Box, OwnerDocument as FuselageOwnerDocument } from '@rocket.chat/fuselage'; +import { OwnerDocument as StyledOwnerDocument } from '@rocket.chat/styled'; +import { TooltipProvider, useUserDisplayName } from '@rocket.chat/ui-client'; +import { useUser, useUserAvatarPath } from '@rocket.chat/ui-contexts'; +import { useCallback, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import MediaCallPopoutView from './MediaCallPopoutView'; +import type { PopoutContainer } from './usePopoutWindow'; + +type MediaCallPopoutWindowProps = { + container: PopoutContainer; + onClosePopout: () => void; +}; +const MediaCallPopoutWindow = ({ container, onClosePopout }: MediaCallPopoutWindowProps) => { + const [fullscreen, setFullscreen] = useState(false); + + const user = useUser(); + const displayName = useUserDisplayName({ name: user?.name, username: user?.username }); + const getUserAvatarPath = useUserAvatarPath(); + const ownUser = useMemo(() => { + return { + displayName: displayName || '', + avatarUrl: getUserAvatarPath({ userId: user?._id || '' }), + }; + }, [displayName, getUserAvatarPath, user?._id]); + + const { root, ownerDocument } = container; + + const onClickFullscreen = useCallback(() => { + const requestFullScreen = async () => { + try { + if (!fullscreen) { + await ownerDocument.documentElement.requestFullscreen(); + setFullscreen(true); + } else { + await ownerDocument.exitFullscreen(); + setFullscreen(false); + } + } catch (error) { + console.error('Error requesting fullscreen', error); + } + }; + void requestFullScreen(); + }, [ownerDocument, fullscreen]); + + const contextValue = useMemo(() => ({ document: ownerDocument }), [ownerDocument]); + + return ( + + + + {createPortal( + + + , + 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..598c216fa732f 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomActivity.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomActivity.tsx @@ -14,8 +14,8 @@ type MediaCallRoomActivityProps = { const MediaCallRoomActivity = ({ children }: MediaCallRoomActivityProps) => { const [showChat, setShowChat] = useState(true); - const user = useUser(); + const user = useUser(); const displayName = useUserDisplayName({ name: user?.name, username: user?.username }); const getUserAvatarPath = useUserAvatarPath(); @@ -28,20 +28,16 @@ const MediaCallRoomActivity = ({ children }: MediaCallRoomActivityProps) => { }; }, [displayName, getUserAvatarPath, user?._id]); - const onClickToggleChat = () => { - setShowChat((prev) => !prev); - }; return ( setShowChat((prev) => !prev)} user={ownUser} containerHeight={borderBoxSize?.blockSize || 0} /> - {showChat && ( {children} diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index fdf221acd8e4e..103c092c26bf6 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx @@ -1,4 +1,4 @@ -import { Box, ButtonGroup } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,8 +16,9 @@ import { ActionStrip, ActionToggleChat, } from '../../components'; +import { useMediaCallInstance } from '../../context/MediaCallInstanceContext'; import { useMediaCallView } from '../../context/MediaCallViewContext'; -import useRoomView from '../../context/useRoomView'; +import useRegisterView from '../../context/useRegisterView'; import { usePlayMediaStream } from '../../providers/usePlayMediaStream'; type MediaCallRoomSectionProps = { @@ -55,8 +56,13 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: onForward, onEndCall, onToggleScreenSharing, + onOpenPopout, + onClosePopout, streams: { remoteScreen, localScreen }, } = useMediaCallView(); + const { currentViews } = useMediaCallInstance(); + + const isPopout = currentViews.includes('popout'); const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; @@ -68,7 +74,7 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: const [remoteStreamRefCallback] = usePlayMediaStream(remoteScreen?.stream ?? null); const [localStreamRefCallback] = usePlayMediaStream(localScreen?.stream ?? null); - useRoomView(); + useRegisterView('room'); const onClickFocusRemoteCard = () => { setFocusedCard((prev) => (prev === 'remote' ? null : 'remote')); @@ -84,7 +90,14 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: const remoteStreamCard = remoteScreen?.active ? ( - @@ -98,7 +111,14 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: focused={focusedCard === 'local'} showStopSharingOnHover > -