From 0c78208fb76f8eebde474d26d926633b4be8f460 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Mon, 9 Mar 2026 16:16:17 +0200 Subject: [PATCH] chore(ui5-popover): use css anchor for positioning --- packages/main/src/Popover.ts | 591 ++++++------------ packages/main/src/PopoverResize.ts | 105 +--- .../main/src/popup-utils/PopoverRegistry.ts | 2 - packages/main/src/themes/Popover.css | 17 + 4 files changed, 226 insertions(+), 489 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 37d3379583d4..c8bc60dd4938 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -4,7 +4,6 @@ import type UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; -import { isIOS } from "@ui5/webcomponents-base/dist/Device.js"; import { isClickInRect, getClosedPopupParent } from "@ui5/webcomponents-base/dist/util/PopupUtils.js"; import clamp from "@ui5/webcomponents-base/dist/util/clamp.js"; import DOMReferenceConverter from "@ui5/webcomponents-base/dist/converters/DOMReference.js"; @@ -31,11 +30,6 @@ type PopoverSize = { height: number; } -type ArrowPosition = { - x: number; - y: number; -} - enum PopoverActualHorizontalAlign { Center = "Center", Left = "Left", @@ -50,13 +44,6 @@ enum PopoverActualPlacement { Bottom = "Bottom", } -type CalculatedPlacement = { - arrow: ArrowPosition, - top: number, - left: number, - actualPlacement: `${PopoverActualPlacement}`, -} - /** * @class * @@ -196,12 +183,6 @@ class Popover extends Popup { @property() actualPlacement: `${PopoverActualPlacement}` = "Right"; - @property({ type: Number, noAttribute: true }) - _maxHeight?: number; - - @property({ type: Number, noAttribute: true }) - _maxWidth?: number; - @property({ noAttribute: true }) _resizeHandlePlacement?: `${ResizeHandlePlacement}`; @@ -221,18 +202,16 @@ class Popover extends Popup { _opener?: HTMLElement | string | null | undefined; _openerRect?: DOMRect; - _preventRepositionAndClose?: boolean; - _top?: number; - _left?: number; - _oldPlacement?: CalculatedPlacement; - _width?: string; - _height?: string; + _openerElement?: HTMLElement | null; _popoverResize: PopoverResize; _initialWidth?: string; _initialHeight?: string; + static _anchorCounter = 0; + _anchorName: string; + static get VIEWPORT_MARGIN() { return 10; // px } @@ -240,6 +219,7 @@ class Popover extends Popup { constructor() { super(); + this._anchorName = `--ui5-popover-anchor-${++Popover._anchorCounter}`; this._popoverResize = new PopoverResize(this); } @@ -295,6 +275,17 @@ class Popover extends Popup { } closePopup(escPressed = false, preventRegistryUpdate = false, preventFocusRestore = false) : void { + // Clean up anchor-name from opener + if (this._openerElement) { + this._openerElement.style.removeProperty("anchor-name"); + this._openerElement = null; + } + + // Clean up CSS anchor positioning inline styles from popover host + this.style.removeProperty("position-anchor"); + this.style.removeProperty("position-area"); + this.style.removeProperty("position-try-fallbacks"); + Object.assign(this.style, { width: this._initialWidth, height: this._initialHeight, @@ -432,462 +423,247 @@ class Popover extends Popup { return Popover.VIEWPORT_MARGIN; } - reposition() { - this._show(); + /** + * Maps placement + horizontalAlign/verticalAlign to a CSS position-area value. + * Uses CSS logical values (start/end) so RTL is handled automatically. + * @private + */ + _getPositionArea(): string { + const placement = this.placement; + const hAlign = this.horizontalAlign; + const vAlign = this.verticalAlign; + + if (placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom) { + const vertical = placement === PopoverPlacement.Top ? "top" : "bottom"; + switch (hAlign) { + case PopoverHorizontalAlign.Start: + return `${vertical} start`; + case PopoverHorizontalAlign.End: + return `${vertical} end`; + case PopoverHorizontalAlign.Stretch: + return `${vertical} span-all`; + case PopoverHorizontalAlign.Center: + default: + return `${vertical} center`; + } + } - if (this.resizable) { - this._resizeHandlePlacement = this._popoverResize.getResizeHandlePlacement(); + // Start/End placements + const horizontal = placement === PopoverPlacement.Start ? "start" : "end"; + switch (vAlign) { + case PopoverVerticalAlign.Top: + return `top ${horizontal}`; + case PopoverVerticalAlign.Bottom: + return `bottom ${horizontal}`; + case PopoverVerticalAlign.Stretch: + return `span-all ${horizontal}`; + case PopoverVerticalAlign.Center: + default: + return `center ${horizontal}`; } } - async _show() { - super._show(); - - const opener = this.getOpenerHTMLElement(this.opener); + /** + * Returns CSS position-try-fallbacks value based on placement. + * @private + */ + _getPositionTryFallbacks(): string { + const placement = this.placement; - if (!opener) { - Object.assign(this.style, { - top: `0px`, - left: `0px`, - }); - return; + if (placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom) { + return "flip-block"; } - if (opener && instanceOfUI5Element(opener) && !opener.getDomRef()) { - return; - } + // Start/End: try inline flip first, then block, then both + return "flip-inline, flip-block, flip-block flip-inline"; + } + reposition() { if (!this._opened) { - this._showOutsideViewport(); + return; } - const popoverSize = this.getPopoverSize(); - let placement; - - if (popoverSize.width === 0 || popoverSize.height === 0) { - // size can not be determined properly at this point, popover will be shown with the next reposition + const opener = this.getOpenerHTMLElement(this.opener); + if (!opener) { return; } - if (this.open) { - // update opener rect if it was changed during the popover being opened - this._openerRect = opener.getBoundingClientRect(); - } + this._openerRect = opener.getBoundingClientRect(); - if (this._oldPlacement && this.shouldCloseDueToNoOpener(this._openerRect!) && this.isFocusWithin()) { - // reuse the old placement as the opener is not available, - // but keep the popover open as the focus is within - placement = this._oldPlacement; - } else { - placement = this.calcPlacement(this._openerRect!, popoverSize); + if (this.isOpenerOutsideViewport(this._openerRect)) { + this.closePopup(); + return; } - if (this._preventRepositionAndClose || this.isOpenerOutsideViewport(this._openerRect!)) { - await this._waitForDomRef(); - return this.closePopup(); + if (this.shouldCloseDueToNoOpener(this._openerRect)) { + if (!this.isFocusWithin()) { + this.closePopup(); + return; + } } - this._oldPlacement = placement; - this.actualPlacement = placement.actualPlacement; - - let left = clamp( - this._left!, - Popover.VIEWPORT_MARGIN, - document.documentElement.clientWidth - popoverSize.width - Popover.VIEWPORT_MARGIN, - ); + this._updateActualPlacement(); + this._updateArrowPosition(); - if (this.actualPlacement === PopoverActualPlacement.Right) { - left = Math.max(left, this._left!); + if (this.shouldCloseDueToOverflow(this.actualPlacement, this._openerRect)) { + this.closePopup(); + return; } - let top = clamp( - this._top!, - Popover.VIEWPORT_MARGIN, - document.documentElement.clientHeight - popoverSize.height - Popover.VIEWPORT_MARGIN, - ); - - if (this.actualPlacement === PopoverActualPlacement.Bottom) { - top = Math.max(top, this._top!); + if (this.resizable) { + this._resizeHandlePlacement = this._popoverResize.getResizeHandlePlacement(); } + } - this.arrowTranslateX = placement.arrow.x; - this.arrowTranslateY = placement.arrow.y; - - top = this._adjustForIOSKeyboard(top); + _show() { + super._show(); - Object.assign(this.style, { - top: `${top}px`, - left: `${left}px`, - }); + const opener = this.getOpenerHTMLElement(this.opener); - if (this._popoverResize.isResized) { + if (!opener) { return; } - if (this.horizontalAlign === PopoverHorizontalAlign.Stretch && this._width) { - this.style.width = this._width; - } - - if (this.verticalAlign === PopoverVerticalAlign.Stretch && this._height) { - this.style.height = this._height; - } - } - - /** - * Adjust the desired top position to compensate for shift of the screen - * caused by opened keyboard on iOS which affects all elements with position:fixed. - * @private - * @param top The target top in px. - * @returns The adjusted top in px. - */ - _adjustForIOSKeyboard(top: number): number { - if (!isIOS()) { - return top; + if (opener && instanceOfUI5Element(opener) && !opener.getDomRef()) { + return; } - const actualTop = Math.ceil(this.getBoundingClientRect().top); - - return top + (Number.parseInt(this.style.top || "0") - actualTop); - } + this._openerElement = opener; - getPopoverSize(calcScrollHeight: boolean = false): PopoverSize { - const rect = this.getBoundingClientRect(); - const width = rect.width; - let height; + // Set anchor-name on the opener element + opener.style.setProperty("anchor-name", this._anchorName); - const domRef = this.getDomRef(); + // Set CSS anchor positioning on the popover host + this.style.setProperty("position-anchor", this._anchorName); + this.style.setProperty("position-area", this._getPositionArea()); + this.style.setProperty("position-try-fallbacks", this._getPositionTryFallbacks()); - if (calcScrollHeight && domRef) { - const header = domRef.querySelector(".ui5-popup-header-root"); - const content = domRef.querySelector(".ui5-popup-content"); - const footer = domRef.querySelector(".ui5-popup-footer-root"); + // Handle stretch sizing + if (this.horizontalAlign === PopoverHorizontalAlign.Stretch && this.isVertical) { + this.style.width = `${opener.getBoundingClientRect().width}px`; + } - height = content?.scrollHeight || 0; - height += header?.scrollHeight || 0; - height += footer?.scrollHeight || 0; - } else { - height = rect.height; + if (this.verticalAlign === PopoverVerticalAlign.Stretch && !this.isVertical) { + this.style.height = `${opener.getBoundingClientRect().height}px`; } - return { width, height }; - } + // After the browser applies CSS anchor positioning, detect actual placement and update arrow + requestAnimationFrame(() => { + if (!this._opened && !this.open) { + return; + } - _showOutsideViewport() { - Object.assign(this.style, { - top: "-10000px", - left: "-10000px", - }); - } + this._openerRect = opener.getBoundingClientRect(); - _isUI5AbstractElement(el: HTMLElement): el is UI5Element { - return instanceOfUI5Element(el) && el.isUI5AbstractElement; - } + if (this.isOpenerOutsideViewport(this._openerRect)) { + this.closePopup(); + return; + } - get arrowDOM() { - return this.shadowRoot!.querySelector(".ui5-popover-arrow")!; - } + if (this.shouldCloseDueToNoOpener(this._openerRect) || this.shouldCloseDueToOverflow(this.actualPlacement, this._openerRect)) { + this.closePopup(); + return; + } - /** - * @protected - */ - focusOpener() { - this.getOpenerHTMLElement(this.opener)?.focus(); + this._updateActualPlacement(); + this._updateArrowPosition(); + + if (this.resizable) { + this._resizeHandlePlacement = this._popoverResize.getResizeHandlePlacement(); + } + }); } /** + * Detects the actual placement side by comparing popover and opener rects + * after the browser applies CSS anchor positioning. * @private */ - calcPlacement(targetRect: DOMRect, popoverSize: PopoverSize): CalculatedPlacement { - let left = Popover.VIEWPORT_MARGIN; - let top = 0; - const allowTargetOverlap = this.allowTargetOverlap; - - const clientWidth = document.documentElement.clientWidth; - const clientHeight = document.documentElement.clientHeight; - - let maxHeight = clientHeight; - let maxWidth = clientWidth; - - const actualPlacement = this.getActualPlacement(targetRect); - - this._preventRepositionAndClose = this.shouldCloseDueToNoOpener(targetRect) || this.shouldCloseDueToOverflow(actualPlacement, targetRect); + _updateActualPlacement() { + const popoverRect = this.getBoundingClientRect(); + const openerRect = this._openerRect; - const isVertical = actualPlacement === PopoverActualPlacement.Top - || actualPlacement === PopoverActualPlacement.Bottom; - - if (!this._popoverResize.isResized) { - if (this.horizontalAlign === PopoverHorizontalAlign.Stretch && isVertical) { - popoverSize.width = targetRect.width; - this._width = `${targetRect.width}px`; - } else if (this.verticalAlign === PopoverVerticalAlign.Stretch && !isVertical) { - popoverSize.height = targetRect.height; - this._height = `${targetRect.height}px`; - } + if (!openerRect) { + return; } - const arrowOffset = this.hideArrow ? 0 : ARROW_SIZE; - - // calc popover positions - switch (actualPlacement) { - case PopoverActualPlacement.Top: - left = this.getVerticalLeft(targetRect, popoverSize); - top = Math.max(targetRect.top - popoverSize.height - arrowOffset, 0); - - if (!allowTargetOverlap) { - maxHeight = targetRect.top - arrowOffset; - } - break; - case PopoverActualPlacement.Bottom: - left = this.getVerticalLeft(targetRect, popoverSize); - top = targetRect.bottom + arrowOffset; - - if (allowTargetOverlap) { - top = Math.max(Math.min(top, clientHeight - popoverSize.height), 0); - } else { - maxHeight = clientHeight - targetRect.bottom - arrowOffset; - } - break; - case PopoverActualPlacement.Left: - left = Math.max(targetRect.left - popoverSize.width - arrowOffset, 0); - top = this.getHorizontalTop(targetRect, popoverSize); - - if (!allowTargetOverlap) { - maxWidth = targetRect.left - arrowOffset; - } - break; - case PopoverActualPlacement.Right: - left = targetRect.left + targetRect.width + arrowOffset; - top = this.getHorizontalTop(targetRect, popoverSize); - - if (allowTargetOverlap) { - left = Math.max(Math.min(left, clientWidth - popoverSize.width), 0); - } else { - maxWidth = clientWidth - targetRect.right - arrowOffset; - } - break; - } + const tolerance = 2; - // correct popover positions - if (isVertical) { - if (popoverSize.width > clientWidth || left < Popover.VIEWPORT_MARGIN) { - left = Popover.VIEWPORT_MARGIN; - } else if (left + popoverSize.width > clientWidth - Popover.VIEWPORT_MARGIN) { - left = clientWidth - Popover.VIEWPORT_MARGIN - popoverSize.width; - } + if (popoverRect.bottom <= openerRect.top + tolerance) { + this.actualPlacement = PopoverActualPlacement.Top; + } else if (popoverRect.top >= openerRect.bottom - tolerance) { + this.actualPlacement = PopoverActualPlacement.Bottom; + } else if (popoverRect.right <= openerRect.left + tolerance) { + this.actualPlacement = PopoverActualPlacement.Left; } else { - if (popoverSize.height > clientHeight || top < Popover.VIEWPORT_MARGIN) { // eslint-disable-line - top = Popover.VIEWPORT_MARGIN; - } else if (top + popoverSize.height > clientHeight - Popover.VIEWPORT_MARGIN) { - top = clientHeight - Popover.VIEWPORT_MARGIN - popoverSize.height; - } - } - - this._maxHeight = Math.round(maxHeight - Popover.VIEWPORT_MARGIN); - this._maxWidth = Math.round(maxWidth - Popover.VIEWPORT_MARGIN); - - if (this._left === undefined || Math.abs(this._left - left) > 1.5) { - this._left = Math.round(left); - } - - if (this._top === undefined || Math.abs(this._top - top) > 1.5) { - this._top = Math.round(top); + this.actualPlacement = PopoverActualPlacement.Right; } - - const borderRadius = Number.parseInt(window.getComputedStyle(this).getPropertyValue("border-radius")); - const arrowPos = this.getArrowPosition(targetRect, popoverSize, left, top, isVertical, borderRadius); - - this._left += this.getRTLCorrectionLeft(); - - return { - arrow: arrowPos, - top: this._top, - left: this._left, - actualPlacement, - }; - } - - get isVertical() : boolean { - return this.placement === PopoverPlacement.Top || this.placement === PopoverPlacement.Bottom; - } - - getRTLCorrectionLeft() { - return parseFloat(window.getComputedStyle(this).left) - this.getBoundingClientRect().left; } /** - * Calculates the position for the arrow. + * After CSS positions the popover, calculates arrow offset to point at the opener center. * @private - * @param targetRect BoundingClientRect of the target element - * @param popoverSize Width and height of the popover - * @param left Left offset of the popover - * @param top Top offset of the popover - * @param isVertical If the popover is positioned vertically to the target element - * @param borderRadius Value of the border-radius property - * @returns Arrow's coordinates */ - getArrowPosition(targetRect: DOMRect, popoverSize: PopoverSize, left: number, top: number, isVertical: boolean, borderRadius: number): ArrowPosition { - const actualHorizontalAlign = this._actualHorizontalAlign; - let arrowXCentered = actualHorizontalAlign === PopoverActualHorizontalAlign.Center || actualHorizontalAlign === PopoverActualHorizontalAlign.Stretch; - - if (actualHorizontalAlign === PopoverActualHorizontalAlign.Right && left <= targetRect.left) { - arrowXCentered = true; + _updateArrowPosition() { + if (this.hideArrow) { + return; } - if (actualHorizontalAlign === PopoverActualHorizontalAlign.Left && left + popoverSize.width >= targetRect.left + targetRect.width) { - arrowXCentered = true; - } + const popoverRect = this.getBoundingClientRect(); + const openerRect = this._openerRect; - let arrowTranslateX = 0; - if (isVertical && arrowXCentered) { - arrowTranslateX = targetRect.left + targetRect.width / 2 - left - popoverSize.width / 2; + if (!openerRect) { + return; } - let arrowTranslateY = 0; - if (!isVertical) { - arrowTranslateY = targetRect.top + targetRect.height / 2 - top - popoverSize.height / 2; - } - - // Restricts the arrow's translate value along each dimension, - // so that the arrow does not clip over the popover's rounded borders. - const safeRangeForArrowY = popoverSize.height / 2 - borderRadius - ARROW_SIZE / 2 - 2; - arrowTranslateY = clamp( - arrowTranslateY, - -safeRangeForArrowY, - safeRangeForArrowY, - ); - - const safeRangeForArrowX = popoverSize.width / 2 - borderRadius - ARROW_SIZE / 2 - 2; - arrowTranslateX = clamp( - arrowTranslateX, - -safeRangeForArrowX, - safeRangeForArrowX, - ); - - return { - x: Math.round(arrowTranslateX), - y: Math.round(arrowTranslateY), - }; - } + const isVerticalPlacement = this.actualPlacement === PopoverActualPlacement.Top + || this.actualPlacement === PopoverActualPlacement.Bottom; - /** - * Fallbacks to new placement, prioritizing `Left` and `Right` placements. - * @private - */ - fallbackPlacement(clientWidth: number, clientHeight: number, targetRect: DOMRect, popoverSize: PopoverSize): PopoverActualPlacement | undefined { - if (targetRect.left > popoverSize.width) { - return PopoverActualPlacement.Left; - } + const borderRadius = Number.parseInt(window.getComputedStyle(this).getPropertyValue("border-radius")); - if (clientWidth - targetRect.right > targetRect.left) { - return PopoverActualPlacement.Right; - } + let arrowTranslateX = 0; + let arrowTranslateY = 0; - if (clientHeight - targetRect.bottom > popoverSize.height) { - return PopoverActualPlacement.Bottom; - } + if (isVerticalPlacement) { + // Arrow X offset: center of opener relative to center of popover + arrowTranslateX = (openerRect.left + openerRect.width / 2) - (popoverRect.left + popoverRect.width / 2); - if (clientHeight - targetRect.bottom < targetRect.top) { - return PopoverActualPlacement.Top; - } - } + const safeRange = popoverRect.width / 2 - borderRadius - ARROW_SIZE / 2 - 2; + arrowTranslateX = clamp(arrowTranslateX, -safeRange, safeRange); + } else { + // Arrow Y offset: center of opener relative to center of popover + arrowTranslateY = (openerRect.top + openerRect.height / 2) - (popoverRect.top + popoverRect.height / 2); - getActualPlacement(targetRect: DOMRect): `${PopoverActualPlacement}` { - const placement = this.placement; - const popoverSize = this.getPopoverSize(!this.allowTargetOverlap); - - let actualPlacement: PopoverActualPlacement = PopoverActualPlacement.Right; - - switch (placement) { - case PopoverPlacement.Start: - actualPlacement = this.isRtl ? PopoverActualPlacement.Right : PopoverActualPlacement.Left; - break; - case PopoverPlacement.End: - actualPlacement = this.isRtl ? PopoverActualPlacement.Left : PopoverActualPlacement.Right; - break; - case PopoverPlacement.Top: - actualPlacement = PopoverActualPlacement.Top; - break; - case PopoverPlacement.Bottom: - actualPlacement = PopoverActualPlacement.Bottom; - break; - } - - const clientWidth = document.documentElement.clientWidth; - let clientHeight = document.documentElement.clientHeight; - let popoverHeight = popoverSize.height; - - if (this.isVertical) { - popoverHeight += this.hideArrow ? 0 : ARROW_SIZE; - clientHeight -= Popover.VIEWPORT_MARGIN; - } - - switch (actualPlacement) { - case PopoverActualPlacement.Top: - if (targetRect.top < popoverHeight - && targetRect.top < clientHeight - targetRect.bottom) { - actualPlacement = PopoverActualPlacement.Bottom; - } - break; - case PopoverActualPlacement.Bottom: - if (clientHeight - targetRect.bottom < popoverHeight - && clientHeight - targetRect.bottom < targetRect.top) { - actualPlacement = PopoverActualPlacement.Top; - } - break; - case PopoverActualPlacement.Left: - if (targetRect.left < popoverSize.width) { - actualPlacement = this.fallbackPlacement(clientWidth, clientHeight, targetRect, popoverSize) || actualPlacement; - } - break; - case PopoverActualPlacement.Right: - if (clientWidth - targetRect.right < popoverSize.width) { - actualPlacement = this.fallbackPlacement(clientWidth, clientHeight, targetRect, popoverSize) || actualPlacement; - } - break; + const safeRange = popoverRect.height / 2 - borderRadius - ARROW_SIZE / 2 - 2; + arrowTranslateY = clamp(arrowTranslateY, -safeRange, safeRange); } - return actualPlacement; + this.arrowTranslateX = Math.round(arrowTranslateX); + this.arrowTranslateY = Math.round(arrowTranslateY); } - getVerticalLeft(targetRect: DOMRect, popoverSize: PopoverSize): number { - const actualHorizontalAlign = this._actualHorizontalAlign; - let left = Popover.VIEWPORT_MARGIN; - - switch (actualHorizontalAlign) { - case PopoverActualHorizontalAlign.Center: - case PopoverActualHorizontalAlign.Stretch: - left = targetRect.left - (popoverSize.width - targetRect.width) / 2; - left = this._popoverResize.getCorrectedLeft(left); - break; - case PopoverActualHorizontalAlign.Left: - left = targetRect.left; - break; - case PopoverActualHorizontalAlign.Right: - left = targetRect.right - popoverSize.width; - break; - } + getPopoverSize(): PopoverSize { + const rect = this.getBoundingClientRect(); + return { width: rect.width, height: rect.height }; + } - return left; + _isUI5AbstractElement(el: HTMLElement): el is UI5Element { + return instanceOfUI5Element(el) && el.isUI5AbstractElement; } - getHorizontalTop(targetRect: DOMRect, popoverSize: PopoverSize): number { - let top = 0; + get arrowDOM() { + return this.shadowRoot!.querySelector(".ui5-popover-arrow")!; + } - switch (this.verticalAlign) { - case PopoverVerticalAlign.Center: - case PopoverVerticalAlign.Stretch: - top = targetRect.top - (popoverSize.height - targetRect.height) / 2; - top = this._popoverResize.getCorrectedTop(top); - break; - case PopoverVerticalAlign.Top: - top = targetRect.top; - break; - case PopoverVerticalAlign.Bottom: - top = targetRect.bottom - popoverSize.height; - break; - } + /** + * @protected + */ + focusOpener() { + this.getOpenerHTMLElement(this.opener)?.focus(); + } - return top; + get isVertical() : boolean { + return this.placement === PopoverPlacement.Top || this.placement === PopoverPlacement.Bottom; } get isModal() { // Required by Popup.js @@ -905,10 +681,7 @@ class Popover extends Popup { get styles() { return { ...super.styles, - root: { - "max-height": this._maxHeight ? `${this._maxHeight}px` : "", - "max-width": this._maxWidth ? `${this._maxWidth}px` : "", - }, + root: {}, arrow: { transform: `translate(${this.arrowTranslateX}px, ${this.arrowTranslateY}px)`, }, diff --git a/packages/main/src/PopoverResize.ts b/packages/main/src/PopoverResize.ts index 37f5614d5af2..7b74b087d6d6 100644 --- a/packages/main/src/PopoverResize.ts +++ b/packages/main/src/PopoverResize.ts @@ -27,16 +27,6 @@ class PopoverResize { _minHeight?: number; _resized = false; - _currentDeltaX?: number; - _currentDeltaY?: number; - - // These variables track the cumulative resize difference throughout the entire resizing process. - // It covers scenarios where: the mouse is pressed down, - // moved, and released; the popover remains open; - // and the mouse is pressed down, moved, and released again. - _totalDeltaX?: number; - _totalDeltaY?: number; - constructor(popover: Popover) { this._popover = popover; this._resizeMouseMoveHandler = this._onResizeMouseMove.bind(this); @@ -52,12 +42,6 @@ class PopoverResize { } this._resized = false; - - delete this._currentDeltaX; - delete this._currentDeltaY; - - delete this._totalDeltaX; - delete this._totalDeltaY; } /** @@ -67,28 +51,6 @@ class PopoverResize { return this._resized; } - /* - * Gets the corrected left position considering resize deltas - */ - getCorrectedLeft(left: number): number { - if (this.isResized) { - left -= this._currentDeltaX || 0; - } - - return left; - } - - /* - * Gets the corrected top position considering resize deltas - */ - getCorrectedTop(top: number): number { - if (this.isResized) { - top -= this._currentDeltaY || 0; - } - - return top; - } - setCorrectResizeHandleClass(allClasses: ClassMap) { switch (this.getResizeHandlePlacement()) { case ResizeHandlePlacement.BottomLeft: @@ -142,7 +104,8 @@ class PopoverResize { popoverCX = -popoverCX; } - switch (popover.getActualPlacement(openerRect)) { + // Use the current actualPlacement from CSS anchor positioning + switch (popover.actualPlacement) { case PopoverActualPlacement.Left: if (isPopoverHeightBiggerThanOpener) { if (popoverCY > openerCY + offset) { @@ -220,7 +183,9 @@ class PopoverResize { } /** - * Handles mouse down event on resize handle + * Handles mouse down event on resize handle. + * Captures the current CSS-anchored position and switches to inline positioning + * so that drag-based resize works without CSS anchor interference. */ onResizeMouseDown(e: MouseEvent) { if (!this._popover.resizable) { @@ -232,8 +197,15 @@ class PopoverResize { this._resized = true; this._initialBoundingRect = this._popover.getBoundingClientRect(); - this._totalDeltaX = this._currentDeltaX; - this._totalDeltaY = this._currentDeltaY; + // Bypass CSS anchor positioning: capture current position and switch to inline styles + const rect = this._initialBoundingRect; + this._popover.style.setProperty("position-area", "unset"); + this._popover.style.setProperty("position-anchor", "unset"); + this._popover.style.setProperty("position-try-fallbacks", "unset"); + Object.assign(this._popover.style, { + top: `${rect.top}px`, + left: `${rect.left}px`, + }); const { minWidth, @@ -252,7 +224,8 @@ class PopoverResize { } /** - * Handles mouse move event during resize + * Handles mouse move event during resize. + * Computes new size and position directly (no calcPlacement dependency). */ private _onResizeMouseMove(e: MouseEvent) { const popover = this._popover; @@ -263,8 +236,10 @@ class PopoverResize { const deltaX = clientX - this._initialClientX!; const deltaY = clientY - this._initialClientY!; - let newWidth, - newHeight; + let newWidth: number; + let newHeight: number; + let newLeft = initialBoundingRect.x; + let newTop = initialBoundingRect.y; // Determine if we're resizing from left or right edge const isResizingFromLeft = resizeHandlePlacement === ResizeHandlePlacement.TopLeft @@ -275,7 +250,6 @@ class PopoverResize { // Calculate width changes if (isResizingFromLeft) { - // Resizing from left edge - width increases when moving left (negative delta) const maxWidthFromLeft = initialBoundingRect.x + initialBoundingRect.width - margin; newWidth = clamp( @@ -284,20 +258,14 @@ class PopoverResize { maxWidthFromLeft, ); - // Adjust left position when resizing from left - // Ensure the left edge respects the viewport margin and the right edge position - const newLeft = clamp( + newLeft = clamp( initialBoundingRect.x + deltaX, margin, initialBoundingRect.x + initialBoundingRect.width - this._minWidth!, ); - // Recalculate width based on actual left position to stay within viewport with margin newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft); - - this._currentDeltaX = (initialBoundingRect.x - newLeft) / 2; } else { - // Resizing from right edge - width increases when moving right (positive delta) const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin; newWidth = clamp( @@ -305,13 +273,10 @@ class PopoverResize { this._minWidth!, maxWidthFromRight, ); - - this._currentDeltaX = (initialBoundingRect.width - newWidth) / 2; } // Calculate height changes if (isResizingFromTop) { - // Resizing from top edge - height increases when moving up (negative delta) const maxHeightFromTop = initialBoundingRect.y + initialBoundingRect.height - margin; newHeight = clamp( @@ -320,20 +285,14 @@ class PopoverResize { maxHeightFromTop, ); - // Adjust top position when resizing from top - // Ensure the top edge respects the viewport margin and the bottom edge position - const newTop = clamp( + newTop = clamp( initialBoundingRect.y + deltaY, margin, initialBoundingRect.y + initialBoundingRect.height - this._minHeight!, ); - // Recalculate height based on actual top position to stay within viewport with margin newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop); - - this._currentDeltaY = (initialBoundingRect.y - newTop) / 2; } else { - // Resizing from bottom edge - height increases when moving down (positive delta) const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin; newHeight = clamp( @@ -341,24 +300,14 @@ class PopoverResize { this._minHeight!, maxHeightFromBottom, ); - - this._currentDeltaY = (initialBoundingRect.height - newHeight) / 2; } - this._currentDeltaX += this._totalDeltaX || 0; - this._currentDeltaY += this._totalDeltaY || 0; - - const placement = this._popover.calcPlacement(this._popover._openerRect!, { - width: newWidth, - height: newHeight, - }); - - this._popover.arrowTranslateX = placement.arrow.x; - this._popover.arrowTranslateY = placement.arrow.y; + // Update arrow position based on new dimensions + popover._updateArrowPosition(); - Object.assign(this._popover.style, { - left: `${placement.left}px`, - top: `${placement.top}px`, + Object.assign(popover.style, { + left: `${newLeft}px`, + top: `${newTop}px`, height: `${newHeight}px`, width: `${newWidth}px`, }); diff --git a/packages/main/src/popup-utils/PopoverRegistry.ts b/packages/main/src/popup-utils/PopoverRegistry.ts index d7b71794f74b..ea6909056a79 100644 --- a/packages/main/src/popup-utils/PopoverRegistry.ts +++ b/packages/main/src/popup-utils/PopoverRegistry.ts @@ -43,8 +43,6 @@ const closePopoversIfLostFocus = () => { const runUpdateInterval = () => { updateInterval = setInterval(() => { - repositionPopovers(); - closePopoversIfLostFocus(); }, intervalTimeout); }; diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css index a85fa99ab4dd..06fef8efbe34 100644 --- a/packages/main/src/themes/Popover.css +++ b/packages/main/src/themes/Popover.css @@ -8,6 +8,23 @@ box-shadow: var(--_ui5_popover_no_arrow_box_shadow); } +/* Arrow-gap margins for CSS anchor positioning */ +:host(:not([hide-arrow])[actual-placement="Bottom"]) { + margin-top: 0.5rem; +} + +:host(:not([hide-arrow])[actual-placement="Top"]) { + margin-bottom: 0.5625rem; +} + +:host(:not([hide-arrow])[actual-placement="Left"]) { + margin-inline-end: 0.5625rem; +} + +:host(:not([hide-arrow])[actual-placement="Right"]) { + margin-inline-start: 0.5625rem; +} + /* pointing upward arrow */ :host([actual-placement="Bottom"]) .ui5-popover-arrow { left: calc(50% - 0.5625rem);