From 94b75a5154f58849513b2931fbae13e44b502807 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:26:06 -0300 Subject: [PATCH 1/5] fix: `@react-aria/overlays` patch not working (#40994) --- ...-aria-overlays-npm-3.25.0-2628866e6e.patch | 539 +++++++++++++++++- yarn.lock | 4 +- 2 files changed, 531 insertions(+), 12 deletions(-) diff --git a/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch index e6b13c5de83ea..50cec88736f0c 100644 --- a/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch +++ b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch @@ -1,5 +1,458 @@ +diff --git a/dist/calculatePosition.main.js b/dist/calculatePosition.main.js +index a7b270bb7e0eb62a0153965bf15d847ad20aa5aa..242e220482ede7cc771de25f41c1e27703d74376 100644 +--- a/dist/calculatePosition.main.js ++++ b/dist/calculatePosition.main.js +@@ -42,28 +42,37 @@ const $5935ba4d7da2c103$var$TOTAL_SIZE = { + height: 'totalHeight' + }; + const $5935ba4d7da2c103$var$PARSED_PLACEMENT_CACHE = {}; +-let $5935ba4d7da2c103$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $5935ba4d7da2c103$var$getWindow = (targetNode)=>{ ++ return (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++}; ++let $5935ba4d7da2c103$var$getVisualViewport = (actualWindow)=>{ ++ return actualWindow.visualViewport || null; ++}; ++function $5935ba4d7da2c103$var$getDocumentElement(node) { ++ return node.ownerDocument.documentElement || document.documentElement; ++} + function $5935ba4d7da2c103$var$getContainerDimensions(containerNode) { ++ const visualViewport = $5935ba4d7da2c103$var$getVisualViewport($5935ba4d7da2c103$var$getWindow(containerNode)); + let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; + let scroll = {}; + var _visualViewport_scale; +- let isPinchZoomedIn = ((_visualViewport_scale = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; ++ let isPinchZoomedIn = ((_visualViewport_scale = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; + if (containerNode.tagName === 'BODY') { +- let documentElement = document.documentElement; ++ let documentElement = $5935ba4d7da2c103$var$getDocumentElement(containerNode); + totalWidth = documentElement.clientWidth; + totalHeight = documentElement.clientHeight; + var _visualViewport_width; +- width = (_visualViewport_width = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; ++ width = (_visualViewport_width = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; + var _visualViewport_height; +- height = (_visualViewport_height = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; ++ height = (_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; + scroll.top = documentElement.scrollTop || containerNode.scrollTop; + scroll.left = documentElement.scrollLeft || containerNode.scrollLeft; + // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with + // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate + // coordinates/values with respect to the visual viewport or with respect to the layout viewport. +- if ($5935ba4d7da2c103$var$visualViewport) { +- top = $5935ba4d7da2c103$var$visualViewport.offsetTop; +- left = $5935ba4d7da2c103$var$visualViewport.offsetLeft; ++ if (visualViewport) { ++ top = visualViewport.offsetTop; ++ left = visualViewport.offsetLeft; + } + } else { + ({ width: width, height: height, top: top, left: left } = $5935ba4d7da2c103$var$getOffset(containerNode)); +@@ -80,9 +89,9 @@ function $5935ba4d7da2c103$var$getContainerDimensions(containerNode) { + scroll.top = 0; + scroll.left = 0; + var _visualViewport_pageTop; +- top = (_visualViewport_pageTop = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; ++ top = (_visualViewport_pageTop = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; + var _visualViewport_pageLeft; +- left = (_visualViewport_pageLeft = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; ++ left = (_visualViewport_pageLeft = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; + } + return { + width: width, +@@ -128,7 +137,8 @@ containerDimensions, padding, containerOffsetWithBoundary) { + else return 0; + } + function $5935ba4d7da2c103$var$getMargins(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return { + top: parseInt(style.marginTop, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, +@@ -275,13 +285,14 @@ function $5935ba4d7da2c103$export$6839422d1f33cee9(placementInput, childOffset, + } + function $5935ba4d7da2c103$export$b3ceb0cbf1056d98(opts) { + let { placement: placement, targetNode: targetNode, overlayNode: overlayNode, scrollNode: scrollNode, padding: padding, shouldFlip: shouldFlip, boundaryElement: boundaryElement, offset: offset, crossOffset: crossOffset, maxHeight: maxHeight, arrowSize: arrowSize = 0, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = opts; ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(targetNode); + let container = overlayNode instanceof HTMLElement ? $5935ba4d7da2c103$var$getContainingBlock(overlayNode) : document.documentElement; + let isViewportContainer = container === document.documentElement; +- const containerPositionStyle = window.getComputedStyle(container).position; ++ const containerPositionStyle = actualWindow.getComputedStyle(container).position; + let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static'; + let childOffset = isViewportContainer ? $5935ba4d7da2c103$var$getOffset(targetNode) : $5935ba4d7da2c103$var$getPosition(targetNode, container); + if (!isViewportContainer) { +- let { marginTop: marginTop, marginLeft: marginLeft } = window.getComputedStyle(targetNode); ++ let { marginTop: marginTop, marginLeft: marginLeft } = actualWindow.getComputedStyle(targetNode); + childOffset.top += parseInt(marginTop, 10) || 0; + childOffset.left += parseInt(marginLeft, 10) || 0; + } +@@ -306,7 +317,7 @@ function $5935ba4d7da2c103$export$b3ceb0cbf1056d98(opts) { + } + function $5935ba4d7da2c103$var$getOffset(node) { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +- let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = document.documentElement; ++ let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = $5935ba4d7da2c103$var$getDocumentElement(node); + return { + top: top + scrollTop - clientTop, + left: left + scrollLeft - clientLeft, +@@ -315,7 +326,8 @@ function $5935ba4d7da2c103$var$getOffset(node) { + }; + } + function $5935ba4d7da2c103$var$getPosition(node, parent) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + let offset; + if (style.position === 'fixed') { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +@@ -328,7 +340,7 @@ function $5935ba4d7da2c103$var$getPosition(node, parent) { + } else { + offset = $5935ba4d7da2c103$var$getOffset(node); + let parentOffset = $5935ba4d7da2c103$var$getOffset(parent); +- let parentStyle = window.getComputedStyle(parent); ++ let parentStyle = actualWindow.getComputedStyle(parent); + parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop; + parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft; + offset.top -= parentOffset.top; +@@ -345,10 +357,11 @@ function $5935ba4d7da2c103$var$getContainingBlock(node) { + // The offsetParent of an element in most cases equals the containing block. + // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent + let offsetParent = node.offsetParent; ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); + // The offsetParent algorithm terminates at the document body, + // even if the body is not a containing block. Double check that + // and use the documentElement if so. +- if (offsetParent && offsetParent === document.body && window.getComputedStyle(offsetParent).position === 'static' && !$5935ba4d7da2c103$var$isContainingBlock(offsetParent)) offsetParent = document.documentElement; ++ if (offsetParent && offsetParent === (node.ownerDocument || document).body && actualWindow.getComputedStyle(offsetParent).position === 'static' && !$5935ba4d7da2c103$var$isContainingBlock(offsetParent)) offsetParent = $5935ba4d7da2c103$var$getDocumentElement(node); + // TODO(later): handle table elements? + // The offsetParent can be null if the element has position: fixed, or a few other cases. + // We have to walk up the tree manually in this case because fixed positioned elements +@@ -358,11 +371,12 @@ function $5935ba4d7da2c103$var$getContainingBlock(node) { + while(offsetParent && !$5935ba4d7da2c103$var$isContainingBlock(offsetParent))offsetParent = offsetParent.parentElement; + } + // Fall back to the viewport. +- return offsetParent || document.documentElement; ++ return offsetParent || $5935ba4d7da2c103$var$getDocumentElement(node); + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + function $5935ba4d7da2c103$var$isContainingBlock(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return style.transform !== 'none' || /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || 'backdropFilter' in style && style.backdropFilter !== 'none' || 'WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none'; + } + +diff --git a/dist/calculatePosition.main.js.map b/dist/calculatePosition.main.js.map +index ad521e103b06701d6efc5361fbaee754f9d0b3b2..860c2cbce8ce457f15813d176f17d10c945775bc 100644 +--- a/dist/calculatePosition.main.js.map ++++ b/dist/calculatePosition.main.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.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 {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = document.documentElement;\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the
).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n let style = window.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = window.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = window.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = document.documentElement;\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n let style = window.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = window.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === document.body &&\n window.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = document.documentElement;\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || document.documentElement;\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n let style = window.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.main.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.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 {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet getWindow = (targetNode?: Element | null): Window => {\n return targetNode?.ownerDocument.defaultView || window;\n};\n\nlet getVisualViewport = (actualWindow: Window): VisualViewport | null => {\n return actualWindow.visualViewport || null;\n};\n\n\nfunction getDocumentElement(node: Element): Element {\n return node.ownerDocument.documentElement || document.documentElement;\n}\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n const visualViewport = getVisualViewport(getWindow(containerNode));\n\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = getDocumentElement(containerNode);\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n const actualWindow = getWindow(targetNode);\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = actualWindow.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = actualWindow.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = getDocumentElement(node);\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n const actualWindow = getWindow(node);\n\n let style = actualWindow.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = actualWindow.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n const actualWindow = getWindow(node);\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === (node.ownerDocument || document).body &&\n actualWindow.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = getDocumentElement(node);\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || getDocumentElement(node);\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.main.js.map"} +\ No newline at end of file +diff --git a/dist/calculatePosition.mjs b/dist/calculatePosition.mjs +index 2b4d227e0547ad0acf92152255b8052a4b30ad3d..bc0435df4048872a693c0a97942aa3ce8fb563d8 100644 +--- a/dist/calculatePosition.mjs ++++ b/dist/calculatePosition.mjs +@@ -36,28 +36,37 @@ const $edcf132a9284368a$var$TOTAL_SIZE = { + height: 'totalHeight' + }; + const $edcf132a9284368a$var$PARSED_PLACEMENT_CACHE = {}; +-let $edcf132a9284368a$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $edcf132a9284368a$var$getWindow = (targetNode)=>{ ++ return (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++}; ++let $edcf132a9284368a$var$getVisualViewport = (actualWindow)=>{ ++ return actualWindow.visualViewport || null; ++}; ++function $edcf132a9284368a$var$getDocumentElement(node) { ++ return node.ownerDocument.documentElement || document.documentElement; ++} + function $edcf132a9284368a$var$getContainerDimensions(containerNode) { ++ const visualViewport = $edcf132a9284368a$var$getVisualViewport($edcf132a9284368a$var$getWindow(containerNode)); + let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; + let scroll = {}; + var _visualViewport_scale; +- let isPinchZoomedIn = ((_visualViewport_scale = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; ++ let isPinchZoomedIn = ((_visualViewport_scale = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; + if (containerNode.tagName === 'BODY') { +- let documentElement = document.documentElement; ++ let documentElement = $edcf132a9284368a$var$getDocumentElement(containerNode); + totalWidth = documentElement.clientWidth; + totalHeight = documentElement.clientHeight; + var _visualViewport_width; +- width = (_visualViewport_width = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; ++ width = (_visualViewport_width = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; + var _visualViewport_height; +- height = (_visualViewport_height = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; ++ height = (_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; + scroll.top = documentElement.scrollTop || containerNode.scrollTop; + scroll.left = documentElement.scrollLeft || containerNode.scrollLeft; + // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with + // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate + // coordinates/values with respect to the visual viewport or with respect to the layout viewport. +- if ($edcf132a9284368a$var$visualViewport) { +- top = $edcf132a9284368a$var$visualViewport.offsetTop; +- left = $edcf132a9284368a$var$visualViewport.offsetLeft; ++ if (visualViewport) { ++ top = visualViewport.offsetTop; ++ left = visualViewport.offsetLeft; + } + } else { + ({ width: width, height: height, top: top, left: left } = $edcf132a9284368a$var$getOffset(containerNode)); +@@ -74,9 +83,9 @@ function $edcf132a9284368a$var$getContainerDimensions(containerNode) { + scroll.top = 0; + scroll.left = 0; + var _visualViewport_pageTop; +- top = (_visualViewport_pageTop = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; ++ top = (_visualViewport_pageTop = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; + var _visualViewport_pageLeft; +- left = (_visualViewport_pageLeft = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; ++ left = (_visualViewport_pageLeft = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; + } + return { + width: width, +@@ -122,7 +131,8 @@ containerDimensions, padding, containerOffsetWithBoundary) { + else return 0; + } + function $edcf132a9284368a$var$getMargins(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return { + top: parseInt(style.marginTop, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, +@@ -269,13 +279,14 @@ function $edcf132a9284368a$export$6839422d1f33cee9(placementInput, childOffset, + } + function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + let { placement: placement, targetNode: targetNode, overlayNode: overlayNode, scrollNode: scrollNode, padding: padding, shouldFlip: shouldFlip, boundaryElement: boundaryElement, offset: offset, crossOffset: crossOffset, maxHeight: maxHeight, arrowSize: arrowSize = 0, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = opts; ++ const actualWindow = $edcf132a9284368a$var$getWindow(targetNode); + let container = overlayNode instanceof HTMLElement ? $edcf132a9284368a$var$getContainingBlock(overlayNode) : document.documentElement; + let isViewportContainer = container === document.documentElement; +- const containerPositionStyle = window.getComputedStyle(container).position; ++ const containerPositionStyle = actualWindow.getComputedStyle(container).position; + let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static'; + let childOffset = isViewportContainer ? $edcf132a9284368a$var$getOffset(targetNode) : $edcf132a9284368a$var$getPosition(targetNode, container); + if (!isViewportContainer) { +- let { marginTop: marginTop, marginLeft: marginLeft } = window.getComputedStyle(targetNode); ++ let { marginTop: marginTop, marginLeft: marginLeft } = actualWindow.getComputedStyle(targetNode); + childOffset.top += parseInt(marginTop, 10) || 0; + childOffset.left += parseInt(marginLeft, 10) || 0; + } +@@ -300,7 +311,7 @@ function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + } + function $edcf132a9284368a$var$getOffset(node) { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +- let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = document.documentElement; ++ let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = $edcf132a9284368a$var$getDocumentElement(node); + return { + top: top + scrollTop - clientTop, + left: left + scrollLeft - clientLeft, +@@ -309,7 +320,8 @@ function $edcf132a9284368a$var$getOffset(node) { + }; + } + function $edcf132a9284368a$var$getPosition(node, parent) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + let offset; + if (style.position === 'fixed') { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +@@ -322,7 +334,7 @@ function $edcf132a9284368a$var$getPosition(node, parent) { + } else { + offset = $edcf132a9284368a$var$getOffset(node); + let parentOffset = $edcf132a9284368a$var$getOffset(parent); +- let parentStyle = window.getComputedStyle(parent); ++ let parentStyle = actualWindow.getComputedStyle(parent); + parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop; + parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft; + offset.top -= parentOffset.top; +@@ -339,10 +351,11 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + // The offsetParent of an element in most cases equals the containing block. + // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent + let offsetParent = node.offsetParent; ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); + // The offsetParent algorithm terminates at the document body, + // even if the body is not a containing block. Double check that + // and use the documentElement if so. +- if (offsetParent && offsetParent === document.body && window.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = document.documentElement; ++ if (offsetParent && offsetParent === (node.ownerDocument || document).body && actualWindow.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = $edcf132a9284368a$var$getDocumentElement(node); + // TODO(later): handle table elements? + // The offsetParent can be null if the element has position: fixed, or a few other cases. + // We have to walk up the tree manually in this case because fixed positioned elements +@@ -352,11 +365,12 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + while(offsetParent && !$edcf132a9284368a$var$isContainingBlock(offsetParent))offsetParent = offsetParent.parentElement; + } + // Fall back to the viewport. +- return offsetParent || document.documentElement; ++ return offsetParent || $edcf132a9284368a$var$getDocumentElement(node); + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + function $edcf132a9284368a$var$isContainingBlock(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return style.transform !== 'none' || /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || 'backdropFilter' in style && style.backdropFilter !== 'none' || 'WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none'; + } + +diff --git a/dist/calculatePosition.module.js b/dist/calculatePosition.module.js +index 2b4d227e0547ad0acf92152255b8052a4b30ad3d..bc0435df4048872a693c0a97942aa3ce8fb563d8 100644 +--- a/dist/calculatePosition.module.js ++++ b/dist/calculatePosition.module.js +@@ -36,28 +36,37 @@ const $edcf132a9284368a$var$TOTAL_SIZE = { + height: 'totalHeight' + }; + const $edcf132a9284368a$var$PARSED_PLACEMENT_CACHE = {}; +-let $edcf132a9284368a$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $edcf132a9284368a$var$getWindow = (targetNode)=>{ ++ return (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++}; ++let $edcf132a9284368a$var$getVisualViewport = (actualWindow)=>{ ++ return actualWindow.visualViewport || null; ++}; ++function $edcf132a9284368a$var$getDocumentElement(node) { ++ return node.ownerDocument.documentElement || document.documentElement; ++} + function $edcf132a9284368a$var$getContainerDimensions(containerNode) { ++ const visualViewport = $edcf132a9284368a$var$getVisualViewport($edcf132a9284368a$var$getWindow(containerNode)); + let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; + let scroll = {}; + var _visualViewport_scale; +- let isPinchZoomedIn = ((_visualViewport_scale = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; ++ let isPinchZoomedIn = ((_visualViewport_scale = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; + if (containerNode.tagName === 'BODY') { +- let documentElement = document.documentElement; ++ let documentElement = $edcf132a9284368a$var$getDocumentElement(containerNode); + totalWidth = documentElement.clientWidth; + totalHeight = documentElement.clientHeight; + var _visualViewport_width; +- width = (_visualViewport_width = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; ++ width = (_visualViewport_width = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; + var _visualViewport_height; +- height = (_visualViewport_height = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; ++ height = (_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; + scroll.top = documentElement.scrollTop || containerNode.scrollTop; + scroll.left = documentElement.scrollLeft || containerNode.scrollLeft; + // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with + // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate + // coordinates/values with respect to the visual viewport or with respect to the layout viewport. +- if ($edcf132a9284368a$var$visualViewport) { +- top = $edcf132a9284368a$var$visualViewport.offsetTop; +- left = $edcf132a9284368a$var$visualViewport.offsetLeft; ++ if (visualViewport) { ++ top = visualViewport.offsetTop; ++ left = visualViewport.offsetLeft; + } + } else { + ({ width: width, height: height, top: top, left: left } = $edcf132a9284368a$var$getOffset(containerNode)); +@@ -74,9 +83,9 @@ function $edcf132a9284368a$var$getContainerDimensions(containerNode) { + scroll.top = 0; + scroll.left = 0; + var _visualViewport_pageTop; +- top = (_visualViewport_pageTop = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; ++ top = (_visualViewport_pageTop = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; + var _visualViewport_pageLeft; +- left = (_visualViewport_pageLeft = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; ++ left = (_visualViewport_pageLeft = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; + } + return { + width: width, +@@ -122,7 +131,8 @@ containerDimensions, padding, containerOffsetWithBoundary) { + else return 0; + } + function $edcf132a9284368a$var$getMargins(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return { + top: parseInt(style.marginTop, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, +@@ -269,13 +279,14 @@ function $edcf132a9284368a$export$6839422d1f33cee9(placementInput, childOffset, + } + function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + let { placement: placement, targetNode: targetNode, overlayNode: overlayNode, scrollNode: scrollNode, padding: padding, shouldFlip: shouldFlip, boundaryElement: boundaryElement, offset: offset, crossOffset: crossOffset, maxHeight: maxHeight, arrowSize: arrowSize = 0, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = opts; ++ const actualWindow = $edcf132a9284368a$var$getWindow(targetNode); + let container = overlayNode instanceof HTMLElement ? $edcf132a9284368a$var$getContainingBlock(overlayNode) : document.documentElement; + let isViewportContainer = container === document.documentElement; +- const containerPositionStyle = window.getComputedStyle(container).position; ++ const containerPositionStyle = actualWindow.getComputedStyle(container).position; + let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static'; + let childOffset = isViewportContainer ? $edcf132a9284368a$var$getOffset(targetNode) : $edcf132a9284368a$var$getPosition(targetNode, container); + if (!isViewportContainer) { +- let { marginTop: marginTop, marginLeft: marginLeft } = window.getComputedStyle(targetNode); ++ let { marginTop: marginTop, marginLeft: marginLeft } = actualWindow.getComputedStyle(targetNode); + childOffset.top += parseInt(marginTop, 10) || 0; + childOffset.left += parseInt(marginLeft, 10) || 0; + } +@@ -300,7 +311,7 @@ function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + } + function $edcf132a9284368a$var$getOffset(node) { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +- let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = document.documentElement; ++ let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = $edcf132a9284368a$var$getDocumentElement(node); + return { + top: top + scrollTop - clientTop, + left: left + scrollLeft - clientLeft, +@@ -309,7 +320,8 @@ function $edcf132a9284368a$var$getOffset(node) { + }; + } + function $edcf132a9284368a$var$getPosition(node, parent) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + let offset; + if (style.position === 'fixed') { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +@@ -322,7 +334,7 @@ function $edcf132a9284368a$var$getPosition(node, parent) { + } else { + offset = $edcf132a9284368a$var$getOffset(node); + let parentOffset = $edcf132a9284368a$var$getOffset(parent); +- let parentStyle = window.getComputedStyle(parent); ++ let parentStyle = actualWindow.getComputedStyle(parent); + parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop; + parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft; + offset.top -= parentOffset.top; +@@ -339,10 +351,11 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + // The offsetParent of an element in most cases equals the containing block. + // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent + let offsetParent = node.offsetParent; ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); + // The offsetParent algorithm terminates at the document body, + // even if the body is not a containing block. Double check that + // and use the documentElement if so. +- if (offsetParent && offsetParent === document.body && window.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = document.documentElement; ++ if (offsetParent && offsetParent === (node.ownerDocument || document).body && actualWindow.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = $edcf132a9284368a$var$getDocumentElement(node); + // TODO(later): handle table elements? + // The offsetParent can be null if the element has position: fixed, or a few other cases. + // We have to walk up the tree manually in this case because fixed positioned elements +@@ -352,11 +365,12 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + while(offsetParent && !$edcf132a9284368a$var$isContainingBlock(offsetParent))offsetParent = offsetParent.parentElement; + } + // Fall back to the viewport. +- return offsetParent || document.documentElement; ++ return offsetParent || $edcf132a9284368a$var$getDocumentElement(node); + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + function $edcf132a9284368a$var$isContainingBlock(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return style.transform !== 'none' || /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || 'backdropFilter' in style && style.backdropFilter !== 'none' || 'WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none'; + } + +diff --git a/dist/calculatePosition.module.js.map b/dist/calculatePosition.module.js.map +index fa03ef022522d4b420f64631f9343e5bd5f510fd..baa979d9a5732a5a1678d9ea9fcd6b74951c6cc2 100644 +--- a/dist/calculatePosition.module.js.map ++++ b/dist/calculatePosition.module.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.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 {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = document.documentElement;\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n let style = window.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = window.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = window.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = document.documentElement;\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n let style = window.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = window.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === document.body &&\n window.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = document.documentElement;\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || document.documentElement;\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n let style = window.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.module.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.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 {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet getWindow = (targetNode?: Element | null): Window => {\n return targetNode?.ownerDocument.defaultView || window;\n};\n\nlet getVisualViewport = (actualWindow: Window): VisualViewport | null => {\n return actualWindow.visualViewport || null;\n};\n\n\nfunction getDocumentElement(node: Element): Element {\n return node.ownerDocument.documentElement || document.documentElement;\n}\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n const visualViewport = getVisualViewport(getWindow(containerNode));\n\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = getDocumentElement(containerNode);\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n const actualWindow = getWindow(targetNode);\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = actualWindow.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = actualWindow.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = getDocumentElement(node);\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n const actualWindow = getWindow(node);\n\n let style = actualWindow.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = actualWindow.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n const actualWindow = getWindow(node);\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === (node.ownerDocument || document).body &&\n actualWindow.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = getDocumentElement(node);\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || getDocumentElement(node);\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.module.js.map"} +\ No newline at end of file diff --git a/dist/useOverlayPosition.main.js b/dist/useOverlayPosition.main.js -index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef85b7a4d93 100644 +index e87062a9b2838ef2e70b22daed6034a34532e804..be61fefe0644bf05f10cf1d0df85b1153680e4db 100644 --- a/dist/useOverlayPosition.main.js +++ b/dist/useOverlayPosition.main.js @@ -25,12 +25,22 @@ $parcel$export(module.exports, "useOverlayPosition", () => $cd94b4896dd97759$exp @@ -26,7 +479,7 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef8 shouldUpdatePosition, placement, overlayRef.current, -@@ -50,15 +60,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { +@@ -50,23 +60,23 @@ 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 @@ -45,6 +498,17 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef8 // 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 + // changes, the focused element appears to stay in the same position. + let anchor = null; +- if (scrollRef.current && scrollRef.current.contains(document.activeElement)) { +- var _document_activeElement; +- let anchorRect = (_document_activeElement = document.activeElement) === null || _document_activeElement === void 0 ? void 0 : _document_activeElement.getBoundingClientRect(); ++ if (scrollRef.current && scrollRef.current.contains(actualWindow.document.activeElement)) { ++ var _actualWindow_document_activeElement; ++ let anchorRect = (_actualWindow_document_activeElement = actualWindow.document.activeElement) === null || _actualWindow_document_activeElement === void 0 ? void 0 : _actualWindow_document_activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + var _anchorRect_top; + // Anchor from the top if the offset is in the top half of the scrollable element, @@ -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; @@ -59,6 +523,17 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef8 } let position = (0, $5935ba4d7da2c103$exports.calculatePosition)({ placement: $cd94b4896dd97759$var$translateRTL(placement, direction), +@@ -115,8 +124,8 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + Object.keys(position.position).forEach((key)=>overlay.style[key] = position.position[key] + 'px'); + overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : ''; + // Restore scroll position relative to anchor element. +- if (anchor && document.activeElement && scrollRef.current) { +- let anchorRect = document.activeElement.getBoundingClientRect(); ++ if (anchor && actualWindow.document.activeElement && scrollRef.current) { ++ let anchorRect = actualWindow.document.activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type]; + scrollRef.current.scrollTop += newOffset - anchor.offset; @@ -129,7 +138,7 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { // eslint-disable-next-line react-hooks/exhaustive-deps (0, $6TXnl$reactariautils.useLayoutEffect)(updatePosition, deps); @@ -110,16 +585,16 @@ index e87062a9b2838ef2e70b22daed6034a34532e804..bc5aeb1749eb2822583932a9daa75ef8 } function $cd94b4896dd97759$var$translateRTL(position, direction) { diff --git a/dist/useOverlayPosition.main.js.map b/dist/useOverlayPosition.main.js.map -index 05aa0e8aaf07ec8b4946a29a6276ac77519813c0..a2caed5c64bd2f1dfd4f3654c8c8f71927be50eb 100644 +index 05aa0e8aaf07ec8b4946a29a6276ac77519813c0..f7954c5d3a07ea0e0e3a65ab3642dc58af0a8eb7 100644 --- a/dist/useOverlayPosition.main.js.map +++ b/dist/useOverlayPosition.main.js.map @@ -1 +1 @@ -{"mappings":";;;;;;;;;;;;AAAA;;;;;;;;;;CAUC;;;;;AAqED,IAAI,uCAAiB,OAAO,aAAa,cAAc,OAAO,cAAc,GAAG;AAMxE,SAAS,0CAAmB,KAAwB;IACzD,IAAI,aAAC,SAAS,EAAC,GAAG,CAAA,GAAA,8BAAQ;IAC1B,IAAI,aACF,YAAY,cACZ,SAAS,cACT,UAAU,aACV,YAAY,uBACZ,YAAY,4BACZ,mBAAmB,gBACnB,aAAa,uBACb,kBAAkB,OAAO,aAAa,cAAc,SAAS,IAAI,GAAG,cACpE,SAAS,gBACT,cAAc,yBACd,uBAAuB,cACvB,SAAS,eACT,OAAO,aACP,SAAS,uBACT,sBAAsB,GACvB,GAAG;IACJ,IAAI,CAAC,UAAU,YAAY,GAAG,CAAA,GAAA,qBAAO,EAAyB;IAE9D,IAAI,OAAO;QACT;QACA;QACA,WAAW,OAAO;QAClB,UAAU,OAAO;QACjB,UAAU,OAAO;QACjB;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD;IAED,4GAA4G;IAC5G,mCAAmC;IACnC,uGAAuG;IACvG,IAAI,YAAY,CAAA,GAAA,mBAAK,EAAE,iDAAA,2DAAA,qCAAgB,KAAK;IAC5C,CAAA,GAAA,sBAAQ,EAAE;QACR,IAAI,QACF,UAAU,OAAO,GAAG,iDAAA,2DAAA,qCAAgB,KAAK;IAE7C,GAAG;QAAC;KAAO;IAEX,IAAI,iBAAiB,CAAA,GAAA,wBAAU,EAAE;QAC/B,IAAI,yBAAyB,SAAS,CAAC,UAAU,CAAC,WAAW,OAAO,IAAI,CAAC,UAAU,OAAO,IAAI,CAAC,iBAC7F;QAGF,IAAI,CAAA,iDAAA,2DAAA,qCAAgB,KAAK,MAAK,UAAU,OAAO,EAC7C;QAGF,0DAA0D;QAC1D,yEAAyE;QACzE,4EAA4E;QAC5E,qEAAqE;QACrE,IAAI,SAA8B;QAClC,IAAI,UAAU,OAAO,IAAI,UAAU,OAAO,CAAC,QAAQ,CAAC,SAAS,aAAa,GAAG;gBAC1D;YAAjB,IAAI,cAAa,0BAAA,SAAS,aAAa,cAAtB,8CAAA,wBAAwB,qBAAqB;YAC9D,IAAI,aAAa,UAAU,OAAO,CAAC,qBAAqB;gBAK7C;YAJX,kFAAkF;YAClF,oCAAoC;YACpC,SAAS;gBACP,MAAM;gBACN,QAAQ,AAAC,CAAA,CAAA,kBAAA,uBAAA,iCAAA,WAAY,GAAG,cAAf,6BAAA,kBAAmB,CAAA,IAAK,WAAW,GAAG;YACjD;YACA,IAAI,OAAO,MAAM,GAAG,WAAW,MAAM,GAAG,GAAG;gBACzC,OAAO,IAAI,GAAG;oBACG;gBAAjB,OAAO,MAAM,GAAG,AAAC,CAAA,CAAA,qBAAA,uBAAA,iCAAA,WAAY,MAAM,cAAlB,gCAAA,qBAAsB,CAAA,IAAK,WAAW,MAAM;YAC/D;QACF;QAEA,0GAA0G;QAC1G,0HAA0H;QAC1H,IAAI,UAAW,WAAW,OAAO;QACjC,IAAI,CAAC,aAAa,WAAW,OAAO,EAAE;gBAGT;YAF3B,QAAQ,KAAK,CAAC,GAAG,GAAG;YACpB,QAAQ,KAAK,CAAC,MAAM,GAAG;gBACI;YAA3B,QAAQ,KAAK,CAAC,SAAS,GAAG,AAAC,CAAA,CAAA,iCAAA,yBAAA,OAAO,cAAc,cAArB,6CAAA,uBAAuB,MAAM,cAA7B,2CAAA,gCAAiC,OAAO,WAAW,AAAD,IAAK;QACpF;QAEA,IAAI,WAAW,CAAA,GAAA,2CAAgB,EAAE;YAC/B,WAAW,mCAAa,WAAW;YACnC,aAAa,WAAW,OAAO;YAC/B,YAAY,UAAU,OAAO;YAC7B,YAAY,UAAU,OAAO,IAAI,WAAW,OAAO;YACnD,SAAS;wBACT;6BACA;oBACA;yBACA;uBACA;uBACA;iCACA;QACF;QAEA,IAAI,CAAC,SAAS,QAAQ,EACpB;QAGF,wGAAwG;QACxG,qGAAqG;QACrG,QAAQ,KAAK,CAAC,GAAG,GAAG;QACpB,QAAQ,KAAK,CAAC,MAAM,GAAG;QACvB,QAAQ,KAAK,CAAC,IAAI,GAAG;QACrB,QAAQ,KAAK,CAAC,KAAK,GAAG;QAEtB,OAAO,IAAI,CAAC,SAAS,QAAQ,EAAE,OAAO,CAAC,CAAA,MAAO,QAAQ,KAAK,CAAC,IAAI,GAAG,AAAC,SAAS,QAAQ,AAAE,CAAC,IAAI,GAAG;QAC/F,QAAQ,KAAK,CAAC,SAAS,GAAG,SAAS,SAAS,IAAI,OAAQ,SAAS,SAAS,GAAG,OAAO;QAEpF,sDAAsD;QACtD,IAAI,UAAU,SAAS,aAAa,IAAI,UAAU,OAAO,EAAE;YACzD,IAAI,aAAa,SAAS,aAAa,CAAC,qBAAqB;YAC7D,IAAI,aAAa,UAAU,OAAO,CAAC,qBAAqB;YACxD,IAAI,YAAY,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC;YACjE,UAAU,OAAO,CAAC,SAAS,IAAI,YAAY,OAAO,MAAM;QAC1D;QAEA,uEAAuE;QACvE,YAAY;IACd,uDAAuD;IACvD,GAAG;IAEH,wCAAwC;IACxC,uDAAuD;IACvD,CAAA,GAAA,qCAAc,EAAE,gBAAgB;IAEhC,mCAAmC;IACnC,gCAAU;IAEV,sEAAsE;IACtE,CAAA,GAAA,uCAAgB,EAAE;QAChB,KAAK;QACL,UAAU;IACZ;IAEA,qEAAqE;IACrE,CAAA,GAAA,uCAAgB,EAAE;QAChB,KAAK;QACL,UAAU;IACZ;IAEA,2FAA2F;IAC3F,iGAAiG;IACjG,IAAI,aAAa,CAAA,GAAA,mBAAK,EAAE;IACxB,CAAA,GAAA,qCAAc,EAAE;QACd,IAAI;QACJ,IAAI,WAAW;YACb,WAAW,OAAO,GAAG;YACrB,aAAa;YAEb,UAAU,WAAW;gBACnB,WAAW,OAAO,GAAG;YACvB,GAAG;YAEH;QACF;QAEA,iIAAiI;QACjI,gHAAgH;QAChH,IAAI,WAAW;YACb,IAAI,WAAW,OAAO,EACpB;QAEJ;QAEA,iDAAA,2DAAA,qCAAgB,gBAAgB,CAAC,UAAU;QAC3C,iDAAA,2DAAA,qCAAgB,gBAAgB,CAAC,UAAU;QAC3C,OAAO;YACL,iDAAA,2DAAA,qCAAgB,mBAAmB,CAAC,UAAU;YAC9C,iDAAA,2DAAA,qCAAgB,mBAAmB,CAAC,UAAU;QAChD;IACF,GAAG;QAAC;KAAe;IAEnB,IAAI,QAAQ,CAAA,GAAA,wBAAU,EAAE;QACtB,IAAI,CAAC,WAAW,OAAO,EACrB,oBAAA,8BAAA;IAEJ,GAAG;QAAC;QAAS;KAAW;IAExB,kFAAkF;IAClF,mEAAmE;IACnE,CAAA,GAAA,0CAAe,EAAE;QACf,YAAY;gBACZ;QACA,SAAS,WAAW;IACtB;QAQiB,qBAGJ;IATb,OAAO;QACL,cAAc;YACZ,OAAO;gBACL,UAAU;gBACV,QAAQ;mBACL,qBAAA,+BAAA,SAAU,QAAQ,AAArB;gBACA,WAAW,CAAA,sBAAA,qBAAA,+BAAA,SAAU,SAAS,cAAnB,iCAAA,sBAAuB;YACpC;QACF;QACA,WAAW,CAAA,sBAAA,qBAAA,+BAAA,SAAU,SAAS,cAAnB,iCAAA,sBAAuB;QAClC,YAAY;YACV,eAAe;YACf,MAAM;YACN,OAAO;gBACL,IAAI,EAAE,qBAAA,+BAAA,SAAU,eAAe;gBAC/B,GAAG,EAAE,qBAAA,+BAAA,SAAU,cAAc;YAC/B;QACF;wBACA;IACF;AACF;AAEA,SAAS,gCAAU,QAAQ;IACzB,CAAA,GAAA,qCAAc,EAAE;QACd,OAAO,gBAAgB,CAAC,UAAU,UAAU;QAC5C,OAAO;YACL,OAAO,mBAAmB,CAAC,UAAU,UAAU;QACjD;IACF,GAAG;QAAC;KAAS;AACf;AAEA,SAAS,mCAAa,QAAQ,EAAE,SAAS;IACvC,IAAI,cAAc,OAChB,OAAO,SAAS,OAAO,CAAC,SAAS,SAAS,OAAO,CAAC,OAAO;IAE3D,OAAO,SAAS,OAAO,CAAC,SAAS,QAAQ,OAAO,CAAC,OAAO;AAC1D","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: RefObjectujZc=38l5qN}3Taf31m)#lGWha#;l0OvddTzi;}+ zENsjr&jHO$9wS9aYg+>FE$E!^uuftJC--&`cP0SBeM4xSxeE8nty!YhY_hNIC*m#B zes9-TwJv`np< vU>#&xd|+I4Eb1dF||dW6Axc zsH|aaQRO-;lw g z0bEisJ?Z4Jvm_OEvp9aoEQ23JG)T8EJ7FY}wTjB9#dm>waH57gr`TZGx*5N6S#zH& zniD4@ s9Q48F;lGbrn$ g|XJ2)3oFlGE)Hu&LuhyVGM;jj3FZZ2E=I0H50s3&`4&(BUfcM zLg3b KX1F$q^9cu^=>uF;Y1z)Wn_9TIr@}Vi-h>m|iER&F 59@P#qRDuQPpa*nq{lYRk|aUf;bAjM2Wd3?QMK@g-MX}!{EuV8 z8m(+GoB8naiYxbz)R9MKOO(+=b6}pZz`RsrOC}RtJqhO4U##3;q{3x|CCU`H3MI-+ zt|_tFw^Q|JzXx^s;)Xet&1r<9KgYvPhf^%+EA|;_ {sp>(#k!k#QgHeN_^52}g*t-B|ZJE|w^aDEmGbVzH zd%%{`qc)#y8#B2Ec`>DxV}uf!qeecp16c~`nM>wX?-k+_14**S7n`%6UOZO`;0x(x zDl6M=`Y!T?DfLR?E(|DZsO+96*T>owW*25N%)Grq*jp^UH?!2|GnDv|PY(8=aTjvV z`TlPBbnW{DYE*#L3+9=(#qCV%qt;>hdGvR3o}Zm~PqS{fMn)s58on6|N5wqh?0**% z-1cVu%*Mmdj}KiJyrw6mF)JyEnU& +u_{FW2D5K~Hdeu)iF{;Z+3g zJ abwn|(e(5={<*9ZHv&T7)N$J4?-prbY`tI8YqFmFR`4cG`wxr80>L3*x3 znCV#qSm8dgq|W}d=6oua^HyP(J`GD-4TWWcxGsA7fUf2fJ1mqO%JPt!FNZv&In3Ur z^^--(FZxN1r~8j(;sX3XCX4sEX{~GN^{~(Uu(-7nALfTYOzharB~+5XqO=7&ux3Pp zDAEF!(%czbB}c}nR#Zd>wwp6vN!xt1ZK^Zy)pXM`u2dbXXgJ(xjg8sM(*O7Y$|IXT zD5+q~Ct1|zy}4T@X1R&`K(Og-1Mjui(UfXO-AUdkUwZ}H992ns#3SA{5qE8Gs6zAM z7ZYa$FMZB@@2Fmaabw+zt!2|Yygz%6Iy)o2Wq#wt$4@BmcwPg{p=zwr?8+7M*`Gt) z+i9~G1TN%Ph8s8?o|!K~Q|2DGY^;DQbAL|M%53fr-0o~~8UypxFBM|ubk5r$y_|A+ zsaefIr=#ZS6hqc7Q#tA%4j)>bgshXs)yi&;t%-TP{ISMrv6isFFJyo+h`_&Xohpcw ztB=+g8XsiLU$#OFt|s_Z=g^7xF{C5Ys32P|{bs{2vAJ`3dmof!8$}ct*$5~+l$q|- z@WV^U{n*psC=(>iE=$5OB)nNJlsgt_240BBF26#>()m^Xv)XB~iXrv^R7~C8zHug$ z3zc}OZ3oTVdnv;`%O<+uj^^Y^PDyo4rcp7`ZYGtcJWwa%^eF7F@gtexum$9i<)iNs z?DEzv+k8BT?W^$Nf#Lf7oiLvBtN&$0$_v>3ebx62dI8z6i=w$G3AnAjtF4p6Q@g+_ z)0+cWEVSW)m&l+I#!!3|g3%EhH4+mMnKZx*z{a3AA>ubNLs(#?)VR!d89-dRQ)y(I z7KN5$2Zdut^gG_@Xj3%WR46(K&9(~FYrddUvP(R`{}1{X?E!zdzck{{?%&w|Pu2gT w+CP8)jM50*3-P}h_n)f&<@1l??_TZ~w*TSU7(!0*=Mi`@-(IY1e^LSb3+YY_djJ3c literal 0 HcmV?d00001 diff --git a/apps/meteor/tests/data/apps/app-packages/app-update-test-faulty_0.0.2.zip b/apps/meteor/tests/data/apps/app-packages/app-update-test-faulty_0.0.2.zip new file mode 100644 index 0000000000000000000000000000000000000000..75a113822ed834e43cbcabe6a9169653af4ec2ea GIT binary patch literal 3698 zcmZ`+XHXN&77e|FbfqPLG$|szt5hMhK x+4-rYU-&YayJ_ZWjoNLc^?040Dtam@AvUipKE2mttfX_S|{ z4ltO6yU$~9ztmB~K1tev?O^Ikp_GR$Stj)hf-*c|aCTCYJfw$aUe0wb^~uFoG9Ccl zRInrbHrzk0kGOZ(8Q}#I-E4RiV7j`GEdRNQM2!TFPw}#tO)gI%t7;qKP5UXVg%S&f z%{}{vGg^PE>BSSWRwo(utm`TqD@k=7epN#ibSHCD7ljg&yX57BUtS%|U)ONnP6*I| zPx{=Rgb>hS9}ZK$ql8+mB)qQY{*FYZezd#*UVFcya?c$p5TWDVt`l%4d#mVq?Z8 <|^n#p}Eo0X>57Sjsv|d_+^8KDe z&zaZZ0O0z|B=A}f=N&ZX3*6GSYDzDW-^2EBTz3DHT{7c;amaSbAqfXD!ReAeS7HEw z`I5uCFqj$4*}>NZ>f+;j8C((?0vR>JOVV~7*491cFlkj&FbU_0;YqnuNQ4ssqa-?0 zSmuZXn=ZIpX`}9Rauuzn6SiL(xRSE%8S2~Fif@k(RrO~k2N;y=q@hXgA8YmsMbU6kc*sbRY93q)CKAcvm6Zq{=aS zjCn_=?p+GX-HM8+hZw!7_*In?Y=}N5a-_z~C8US#-C&(QF_M2mz-ow=!Rjaf^ExGI z9ki%Fmg9SMmD|eoPl)fJe!*Fno*@gLV)dBcv~;QK4qaETib#w2@jax^dlkMxA(q#8 zMEl^R+YbJ6bD8enZ}K}~%+~eE{)=-e0N^3@U;p}%(_?Q5n73QC`H3=f9OrALrylu9 zV9ks;CXsB35^mm1Ub)7nGZuM6X_C>3=IuUQIt{BF_;{C`X@!wB?7bKVt7+S(oCf30 z-qI&OPIt~2FefV8Kh;KYA&ZAU7efOkAGaxzWHJbo003W`E0YQUJrW84&>_yK!{wvK z!orkPaWDz3Uz$}pH0};)CqRXg^oF?jtNeT!Ny&$gACIC&ZwvW|s B>eI$NvB#EJ${?j%Jb6)ArtF%e&`P_b|-Ursc4i{>a z-`;PNy&(e~Gk2BHglc7Kxd?<=ZVeX(t7a8?e84GgT~X}m76Y2E6Z?3Sxu1-`#xgY# z`HI_IaV@(oXr-T$C)j`QXbmASOkG9t#tU8JGUxGTrG}y^ikh#aqzM))Qzjb{+?Y0* z?pwv2+_kN%-#t?mh@na_$=P0x69x;k(l*V4jHyDBECy?07_(Hd{;E^4S6&Iw`5j2U zc~-AYC0igq=QY05JBQ^3E-du!-1K<3Is_b>>R)E8x-rWl)~W0*e@=rt8LNW*o*T5E z2yTKMnSaZXJyIK}04a{55V(mL1*=EzQ5HkDdWqS+JewM?qF&9YrCsc8?maJf**bA@ znnRW}>KMy{*mw|AFca|7z;rR2Q7L4Lwq4G^N`9u2a$P9@n%X_gejv{Ra-NOyaiSH* zF$bIW*hIAp#ewe^OL7zQ=zB-kC!;{aqy9m!I>4EhXq2#H6YNtk65_)H>ilfa3k=(m zTa4u{8A?xx=oDJ#B|1AcG-UiSz|1=TYu}Ij?X~hdU&P9H3zUK|*Rd(_>UoD=d%pc8 zwaYQ!O6B?@=+|ZGlx$$p?P7?Yh@TP16Vys2E33nX%jTT9=`O{0>~iRL|G-bF#GaPg zcGymY-;vX~<#w-5O)(-XLq<`!dzmc=FJBaakl~6dqsup-y@hym{0%y A>w`Cf z>Fo fPpGs$)ny|?!d{o*hw zPJ+Q}_Xp8cSu8i&acu=i=+*^i{c+*DspoOc_L)OL2UW@Ai-<^@w9087sxI7)4l^bB zRK$VCYMx;juAi(Hs=UC{EnMFtzgXpH7<9jXtvd5XQ0y2Q)pCWzZnrKuzenM<9nvMm z9{9}FP8@uV?k?SvktGfn<*bRPd;60tc|R1DHSDY^J QkxYn zvJ7kYV~J&&uRTb&;XxBtS4lw#&jD%u`l2Jf_o=?rOE96LE^YMPi^UBF2#hY`D|1r! zZb!hUC+)4%!`A}<<_-wX8LXs4g_K2kj${L enAUWP~c*s z>j=hB5NQk+9yB0CW3HOcxCM=6HCTHnv*81`r@gB>Kc~vmfB6RGGA?S~`I)HD(-lw) zQT=4p#SxYE$-T%_t7JH U#VH^SFv^c0A}E@D{xDSm+BgB zgx+&q{h5z5N!WMn8ckli5#y|@(<5pfjy1Q3BK#z2eanL^b@ESp(4Aws*F|}(@Xc^W zsz(adap$zQx~ZC&E5b%hFO$<34(h?I>n%>BW|`3oU94Gq0=qxk#HtBlLXsJnkYWA! zY0y2Lm1O+g&OEgT>&Xabsi4SrGedI ?!;J-tYN9lksdOsiu3H zeza*}=O%DB3_h=Pm`=kVRRc5Y)}`IzzaIl{v~|F26~M~MDYTB&5yuuQl+h!LARc&N zewwijlc}zr1as>TR_>3IVX{IJrHb2y5~ZeBl~|qHsRnaufha#LnnT%=MlkwAJoF5m zYC~Usz(^x+&g-it+LGxA0n+|5ikag5b%En}-t(Erqs}{0u{xBKX6MJC KS!mD}(Y%5u}`R>|V$Ti4|DXpEPC=t186i~Ym zrS3d*&tmXdBdlE6x!L14ntPB@v{(V)3+ZDjE!}PUBK(mljUw?Eltk80*(+bJpA`jn zgxjN;ch~Uy%O!W_SNeTN65sR5K}{NefiE~~Z->p*)Fx1O1=v1ko_|}^&a^RZ2QSE{ zSIK>He(E#Fy4xBNol@B_X)F{K^MG^kT}*J>tBrGeZ-0Njfx2Lvo}|XYO##fpO)k3E zzhMTNG#^m+Uul253OfmUfaQb!b`ghF;<@)9FOkRynwf4FyMrOyvAHh~4;CKXm8v Qfx7pfOh}^-v4RtVJ!!zaK_k4!wxsIUb=M5kQEkbdf(^>V!Y%J%kLX JudUpg+}+52SieoyTUZM`1$ z`6a7cYw>XZnWIGHuRMGO*$YYskPB;O1du#EU?tt_ipR~d392Z+2 z=tQX`Z 5U1T*we@*9IF*B;Tyl8L6<@J%-h!4*Aa5{(HbM4hZzf2ZBvHV6a1@k>4g2SWFXS1 zz}xPF7U;;)yv6+e4kg(}VFgAueAf(wneNQ6BND6?dlnpJihw$1OSp!FHOqx^$0961 z-@ kszeJQocA-6Q^F z@H>ZG_Uq6~qq#H*n1hptgPV)9V_>EEjiJ#|NQ22!;b9|;q4+o$b5CsCNK9CG+5j^I zoq*f`i{HSEjFMDHewO|s4T#HdD~V{+Cf9aFlDi_K-| _1U|r!@ZD{R^de>HbD{{AYr{Ym5IR_%lkvv6tfI lpH1@rqW+htKZw7|p3?poZN^|S@;`^9mvjBn`_ui2{sCoj+1LO8 literal 0 HcmV?d00001 diff --git a/apps/meteor/tests/data/apps/app-packages/app-update-test_0.0.1.zip b/apps/meteor/tests/data/apps/app-packages/app-update-test_0.0.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..6bd98799127a8a548cfa65718a6e7b1ab4c994e4 GIT binary patch literal 3533 zcmZ{n1yqz>x5tM@LPWY{K }WxHF%j|s3U z$G(1SbdS&c2>Df$xDqApE4pJ~WG>OYvp1C5Voov+h9kSrCl)m#jq>E#Id$%0m@tU= z0)gX`HUyjdBWdam{lhqh7;W&1zP@Ce?SZSm1JuP%@;DVDqujQ?*>D2>5q6O(e!k$2 zHiRisPh)H;>KMeD`C+uZim{*!!9mc|rKr67&L(BA6sH=mqeZd71pkvD>HiiNm(#xn zDbBL}@0>i`eNs@`uS9P}@BN}phrc85m+DE(KEk{36U)gECr^C%rO|(Sr;UZo$bPwO z_oDM`1{pvC==L7sw|U*4t0H9v98}@{q!pOozr%f7ue{LtKzD7qy9$$}Q?H%t>PRwfu6PIITjlG&eTk=sFdz-IXdf3j^K F>fh;Qq>gRSjqSE}XNyhr94|cPB*VFU2u(f-hZL z{Rq$d!j>_Gys1Dwv^M7;R9Yfd9-Z<{AxhS`YDp&Psk3nWkqy&(&`mE{rW<1BnV>9e z6d&`N)J{M?2mE@m-TDJcWY1c$t6c(`zpE6dN{c(hoOiVkNtiq-Am!|5Ne2KRH;%O+ z0))L6Abdgs2s7k=3{3TxW2YcaS1Ud^@ML4lZ)|LA*9|8E0LYeZTIm7aU=L6ZaJU|Z z5D@uSZsVj)&Eti$;Q0%dz*gV|sO0h}bgOnUr)|pdb)g4UYzLescq;^Hu4%h~$IE%) zXGcms90Z)&1Y>odj7`VdNR mcVl3$ zY+Glz9+1cC7iE~1y>YDr>m>zq7dH%)c?SwPjBoZ6rRFg WpA^|`^HOzErY%guf8TTn2#W nP#+61d)VF;g;+;xf WNWXLCocm|wtY6B#cPyLW7`@$=G3WmD z@lj!0a3}~JVa7NDy-!#$PHC`$Y>7=70;RpB<_7K|P&2s@!}(lD*oKQmOC?A7kd3r^ z7$; (?_a1ltoPj@@4aJbb!V{HFZ~1G+n{u? zmE~L_O-TejJX}5~CHOU^KJ&m`$NtTEw!{rJ396z8h>35ZVLHOBuiUag+tf9-uN!RZ zkZX?WFH_+yC^-u!N CY`SW8qLIVp(<3M-|uLT-)Jrg!wWhr@-F*y)WQS zt7x8{vFJFynN #4za74G@h5UR&Eo0+86Pz&ug1}jp3KRXo?5%DNjQp-&` zaL^vFduak8ql3cokyb*-mx)jx!^ qhSMhbw}SICS0h^GM>42${N~^NY`Ts z4Y9myK}|`?x|-8IhN`(M1|zTM7y>C&H$}_Vm`vtS)N!kfieCovg^xUu`~I~${d1&Q z_M(d9*QxRdEw>QKy4>9DXqS4{5y_r7CxKTlm?Xo$yE|Am#3dd_coh^ >x>Zfz6tSfF|smLzASA 9kbNvUmso2XaJDSLL+Rhd!^+2vO3BV{;Auh_Ru&T6|1ChN`u zGc>ce=mfL`{fZmAG|PxNHtKA~Zm@H7 yUB2X ?t2z9h$GO=eVB5nSXBYLmOp1macECQH9M+sUL4Z}jdI4cWeI@mG(F?T zm9b$H#+`T aHD+0yEsGyk`-E4=W+lrx$o(+Y{-ER=mnt>h =WCJe@~PvK zmVaJZ#3wTk)n>Y+tIuns0X=>ruYTnpw-pqooR&MCR9DI!IoH9KDOGUcQ~%?WV?~Ic zE@^re>IG|&O3F>1JoL*Kk|90AaPeW6y&Ku9#qKMD{OZKz#uhANJqw>ctdL!uT}OFd zr^uaGfD8f=l&OvlOBPbAQK#*W2kG{Kq6SE%Ha+~UcUIc|b%yReTg-K3RRpSY;h RcT>|ESvv? z4-d{hCQtzGtoJIF=~>TNe`_z;0xcxNBNpvVEfTpl??;8I*Z c@Ws9uf?{(XrM0 z&c7I?-j?X4(p*TTQ)Q{|sfrA3Gs;^|hM4zvH{Yc7w0STQ-*wn=a7s|Wd#F($v*-Iw zOSdHL9F}2WGI!U7ctdmT9R!8Qyt@#4^zmkOIMC=(HDjkI>}7lMWa`rPlB=7L`}GF8 zhIPDZE1iCCnI>E%A-I+A$?1whr8dx#B(W$Vcg!|(3Vlgn`9kPvS)xEmG%+Rx zUooXFX>YG%GQp+mTcA+Q_SE`2yR_C@pmYTky9kC`a)N#}lYR7;zWH$)W5)X*W{19) z`Fk_YlcxJ2na+2L8<;I>JJRF2Fb|sJvJKAZG}BFhDm5`3piq04M&MMhu+& HPq?ztB4D+Qu_j*lCE09U=8jj>y6~78Aq)_2)sQ@!5&pAu;REf zI*!>&v4NJZ?%T3)Wyav>>47}Kt>e+?JCcQ6Iqq{`IU@JIS}$u^#*?b{MEO#09oGhX zuF}{cx+)Eay1nTRD&*H?-BywhqOk_V`~8+IMo5* fE0_E1ZOse_5t(&!IfvD35^mrP% z2GPq2LLWT|7Td9c{whu^oiKXXX#caBFSd3rO8)l+k&Zvzd? u?dzlIBd4wFM_QLc2)Fdu0>L@|RDi=3m-~$D zECzb5gTBV0A_?ge&!b2pG63KyPP!00d_{u)zoq#Dll9@ht$#E)|LORj{m9>rK{)gO z=uZA@@Q3&R3ADel_0K2{x5UxqANv2N*KgSUXD { + let app: App; + + before((done) => getCredentials(done)); + + before(async () => { + await cleanupApps(); + app = await installLocalTestPackage(appUpdateTest); + }); + + after(() => cleanupApps()); + + describe('[App user presence]', () => { + it('should keep the app user online after the app is updated', async () => { + const appUserBefore = await getUserByUsername(APP_USERNAME); + expect(appUserBefore.status).to.be.equal('online'); + + await request + .post(apps(`/${app.id}`)) + .set(credentials) + .attach('app', appUpdateTest) + .expect(200); + + const appUserAfter = await getUserByUsername(APP_USERNAME); + expect(appUserAfter.status).to.be.equal('online'); + }); + + it('should leave the app user unchanged when the update fails to apply', async () => { + const appUserBefore = await getUserByUsername(APP_USERNAME); + expect(appUserBefore.status).to.be.equal('online'); + + await request + .post(apps(`/${app.id}`)) + .set(credentials) + .attach('app', appUpdateTestBroken) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }); + + const appUserAfter = await getUserByUsername(APP_USERNAME); + expect(appUserAfter.status).to.be.equal('online'); + }); + + it('should take the app user offline when the updated app ends up disabled', async () => { + const appUserBefore = await getUserByUsername(APP_USERNAME); + expect(appUserBefore.status).to.be.equal('online'); + + await request + .post(apps(`/${app.id}`)) + .set(credentials) + .attach('app', appUpdateTestFaulty) + .expect(200) + .expect((res) => { + expect(res.body.app).to.have.property('status', 'disabled'); + }); + + const appUserAfter = await getUserByUsername(APP_USERNAME); + expect(appUserAfter.status).to.be.equal('offline'); + }); + }); +}); diff --git a/packages/apps/src/server/AppManager.ts b/packages/apps/src/server/AppManager.ts index 64b6ed651617a..ce5656a3b62f8 100644 --- a/packages/apps/src/server/AppManager.ts +++ b/packages/apps/src/server/AppManager.ts @@ -731,7 +731,7 @@ export class AppManager { } // If there is any error during disabling, it doesn't really matter - await this.disable(old.id).catch(() => {}); + await this.disable(old.id, AppStatus.DISABLED, true).catch(() => {}); const descriptor: IAppStorageItem = { ...old, @@ -784,7 +784,7 @@ export class AppManager { if (updateOptions.loadApp) { const shouldEnableApp = AppStatusUtils.isEnabled(old.status); if (shouldEnableApp) { - await this.updateAndStartupLocal(stored, app); + await this.updateAndStartupLocal(stored, app, false); } else { await this.updateAndInitializeLocal(stored, app); } @@ -834,9 +834,9 @@ export class AppManager { return app; } - public async updateAndStartupLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer) { + public async updateAndStartupLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer, silenceStatus = true) { const app = await this.updateLocal(stored, appPackageOrInstance); - await this.runStartUpProcess(stored, app, true); + await this.runStartUpProcess(stored, app, silenceStatus); } public async updateAndInitializeLocal(stored: IAppStorageItem, appPackageOrInstance: ProxiedApp | Buffer) { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 397553b37caf3..142220a483b85 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -296,7 +296,7 @@ export type EventSignatures = { 'watch.priorities'(data: { clientAction: ClientAction; id: ILivechatPriority['_id']; diff?: Record }): void; 'apps.added'(appId: string): void; 'apps.removed'(appId: string): void; - 'apps.updated'(appId: string): void; + 'apps.updated'(appId: string, originInstanceId?: string): void; 'apps.statusUpdate'(appId: string, status: AppStatus): void; 'apps.settingUpdated'(appId: string, setting: AppsSetting): void; 'command.added'(command: string): void; From ff751747f8a0637888364bde42ae18ac92a38768 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:43:36 -0300 Subject: [PATCH 4/5] feat: Voice call popup window (#40202) Co-authored-by: Copilot --- .changeset/upset-rats-work.md | 7 + .../client/providers/MediaCallProvider.tsx | 5 +- .../meteor-inject-initial/lib/inject-core.js | 2 + apps/meteor/public/voice-call-popup.html | 1 + .../e2e/page-objects/fragments/voice-calls.ts | 386 +++++++++++++++--- apps/meteor/tests/e2e/voice-calls-ee.spec.ts | 364 +++++++++++++++-- packages/i18n/src/locales/en.i18n.json | 7 + packages/ui-voip/package.json | 5 +- .../ui-voip/src/components/ToggleButton.tsx | 22 +- .../ui-voip/src/components/Widget/Widget.tsx | 10 +- .../src/components/Widget/WidgetHeader.tsx | 4 + .../Widget/__snapshots__/Widget.spec.tsx.snap | 15 +- .../src/context/MediaCallInstanceContext.ts | 15 +- .../src/context/MediaCallViewContext.ts | 4 + packages/ui-voip/src/context/index.ts | 1 + .../src/context/usePeekMediaSessionCallId.ts | 25 ++ .../usePeekMediaSessionPeerInfo.spec.tsx | 5 +- .../context/usePeekMediaSessionState.spec.tsx | 5 +- .../src/context/usePeerAutocomplete.spec.tsx | 5 +- .../ui-voip/src/context/useRegisterView.ts | 18 + packages/ui-voip/src/context/useRoomView.ts | 16 - .../ui-voip/src/generate-landing-view.tsx | 136 ++++++ .../useMediaCallOpenRoomTracker.spec.tsx | 5 +- .../providers/MediaCallInstanceProvider.tsx | 17 +- .../src/providers/MediaCallProvider.tsx | 10 +- .../src/providers/MediaCallViewProvider.tsx | 24 +- .../src/providers/MockedMediaCallProvider.tsx | 10 +- .../src/providers/useAvailableViewTracker.ts | 85 ++++ .../src/providers/useMediaSessionInstance.ts | 21 +- .../ui-voip/src/views/MediaCallPopout.tsx | 49 +++ .../ui-voip/src/views/MediaCallPopoutView.tsx | 171 ++++++++ .../src/views/MediaCallPopoutWindow.tsx | 77 ++++ .../MediaCallRoomSection.tsx | 64 ++- .../views/MediaCallWidget/MediaCallWidget.tsx | 6 +- .../MediaCallWidget/OngoingCallWithScreen.tsx | 64 ++- .../MediaCallWidget.spec.tsx.snap | 104 +++-- packages/ui-voip/src/views/usePopoutWindow.ts | 182 +++++++++ yarn.lock | 2 + 38 files changed, 1745 insertions(+), 204 deletions(-) create mode 100644 .changeset/upset-rats-work.md create mode 120000 apps/meteor/public/voice-call-popup.html create mode 100644 packages/ui-voip/src/context/usePeekMediaSessionCallId.ts create mode 100644 packages/ui-voip/src/context/useRegisterView.ts delete mode 100644 packages/ui-voip/src/context/useRoomView.ts create mode 100644 packages/ui-voip/src/generate-landing-view.tsx create mode 100644 packages/ui-voip/src/providers/useAvailableViewTracker.ts create mode 100644 packages/ui-voip/src/views/MediaCallPopout.tsx create mode 100644 packages/ui-voip/src/views/MediaCallPopoutView.tsx create mode 100644 packages/ui-voip/src/views/MediaCallPopoutWindow.tsx create mode 100644 packages/ui-voip/src/views/usePopoutWindow.ts diff --git a/.changeset/upset-rats-work.md b/.changeset/upset-rats-work.md new file mode 100644 index 0000000000000..d84f7439c76be --- /dev/null +++ b/.changeset/upset-rats-work.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/ui-voip": minor +--- + +Introduces popout functionality for voice calls 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/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/meteor/tests/e2e/page-objects/fragments/voice-calls.ts b/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts index c02b4db8945e0..1570c0c972e58 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts @@ -2,101 +2,385 @@ import type { Locator, Page } from '@playwright/test'; import { expect } from '../../utils/test'; -export class VoiceCalls { +const timerToSeconds = (time: string): number => { + const [seconds, minutes, hours = '00'] = time.split(':').reverse(); + if (!seconds || !minutes) { + throw new Error('Voice call timer has an invalid time format'); + } + return Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds); +}; + +export class VoiceCallControls { + private readonly _controls: Locator; + + constructor(controls: Locator) { + this._controls = controls; + } + + get call(): Locator { + return this._controls.getByRole('button', { name: 'Call', exact: true }); + } + + get accept(): Locator { + return this._controls.getByRole('button', { name: 'Accept', exact: true }); + } + + get hangup(): Locator { + return this._controls.getByRole('button', { name: /End call|Reject/, exact: true }); + } + + get cancel(): Locator { + return this._controls.getByRole('button', { name: 'Cancel', exact: true }); + } + + get dialpad(): Locator { + return this._controls.getByRole('button', { name: /Dialpad/i }); + } + + get mute(): Locator { + return this._controls.getByRole('button', { name: /Mute/i }); + } + + get hold(): Locator { + return this._controls.getByRole('button', { name: /Hold|Resume/i }); + } + + get transfer(): Locator { + return this._controls.getByRole('button', { name: 'Forward', exact: true }); + } + + get shareScreen(): Locator { + return this._controls.getByRole('button', { name: /Share screen|Stop sharing screen/i }); + } + + get toggleChat(): Locator { + return this._controls.getByRole('button', { name: /Hide chat|Show chat/i }); + } + + get directMessage(): Locator { + return this._controls.getByRole('button', { name: /Direct message/i }); + } + + get popout(): Locator { + return this._controls.getByRole('button', { name: /Open in new window|Return to main window/i }); + } +} + +export class TransferModal { + private readonly root: Locator; + private readonly page: Page; - constructor(page: Page) { + constructor(page: Page, root: Locator) { this.page = page; + this.root = root; + } + + get content(): Locator { + return this.root; + } + + get input(): Locator { + return this.root.getByRole('textbox'); + } + + get hangUpAndTransfer(): Locator { + return this.root.getByRole('button', { name: 'Hang up and transfer call', exact: true }); + } + + async transferCall(username: string): Promise { + // await this.input.click(); + await this.input.fill(username); + // Options have to be gotten from the page since the Options are rendered on a portal + await this.page.getByRole('option', { name: username }).getByRole('figure').click(); + await expect(this.content).toContainText(username); + await this.hangUpAndTransfer.click(); + } +} + +export class Widget { + private readonly root: Locator; + + private readonly callControls: VoiceCallControls; + + private readonly headerControls: VoiceCallControls; + + private readonly transferModal: TransferModal; + + constructor(page: Page) { + this.transferModal = new TransferModal(page, page.getByRole('dialog', { name: 'Transfer call' })); + this.root = page.getByRole('dialog', { name: 'Voice call', exact: false }); + this.callControls = new VoiceCallControls(this.root.getByRole('group')); + this.headerControls = new VoiceCallControls(this.root.getByRole('banner')); + } + + get content(): Locator { + return this.root; + } + + get controls(): VoiceCallControls { + return this.callControls; + } + + get timer(): Locator { + return this.root.getByRole('time'); + } + + get btnShowCallHere(): Locator { + return this.root.getByRole('button', { name: 'Show call here' }); + } + + async showCallHere(): Promise { + await this.btnShowCallHere.click(); + await expect(this.btnShowCallHere).not.toBeVisible(); + } + + async getTimerContentInSeconds() { + const text = await this.timer.innerText(); + return timerToSeconds(text); } async initiateCall(): Promise { - await expect(this.callWidget).toBeVisible(); - await this.btnCall.click(); - await expect(this.btnCancel).toBeVisible(); + await this.callControls.call.click(); + await expect(this.callControls.cancel).toBeVisible(); } async acceptCall(): Promise { - await expect(this.btnAcceptCall).toBeVisible(); - await this.btnAcceptCall.click(); - await expect(this.btnOpenDialpad).toBeVisible(); + await this.callControls.accept.click(); + await expect(this.callControls.hangup).toBeVisible(); } - async rejectCall(): Promise { - await expect(this.btnRejectCall).toBeVisible(); - await this.btnRejectCall.click(); + async endCall(): Promise { + await this.callControls.hangup.click(); + await expect(this.content).not.toBeVisible(); } - get callWidget(): Locator { - return this.page.getByRole('article', { name: 'New call' }); + async goToDm(): Promise { + await this.headerControls.directMessage.click(); + await expect(this.content).not.toBeVisible(); } - get btnCall(): Locator { - return this.page.getByRole('button', { name: 'Call', exact: true }); + async transferCall(username: string): Promise { + await this.callControls.transfer.click(); + await expect(this.transferModal.content).toBeVisible(); + await this.transferModal.transferCall(username); + await expect(this.transferModal.content).not.toBeVisible(); + await expect(this.content).not.toBeVisible(); } - get callControlGroup(): Locator { - return this.page.getByRole('article').getByRole('group'); + async muteSelf(): Promise { + await this.callControls.mute.click(); + await expect(this.callControls.mute).toHaveAttribute('title', 'Unmute'); } - btnEndCall(name: string): Locator { - return this.callControlGroup.getByRole('button', { name: `End call with ${name}` }); + async unmuteSelf(): Promise { + await this.callControls.mute.click(); + await expect(this.callControls.mute).toHaveAttribute('title', 'Mute'); } - get btnCancel(): Locator { - return this.page.getByRole('button', { name: 'Cancel', exact: true }); + async holdSelf(): Promise { + await this.callControls.hold.click(); + await expect(this.callControls.hold).toHaveAttribute('title', 'Resume'); } - get btnAcceptCall(): Locator { - return this.page.getByRole('button', { name: 'Accept', exact: true }); + async resumeSelf(): Promise { + await this.callControls.hold.click(); + await expect(this.callControls.hold).toHaveAttribute('title', 'Hold'); } - get btnRejectCall(): Locator { - return this.page.getByRole('button', { name: 'Reject', exact: true }); + async openDialpad(): Promise { + await this.callControls.dialpad.click(); + await expect(this.callControls.dialpad).toHaveAttribute('title', 'Close dialpad'); } - get btnOpenDialpad(): Locator { - return this.page.getByRole('button', { name: /Dialpad/i }); + async closeDialpad(): Promise { + await this.callControls.dialpad.click(); + await expect(this.callControls.dialpad).toHaveAttribute('title', 'Open dialpad'); } - get btnMute(): Locator { - return this.page.getByRole('button', { name: /Mute/i }); + get allScreenShareVideos(): Locator { + return this.root.locator('video'); } - get btnHold(): Locator { - return this.page.getByRole('button', { name: /Hold|Resume/i }); + async shareScreen(): Promise { + await this.callControls.shareScreen.click(); + await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Stop sharing screen'); } - get btnTransfer(): Locator { - return this.page.getByRole('button', { name: 'Forward', exact: true }); + async stopSharing(): Promise { + await this.callControls.shareScreen.click(); + await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Share screen'); } - get transferModal(): Locator { - return this.page.getByRole('dialog', { name: 'Transfer call' }); + async openPopout(): Promise { + await this.headerControls.popout.click(); } - get inputUsername(): Locator { - return this.transferModal.getByRole('textbox', { name: 'Enter username or number' }); + peerCard(username: string): Locator { + return this.root.getByText(username); } +} + +export class RoomSection { + private readonly root: Locator; + + public readonly callControls: VoiceCallControls; - get btnHangupAndTransfer(): Locator { - return this.transferModal.getByRole('button', { name: 'Hang up and transfer call', exact: true }); + public readonly otherControls: VoiceCallControls; + + constructor(root: Locator) { + this.root = root; + // Right now there are 2 control groups in the room view, with no discrimination between them + this.callControls = new VoiceCallControls(this.root.getByRole('group').first()); + this.otherControls = new VoiceCallControls(this.root.getByRole('group').last()); } - get callTransferWidget(): Locator { - return this.page.getByRole('heading', { name: 'Transferring call...' }); + get content(): Locator { + return this.root; } - get incommingCallTransferWidget(): Locator { - return this.page.getByRole('heading', { name: 'Incoming call transfer...' }); + get controls(): VoiceCallControls { + return this.callControls; } - async transferCall(username: string): Promise { - await this.btnTransfer.click(); - await expect(this.transferModal).toBeVisible(); - await this.inputUsername.click(); - await this.inputUsername.fill(username); - await this.page.getByRole('option', { name: username }).getByRole('figure').click(); - await expect(this.transferModal).toContainText(username); - await this.btnHangupAndTransfer.click(); + get timer(): Locator { + return this.root.getByRole('time'); + } + + get allScreenShareVideos(): Locator { + return this.root.locator('video'); + } + + get btnShowCallHere(): Locator { + return this.root.getByRole('button', { name: 'Show call here' }); + } + + async showCallHere(): Promise { + await this.btnShowCallHere.click(); + await expect(this.btnShowCallHere).not.toBeVisible(); + } + + async getTimerContentInSeconds() { + const text = await this.timer.innerText(); + return timerToSeconds(text); + } + + async endCall(): Promise { + await this.callControls.hangup.click(); + await expect(this.content).not.toBeVisible(); + } + + async hideChat(): Promise { + await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Hide chat/i); + await this.otherControls.toggleChat.click(); + await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Show chat/i); + } + + async showChat(): Promise { + await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Show chat/i); + await this.otherControls.toggleChat.click(); + await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Hide chat/i); + } + + async muteSelf(): Promise { + await this.callControls.mute.click(); + await expect(this.callControls.mute).toHaveAttribute('title', 'Unmute'); + } + + async unmuteSelf(): Promise { + await this.callControls.mute.click(); + await expect(this.callControls.mute).toHaveAttribute('title', 'Mute'); + } + + async holdSelf(): Promise { + await this.callControls.hold.click(); + await expect(this.callControls.hold).toHaveAttribute('title', 'Resume'); + } + + async resumeSelf(): Promise { + await this.callControls.hold.click(); + await expect(this.callControls.hold).toHaveAttribute('title', 'Hold'); + } + + async shareScreen(): Promise { + await this.callControls.shareScreen.click(); + await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Stop sharing screen'); + } + + async stopSharing(): Promise { + await this.callControls.shareScreen.click(); + await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Share screen'); + } + + peerCard(username: string): Locator { + return this.root.getByText(username); + } + + async openPopout(): Promise { + await this.otherControls.popout.click(); + } +} + +export class PopoutPage extends RoomSection { + private readonly page: Page; + + constructor(page: Page) { + super(page.getByRole('main', { name: 'Voice call' })); + this.page = page; + } + + override async endCall(): Promise { + const pageClosed = new Promise((resolve) => this.page.on('close', () => resolve(true))); + await this.callControls.hangup.click(); + await expect(pageClosed).resolves.toBe(true); + } +} + +export class VoiceCalls { + public readonly widget: Widget; + + public readonly roomSection: RoomSection; + + public popoutPage: PopoutPage | undefined; + + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + this.widget = new Widget(page); + this.roomSection = new RoomSection(page.getByRole('region', { name: 'Voice call' })); + } + + get popout(): PopoutPage { + if (!this.popoutPage) { + throw new Error('Voice call - Popout is not open (call openPopout(Room|Widget) first)'); + } + return this.popoutPage; + } + + // Open the popout from the room section + async openPopoutRoom(): Promise { + const [popout] = await Promise.all([this.page.waitForEvent('popup'), this.roomSection.openPopout()]); + if (!popout) { + throw new Error('Voice call - Failed to open popout'); + } + await popout.waitForLoadState(); + popout.on('close', () => { + this.popoutPage = undefined; + }); + this.popoutPage = new PopoutPage(popout); + } + + // Open the popout from the widget + async openPopoutWidget(): Promise { + const [popout] = await Promise.all([this.page.waitForEvent('popup'), this.widget.openPopout()]); + if (!popout) { + throw new Error('Voice call - Failed to open popout'); + } + await popout.waitForLoadState(); + popout.on('close', () => { + this.popoutPage = undefined; + }); + this.popoutPage = new PopoutPage(popout); } } diff --git a/apps/meteor/tests/e2e/voice-calls-ee.spec.ts b/apps/meteor/tests/e2e/voice-calls-ee.spec.ts index e699e8bee96d0..ad93f25a37d42 100644 --- a/apps/meteor/tests/e2e/voice-calls-ee.spec.ts +++ b/apps/meteor/tests/e2e/voice-calls-ee.spec.ts @@ -28,6 +28,7 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { test.afterAll(async ({ api }) => { await setSettingValueById(api, 'VoIP_TeamCollab_Screen_Sharing_Enabled', true); + await Promise.all([...sessions.map(({ page }) => page.close())]); }); test('should initiate voice call from direct message', async () => { @@ -40,18 +41,18 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { await test.step('initiate a voice call from room toolbar', async () => { await user1.poHomeChannel.content.btnVoiceCall.click(); - await expect(user1.poHomeChannel.voiceCalls.callWidget).toBeVisible(); - await user1.poHomeChannel.voiceCalls.initiateCall(); + await expect(user1.poHomeChannel.voiceCalls.widget.content).toBeVisible(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); }); await test.step('user2 accepts the call', async () => { - await user2.poHomeChannel.voiceCalls.acceptCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); }); await test.step('user2 ends the call', async () => { - await user2.poHomeChannel.voiceCalls.btnEndCall('user1').click(); - await expect(user2.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); - await expect(user1.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); + await user2.poHomeChannel.voiceCalls.widget.endCall(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); }); }); @@ -61,44 +62,37 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { await user1.poHomeChannel.navbar.openChat('user2'); await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); await user1.poHomeChannel.content.btnVoiceCall.click(); - await user1.poHomeChannel.voiceCalls.initiateCall(); - await user2.poHomeChannel.voiceCalls.acceptCall(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); }); await test.step('should mute/unmute microphone from user1', async () => { // User1 mutes microphone - await user1.poHomeChannel.voiceCalls.btnMute.click(); - await expect(user1.poHomeChannel.voiceCalls.btnMute).toHaveAttribute('title', 'Unmute'); + await user1.poHomeChannel.voiceCalls.widget.muteSelf(); // User1 unmutes microphone - await user1.poHomeChannel.voiceCalls.btnMute.click(); - await expect(user1.poHomeChannel.voiceCalls.btnMute).toHaveAttribute('title', 'Mute'); + await user1.poHomeChannel.voiceCalls.widget.unmuteSelf(); }); await test.step('should put call on hold from user1', async () => { // User1 puts call on hold - await user1.poHomeChannel.voiceCalls.btnHold.click(); - await expect(user1.poHomeChannel.voiceCalls.btnHold).toHaveAttribute('title', 'Resume'); + await user1.poHomeChannel.voiceCalls.widget.holdSelf(); // User1 resumes call - await user1.poHomeChannel.voiceCalls.btnHold.click(); - await expect(user1.poHomeChannel.voiceCalls.btnHold).toHaveAttribute('title', 'Hold'); + await user1.poHomeChannel.voiceCalls.widget.resumeSelf(); }); await test.step('should access dialpad during call', async () => { // User1 opens dial pad - await user1.poHomeChannel.voiceCalls.btnOpenDialpad.click(); - await expect(user1.poHomeChannel.voiceCalls.btnOpenDialpad).toHaveAttribute('title', 'Close dialpad'); + await user1.poHomeChannel.voiceCalls.widget.openDialpad(); // User1 closes dial pad - await user1.poHomeChannel.voiceCalls.btnOpenDialpad.click(); - await expect(user1.poHomeChannel.voiceCalls.btnOpenDialpad).toHaveAttribute('title', 'Open dialpad'); + await user1.poHomeChannel.voiceCalls.widget.closeDialpad(); }); await test.step('should end the call from user1', async () => { - await user1.poHomeChannel.voiceCalls.btnEndCall('user2').click(); - await expect(user1.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); - await expect(user2.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); + await user1.poHomeChannel.voiceCalls.widget.endCall(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); }); }); @@ -115,27 +109,29 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { await user1.poHomeChannel.navbar.openChat('user2'); await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); await user1.poHomeChannel.content.btnVoiceCall.click(); - await user1.poHomeChannel.voiceCalls.initiateCall(); - await user2.poHomeChannel.voiceCalls.acceptCall(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); }); await test.step('user1 transfers call to user3', async () => { - await user1.poHomeChannel.voiceCalls.transferCall('user3'); + await user1.poHomeChannel.voiceCalls.widget.transferCall('user3'); await user1.poHomeChannel.toastMessage.waitForDisplay({ type: 'success' }); - await expect(user1.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); - await expect(user2.poHomeChannel.voiceCalls.callTransferWidget).toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).toHaveAccessibleName(/Transferring call.../gi); }); await test.step('user3 receives transferred call', async () => { - await expect(user3.poHomeChannel.voiceCalls.incommingCallTransferWidget).toBeVisible(); - await user3.poHomeChannel.voiceCalls.acceptCall(); + await expect(user3.poHomeChannel.voiceCalls.widget.content).toBeVisible(); + await expect(user3.poHomeChannel.voiceCalls.widget.content).toHaveAccessibleName(/Incoming call transfer.../gi); + await user3.poHomeChannel.voiceCalls.widget.acceptCall(); }); await test.step('user3 ends the call', async () => { - await user3.poHomeChannel.voiceCalls.btnEndCall('user2').click(); - await expect(user3.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); - await expect(user2.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); + await user3.poHomeChannel.voiceCalls.widget.endCall(); + await expect(user3.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); }); + await user3.page.close(); }); @@ -146,16 +142,310 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { await user1.poHomeChannel.navbar.openChat('user2'); await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); await user1.poHomeChannel.content.btnVoiceCall.click(); - await user1.poHomeChannel.voiceCalls.initiateCall(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); }); await test.step('user2 declines the call', async () => { - await user2.poHomeChannel.voiceCalls.btnRejectCall.click(); + await user2.poHomeChannel.voiceCalls.widget.endCall(); }); await test.step('Verify call widget disappears', async () => { - await expect(user1.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); - await expect(user2.poHomeChannel.voiceCalls.callWidget).not.toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + }); + }); +}); + +test.describe('Internal Voice Calls - In-room view - Enterprise Edition', () => { + test.skip(!IS_EE, 'Enterprise Edition Only'); + let sessions: { page: Page; poHomeChannel: HomeChannel }[]; + + test.beforeAll(async ({ api }) => { + await Promise.all([ + api.post('/users.setStatus', { status: 'online', username: 'user1' }), + api.post('/users.setStatus', { status: 'online', username: 'user2' }), + ]); + }); + + test.beforeAll(async ({ browser }) => { + sessions = await Promise.all([ + createAuxContext(browser, Users.user1).then(({ page }) => ({ page, poHomeChannel: new HomeChannel(page) })), + createAuxContext(browser, Users.user2).then(({ page }) => ({ page, poHomeChannel: new HomeChannel(page) })), + ]); + }); + + test.afterAll(async () => { + await Promise.all([...sessions.map(({ page }) => page.close())]); + }); + + test('should show in-room view when navigating to peer DM during call', async () => { + const [user1, user2] = sessions; + + await test.step('establish call from user1 DM with user2', async () => { + await user1.poHomeChannel.navbar.openChat('user2'); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + await user1.poHomeChannel.content.btnVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); + }); + + await test.step('widget should be hidden and room view should be visible', async () => { + await expect(user1.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).toBeVisible(); + }); + + await test.step('user cards should have correct names', async () => { + await expect(user1.poHomeChannel.voiceCalls.roomSection.peerCard('user1')).toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.peerCard('user2')).toBeVisible(); + }); + + await test.step('mute/unmute and hold/resume', async () => { + await user1.poHomeChannel.voiceCalls.roomSection.muteSelf(); + await user1.poHomeChannel.voiceCalls.roomSection.unmuteSelf(); + + await user1.poHomeChannel.voiceCalls.roomSection.holdSelf(); + await user1.poHomeChannel.voiceCalls.roomSection.resumeSelf(); + }); + + await test.step('should change chat visibility', async () => { + // Visible by default + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).toBeVisible(); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.otherControls.toggleChat).toHaveAccessibleName(/Hide chat/i); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + + // Hide + await user1.poHomeChannel.voiceCalls.roomSection.hideChat(); + await expect(user1.poHomeChannel.composer.inputMessage).not.toBeVisible(); + + // Show + await user1.poHomeChannel.voiceCalls.roomSection.showChat(); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + }); + + await test.step('end call to clean up', async () => { + await user1.poHomeChannel.voiceCalls.roomSection.endCall(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.roomSection.content).not.toBeVisible(); + }); + }); + + test('should switch views when navigating into and away from peer DM', async () => { + const [user1, user2] = sessions; + + await test.step('establish call and navigate to peer DM', async () => { + await user1.poHomeChannel.navbar.openChat('user2'); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + await user1.poHomeChannel.content.btnVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); + }); + + await test.step('navigate user1 away from peer DM', async () => { + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).toBeVisible(); + const initialRoomViewTimer = await user1.poHomeChannel.voiceCalls.roomSection.getTimerContentInSeconds(); + + await user1.poHomeChannel.navbar.openChat('general'); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + + await expect(user1.poHomeChannel.voiceCalls.widget.content).toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).not.toBeVisible(); + + const widgetTimer = await user1.poHomeChannel.voiceCalls.widget.getTimerContentInSeconds(); + expect(widgetTimer).toBeGreaterThanOrEqual(initialRoomViewTimer); + }); + + await test.step('return to peer DM (go to dm button)', async () => { + await expect(user1.poHomeChannel.voiceCalls.widget.content).toBeVisible(); + const initialWidgetTimer = await user1.poHomeChannel.voiceCalls.widget.getTimerContentInSeconds(); + + await user1.poHomeChannel.voiceCalls.widget.goToDm(); + + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + + const roomTimer = await user1.poHomeChannel.voiceCalls.roomSection.getTimerContentInSeconds(); + expect(roomTimer).toBeGreaterThanOrEqual(initialWidgetTimer); + }); + + await test.step('end call to clean up', async () => { + await user1.poHomeChannel.voiceCalls.roomSection.endCall(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + }); + }); + + // Maybe flaky + test('Screen sharing', async () => { + const [user1, user2] = sessions; + + await test.step('establish call and navigate to peer DM', async () => { + await user1.poHomeChannel.navbar.openChat('user2'); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + await user1.poHomeChannel.content.btnVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).toBeVisible(); + }); + + await test.step('start screen sharing with both users', async () => { + await user1.poHomeChannel.voiceCalls.roomSection.shareScreen(); + + await expect(user1.poHomeChannel.voiceCalls.roomSection.allScreenShareVideos).toHaveCount(1); + await expect(user2.poHomeChannel.voiceCalls.widget.allScreenShareVideos).toHaveCount(1); + + await user2.poHomeChannel.voiceCalls.widget.shareScreen(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.allScreenShareVideos).toHaveCount(2); + await expect(user2.poHomeChannel.voiceCalls.widget.allScreenShareVideos).toHaveCount(2); + }); + + await test.step('stop sharing with user1 through room view', async () => { + await user1.poHomeChannel.voiceCalls.roomSection.stopSharing(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.allScreenShareVideos).toHaveCount(1); + await expect(user2.poHomeChannel.voiceCalls.widget.allScreenShareVideos).toHaveCount(1); + }); + + await test.step('stop sharing with user2 through widget', async () => { + await user2.poHomeChannel.voiceCalls.widget.stopSharing(); + + await expect(user1.poHomeChannel.voiceCalls.roomSection.allScreenShareVideos).toHaveCount(0); + await expect(user2.poHomeChannel.voiceCalls.widget.allScreenShareVideos).toHaveCount(0); + }); + + await test.step('end call to clean up', async () => { + await user1.poHomeChannel.voiceCalls.roomSection.endCall(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.roomSection.content).not.toBeVisible(); + }); + }); +}); + +test.describe('Internal Voice Calls - Popout view - Enterprise Edition', () => { + test.skip(!IS_EE, 'Enterprise Edition Only'); + let sessions: { page: Page; poHomeChannel: HomeChannel }[]; + + test.beforeAll(async ({ api }) => { + await Promise.all([ + api.post('/users.setStatus', { status: 'online', username: 'user1' }), + api.post('/users.setStatus', { status: 'online', username: 'user2' }), + ]); + }); + + test.beforeAll(async ({ browser }) => { + sessions = await Promise.all([ + createAuxContext(browser, Users.user1).then(({ page }) => ({ page, poHomeChannel: new HomeChannel(page) })), + createAuxContext(browser, Users.user2).then(({ page }) => ({ page, poHomeChannel: new HomeChannel(page) })), + ]); + }); + + test.afterAll(async () => { + await Promise.all([...sessions.map(({ page }) => page.close())]); + }); + + test('should show popout view', async () => { + const [user1, user2] = sessions; + + await test.step('establish call from user1 DM with user2', async () => { + await user1.poHomeChannel.navbar.openChat('user2'); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + await user1.poHomeChannel.content.btnVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); + }); + + await test.step('should open popout for both users', async () => { + await user1.poHomeChannel.voiceCalls.openPopoutRoom(); + await expect(user1.poHomeChannel.voiceCalls.popout.content).toBeVisible(); + + await user2.poHomeChannel.voiceCalls.openPopoutWidget(); + await expect(user2.poHomeChannel.voiceCalls.popout.content).toBeVisible(); + }); + + await test.step('user cards should have correct names', async () => { + await expect(user1.poHomeChannel.voiceCalls.popout.peerCard('user1')).toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.popout.peerCard('user2')).toBeVisible(); + + await expect(user2.poHomeChannel.voiceCalls.popout.peerCard('user1')).toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.popout.peerCard('user2')).toBeVisible(); + }); + + await test.step('mute/unmute and hold/resume', async () => { + await user1.poHomeChannel.voiceCalls.popout.muteSelf(); + await user2.poHomeChannel.voiceCalls.popout.muteSelf(); + + await user1.poHomeChannel.voiceCalls.popout.unmuteSelf(); + await user2.poHomeChannel.voiceCalls.popout.unmuteSelf(); + + await user1.poHomeChannel.voiceCalls.popout.holdSelf(); + await user2.poHomeChannel.voiceCalls.popout.holdSelf(); + + await user1.poHomeChannel.voiceCalls.popout.resumeSelf(); + await user2.poHomeChannel.voiceCalls.popout.resumeSelf(); + }); + + await test.step('Widget "Show call here" button', async () => { + await expect(user2.poHomeChannel.voiceCalls.widget.btnShowCallHere).toBeVisible(); + await user2.poHomeChannel.voiceCalls.widget.showCallHere(); + }); + + await test.step('Room "Show call here" button', async () => { + await expect(user1.poHomeChannel.voiceCalls.roomSection.btnShowCallHere).toBeVisible(); + await user1.poHomeChannel.voiceCalls.roomSection.showCallHere(); + }); + + await test.step('end call from within popup', async () => { + await user1.poHomeChannel.voiceCalls.openPopoutRoom(); + await expect(user1.poHomeChannel.voiceCalls.popout.content).toBeVisible(); + + await user1.poHomeChannel.voiceCalls.popout.endCall(); + await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.roomSection.content).not.toBeVisible(); + }); + }); + + // Maybe flaky + test('Screen sharing', async () => { + const [user1, user2] = sessions; + + await test.step('establish call and open popout', async () => { + await user1.poHomeChannel.navbar.openChat('user2'); + await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); + await user1.poHomeChannel.content.btnVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user2.poHomeChannel.voiceCalls.widget.acceptCall(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).toBeVisible(); + + await user1.poHomeChannel.voiceCalls.openPopoutRoom(); + await expect(user1.poHomeChannel.voiceCalls.popout.content).toBeVisible(); + + await user2.poHomeChannel.voiceCalls.openPopoutWidget(); + await expect(user2.poHomeChannel.voiceCalls.popout.content).toBeVisible(); + }); + + await test.step('start screen sharing with both users', async () => { + await user1.poHomeChannel.voiceCalls.popout.shareScreen(); + + await expect(user1.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(1); + await expect(user2.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(1); + + await user2.poHomeChannel.voiceCalls.popout.shareScreen(); + await expect(user1.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(2); + await expect(user2.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(2); + }); + + await test.step('stop sharing with both users through popout', async () => { + await user1.poHomeChannel.voiceCalls.popout.stopSharing(); + await expect(user1.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(1); + await expect(user2.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(1); + + await user2.poHomeChannel.voiceCalls.popout.stopSharing(); + await expect(user1.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(0); + await expect(user2.poHomeChannel.voiceCalls.popout.allScreenShareVideos).toHaveCount(0); + }); + + await test.step('end call with user2 from the popout', async () => { + await user2.poHomeChannel.voiceCalls.popout.endCall(); + await expect(user1.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); + await expect(user1.poHomeChannel.voiceCalls.roomSection.content).not.toBeVisible(); }); }); }); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index fead21f90a2ef..7b6bd14c5e9c5 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1055,6 +1055,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", @@ -2159,6 +2160,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", @@ -2224,6 +2226,7 @@ "Failed_to_generate_invite_link": "Failed to generate invite link", "Failed_to_transfer_call": "Failed to transfer call", "Failed_to_validate_invite_token": "Failed to validate invite token", + "Failed_to_open_call_window": "Failed to open call window", "Failure": "Failure", "Fallback_forward_department": "Fallback department for forwarding", "Fallback_forward_department_description": "Allows you to define a fallback department which will receive the chats forwarded to this one in case there's no online agents at the moment", @@ -4069,6 +4072,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", @@ -4614,6 +4618,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", @@ -4984,6 +4989,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", @@ -6031,6 +6037,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", diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index cae9475f57e75..76e16c947bc1a 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 .", @@ -51,6 +52,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", @@ -58,6 +60,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/components/Widget/Widget.tsx b/packages/ui-voip/src/components/Widget/Widget.tsx index d199f0ef0ac81..56c43ea828bba 100644 --- a/packages/ui-voip/src/components/Widget/Widget.tsx +++ b/packages/ui-voip/src/components/Widget/Widget.tsx @@ -10,8 +10,9 @@ import { type LastKnownPosition } from '../../providers/useWidgetPositionTracker // TODO: Initial position from the draggable api instead of style props // TODO: A11Y -const WidgetBase = styled('article')` +const WidgetBase = styled('div')` position: fixed; + padding: 0; right: 2em; top: 11em; display: flex; @@ -45,7 +46,12 @@ const Widget = ({ children, onChangePosition, restorePosition, ...props }: Widge return ( - diff --git a/packages/ui-voip/src/components/Widget/WidgetHeader.tsx b/packages/ui-voip/src/components/Widget/WidgetHeader.tsx index 733c8c21441cc..02c3495ad1297 100644 --- a/packages/ui-voip/src/components/Widget/WidgetHeader.tsx +++ b/packages/ui-voip/src/components/Widget/WidgetHeader.tsx @@ -1,5 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; import type { ReactNode } from 'react'; +import { VisuallyHidden } from 'react-aria'; +import { useTranslation } from 'react-i18next'; type WidgetHeaderProps = { title: ReactNode; @@ -8,8 +10,10 @@ type WidgetHeaderProps = { // TODO: A11Y - duration/title const WidgetHeader = ({ title, children }: WidgetHeaderProps) => { + const { t } = useTranslation(); return (+ {children} + + ); +}; + +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..99fee03d46104 --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopoutWindow.tsx @@ -0,0 +1,77 @@ +import { Box, OwnerDocument as FuselageOwnerDocument } from '@rocket.chat/fuselage'; +import { OwnerDocument as StyledOwnerDocument } from '@rocket.chat/styled'; +import { ModalProvider, ModalRegion, 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'; +import MediaCallViewProvider from '../providers/MediaCallViewProvider'; + +type MediaCallPopoutWindowProps = { + container: PopoutContainer; + onClosePopout: () => void; +}; +const MediaCallPopoutWindow = ({ container, onClosePopout }: MediaCallPopoutWindowProps) => { + const [fullscreen, setFullscreen] = useState(false); + const [region] = useState(() => Symbol()); + + 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 ( +{t('Voice_call')} {title} diff --git a/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap b/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap index 26354a3750a1d..041b73b7e6363 100644 --- a/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap +++ b/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap @@ -7,9 +7,10 @@ exports[`renders FullWidget without crashing 1`] = ` data-focus-scope-start="true" hidden="" /> -+ void; +type UnregisterView = (view: AvailableViews) => void; + type MediaCallInstanceContextValue = { instance: MediaSignalingSession | undefined; signalEmitter: Emitter +-+ Voice_call +
; 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/index.ts b/packages/ui-voip/src/context/index.ts index 5df5f42764a00..3a294e90c0d24 100644 --- a/packages/ui-voip/src/context/index.ts +++ b/packages/ui-voip/src/context/index.ts @@ -4,6 +4,7 @@ export { useMediaCallView } from './MediaCallViewContext'; export { usePeerAutocomplete, isFirstPeerAutocompleteOption } from './usePeerAutocomplete'; export { useWidgetExternalControls } from './useWidgetExternalControls'; export { usePeekMediaSessionState } from './usePeekMediaSessionState'; +export { usePeekMediaSessionCallId } from './usePeekMediaSessionCallId'; export { usePeekMediaSessionPeerInfo } from './usePeekMediaSessionPeerInfo'; export { usePeekMediaSessionFeatures } from './usePeekMediaSessionFeatures'; export type { PeekMediaSessionStateReturn } from './usePeekMediaSessionState'; diff --git a/packages/ui-voip/src/context/usePeekMediaSessionCallId.ts b/packages/ui-voip/src/context/usePeekMediaSessionCallId.ts new file mode 100644 index 0000000000000..adfc4768d9ae1 --- /dev/null +++ b/packages/ui-voip/src/context/usePeekMediaSessionCallId.ts @@ -0,0 +1,25 @@ +import { useCallback, useSyncExternalStore } from 'react'; + +import { useMediaCallInstance } from './MediaCallInstanceContext'; + +export type PeekMediaSessionCallIdReturn = string | undefined; + +export const usePeekMediaSessionCallId = (): PeekMediaSessionCallIdReturn => { + const { instance } = useMediaCallInstance(); + + const subscribe = useCallback( + (onStoreChange: () => void): (() => void) => { + if (!instance) { + return () => undefined; + } + return instance?.on('sessionStateChange', onStoreChange); + }, + [instance], + ); + + const getSnapshot = useCallback(() => { + return instance?.getState()?.tempCallId || undefined; + }, [instance]); + + return useSyncExternalStore(subscribe, getSnapshot); +}; 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..b4befb641cb7c --- /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..66aa5489cac3c 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 ( - ); diff --git a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx index bbf30e8a9fdea..917235745caa1 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, @@ -23,7 +23,6 @@ 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 +35,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 +208,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 +249,8 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { onAccept, onSelectPeer, onToggleScreenSharing, + onOpenPopout, + onClosePopout, streams, widgetPositionTracker: { onChangePosition, @@ -249,14 +258,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { }, }; - return ( -+ + ++ ++ {children} - - ); + return- - {children} -- {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/providers/useMediaSessionInstance.ts b/packages/ui-voip/src/providers/useMediaSessionInstance.ts index 701027dcd1f14..720003c8f2460 100644 --- a/packages/ui-voip/src/providers/useMediaSessionInstance.ts +++ b/packages/ui-voip/src/providers/useMediaSessionInstance.ts @@ -44,6 +44,8 @@ class MediaSessionStore extends Emitter { private logger = new MediaCallLogger(); + private popoutWindow: Window | undefined; + constructor() { super(); } @@ -95,11 +97,12 @@ class MediaSessionStore extends Emitter { private async getDisplayMedia(constraints: MediaStreamConstraints) { try { - if (!navigator?.mediaDevices?.getDisplayMedia) { + const actualWindow = this.popoutWindow || window; + if (!actualWindow.navigator?.mediaDevices?.getDisplayMedia) { throw new Error('getDisplayMedia is not supported'); } - const stream = await navigator.mediaDevices.getDisplayMedia(constraints); + const stream = await actualWindow.navigator.mediaDevices.getDisplayMedia(constraints); if (!stream) { this.logger.log('MediaSessionStore - getDisplayMedia - no stream returned'); throw new Error('MediaSessionStore - getDisplayMedia - Failed to get display media'); @@ -196,10 +199,24 @@ class MediaSessionStore extends Emitter { void this.sessionInstance.processSignal(signal); } + + public setPopoutWindow(popoutWindow?: Window) { + if (!popoutWindow) { + this.popoutWindow = undefined; + } + this.popoutWindow = popoutWindow; + } } const mediaSession = new MediaSessionStore(); +export const useSetPopoutWindow = (popoutWindow?: Window) => { + useEffect(() => { + mediaSession.setPopoutWindow(popoutWindow); + return () => mediaSession.setPopoutWindow(undefined); + }); +}; + export const useMediaSessionInstance = (userId?: string) => { const { t } = useTranslation(); const iceServers = useIceServers(); diff --git a/packages/ui-voip/src/views/MediaCallPopout.tsx b/packages/ui-voip/src/views/MediaCallPopout.tsx new file mode 100644 index 0000000000000..ed221ebad7d06 --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopout.tsx @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useLayoutEffect } from 'react'; + +import { useMediaCallInstance, usePeekMediaSessionState, usePeekMediaSessionCallId } from '../context'; +import MediaCallPopoutWindow from './MediaCallPopoutWindow'; +import { usePopoutWindow } from './usePopoutWindow'; +import { useSetPopoutWindow } from '../providers/useMediaSessionInstance'; + +const MediaCallPopout = () => { + const { currentViews, unregisterView } = useMediaCallInstance(); + const callState = usePeekMediaSessionState(); + const callId = usePeekMediaSessionCallId(); + + const onClosePopout = useCallback(() => { + unregisterView('popout'); + }, [unregisterView]); + + const { container, closePopoutWindow, openPopoutWindow } = usePopoutWindow(onClosePopout); + + useSetPopoutWindow(container?.ownerDocument.defaultView || undefined); + + const onClosePopoutAndWindow = useCallback(() => { + onClosePopout(); + closePopoutWindow(); + }, [onClosePopout, closePopoutWindow]); + + useLayoutEffect(() => { + if (callState !== 'ongoing') { + onClosePopout(); + } + }, [callState, onClosePopout]); + + useEffect(() => { + queueMicrotask(() => { + if (currentViews.includes('popout') && callId) { + void openPopoutWindow(callId); + return; + } + closePopoutWindow(); + }); + }, [currentViews, openPopoutWindow, closePopoutWindow, callId]); + + 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..c03827dfe51ba --- /dev/null +++ b/packages/ui-voip/src/views/MediaCallPopoutView.tsx @@ -0,0 +1,171 @@ +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, + ActionStrip, +} from '../components'; +import { useMediaCallView } from '../context/MediaCallViewContext'; +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 ( ++ + } + rightSlot={ ++ ++ ++ + {focusedCard !== 'remote' && remoteStreamCard} + {focusedCard !== 'local' && localStreamCard} + + + + + } + > ++ + + + + + + + + + + ); +}; + +export default MediaCallPopoutWindow; diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index fdf221acd8e4e..0429312e03f6e 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 ? (+ ++ ++ ++ {createPortal( + ++ , + root, + )} ++ + - @@ -98,7 +111,14 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: focused={focusedCard === 'local'} showStopSharingOnHover > -+ @@ -114,15 +134,29 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: overflow='hidden' display='flex' flexDirection='column' + is='section' + aria-label={t('Voice_call')} {...getSplitStyles(showChat)} > - - + {isPopout && ( +- - {focusedCard !== 'remote' && remoteStreamCard} - {focusedCard !== 'local' && localStreamCard} - + + )} + {!isPopout && ( ++ {t('Call_open_separate_window')} + + ++ + )}+ + {focusedCard !== 'remote' && remoteStreamCard} + {focusedCard !== 'local' && localStreamCard} + + } diff --git a/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx b/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx index c1048541e66da..ca4551c7c8bd8 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.tsx @@ -1,15 +1,15 @@ import { OngoingCall, NewCall, IncomingCall, OutgoingCall, IncomingCallTransfer, OutgoingCallTransfer } from '..'; import OngoingCallWithScreen from './OngoingCallWithScreen'; -import { useMediaCallInstance } from '../../context/MediaCallInstanceContext'; import { useMediaCallView } from '../../context/MediaCallViewContext'; +import useRegisterView from '../../context/useRegisterView'; const MediaCallWidget = () => { - const { inRoomView } = useMediaCallInstance(); + const currentViews = useRegisterView('widget'); const { sessionState: { state, hidden, transferredBy, peerInfo, supportedFeatures }, } = useMediaCallView(); - if (hidden || inRoomView) { + if (hidden || !currentViews.includes('widget')) { return null; } diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx index 7c188b7c85f2a..e8a3aed87d14c 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx @@ -1,4 +1,4 @@ -import { Box, ButtonGroup } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useTranslation } from 'react-i18next'; import { @@ -17,6 +17,7 @@ import { CardWidgetContainer, StreamCard, } from '../../components'; +import { useMediaCallInstance } from '../../context'; import { useMediaCallView } from '../../context/MediaCallViewContext'; import { usePlayMediaStream } from '../../providers/usePlayMediaStream'; @@ -30,11 +31,15 @@ const OngoingCall = () => { onForward, onEndCall, onClickDirectMessage, + onOpenPopout, streams, onToggleScreenSharing, widgetPositionTracker, + onClosePopout, } = useMediaCallView(); const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; + const { currentViews } = useMediaCallInstance(); + const isPopout = currentViews.includes('popout'); const { localScreen, remoteScreen } = streams; @@ -59,29 +64,56 @@ const OngoingCall = () => { {onClickDirectMessage && ( )} + + + - {remoteScreen?.active && ( - - - )} - {localScreen?.active && ( -- - -- - -- - -+ {isPopout && ( + + + {localScreen?.active && ( + )} + + {!isPopout && ( + <> + {remoteScreen?.active && ( ++ )} + + )} + {localScreen?.active && ( ++ + ++ + )} + > + )}+ ++ + ++ diff --git a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap index d5423e552f3e5..ec457f006cea5 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap +++ b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap @@ -7,9 +7,10 @@ exports[`renders IncomingCall without crashing 1`] = ` data-focus-scope-start="true" hidden="" /> - + - +-+ Voice_call +
+ - +-+ Voice_call +
+ - +-+ Voice_call +
+ - +-+ Voice_call ++
+ - +-+ Voice_call +
+ { + const landingPageRoot = externalWindow.document.querySelector('#root'); + if (!landingPageRoot) { + throw new Error('usePopoutWindow - createRootElement - landingPageRoot not found'); + } + + const newRoot = externalWindow.document.createElement('div'); + newRoot.style.width = '100%'; + newRoot.style.height = '100%'; + + landingPageRoot.appendChild(newRoot); + + return newRoot; +}; + +const replaceWithTranslationString = (t: TFunction, externalDocument: Document) => { + externalDocument.querySelectorAll('[data-i18n]').forEach((el) => { + el.textContent = t(el.getAttribute('data-i18n') as string); + }); +}; + +const changeTheme = (ownerDocument: Document, theme?: string) => { + if (theme) { + ownerDocument.documentElement.setAttribute('data-color-scheme', theme); + } else { + ownerDocument.documentElement.removeAttribute('data-color-scheme'); + } +}; + +const openExternalWindow = async (callId: string, theme: string) => { + const externalWindow = window.open('/voice-call-popup.html', callId, 'width=800,height=500,popup'); + + if (!externalWindow) { + throw new Error('No window was opened'); + } + + changeTheme(externalWindow.document, theme); + + const root = await new Promise +-+ Voice_call +
((resolve, reject) => { + let createRootTimeout: NodeJS.Timeout | null = null; + let attempt = 1; + let created = false; + const attemptCreateRoot = () => { + // In case `onload` runs after the root has been created already + if (created === true) { + return; + } + + if (createRootTimeout) { + clearTimeout(createRootTimeout); + } + + try { + const root = createRootElement(externalWindow); + created = true; + resolve(root); + } catch (error) { + // arround ~ 5 seconds total timeout + if (attempt > 9) { + reject(error as Error); + externalWindow.close(); + return; + } + attempt += 1; + createRootTimeout = setTimeout(attemptCreateRoot, attempt * 100); + } + }; + + // DOMContentLoaded nor readyState can be used because they have already fired + // onload takes longer to fire but could also have already been fired + externalWindow.onload = () => { + attemptCreateRoot(); + }; + + // If onload takes too long to start it could mean it has already fired + // So we start polling for the element. It could still not have been fired. + setTimeout(() => { + attemptCreateRoot(); + }, 500); + }); + + return { root, externalWindow }; +}; + +export type PopoutContainer = { root: HTMLDivElement; ownerDocument: Document }; +type PopoutRef = { root: HTMLDivElement; externalWindow: Window; closing: boolean }; +type OpenPopoutWindow = (callId: string) => Promise ; +type ClosePopoutWindow = () => void; + +type UsePopoutWindowReturn = { + container: PopoutContainer | null; + openPopoutWindow: OpenPopoutWindow; + closePopoutWindow: ClosePopoutWindow; +}; + +export const usePopoutWindow = (onBeforeUnload: () => void): UsePopoutWindowReturn => { + const popoutRef = useRef (null); + const [container, setContainer] = useState (null); + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [, , theme] = useThemeMode(); + + const openPopoutWindow = useCallback( + async (callId: string) => { + if (!!popoutRef.current && popoutRef.current.externalWindow?.closed === false) { + return; + } + + try { + const result = await openExternalWindow(callId, theme); + if (!result) { + onBeforeUnload(); + } + const { root, externalWindow } = result; + popoutRef.current = { root, externalWindow, closing: false }; + setContainer({ root, ownerDocument: externalWindow.document }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: t('Failed_to_open_call_window') }); + console.error('Failed to open popout', error); + onBeforeUnload(); + } + }, + [onBeforeUnload, theme, dispatchToastMessage, t], + ); + + const closePopoutWindow = useCallback(() => { + if (popoutRef.current !== null && popoutRef.current?.externalWindow?.closed !== true && !popoutRef.current.closing) { + popoutRef.current.externalWindow.close(); + popoutRef.current = null; + setContainer(null); + } + }, []); + + useEffect(() => { + const externalWindow = popoutRef.current?.externalWindow; + if (!externalWindow || !container) return; + + const handleBeforeUnload = () => { + if (popoutRef.current) { + popoutRef.current.closing = true; + } + onBeforeUnload(); + popoutRef.current = null; + setContainer(null); + }; + + externalWindow.addEventListener('beforeunload', handleBeforeUnload); + window.addEventListener('beforeunload', closePopoutWindow); + + return () => { + externalWindow.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener('beforeunload', closePopoutWindow); + }; + }, [container, onBeforeUnload, closePopoutWindow]); + + useEffect(() => { + if (!container) { + return; + } + replaceWithTranslationString(t, container.ownerDocument); + }, [container, t]); + + useEffect(() => { + if (!container) { + return; + } + changeTheme(container.ownerDocument, theme); + }, [container, theme]); + + return { + container, + openPopoutWindow, + closePopoutWindow, + }; +}; diff --git a/yarn.lock b/yarn.lock index a57be7b2481a4..a403cb9783e68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11163,6 +11163,7 @@ __metadata: "@testing-library/user-event": "npm:~14.6.1" "@types/jest": "npm:~30.0.0" "@types/jest-axe": "npm:~3.5.9" + "@types/node": "npm:~22.19.17" "@types/react": "npm:~18.3.28" "@types/react-dom": "npm:~18.3.7" date-fns: "npm:~4.1.0" @@ -11170,6 +11171,7 @@ __metadata: i18next: "npm:~23.4.9" jest: "npm:~30.2.0" jest-axe: "npm:~10.0.0" + jsdom: "npm:^26.1.0" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-i18next: "npm:~13.2.2" From 886bb3d31dd90c1b68063e1ba4e145dec7a98a79 Mon Sep 17 00:00:00 2001 From: jonas_florencio <79267723+jonasflorencio@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:44:24 -0300 Subject: [PATCH 5/5] fix: Imported fixes 06-19-26 (#41017) Co-authored-by: Nazareno Bucciarelli --- .changeset/dull-trams-follow.md | 5 ++ .../meteor/app/integrations/server/api/api.ts | 10 +++ .../incoming/addIncomingIntegration.ts | 7 +- .../incoming/updateIncomingIntegration.ts | 7 +- .../incoming/IncomingWebhookForm.tsx | 4 +- apps/meteor/tests/e2e/administration.spec.ts | 2 +- .../end-to-end/api/incoming-integrations.ts | 90 ++++++++++++++++++- packages/i18n/src/locales/en.i18n.json | 1 + 8 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 .changeset/dull-trams-follow.md diff --git a/.changeset/dull-trams-follow.md b/.changeset/dull-trams-follow.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/dull-trams-follow.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index 25019e64d3799..de016532e92e2 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -17,6 +17,7 @@ import { loggerMiddleware } from '../../../api/server/middlewares/logger'; import { metricsMiddleware } from '../../../api/server/middlewares/metrics'; import { tracerSpanMiddleware } from '../../../api/server/middlewares/tracer'; import type { APIActionContext } from '../../../api/server/router'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import type { WebhookResponseItem } from '../../../lib/server/functions/processWebhookMessage'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { metrics } from '../../../metrics/server'; @@ -168,6 +169,15 @@ async function executeIntegrationRest( return API.v1.unavailable('Service Unavailable'); } + if (!(await hasPermissionAsync(this.user._id, 'message-impersonate'))) { + incomingLogger.error({ + msg: 'Error trying to execute integration', + integration: this.request.integration.name, + error: 'The assigned user from the integration must have the message-impersonate permission.', + }); + return API.v1.failure('error-user-lacks-message-impersonate-permission'); + } + const defaultValues = { channel: this.request.integration.channel, alias: this.request.integration.alias || '', diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 95c617cb250fe..b755317c06945 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -6,7 +6,6 @@ import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; @@ -83,9 +82,9 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn validateScriptEngine(integration.scriptEngine ?? 'isolated-vm'); } - const user = await Users.findOne({ username: integration.username }); + const user = await Users.findOneByUsername(integration.username, { projection: { _id: 1 } }); - if (!user) { + if (!user || !(await hasPermissionAsync(user._id, 'message-impersonate'))) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addIncomingIntegration', }); @@ -158,8 +157,6 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn } } - await addUserRolesAsync(user._id, ['bot']); - const strippedIntegrationData = removeEmpty(integrationData); const { insertedId } = await Integrations.insertOne(strippedIntegrationData); diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index ae6a6020bdb5e..eabc9e56a8372 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -4,7 +4,6 @@ import { Integrations, Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { wrapExceptions } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; -import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; @@ -144,16 +143,14 @@ export const updateIncomingIntegration = async ( } const username = 'username' in integration ? integration.username : currentIntegration.username; - const user = await Users.findOne({ username }); + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - if (!user?._id) { + if (!user || !(await hasPermissionAsync(user._id, 'message-impersonate'))) { throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration', }); } - await addUserRolesAsync(user._id, ['bot']); - const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index 166fef8202e74..f8a84877c2eed 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx @@ -208,7 +208,9 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized {t('Choose_the_username_that_this_integration_will_post_as')} -{t('Should_exists_a_user_with_this_username')} ++ {t('User_must_exist_and_have_permission', { permission: 'message-impersonate' })} + {errors?.username && ({errors.username.message} diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 53b310b1f6051..0afad3ca97a3a 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -442,7 +442,7 @@ test.describe.parallel('administration', () => { await poAdminIntegrations.btnNew.click(); await poAdminIntegrations.inputName.fill(incomingIntegrationName); await poAdminIntegrations.inputPostToChannel.fill('#general'); - await poAdminIntegrations.inputPostAs.fill(Users.admin.data.username); + await poAdminIntegrations.inputPostAs.fill('rocket.cat'); await poAdminIntegrations.btnSave.click(); await expect(poAdminIntegrations.inputWebhookUrl).not.toHaveValue('Will be available here after saving.'); diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 69d609b9b8fb6..151ffe79f6f35 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -7,7 +7,8 @@ import { after, before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; -import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { addPermissions, updatePermission, updateSetting } from '../../data/permissions.helper'; +import { assignRoleToUser } from '../../data/roles.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; import { adminUsername, password } from '../../data/user'; @@ -207,6 +208,30 @@ describe('[Incoming Integrations]', () => { }); }); + describe('With both manage-own-incoming-integrations and manage-incoming-integrations', () => { + it('should not allow users without a role with the message-impersonate permission to be assigned to new incoming integrations', async () => { + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test', + enabled: true, + alias: 'test', + username: adminUsername, + scriptEnabled: false, + overrideDestinationChannelEnabled: true, + channel: '#general', + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Invalid user [error-invalid-user]'); + }); + }); + }); + describe('Incoming Integration execution', () => { it('should return an error when the user sends an invalid type of integration', (done) => { void request @@ -362,6 +387,33 @@ describe('[Incoming Integrations]', () => { expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; }); + + describe('When the user does not have a role with the message-impersonate permission', () => { + before(async () => { + await addPermissions({ + 'message-impersonate': ['user'], + }); + }); + + after(async () => { + await addPermissions({ + 'message-impersonate': ['bot', 'app'], + }); + }); + + it('should return an error when executing incoming integration', async () => { + await request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: 'Example message', + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'error-user-lacks-message-impersonate-permission'); + }); + }); + }); }); describe('Script integration tests', () => { @@ -854,11 +906,19 @@ describe('[Incoming Integrations]', () => { let sendUserCredentials: Credentials; before(async () => { + await addPermissions({ + 'message-impersonate': ['user'], + }); senderUser = await createUser(); sendUserCredentials = await login(senderUser.username, password); }); - after(() => deleteUser(senderUser)); + after(async () => { + await addPermissions({ + 'message-impersonate': ['bot', 'app'], + }); + await deleteUser(senderUser); + }); it('should update an integration by id and return the new data', (done) => { void request @@ -869,7 +929,7 @@ describe('[Incoming Integrations]', () => { name: 'Incoming test updated', enabled: true, alias: 'test updated', - username: 'rocket.cat', + username: senderUser.username, scriptEnabled: true, overrideDestinationChannelEnabled: true, channel: '#general', @@ -888,6 +948,27 @@ describe('[Incoming Integrations]', () => { .end(done); }); + it('should not allow users without a role with the message-impersonate permission to be assigned to existing incoming integrations', async () => { + await request + .put(api('integrations.update')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test update attempt', + enabled: true, + alias: 'test update attempt', + username: 'rocket.cat', + scriptEnabled: true, + overrideDestinationChannelEnabled: true, + channel: '#general', + integrationId: integration._id, + }) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Invalid Post As User [error-invalid-post-as-user]'); + }); + }); + it('should have integration updated on subsequent gets', (done) => { void request .get(api('integrations.get')) @@ -1100,8 +1181,9 @@ describe('[Incoming Integrations]', () => { let integrationMixed2: IIntegration; before(async () => { - nonMemberUser = await createUser({ username: `g_${Random.id()}` }); + nonMemberUser = await createUser({ username: `g_${Random.id()}`, roles: ['bot'] }); privateTeam = await createTeam(credentials, `private.team.${Random.id()}`, TeamType.PRIVATE); + await assignRoleToUser({ username: adminUsername, roleId: 'bot' }); const [publicInPrivateResponse, privateRoomResponse, publicRoomResponse] = await Promise.all([ createRoom({ diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 7b6bd14c5e9c5..a70cb21ca117d 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5757,6 +5757,7 @@ "User_management": "User Management", "User_mentions_only": "User mentions only", "User_menu": "User menu", + "User_must_exist_and_have_permission": "The user must exist and have the {{permission}} permission.", "User_muted": "User Muted", "User_muted_by": "User {{user_muted}} muted by {{user_by}}.", "User_not_found": "User not found",