From 02b434baa390918ee959f3049b3e18f476d25bba Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Wed, 5 Nov 2025 16:37:13 +0200 Subject: [PATCH 01/22] chore: add test page --- packages/main/test/pages/PopoverResize.html | 85 +++++++++++++++++++ .../main/test/pages/styles/PopoverResize.css | 24 ++++++ 2 files changed, 109 insertions(+) create mode 100644 packages/main/test/pages/PopoverResize.html create mode 100644 packages/main/test/pages/styles/PopoverResize.css diff --git a/packages/main/test/pages/PopoverResize.html b/packages/main/test/pages/PopoverResize.html new file mode 100644 index 000000000000..c6669440ac41 --- /dev/null +++ b/packages/main/test/pages/PopoverResize.html @@ -0,0 +1,85 @@ + + + + + + + Popover + + + + + + + + +
+

Popover Resize

+
+
+ Placement: + + Start + End + Top + Bottom + +
+
+ Horizontal Align: + + Center + Start + End + Stretch + +
+
+ Vertical Align: + + Center + Top + Bottom + Stretch + +
+
+ Show Arrow: +
+
+
+ Open Popover + +
+ This is a Popover control. +
+
+
+
+ + diff --git a/packages/main/test/pages/styles/PopoverResize.css b/packages/main/test/pages/styles/PopoverResize.css new file mode 100644 index 000000000000..f6534232eb09 --- /dev/null +++ b/packages/main/test/pages/styles/PopoverResize.css @@ -0,0 +1,24 @@ +.pageContainer { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; +} + +h1 { + color: var(--sapGroup_TitleTextColor); + font-size: var(--sapFontHeader5Size); + font-family: var(--sapFontHeaderFamily); +} + +.popoverSettings div { + display: flex; + align-items: center; +} + +.popoverOpenerContainer { + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file From 4f1068a2b15f704cde1db3c2f861c24c3d889072 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 6 Nov 2025 11:00:18 +0200 Subject: [PATCH 02/22] chore: improve test page --- packages/main/src/Popover.ts | 12 +++++++++++- packages/main/test/pages/PopoverResize.html | 6 +++++- packages/main/test/pages/styles/PopoverResize.css | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 3f10d1ea4b72..db1359c70466 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -121,7 +121,7 @@ class Popover extends Popup { /** * Defines whether the component should close when - * clicking/tapping outside of the popover. + * clicking/tapping outside the popover. * If enabled, it blocks any interaction with the background. * @default false * @public @@ -147,6 +147,16 @@ class Popover extends Popup { @property({ type: Boolean }) allowTargetOverlap = false; + /** + * Determines whether the component is resizable. + * **Note:** This property is effective only on Desktop + * @default false + * @public + * @since 2.17.0 + */ + @property({ type: Boolean }) + resizable = false; + /** * Sets the X translation of the arrow * @private diff --git a/packages/main/test/pages/PopoverResize.html b/packages/main/test/pages/PopoverResize.html index c6669440ac41..013bb9129f2d 100644 --- a/packages/main/test/pages/PopoverResize.html +++ b/packages/main/test/pages/PopoverResize.html @@ -31,6 +31,10 @@ vAlignSwitch.addEventListener("ui5-selection-change", event => { popoverId.verticalAlign = event.detail.selectedItems[0].getAttribute("data-ui5-value"); }); + + hideArrowSwitch.addEventListener("ui5-change", event => { + popoverId.hideArrow = hideArrowSwitch.checked; + }) }); @@ -67,7 +71,7 @@

Popover Resize

- Show Arrow: + Hide Arrow:
diff --git a/packages/main/test/pages/styles/PopoverResize.css b/packages/main/test/pages/styles/PopoverResize.css index f6534232eb09..f98922a1059b 100644 --- a/packages/main/test/pages/styles/PopoverResize.css +++ b/packages/main/test/pages/styles/PopoverResize.css @@ -14,6 +14,7 @@ h1 { .popoverSettings div { display: flex; align-items: center; + gap: 0.5rem; } .popoverOpenerContainer { From d5f7f457d288738ed0bc5ba24202abb35fc6dd9c Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 6 Nov 2025 16:39:08 +0200 Subject: [PATCH 03/22] chore: render the resize corner icon --- packages/main/src/Popover.ts | 2 + packages/main/src/PopoverTemplate.tsx | 8 +++ packages/main/src/themes/Popover.css | 69 +++++++++++++++++++++ packages/main/test/pages/PopoverResize.html | 18 ++++-- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index db1359c70466..c6c4a4ba4c2e 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -831,6 +831,8 @@ class Popover extends Popup { const allClasses = super.classes; allClasses.root["ui5-popover-root"] = true; + allClasses.root["ui5-popover-resize-handle-bottom-right"] = this.resizable; + return allClasses; } diff --git a/packages/main/src/PopoverTemplate.tsx b/packages/main/src/PopoverTemplate.tsx index f31cb442d788..3ea9744be5ff 100644 --- a/packages/main/src/PopoverTemplate.tsx +++ b/packages/main/src/PopoverTemplate.tsx @@ -1,3 +1,5 @@ +import Icon from "./Icon.js"; +import resizeCorner from "@ui5/webcomponents-icons/dist/resize-corner.js"; import type Popover from "./Popover.js"; import PopupTemplate from "./PopupTemplate.js"; import Title from "./Title.js"; @@ -32,5 +34,11 @@ function afterContent(this: Popover) { } + + {this.resizable && +
+ +
+ } ); } diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css index ea16b3b47dae..28d7daacf7dc 100644 --- a/packages/main/src/themes/Popover.css +++ b/packages/main/src/themes/Popover.css @@ -90,3 +90,72 @@ :host([modal]) .ui5-block-layer { display: block; } + +/* resize handle */ + +.ui5-popover-resize-handle { + position: absolute; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + z-index: 10; +} + +.ui5-popover-resize-handle .ui5-popover-resize-handle-icon { + position: absolute; + width: 1rem; + height: 1rem; + cursor: inherit; +} + +.ui5-popover-resize-handle-top-right .ui5-popover-resize-handle { + top: -0.5rem; + right: -0.5rem; + cursor: ne-resize; +} + +.ui5-popover-resize-handle-top-right .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { + bottom: 0; + left: 0; + transform: rotate(270deg); +} + +.ui5-popover-resize-handle-top-left .ui5-popover-resize-handle { + top: -0.5rem; + left: -0.5rem; + cursor: nw-resize; +} + +.ui5-popover-resize-handle-top-left .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { + bottom: 0; + right: 0; + transform: rotate(180deg); +} + +.ui5-popover-resize-handle-bottom-left .ui5-popover-resize-handle { + bottom: -0.5rem; + left: -0.5rem; + cursor: ne-resize; +} + +.ui5-popover-resize-handle-bottom-left .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { + top: 0; + right: 0; + transform: rotate(90deg); +} + +.ui5-popover-resize-handle-bottom-right .ui5-popover-resize-handle { + bottom: -0.5rem; + right: -0.5rem; + cursor: nw-resize; +} + +.ui5-popover-resize-handle-bottom-right .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { + top: 0; + left: 0; +} + +.ui5-popover-resizing, +.ui5-popover-resizing * { + user-select: none !important; +} diff --git a/packages/main/test/pages/PopoverResize.html b/packages/main/test/pages/PopoverResize.html index 013bb9129f2d..35c8d47605bd 100644 --- a/packages/main/test/pages/PopoverResize.html +++ b/packages/main/test/pages/PopoverResize.html @@ -39,12 +39,12 @@ - +
-

Popover Resize

+ Popover Resize
- Placement: + Placement Start End @@ -53,7 +53,7 @@

Popover Resize

- Horizontal Align: + Horizontal Align Center Start @@ -62,7 +62,7 @@

Popover Resize

- Vertical Align: + Vertical Align Center Top @@ -71,17 +71,23 @@

Popover Resize

- Hide Arrow: + Hide Arrow +
Open Popover
This is a Popover control.
+ + OK +
From d33a75f11cf4a90819a4871a4207937e40d14f38 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Fri, 7 Nov 2025 12:52:38 +0200 Subject: [PATCH 04/22] chore: add ResizeHandlePlacement --- packages/main/src/Popover.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index c6c4a4ba4c2e..9b80fbae5d48 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -39,6 +39,13 @@ type CalculatedPlacement = { placement: `${PopoverPlacement}`, } +enum ResizeHandlePlacement { + TopLeft = "TopLeft", + TopRight = "TopRight", + BottomLeft = "BottomLeft", + BottomRight = "BottomRight", +} + /** * @class * @@ -178,6 +185,9 @@ class Popover extends Popup { @property() actualPlacement: `${PopoverPlacement}` = "End"; + @property() + _resizeHandlePlacement: `${ResizeHandlePlacement}` = "BottomRight"; + @property({ type: Number, noAttribute: true }) _maxHeight?: number; From 18824994a2b60808eb9aee5fc2665a697f166c52 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Fri, 7 Nov 2025 16:10:02 +0200 Subject: [PATCH 05/22] chore: add some resizing structure --- packages/main/src/Popover.ts | 143 ++++++++++++++++++++++++++ packages/main/src/PopoverTemplate.tsx | 8 +- packages/main/src/themes/Popover.css | 11 +- 3 files changed, 154 insertions(+), 8 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 9b80fbae5d48..1566f3be8ad5 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -217,12 +217,30 @@ class Popover extends Popup { _width?: string; _height?: string; + _resizeMouseMoveHandler: (e: MouseEvent) => void; + _resizeMouseUpHandler: (e: MouseEvent) => void; + _y?: number; + _x?: number; + _isRTL?: boolean; + _initialX?: number; + _initialY?: number; + _initialWidth?: number; + _initialHeight?: number; + _initialTop?: number; + _initialLeft?: number; + _minWidth?: number; + _cachedMinHeight?: number; + _draggedOrResized = false; + static get VIEWPORT_MARGIN() { return 10; // px } constructor() { super(); + + this._resizeMouseMoveHandler = this._onResizeMouseMove.bind(this); + this._resizeMouseUpHandler = this._onResizeMouseUp.bind(this); } /** @@ -873,6 +891,131 @@ class Popover extends Popup { return this.horizontalAlign; } + + get _showResizeHandle() { + return this.resizable && this.onDesktop; + } + + _onResizeMouseDown(e: MouseEvent) { + if (!this.resizable) { + return; + } + + e.preventDefault(); + + const { + top, + left, + } = this.getBoundingClientRect(); + const { + width, + height, + minWidth, + } = window.getComputedStyle(this); + + this._initialX = e.clientX; + this._initialY = e.clientY; + this._initialWidth = Number.parseFloat(width); + this._initialHeight = Number.parseFloat(height); + this._initialTop = top; + this._initialLeft = left; + this._minWidth = Number.parseFloat(minWidth); + // this._cachedMinHeight = this._minHeight; + + Object.assign(this.style, { + top: `${top}px`, + left: `${left}px`, + }); + + this._draggedOrResized = true; + this._attachMouseResizeHandlers(); + } + + _onResizeMouseMove(e: MouseEvent) { + const { clientX, clientY } = e; + + let newWidth, + newLeft; + + if (this._isRTL) { + newWidth = clamp( + this._initialWidth! - (clientX - this._initialX!), + this._minWidth!, + this._initialLeft! + this._initialWidth!, + ); + + // check if width is changed to avoid "left" jumping when max width is reached + Object.assign(this.style, { + width: `${newWidth}px`, + }); + + const deltaWidth = newWidth - this.getBoundingClientRect().width; + const rightEdge = this._initialLeft! + this._initialWidth! + deltaWidth; + + newLeft = clamp( + rightEdge - newWidth, + 0, + rightEdge - this._minWidth!, + ); + } else { + newWidth = clamp( + this._initialWidth! + (clientX - this._initialX!), + this._minWidth!, + window.innerWidth - this._initialLeft!, + ); + } + + const newHeight = clamp( + this._initialHeight! + (clientY - this._initialY!), + this._cachedMinHeight!, + window.innerHeight - this._initialTop!, + ); + + Object.assign(this.style, { + height: `${newHeight}px`, + width: `${newWidth}px`, + left: this._isRTL ? `${newLeft}px` : undefined, + }); + } + + _onResizeMouseUp() { + delete this._initialX; + delete this._initialY; + delete this._initialWidth; + delete this._initialHeight; + delete this._initialTop; + delete this._initialLeft; + delete this._minWidth; + delete this._cachedMinHeight; + + this._detachMouseResizeHandlers(); + } + + _handleDragStart(e: DragEvent) { + if (this.draggable) { + e.preventDefault(); + } + } + + _attachMouseResizeHandlers() { + window.addEventListener("mousemove", this._resizeMouseMoveHandler); + window.addEventListener("mouseup", this._resizeMouseUpHandler); + this.addEventListener("ui5-before-close", this._revertSize, { once: true }); + } + + _detachMouseResizeHandlers() { + window.removeEventListener("mousemove", this._resizeMouseMoveHandler); + window.removeEventListener("mouseup", this._resizeMouseUpHandler); + } + + _revertSize = () => { + Object.assign(this.style, { + top: "", + left: "", + width: "", + height: "", + }); + } } const instanceOfPopover = (object: any): object is Popover => { diff --git a/packages/main/src/PopoverTemplate.tsx b/packages/main/src/PopoverTemplate.tsx index 3ea9744be5ff..c5f9bfbfc124 100644 --- a/packages/main/src/PopoverTemplate.tsx +++ b/packages/main/src/PopoverTemplate.tsx @@ -35,9 +35,11 @@ function afterContent(this: Popover) { } - {this.resizable && -
- + {this._showResizeHandle && +
+
} ); diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css index 28d7daacf7dc..cc5d5868bfac 100644 --- a/packages/main/src/themes/Popover.css +++ b/packages/main/src/themes/Popover.css @@ -101,11 +101,12 @@ z-index: 10; } -.ui5-popover-resize-handle .ui5-popover-resize-handle-icon { +.ui5-popover-resize-handle [ui5-icon] { position: absolute; width: 1rem; height: 1rem; cursor: inherit; + color: var(--sapButton_Lite_TextColor); } .ui5-popover-resize-handle-top-right .ui5-popover-resize-handle { @@ -114,7 +115,7 @@ cursor: ne-resize; } -.ui5-popover-resize-handle-top-right .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { +.ui5-popover-resize-handle-top-right .ui5-popover-resize-handle [ui5-icon] { bottom: 0; left: 0; transform: rotate(270deg); @@ -126,7 +127,7 @@ cursor: nw-resize; } -.ui5-popover-resize-handle-top-left .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { +.ui5-popover-resize-handle-top-left .ui5-popover-resize-handle [ui5-icon] { bottom: 0; right: 0; transform: rotate(180deg); @@ -138,7 +139,7 @@ cursor: ne-resize; } -.ui5-popover-resize-handle-bottom-left .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { +.ui5-popover-resize-handle-bottom-left .ui5-popover-resize-handle [ui5-icon] { top: 0; right: 0; transform: rotate(90deg); @@ -150,7 +151,7 @@ cursor: nw-resize; } -.ui5-popover-resize-handle-bottom-right .ui5-popover-resize-handle .ui5-popover-resize-handle-icon { +.ui5-popover-resize-handle-bottom-right .ui5-popover-resize-handle [ui5-icon] { top: 0; left: 0; } From ef6b0cd80fa500cd59d9356120baa6a792e33d82 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Mon, 10 Nov 2025 11:22:25 +0200 Subject: [PATCH 06/22] chore: calc resizeHandlePlacement --- packages/main/src/Popover.ts | 84 +++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 506cb7d7424b..26692ec8663c 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -199,9 +199,6 @@ class Popover extends Popup { @property() actualPlacement: `${PopoverActualPlacement}` = "Right"; - @property() - _resizeHandlePlacement: `${ResizeHandlePlacement}` = "BottomRight"; - @property({ type: Number, noAttribute: true }) _maxHeight?: number; @@ -246,6 +243,8 @@ class Popover extends Popup { _cachedMinHeight?: number; _draggedOrResized = false; + _resizeHandlePlacement?: `${ResizeHandlePlacement}`; + static get VIEWPORT_MARGIN() { return 10; // px } @@ -888,7 +887,22 @@ class Popover extends Popup { const allClasses = super.classes; allClasses.root["ui5-popover-root"] = true; - allClasses.root["ui5-popover-resize-handle-bottom-right"] = this.resizable; + if (this.resizable) { + switch (this._getResizeHandlePlacement()) { + case ResizeHandlePlacement.BottomLeft: + allClasses.root["ui5-popover-resize-handle-bottom-left"] = true; + break; + case ResizeHandlePlacement.BottomRight: + allClasses.root["ui5-popover-resize-handle-bottom-right"] = true; + break; + case ResizeHandlePlacement.TopLeft: + allClasses.root["ui5-popover-resize-handle-top-left"] = true; + break; + case ResizeHandlePlacement.TopRight: + allClasses.root["ui5-popover-resize-handle-top-right"] = true; + break; + } + } return allClasses; } @@ -929,6 +943,57 @@ class Popover extends Popup { return this.resizable && this.onDesktop; } + _getResizeHandlePlacement() { + if (this._resizeHandlePlacement) { + return this._resizeHandlePlacement; + } + + const offset = 2; + + const opener = this.getOpenerHTMLElement(this.opener); + const openerRect = opener!.getBoundingClientRect(); + const popoverWrapperRect = this.getBoundingClientRect(); + + let openerCX = Math.floor(openerRect.x + openerRect.width / 2); + const openerCY = Math.floor(openerRect.y + openerRect.height / 2); + + let popoverCX = Math.floor(popoverWrapperRect.x + popoverWrapperRect.width / 2); + const popoverCY = Math.floor(popoverWrapperRect.y + popoverWrapperRect.height / 2); + + if (this.isRtl) { + openerCX = -openerCX; + popoverCX = -popoverCX; + } + + switch (this.getActualPlacement(openerRect)) { + case PopoverActualPlacement.Left: + if (popoverCY > openerCY + offset) { + return ResizeHandlePlacement.BottomLeft; + } + + return ResizeHandlePlacement.TopLeft; + case PopoverActualPlacement.Right: + if (popoverCY + offset < openerCY) { + return ResizeHandlePlacement.TopRight; + } + + return ResizeHandlePlacement.BottomRight; + case PopoverActualPlacement.Bottom: + if (popoverCX + offset < openerCX) { + return ResizeHandlePlacement.BottomLeft; + } + + return ResizeHandlePlacement.BottomRight; + case PopoverActualPlacement.Top: + default: + if (popoverCX + offset < openerCX) { + return ResizeHandlePlacement.TopLeft; + } + + return ResizeHandlePlacement.TopRight; + } + } + _onResizeMouseDown(e: MouseEvent) { if (!this.resizable) { return; @@ -940,6 +1005,7 @@ class Popover extends Popup { top, left, } = this.getBoundingClientRect(); + const { width, height, @@ -955,6 +1021,8 @@ class Popover extends Popup { this._minWidth = Number.parseFloat(minWidth); // this._cachedMinHeight = this._minHeight; + this._resizeHandlePlacement = this._getResizeHandlePlacement(); + Object.assign(this.style, { top: `${top}px`, left: `${left}px`, @@ -1021,13 +1089,9 @@ class Popover extends Popup { delete this._minWidth; delete this._cachedMinHeight; - this._detachMouseResizeHandlers(); - } + delete this._resizeHandlePlacement; - _handleDragStart(e: DragEvent) { - if (this.draggable) { - e.preventDefault(); - } + this._detachMouseResizeHandlers(); } _attachMouseResizeHandlers() { From a167b863c728ffa03aabcdb5dc15cdc93e17d229 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Mon, 10 Nov 2025 13:55:20 +0200 Subject: [PATCH 07/22] chore: improve calculations --- packages/main/src/Popover.ts | 94 ++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 26692ec8663c..f444a7919377 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -241,7 +241,7 @@ class Popover extends Popup { _initialLeft?: number; _minWidth?: number; _cachedMinHeight?: number; - _draggedOrResized = false; + _resized = false; _resizeHandlePlacement?: `${ResizeHandlePlacement}`; @@ -1010,6 +1010,7 @@ class Popover extends Popup { width, height, minWidth, + minHeight, } = window.getComputedStyle(this); this._initialX = e.clientX; @@ -1019,7 +1020,7 @@ class Popover extends Popup { this._initialTop = top; this._initialLeft = left; this._minWidth = Number.parseFloat(minWidth); - // this._cachedMinHeight = this._minHeight; + this._cachedMinHeight = Number.parseFloat(minHeight); this._resizeHandlePlacement = this._getResizeHandlePlacement(); @@ -1028,54 +1029,97 @@ class Popover extends Popup { left: `${left}px`, }); - this._draggedOrResized = true; + this._resized = true; this._attachMouseResizeHandlers(); } _onResizeMouseMove(e: MouseEvent) { const { clientX, clientY } = e; + const placement = this._resizeHandlePlacement; + const margin = Popover.VIEWPORT_MARGIN; let newWidth, - newLeft; + newHeight, + newLeft, + newTop; + + // Determine if we're resizing from left or right edge + const isResizingFromLeft = placement === ResizeHandlePlacement.TopLeft + || placement === ResizeHandlePlacement.BottomLeft; + const isResizingFromTop = placement === ResizeHandlePlacement.TopLeft + || placement === ResizeHandlePlacement.TopRight; + + // Calculate width changes + if (isResizingFromLeft) { + // Resizing from left edge - width increases when moving left (negative delta) + const deltaX = clientX - this._initialX!; + const maxWidthFromLeft = this._initialLeft! + this._initialWidth! - margin; - if (this._isRTL) { newWidth = clamp( - this._initialWidth! - (clientX - this._initialX!), + this._initialWidth! - deltaX, this._minWidth!, - this._initialLeft! + this._initialWidth!, + maxWidthFromLeft, ); - // check if width is changed to avoid "left" jumping when max width is reached - Object.assign(this.style, { - width: `${newWidth}px`, - }); - - const deltaWidth = newWidth - this.getBoundingClientRect().width; - const rightEdge = this._initialLeft! + this._initialWidth! + deltaWidth; - + // Adjust left position when resizing from left + // Ensure the left edge respects the viewport margin and the right edge position newLeft = clamp( - rightEdge - newWidth, - 0, - rightEdge - this._minWidth!, + this._initialLeft! + deltaX, + margin, + this._initialLeft! + this._initialWidth! - this._minWidth!, ); + + // Recalculate width based on actual left position to stay within viewport with margin + newWidth = Math.min(newWidth, this._initialLeft! + this._initialWidth! - newLeft); } else { + // Resizing from right edge - width increases when moving right (positive delta) + const maxWidthFromRight = window.innerWidth - this._initialLeft! - margin; + newWidth = clamp( this._initialWidth! + (clientX - this._initialX!), this._minWidth!, - window.innerWidth - this._initialLeft!, + maxWidthFromRight, ); } - const newHeight = clamp( - this._initialHeight! + (clientY - this._initialY!), - this._cachedMinHeight!, - window.innerHeight - this._initialTop!, - ); + // Calculate height changes + if (isResizingFromTop) { + // Resizing from top edge - height increases when moving up (negative delta) + const deltaY = clientY - this._initialY!; + const maxHeightFromTop = this._initialTop! + this._initialHeight! - margin; + + newHeight = clamp( + this._initialHeight! - deltaY, + this._cachedMinHeight!, + maxHeightFromTop, + ); + + // Adjust top position when resizing from top + // Ensure the top edge respects the viewport margin and the bottom edge position + newTop = clamp( + this._initialTop! + deltaY, + margin, + this._initialTop! + this._initialHeight! - this._cachedMinHeight!, + ); + + // Recalculate height based on actual top position to stay within viewport with margin + newHeight = Math.min(newHeight, this._initialTop! + this._initialHeight! - newTop); + } else { + // Resizing from bottom edge - height increases when moving down (positive delta) + const maxHeightFromBottom = window.innerHeight - this._initialTop! - margin; + + newHeight = clamp( + this._initialHeight! + (clientY - this._initialY!), + this._cachedMinHeight!, + maxHeightFromBottom, + ); + } Object.assign(this.style, { height: `${newHeight}px`, width: `${newWidth}px`, - left: this._isRTL ? `${newLeft}px` : undefined, + left: newLeft !== undefined ? `${newLeft}px` : undefined, + top: newTop !== undefined ? `${newTop}px` : undefined, }); } From 6398e8e79a594cbfffa45c7a279200e046e24e18 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Tue, 11 Nov 2025 15:05:19 +0200 Subject: [PATCH 08/22] chore: handle min sizes --- packages/main/src/Popover.ts | 108 +++++++++++++++-------------------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index f444a7919377..fc52e7207018 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -230,17 +230,12 @@ class Popover extends Popup { _resizeMouseMoveHandler: (e: MouseEvent) => void; _resizeMouseUpHandler: (e: MouseEvent) => void; - _y?: number; - _x?: number; - _isRTL?: boolean; - _initialX?: number; - _initialY?: number; - _initialWidth?: number; - _initialHeight?: number; - _initialTop?: number; - _initialLeft?: number; + _initialClientX?: number; + _initialClientY?: number; + + _initialBoundingRect?: DOMRect; _minWidth?: number; - _cachedMinHeight?: number; + _minHeight?: number; _resized = false; _resizeHandlePlacement?: `${ResizeHandlePlacement}`; @@ -1001,62 +996,55 @@ class Popover extends Popup { e.preventDefault(); - const { - top, - left, - } = this.getBoundingClientRect(); + this._resized = true; + this._initialBoundingRect = this.getBoundingClientRect(); const { - width, - height, minWidth, minHeight, } = window.getComputedStyle(this); - this._initialX = e.clientX; - this._initialY = e.clientY; - this._initialWidth = Number.parseFloat(width); - this._initialHeight = Number.parseFloat(height); - this._initialTop = top; - this._initialLeft = left; - this._minWidth = Number.parseFloat(minWidth); - this._cachedMinHeight = Number.parseFloat(minHeight); + const domRefComputedStyle = window.getComputedStyle(this._getRealDomRef!()); - this._resizeHandlePlacement = this._getResizeHandlePlacement(); + this._initialClientX = e.clientX; + this._initialClientY = e.clientY; - Object.assign(this.style, { - top: `${top}px`, - left: `${left}px`, - }); + this._minWidth = Math.max(Number.parseFloat(minWidth), Number.parseFloat(domRefComputedStyle.minWidth)); + this._minHeight = Number.parseFloat(minHeight); + + this._resizeHandlePlacement = this._getResizeHandlePlacement(); - this._resized = true; this._attachMouseResizeHandlers(); } _onResizeMouseMove(e: MouseEvent) { + const margin = Popover.VIEWPORT_MARGIN; const { clientX, clientY } = e; const placement = this._resizeHandlePlacement; - const margin = Popover.VIEWPORT_MARGIN; + const initialBoundingRect = this._initialBoundingRect!; + const deltaX = clientX - this._initialClientX!; + const deltaY = clientY - this._initialClientY!; + + let newLeft = initialBoundingRect.x; + let newTop = initialBoundingRect.y; let newWidth, - newHeight, - newLeft, - newTop; + newHeight; // Determine if we're resizing from left or right edge const isResizingFromLeft = placement === ResizeHandlePlacement.TopLeft || placement === ResizeHandlePlacement.BottomLeft; + const isResizingFromTop = placement === ResizeHandlePlacement.TopLeft || placement === ResizeHandlePlacement.TopRight; // Calculate width changes if (isResizingFromLeft) { // Resizing from left edge - width increases when moving left (negative delta) - const deltaX = clientX - this._initialX!; - const maxWidthFromLeft = this._initialLeft! + this._initialWidth! - margin; + const maxWidthFromLeft = initialBoundingRect.x + initialBoundingRect.width - margin; newWidth = clamp( - this._initialWidth! - deltaX, + initialBoundingRect.width - deltaX, this._minWidth!, maxWidthFromLeft, ); @@ -1064,19 +1052,19 @@ class Popover extends Popup { // Adjust left position when resizing from left // Ensure the left edge respects the viewport margin and the right edge position newLeft = clamp( - this._initialLeft! + deltaX, + initialBoundingRect.x + deltaX, margin, - this._initialLeft! + this._initialWidth! - this._minWidth!, + initialBoundingRect.x + initialBoundingRect.width - this._minWidth!, ); // Recalculate width based on actual left position to stay within viewport with margin - newWidth = Math.min(newWidth, this._initialLeft! + this._initialWidth! - newLeft); + newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft); } else { // Resizing from right edge - width increases when moving right (positive delta) - const maxWidthFromRight = window.innerWidth - this._initialLeft! - margin; + const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin; newWidth = clamp( - this._initialWidth! + (clientX - this._initialX!), + initialBoundingRect.width + deltaX, this._minWidth!, maxWidthFromRight, ); @@ -1085,32 +1073,31 @@ class Popover extends Popup { // Calculate height changes if (isResizingFromTop) { // Resizing from top edge - height increases when moving up (negative delta) - const deltaY = clientY - this._initialY!; - const maxHeightFromTop = this._initialTop! + this._initialHeight! - margin; + const maxHeightFromTop = initialBoundingRect.y + initialBoundingRect.height - margin; newHeight = clamp( - this._initialHeight! - deltaY, - this._cachedMinHeight!, + initialBoundingRect.height - deltaY, + this._minHeight!, maxHeightFromTop, ); // Adjust top position when resizing from top // Ensure the top edge respects the viewport margin and the bottom edge position newTop = clamp( - this._initialTop! + deltaY, + initialBoundingRect.y + deltaY, margin, - this._initialTop! + this._initialHeight! - this._cachedMinHeight!, + initialBoundingRect.y + initialBoundingRect.height - this._minHeight!, ); // Recalculate height based on actual top position to stay within viewport with margin - newHeight = Math.min(newHeight, this._initialTop! + this._initialHeight! - newTop); + newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop); } else { // Resizing from bottom edge - height increases when moving down (positive delta) - const maxHeightFromBottom = window.innerHeight - this._initialTop! - margin; + const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin; newHeight = clamp( - this._initialHeight! + (clientY - this._initialY!), - this._cachedMinHeight!, + initialBoundingRect.height + deltaY, + this._minHeight!, maxHeightFromBottom, ); } @@ -1118,20 +1105,17 @@ class Popover extends Popup { Object.assign(this.style, { height: `${newHeight}px`, width: `${newWidth}px`, - left: newLeft !== undefined ? `${newLeft}px` : undefined, - top: newTop !== undefined ? `${newTop}px` : undefined, + left: `${newLeft}px`, + top: `${newTop}px`, }); } _onResizeMouseUp() { - delete this._initialX; - delete this._initialY; - delete this._initialWidth; - delete this._initialHeight; - delete this._initialTop; - delete this._initialLeft; + delete this._initialClientX; + delete this._initialClientY; + delete this._initialBoundingRect; delete this._minWidth; - delete this._cachedMinHeight; + delete this._minHeight; delete this._resizeHandlePlacement; @@ -1150,6 +1134,8 @@ class Popover extends Popup { } _revertSize = () => { + this._resized = false; + Object.assign(this.style, { top: "", left: "", From bb5f509e62d7f8b0de7a889cc477c9b9e8a527eb Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 13 Nov 2025 15:52:35 +0200 Subject: [PATCH 09/22] fix: resize icon placing in rtl mode --- packages/main/src/Popover.ts | 18 ++++++++++++++---- packages/main/src/themes/Popover.css | 13 ++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index fc52e7207018..86ee8f3aeeee 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -815,6 +815,10 @@ class Popover extends Popup { } getVerticalLeft(targetRect: DOMRect, popoverSize: PopoverSize): number { + if (this._resized) { + return this._left!; + } + const actualHorizontalAlign = this._actualHorizontalAlign; let left = Popover.VIEWPORT_MARGIN; @@ -835,6 +839,10 @@ class Popover extends Popup { } getHorizontalTop(targetRect: DOMRect, popoverSize: PopoverSize): number { + if (this._resized) { + return this._top!; + } + let top = 0; switch (this.verticalAlign) { @@ -881,6 +889,7 @@ class Popover extends Popup { get classes() { const allClasses = super.classes; allClasses.root["ui5-popover-root"] = true; + allClasses.root["ui5-popover-rtl"] = this.isRtl; if (this.resizable) { switch (this._getResizeHandlePlacement()) { @@ -944,6 +953,7 @@ class Popover extends Popup { } const offset = 2; + const isRtl = this.isRtl; const opener = this.getOpenerHTMLElement(this.opener); const openerRect = opener!.getBoundingClientRect(); @@ -975,17 +985,17 @@ class Popover extends Popup { return ResizeHandlePlacement.BottomRight; case PopoverActualPlacement.Bottom: if (popoverCX + offset < openerCX) { - return ResizeHandlePlacement.BottomLeft; + return isRtl ? ResizeHandlePlacement.BottomRight : ResizeHandlePlacement.BottomLeft; } - return ResizeHandlePlacement.BottomRight; + return isRtl ? ResizeHandlePlacement.BottomLeft : ResizeHandlePlacement.BottomRight; case PopoverActualPlacement.Top: default: if (popoverCX + offset < openerCX) { - return ResizeHandlePlacement.TopLeft; + return isRtl ? ResizeHandlePlacement.TopRight : ResizeHandlePlacement.TopLeft; } - return ResizeHandlePlacement.TopRight; + return isRtl ? ResizeHandlePlacement.TopLeft : ResizeHandlePlacement.TopRight; } } diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css index b0769d23b30c..49f71f7c17e6 100644 --- a/packages/main/src/themes/Popover.css +++ b/packages/main/src/themes/Popover.css @@ -107,6 +107,13 @@ height: 1rem; cursor: inherit; color: var(--sapButton_Lite_TextColor); + --rotAngle: 0; + --scaleX: 1; + transform: rotate(var(--rotAngle)) scaleX(var(--scaleX)); +} + +.ui5-popover-rtl .ui5-popover-resize-handle [ui5-icon] { + --scaleX: -1; } .ui5-popover-resize-handle-top-right .ui5-popover-resize-handle { @@ -118,7 +125,7 @@ .ui5-popover-resize-handle-top-right .ui5-popover-resize-handle [ui5-icon] { bottom: 0; left: 0; - transform: rotate(270deg); + --rotAngle: 270deg; } .ui5-popover-resize-handle-top-left .ui5-popover-resize-handle { @@ -130,7 +137,7 @@ .ui5-popover-resize-handle-top-left .ui5-popover-resize-handle [ui5-icon] { bottom: 0; right: 0; - transform: rotate(180deg); + --rotAngle: 180deg; } .ui5-popover-resize-handle-bottom-left .ui5-popover-resize-handle { @@ -142,7 +149,7 @@ .ui5-popover-resize-handle-bottom-left .ui5-popover-resize-handle [ui5-icon] { top: 0; right: 0; - transform: rotate(90deg); + --rotAngle: 90deg; } .ui5-popover-resize-handle-bottom-right .ui5-popover-resize-handle { From 1a05a21051dd46a1edf20790724512686eb26960 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Fri, 14 Nov 2025 15:46:08 +0200 Subject: [PATCH 10/22] fix: resize positioning when centered --- packages/main/src/Popover.ts | 59 +++++++++++-------- packages/main/test/pages/PopoverResize.html | 2 +- .../main/test/pages/styles/PopoverResize.css | 5 ++ 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 86ee8f3aeeee..43d89e505510 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -230,15 +230,18 @@ class Popover extends Popup { _resizeMouseMoveHandler: (e: MouseEvent) => void; _resizeMouseUpHandler: (e: MouseEvent) => void; + + _resizeHandlePlacement?: `${ResizeHandlePlacement}`; + _initialClientX?: number; _initialClientY?: number; - _initialBoundingRect?: DOMRect; _minWidth?: number; _minHeight?: number; _resized = false; - _resizeHandlePlacement?: `${ResizeHandlePlacement}`; + _resizeDeltaX?: number; + _resizeDeltaY?: number; static get VIEWPORT_MARGIN() { return 10; // px @@ -494,6 +497,10 @@ class Popover extends Popup { left: `${left}px`, }); + if (this._resized) { + return; + } + if (this.horizontalAlign === PopoverHorizontalAlign.Stretch && this._width) { this.style.width = this._width; } @@ -585,12 +592,14 @@ class Popover extends Popup { const isVertical = actualPlacement === PopoverActualPlacement.Top || actualPlacement === PopoverActualPlacement.Bottom; - 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 (!this._resized) { + 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`; + } } const arrowOffset = this.hideArrow ? 0 : ARROW_SIZE; @@ -815,10 +824,6 @@ class Popover extends Popup { } getVerticalLeft(targetRect: DOMRect, popoverSize: PopoverSize): number { - if (this._resized) { - return this._left!; - } - const actualHorizontalAlign = this._actualHorizontalAlign; let left = Popover.VIEWPORT_MARGIN; @@ -826,6 +831,9 @@ class Popover extends Popup { case PopoverActualHorizontalAlign.Center: case PopoverActualHorizontalAlign.Stretch: left = targetRect.left - (popoverSize.width - targetRect.width) / 2; + if (this._resized) { + left -= this._resizeDeltaX || 0; + } break; case PopoverActualHorizontalAlign.Left: left = targetRect.left; @@ -839,16 +847,15 @@ class Popover extends Popup { } getHorizontalTop(targetRect: DOMRect, popoverSize: PopoverSize): number { - if (this._resized) { - return this._top!; - } - let top = 0; switch (this.verticalAlign) { case PopoverVerticalAlign.Center: case PopoverVerticalAlign.Stretch: top = targetRect.top - (popoverSize.height - targetRect.height) / 2; + if (this._resized) { + top -= this._resizeDeltaY || 0; + } break; case PopoverVerticalAlign.Top: top = targetRect.top; @@ -1035,9 +1042,6 @@ class Popover extends Popup { const deltaX = clientX - this._initialClientX!; const deltaY = clientY - this._initialClientY!; - let newLeft = initialBoundingRect.x; - let newTop = initialBoundingRect.y; - let newWidth, newHeight; @@ -1061,7 +1065,7 @@ class Popover extends Popup { // Adjust left position when resizing from left // Ensure the left edge respects the viewport margin and the right edge position - newLeft = clamp( + const newLeft = clamp( initialBoundingRect.x + deltaX, margin, initialBoundingRect.x + initialBoundingRect.width - this._minWidth!, @@ -1069,6 +1073,8 @@ class Popover extends Popup { // Recalculate width based on actual left position to stay within viewport with margin newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft); + + this._resizeDeltaX = (initialBoundingRect.x - newLeft) / 2; } else { // Resizing from right edge - width increases when moving right (positive delta) const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin; @@ -1078,6 +1084,8 @@ class Popover extends Popup { this._minWidth!, maxWidthFromRight, ); + + this._resizeDeltaX = (initialBoundingRect.width - newWidth) / 2; } // Calculate height changes @@ -1093,7 +1101,7 @@ class Popover extends Popup { // Adjust top position when resizing from top // Ensure the top edge respects the viewport margin and the bottom edge position - newTop = clamp( + const newTop = clamp( initialBoundingRect.y + deltaY, margin, initialBoundingRect.y + initialBoundingRect.height - this._minHeight!, @@ -1101,6 +1109,8 @@ class Popover extends Popup { // Recalculate height based on actual top position to stay within viewport with margin newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop); + + this._resizeDeltaY = (initialBoundingRect.y - newTop) / 2; } else { // Resizing from bottom edge - height increases when moving down (positive delta) const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin; @@ -1110,13 +1120,13 @@ class Popover extends Popup { this._minHeight!, maxHeightFromBottom, ); + + this._resizeDeltaY = (initialBoundingRect.height - newHeight) / 2; } Object.assign(this.style, { height: `${newHeight}px`, width: `${newWidth}px`, - left: `${newLeft}px`, - top: `${newTop}px`, }); } @@ -1146,6 +1156,9 @@ class Popover extends Popup { _revertSize = () => { this._resized = false; + delete this._resizeDeltaX; + delete this._resizeDeltaY; + Object.assign(this.style, { top: "", left: "", diff --git a/packages/main/test/pages/PopoverResize.html b/packages/main/test/pages/PopoverResize.html index 35c8d47605bd..0227c47fcd22 100644 --- a/packages/main/test/pages/PopoverResize.html +++ b/packages/main/test/pages/PopoverResize.html @@ -39,7 +39,7 @@ - +
Popover Resize
diff --git a/packages/main/test/pages/styles/PopoverResize.css b/packages/main/test/pages/styles/PopoverResize.css index f98922a1059b..dc197f08abe3 100644 --- a/packages/main/test/pages/styles/PopoverResize.css +++ b/packages/main/test/pages/styles/PopoverResize.css @@ -1,8 +1,13 @@ +body { + background-color: var(--sapBackgroundColor); +} + .pageContainer { position: absolute; inset: 0; display: flex; flex-direction: column; + padding: 1rem; } h1 { From bf1d23d1af85908924823f9c8e0124729251bd17 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Fri, 14 Nov 2025 16:25:31 +0200 Subject: [PATCH 11/22] fix: fix re-resizing --- packages/main/src/Popover.ts | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 43d89e505510..d0ff4a64c4c8 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -240,8 +240,11 @@ class Popover extends Popup { _minHeight?: number; _resized = false; - _resizeDeltaX?: number; - _resizeDeltaY?: number; + _resizingDeltaX?: number; + _resizingDeltaY?: number; + + _resizedDeltaX?: number; + _resizedDeltaY?: number; static get VIEWPORT_MARGIN() { return 10; // px @@ -832,7 +835,7 @@ class Popover extends Popup { case PopoverActualHorizontalAlign.Stretch: left = targetRect.left - (popoverSize.width - targetRect.width) / 2; if (this._resized) { - left -= this._resizeDeltaX || 0; + left -= this._resizingDeltaX || 0; } break; case PopoverActualHorizontalAlign.Left: @@ -854,7 +857,7 @@ class Popover extends Popup { case PopoverVerticalAlign.Stretch: top = targetRect.top - (popoverSize.height - targetRect.height) / 2; if (this._resized) { - top -= this._resizeDeltaY || 0; + top -= this._resizingDeltaY || 0; } break; case PopoverVerticalAlign.Top: @@ -1016,6 +1019,9 @@ class Popover extends Popup { this._resized = true; this._initialBoundingRect = this.getBoundingClientRect(); + this._resizedDeltaX = this._resizingDeltaX; + this._resizedDeltaY = this._resizingDeltaY; + const { minWidth, minHeight, @@ -1074,7 +1080,7 @@ class Popover extends Popup { // Recalculate width based on actual left position to stay within viewport with margin newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft); - this._resizeDeltaX = (initialBoundingRect.x - newLeft) / 2; + this._resizingDeltaX = (initialBoundingRect.x - newLeft) / 2; } else { // Resizing from right edge - width increases when moving right (positive delta) const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin; @@ -1085,7 +1091,7 @@ class Popover extends Popup { maxWidthFromRight, ); - this._resizeDeltaX = (initialBoundingRect.width - newWidth) / 2; + this._resizingDeltaX = (initialBoundingRect.width - newWidth) / 2; } // Calculate height changes @@ -1110,7 +1116,7 @@ class Popover extends Popup { // Recalculate height based on actual top position to stay within viewport with margin newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop); - this._resizeDeltaY = (initialBoundingRect.y - newTop) / 2; + this._resizingDeltaY = (initialBoundingRect.y - newTop) / 2; } else { // Resizing from bottom edge - height increases when moving down (positive delta) const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin; @@ -1121,9 +1127,12 @@ class Popover extends Popup { maxHeightFromBottom, ); - this._resizeDeltaY = (initialBoundingRect.height - newHeight) / 2; + this._resizingDeltaY = (initialBoundingRect.height - newHeight) / 2; } + this._resizingDeltaX += this._resizedDeltaX || 0; + this._resizingDeltaY += this._resizedDeltaY || 0; + Object.assign(this.style, { height: `${newHeight}px`, width: `${newWidth}px`, @@ -1145,7 +1154,7 @@ class Popover extends Popup { _attachMouseResizeHandlers() { window.addEventListener("mousemove", this._resizeMouseMoveHandler); window.addEventListener("mouseup", this._resizeMouseUpHandler); - this.addEventListener("ui5-before-close", this._revertSize, { once: true }); + this.addEventListener("ui5-before-close", this._revertResizeSettings, { once: true }); } _detachMouseResizeHandlers() { @@ -1153,15 +1162,16 @@ class Popover extends Popup { window.removeEventListener("mouseup", this._resizeMouseUpHandler); } - _revertSize = () => { + _revertResizeSettings = () => { this._resized = false; - delete this._resizeDeltaX; - delete this._resizeDeltaY; + delete this._resizingDeltaX; + delete this._resizingDeltaY; + + delete this._resizedDeltaX; + delete this._resizedDeltaY; Object.assign(this.style, { - top: "", - left: "", width: "", height: "", }); From 090e0024b9acd00de0a95b8cf2213cb98715dfa3 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Mon, 17 Nov 2025 14:41:04 +0200 Subject: [PATCH 12/22] fix: clicking outside the resize handle icon --- packages/main/src/Popover.ts | 54 +++++++++++++------ .../main/src/popup-utils/PopoverRegistry.ts | 2 +- packages/main/src/themes/Popover.css | 2 +- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index d0ff4a64c4c8..979c5e5b6219 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -4,7 +4,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import { isIOS } from "@ui5/webcomponents-base/dist/Device.js"; -import { getClosedPopupParent } from "@ui5/webcomponents-base/dist/util/PopupUtils.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"; import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js"; @@ -246,6 +246,9 @@ class Popover extends Popup { _resizedDeltaX?: number; _resizedDeltaY?: number; + _initialWidth?: string; + _initialHeight?: string; + static get VIEWPORT_MARGIN() { return 10; // px } @@ -300,11 +303,33 @@ class Popover extends Popup { return; } + this._initialWidth = this.style.width; + this._initialHeight = this.style.height; + this._openerRect = opener.getBoundingClientRect(); await super.openPopup(); } + closePopup(escPressed = false, preventRegistryUpdate = false, preventFocusRestore = false) : void { + Object.assign(this.style, { + width: this._initialWidth, + height: this._initialHeight, + }); + + if (this._resized) { + this._resized = false; + + delete this._resizingDeltaX; + delete this._resizingDeltaY; + + delete this._resizedDeltaX; + delete this._resizedDeltaY; + } + + super.closePopup(escPressed, preventRegistryUpdate, preventFocusRestore); + } + isOpenerClicked(e: MouseEvent) { const target = e.target as HTMLElement; const opener = this.getOpenerHTMLElement(this.opener); @@ -324,6 +349,17 @@ class Popover extends Popup { return e.composedPath().indexOf(opener) > -1; } + isClicked(e: MouseEvent) { + if (this._showResizeHandle) { + const resizeHandle = this.shadowRoot!.querySelector(".ui5-popover-resize-handle"); + if (resizeHandle === e.composedPath()[0]) { + return true; + } + } + + return isClickInRect(e, this.getBoundingClientRect()); + } + /** * Override for the _addOpenedPopup hook, which would otherwise just call addOpenedPopup(this) * @private @@ -1154,28 +1190,12 @@ class Popover extends Popup { _attachMouseResizeHandlers() { window.addEventListener("mousemove", this._resizeMouseMoveHandler); window.addEventListener("mouseup", this._resizeMouseUpHandler); - this.addEventListener("ui5-before-close", this._revertResizeSettings, { once: true }); } _detachMouseResizeHandlers() { window.removeEventListener("mousemove", this._resizeMouseMoveHandler); window.removeEventListener("mouseup", this._resizeMouseUpHandler); } - - _revertResizeSettings = () => { - this._resized = false; - - delete this._resizingDeltaX; - delete this._resizingDeltaY; - - delete this._resizedDeltaX; - delete this._resizedDeltaY; - - Object.assign(this.style, { - width: "", - height: "", - }); - } } const instanceOfPopover = (object: any): object is Popover => { diff --git a/packages/main/src/popup-utils/PopoverRegistry.ts b/packages/main/src/popup-utils/PopoverRegistry.ts index c08664fa10fa..11dd2287d893 100644 --- a/packages/main/src/popup-utils/PopoverRegistry.ts +++ b/packages/main/src/popup-utils/PopoverRegistry.ts @@ -100,7 +100,7 @@ const clickHandler = (event: MouseEvent) => { return; } - if (isClickInRect(event, popup.getBoundingClientRect())) { + if ((popup as Popover).isClicked(event)) { break; } diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css index 49f71f7c17e6..6da2bc2ed983 100644 --- a/packages/main/src/themes/Popover.css +++ b/packages/main/src/themes/Popover.css @@ -98,7 +98,7 @@ width: 1.5rem; height: 1.5rem; border-radius: 50%; - z-index: 10; + z-index: 1; } .ui5-popover-resize-handle [ui5-icon] { From c27c937af83d17f6c716186e756a6831555dcd2a Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Mon, 17 Nov 2025 14:48:46 +0200 Subject: [PATCH 13/22] fix: lint error --- packages/main/src/popup-utils/PopoverRegistry.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/main/src/popup-utils/PopoverRegistry.ts b/packages/main/src/popup-utils/PopoverRegistry.ts index 11dd2287d893..481e57af4ecb 100644 --- a/packages/main/src/popup-utils/PopoverRegistry.ts +++ b/packages/main/src/popup-utils/PopoverRegistry.ts @@ -1,4 +1,3 @@ -import { isClickInRect } from "@ui5/webcomponents-base/dist/util/PopupUtils.js"; import type { Interval } from "@ui5/webcomponents-base/dist/types.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; import getParentElement from "@ui5/webcomponents-base/dist/util/getParentElement.js"; From 99b6fe97310e551b500fba31e7256b40eed76c00 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Mon, 17 Nov 2025 15:05:10 +0200 Subject: [PATCH 14/22] chore: rename props --- packages/main/src/Popover.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 979c5e5b6219..ca6a26ded677 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -240,11 +240,11 @@ class Popover extends Popup { _minHeight?: number; _resized = false; - _resizingDeltaX?: number; - _resizingDeltaY?: number; + _currDeltaX?: number; + _currDeltaY?: number; - _resizedDeltaX?: number; - _resizedDeltaY?: number; + _accumulatedDeltaX?: number; + _accumulatedDeltaY?: number; _initialWidth?: string; _initialHeight?: string; @@ -320,11 +320,11 @@ class Popover extends Popup { if (this._resized) { this._resized = false; - delete this._resizingDeltaX; - delete this._resizingDeltaY; + delete this._currDeltaX; + delete this._currDeltaY; - delete this._resizedDeltaX; - delete this._resizedDeltaY; + delete this._accumulatedDeltaX; + delete this._accumulatedDeltaY; } super.closePopup(escPressed, preventRegistryUpdate, preventFocusRestore); @@ -871,7 +871,7 @@ class Popover extends Popup { case PopoverActualHorizontalAlign.Stretch: left = targetRect.left - (popoverSize.width - targetRect.width) / 2; if (this._resized) { - left -= this._resizingDeltaX || 0; + left -= this._currDeltaX || 0; } break; case PopoverActualHorizontalAlign.Left: @@ -893,7 +893,7 @@ class Popover extends Popup { case PopoverVerticalAlign.Stretch: top = targetRect.top - (popoverSize.height - targetRect.height) / 2; if (this._resized) { - top -= this._resizingDeltaY || 0; + top -= this._currDeltaY || 0; } break; case PopoverVerticalAlign.Top: @@ -1055,8 +1055,8 @@ class Popover extends Popup { this._resized = true; this._initialBoundingRect = this.getBoundingClientRect(); - this._resizedDeltaX = this._resizingDeltaX; - this._resizedDeltaY = this._resizingDeltaY; + this._accumulatedDeltaX = this._currDeltaX; + this._accumulatedDeltaY = this._currDeltaY; const { minWidth, @@ -1116,7 +1116,7 @@ class Popover extends Popup { // Recalculate width based on actual left position to stay within viewport with margin newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft); - this._resizingDeltaX = (initialBoundingRect.x - newLeft) / 2; + this._currDeltaX = (initialBoundingRect.x - newLeft) / 2; } else { // Resizing from right edge - width increases when moving right (positive delta) const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin; @@ -1127,7 +1127,7 @@ class Popover extends Popup { maxWidthFromRight, ); - this._resizingDeltaX = (initialBoundingRect.width - newWidth) / 2; + this._currDeltaX = (initialBoundingRect.width - newWidth) / 2; } // Calculate height changes @@ -1152,7 +1152,7 @@ class Popover extends Popup { // Recalculate height based on actual top position to stay within viewport with margin newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop); - this._resizingDeltaY = (initialBoundingRect.y - newTop) / 2; + this._currDeltaY = (initialBoundingRect.y - newTop) / 2; } else { // Resizing from bottom edge - height increases when moving down (positive delta) const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin; @@ -1163,11 +1163,11 @@ class Popover extends Popup { maxHeightFromBottom, ); - this._resizingDeltaY = (initialBoundingRect.height - newHeight) / 2; + this._currDeltaY = (initialBoundingRect.height - newHeight) / 2; } - this._resizingDeltaX += this._resizedDeltaX || 0; - this._resizingDeltaY += this._resizedDeltaY || 0; + this._currDeltaX += this._accumulatedDeltaX || 0; + this._currDeltaY += this._accumulatedDeltaY || 0; Object.assign(this.style, { height: `${newHeight}px`, From a49c5126fe4d030751de390c8b07225c45b5acc3 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Tue, 18 Nov 2025 14:19:46 +0200 Subject: [PATCH 15/22] chore: add tests --- .../main/cypress/specs/PopoverResize.cy.tsx | 803 ++++++++++++++++++ 1 file changed, 803 insertions(+) create mode 100644 packages/main/cypress/specs/PopoverResize.cy.tsx diff --git a/packages/main/cypress/specs/PopoverResize.cy.tsx b/packages/main/cypress/specs/PopoverResize.cy.tsx new file mode 100644 index 000000000000..7fe696368952 --- /dev/null +++ b/packages/main/cypress/specs/PopoverResize.cy.tsx @@ -0,0 +1,803 @@ +import "@ui5/webcomponents-base/dist/features/F6Navigation.js"; +import Popover from "../../src/Popover.js"; +import Button from "../../src/Button.js"; + +describe("Popover Resize Functionality", () => { + beforeEach(() => { + cy.viewport(1200, 800); + }); + + describe("Resizable Property", () => { + it("should render resize handle when resizable is true", () => { + cy.mount( + <> + + +
Resizable content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .should("exist") + .and("be.visible"); + }); + + it("should not render resize handle when resizable is false", () => { + cy.mount( + <> + + +
Non-resizable content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .should("not.exist"); + }); + + it("should toggle resize handle when resizable property changes", () => { + cy.mount( + <> + + +
Content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .should("not.exist"); + + cy.get("[ui5-popover]").invoke("prop", "resizable", true); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .should("exist") + .and("be.visible"); + }); + }); + + describe("Resize Handle Placement", () => { + it("should position resize handle at bottom-right when popover is to the right of opener", () => { + cy.mount( + <> + + +
Content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-bottom-right"); + }); + + it("should position resize handle at top-left when popover is to the left of opener", () => { + cy.mount( + <> + + +
Content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-top-left"); + }); + + it("should position resize handle at top-right when popover is above opener", () => { + cy.mount( + <> + + +
Content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-top-right"); + }); + + it("should position resize handle at bottom-right when popover is below opener", () => { + cy.mount( + <> + + +
Content
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-bottom-right"); + }); + }); + + describe("Resize Handle Placement in RTL", () => { + it("should position resize handle correctly in RTL for right placement", () => { + cy.mount( +
+ + +
Content
+
+
+ ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .should("exist"); + }); + + it("should position resize handle correctly in RTL for left placement", () => { + cy.mount( +
+ + +
Content
+
+
+ ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .should("exist"); + }); + }); + + describe("Resize Interaction", () => { + it("should resize popover width when dragging from right edge", () => { + cy.mount( + <> + + +
+ Resizable content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(500, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + expect(newWidth).to.not.equal(initialWidth); + }); + }); + + it("should resize popover height when dragging from bottom edge", () => { + cy.mount( + <> + + +
+ Resizable content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialHeight: number; + cy.get("[ui5-popover]").then($popover => { + initialHeight = $popover[0].getBoundingClientRect().height; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(300, 400) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newHeight = $popover[0].getBoundingClientRect().height; + expect(newHeight).to.not.equal(initialHeight); + }); + }); + + it("should respect minimum width during resize", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(100, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const width = $popover[0].getBoundingClientRect().width; + expect(width).to.be.at.least(150); + }); + }); + + it("should respect viewport margins during resize", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(2000, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + expect(rect.right).to.be.lessThan(window.innerWidth - 10); + }); + }); + + it("should maintain resized size when popover is repositioned", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(600, 400) + .realMouseUp(); + + let resizedWidth: number; + let resizedHeight: number; + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + resizedWidth = rect.width; + resizedHeight = rect.height; + }); + + // Trigger a reposition by resizing the window + cy.viewport(1300, 900); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + // The size should be maintained (with some tolerance for rounding) + expect(Math.abs(rect.width - resizedWidth)).to.be.lessThan(5); + expect(Math.abs(rect.height - resizedHeight)).to.be.lessThan(5); + }); + }); + }); + + describe("Resize State Reset", () => { + it("should reset size when popover is closed and reopened", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").invoke("prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(600, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const resizedWidth = $popover[0].getBoundingClientRect().width; + expect(resizedWidth).to.not.equal(initialWidth); + }); + + cy.get("[ui5-popover]").invoke("prop", "open", false); + cy.get("[ui5-popover]").should("not.be.visible"); + + cy.get("[ui5-popover]").invoke("prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]").then($popover => { + const reopenedWidth = $popover[0].getBoundingClientRect().width; + expect(Math.abs(reopenedWidth - initialWidth)).to.be.lessThan(5); + }); + }); + }); + + describe("Resize with Different Placements", () => { + it("should resize correctly with Top placement", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialSize: { width: number; height: number }; + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + initialSize = { width: rect.width, height: rect.height }; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(500, 200) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + expect(rect.width).to.not.equal(initialSize.width); + }); + }); + + it.only("should resize correctly with Bottom placement", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialHeight: number; + cy.get("[ui5-popover]").then($popover => { + initialHeight = $popover[0].getBoundingClientRect().height; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(400, 500) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newHeight = $popover[0].getBoundingClientRect().height; + expect(newHeight).to.not.equal(initialHeight); + }); + }); + + it("should resize correctly with Start placement", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(400, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + expect(newWidth).to.not.equal(initialWidth); + }); + }); + + it("should resize correctly with End placement", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(500, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + expect(newWidth).to.not.equal(initialWidth); + }); + }); + }); + + describe("Resize with Modal Popover", () => { + it("should resize modal popover correctly", () => { + cy.mount( + <> + + +
+ Modal resizable content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(550, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + expect(newWidth).to.not.equal(initialWidth); + }); + }); + }); + + describe("Resize with Header and Footer", () => { + it("should resize popover with header and footer correctly", () => { + cy.mount( + <> + + +
+ Content with header and footer +
+
+ +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialSize: { width: number; height: number }; + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + initialSize = { width: rect.width, height: rect.height }; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(600, 450) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + expect(rect.width).to.not.equal(initialSize.width); + expect(rect.height).to.not.equal(initialSize.height); + }); + }); + }); + + describe("Resize Handle Click Detection", () => { + it("should detect clicks on resize handle to prevent popover close", () => { + cy.mount( + <> + + +
+ Content +
+
+ + ); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "topLeft" }); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + }); + }); + + describe("Resize with Arrow", () => { + it("should resize popover with arrow correctly", () => { + cy.mount( + <> + + +
+ Content with arrow +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-arrow") + .should("be.visible"); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(550, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + expect(newWidth).to.not.equal(initialWidth); + }); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-arrow") + .should("be.visible"); + }); + + it("should resize popover without arrow correctly", () => { + cy.mount( + <> + + +
+ Content without arrow +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-arrow") + .should("not.be.visible"); + + let initialWidth: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(550, 300) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + expect(newWidth).to.not.equal(initialWidth); + }); + }); + }); +}); From 0d7339b8bc75060aabef5a49d354ce2eadaf2b71 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Tue, 18 Nov 2025 16:50:03 +0200 Subject: [PATCH 16/22] chore: fix tests --- packages/main/cypress/specs/PopoverResize.cy.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/main/cypress/specs/PopoverResize.cy.tsx b/packages/main/cypress/specs/PopoverResize.cy.tsx index 7fe696368952..ba428e72696b 100644 --- a/packages/main/cypress/specs/PopoverResize.cy.tsx +++ b/packages/main/cypress/specs/PopoverResize.cy.tsx @@ -394,7 +394,7 @@ describe("Popover Resize Functionality", () => { it("should reset size when popover is closed and reopened", () => { cy.mount( <> - @@ -420,7 +420,7 @@ describe("Popover Resize Functionality", () => { .shadow() .find(".ui5-popover-resize-handle") .realMouseDown({ position: "center" }) - .realMouseMove(600, 300) + .realMouseMove(300, 300) .realMouseUp(); cy.get("[ui5-popover]").then($popover => { @@ -480,7 +480,7 @@ describe("Popover Resize Functionality", () => { }); }); - it.only("should resize correctly with Bottom placement", () => { + it("should resize correctly with Bottom placement", () => { cy.mount( <> - +
Content
); + cy.get("[ui5-popover]").invoke("prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); cy.get("[ui5-popover]") .shadow() - .find(".ui5-popover-resize-handle") - .should("exist"); + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-top-left"); }); - it("should position resize handle correctly in RTL for left placement", () => { + it("should position resize handle at bottom-right when popover is to the right of opener", () => { cy.mount(
- - +
Content
); + cy.get("[ui5-popover]").invoke("prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); cy.get("[ui5-popover]") .shadow() - .find(".ui5-popover-resize-handle") - .should("exist"); + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-bottom-right"); + }); + + it("should position resize handle at top-left when popover is above opener", () => { + cy.mount( +
+ + +
Content
+
+
+ ); + + cy.get("[ui5-popover]").invoke("prop", "open", true); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-top-left"); + }); + + it("should position resize handle at bottom-left when popover is below opener", () => { + cy.mount( +
+ + +
Content
+
+
+ ); + + cy.get("[ui5-popover]").invoke("prop", "open", true); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popup-root") + .should("have.class", "ui5-popover-resize-handle-bottom-left"); }); }); describe("Resize Interaction", () => { - it("should resize popover width when dragging from right edge", () => { + it("should resize correctly with Top placement", () => { cy.mount( <> - - -
- Resizable content + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialSize: { width: number; height: number }; + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + initialSize = { width: rect.width, height: rect.height }; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(50, 50) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const rect = $popover[0].getBoundingClientRect(); + expect(rect.width).be.greaterThan(initialSize.width); + expect(rect.height).be.lessThan(initialSize.height); + }); + }); + + it("should resize correctly with Bottom placement", () => { + cy.mount( + <> + + +
+ Content
@@ -239,15 +326,57 @@ describe("Popover Resize Functionality", () => { }); }); - it("should resize popover height when dragging from bottom edge", () => { + it("should resize correctly with Start placement", () => { cy.mount( <> - - -
- Resizable content + +
+ Content +
+
+ + ); + + cy.get("[ui5-popover]").ui5PopoverOpened(); + + let initialWidth: number; + let initialHeight: number; + cy.get("[ui5-popover]").then($popover => { + initialWidth = $popover[0].getBoundingClientRect().width; + initialHeight = $popover[0].getBoundingClientRect().height; + }); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + + cy.get("[ui5-popover]") + .shadow() + .find(".ui5-popover-resize-handle") + .realMouseDown({ position: "center" }) + .realMouseMove(-50, -50) + .realMouseUp(); + + cy.get("[ui5-popover]").then($popover => { + const newWidth = $popover[0].getBoundingClientRect().width; + const newHeight = $popover[0].getBoundingClientRect().height; + + expect(newWidth).be.greaterThan(initialWidth); + expect(newHeight).be.greaterThan(initialHeight); + }); + }); + + it("should resize correctly with End placement", () => { + cy.mount( + <> + + +
+ Content
@@ -257,6 +386,7 @@ describe("Popover Resize Functionality", () => { let initialWidth: number; let initialHeight: number; + cy.get("[ui5-popover]").then($popover => { initialWidth = $popover[0].getBoundingClientRect().width; initialHeight = $popover[0].getBoundingClientRect().height; @@ -404,27 +534,27 @@ describe("Popover Resize Functionality", () => { }); }); - describe("Resize State Reset", () => { - it("should reset size when popover is closed and reopened", () => { + describe("Resize Interaction in RTL", () => { + it("should resize correctly with Top placement", () => { cy.mount( - <> - - -
+ +
Content
- +
); - cy.get("[ui5-popover]").invoke("prop", "open", true); cy.get("[ui5-popover]").ui5PopoverOpened(); - let initialWidth: number; + let initialSize: { width: number; height: number }; cy.get("[ui5-popover]").then($popover => { - initialWidth = $popover[0].getBoundingClientRect().width; + const rect = $popover[0].getBoundingClientRect(); + initialSize = { width: rect.width, height: rect.height }; }); // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -434,48 +564,37 @@ describe("Popover Resize Functionality", () => { .shadow() .find(".ui5-popover-resize-handle") .realMouseDown({ position: "center" }) - .realMouseMove(50, 50) + .realMouseMove(-50, 50) .realMouseUp(); cy.get("[ui5-popover]").then($popover => { - const resizedWidth = $popover[0].getBoundingClientRect().width; - expect(resizedWidth).be.greaterThan(initialWidth); - }); - - cy.get("[ui5-popover]").invoke("prop", "open", false); - cy.get("[ui5-popover]").should("not.be.visible"); - - cy.get("[ui5-popover]").invoke("prop", "open", true); - cy.get("[ui5-popover]").ui5PopoverOpened(); - - cy.get("[ui5-popover]").then($popover => { - const reopenedWidth = $popover[0].getBoundingClientRect().width; - expect(Math.abs(reopenedWidth - initialWidth)).to.be.lessThan(5); + const rect = $popover[0].getBoundingClientRect(); + expect(rect.width).be.greaterThan(initialSize.width); + expect(rect.height).be.lessThan(initialSize.height); }); }); - }); - describe("Resize with Different Placements", () => { - it("should resize correctly with Top placement", () => { + it("should resize correctly with Bottom placement", () => { cy.mount( - <> - - +
Content
- +
); cy.get("[ui5-popover]").ui5PopoverOpened(); - let initialSize: { width: number; height: number }; + let initialWidth: number; + let initialHeight: number; cy.get("[ui5-popover]").then($popover => { - const rect = $popover[0].getBoundingClientRect(); - initialSize = { width: rect.width, height: rect.height }; + initialWidth = $popover[0].getBoundingClientRect().width; + initialHeight = $popover[0].getBoundingClientRect().height; }); // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -485,28 +604,30 @@ describe("Popover Resize Functionality", () => { .shadow() .find(".ui5-popover-resize-handle") .realMouseDown({ position: "center" }) - .realMouseMove(50, 50) + .realMouseMove(-50, 50) .realMouseUp(); cy.get("[ui5-popover]").then($popover => { - const rect = $popover[0].getBoundingClientRect(); - expect(rect.width).be.greaterThan(initialSize.width); - expect(rect.height).be.lessThan(initialSize.height); + const newWidth = $popover[0].getBoundingClientRect().width; + const newHeight = $popover[0].getBoundingClientRect().height; + + expect(newWidth).be.greaterThan(initialWidth); + expect(newHeight).be.greaterThan(initialHeight); }); }); - it("should resize correctly with Bottom placement", () => { + it("should resize correctly with Start placement", () => { cy.mount( - <> - - -
+ +
Content
- +
); cy.get("[ui5-popover]").ui5PopoverOpened(); @@ -537,25 +658,28 @@ describe("Popover Resize Functionality", () => { }); }); - it("should resize correctly with Start placement", () => { + it("should resize correctly with End placement", () => { cy.mount( - <> - - +
Content
- +
); cy.get("[ui5-popover]").ui5PopoverOpened(); let initialWidth: number; + let initialHeight: number; + cy.get("[ui5-popover]").then($popover => { initialWidth = $popover[0].getBoundingClientRect().width; + initialHeight = $popover[0].getBoundingClientRect().height; }); // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -570,17 +694,22 @@ describe("Popover Resize Functionality", () => { cy.get("[ui5-popover]").then($popover => { const newWidth = $popover[0].getBoundingClientRect().width; + const newHeight = $popover[0].getBoundingClientRect().height; + expect(newWidth).be.greaterThan(initialWidth); + expect(newHeight).be.greaterThan(initialHeight); }); }); + }); - it("should resize correctly with End placement", () => { + describe("Resize State Reset", () => { + it("should reset size when popover is closed and reopened", () => { cy.mount( <> - - +
Content
@@ -588,14 +717,12 @@ describe("Popover Resize Functionality", () => { ); + cy.get("[ui5-popover]").invoke("prop", "open", true); cy.get("[ui5-popover]").ui5PopoverOpened(); let initialWidth: number; - let initialHeight: number; - cy.get("[ui5-popover]").then($popover => { initialWidth = $popover[0].getBoundingClientRect().width; - initialHeight = $popover[0].getBoundingClientRect().height; }); // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -609,11 +736,19 @@ describe("Popover Resize Functionality", () => { .realMouseUp(); cy.get("[ui5-popover]").then($popover => { - const newWidth = $popover[0].getBoundingClientRect().width; - const newHeight = $popover[0].getBoundingClientRect().height; + const resizedWidth = $popover[0].getBoundingClientRect().width; + expect(resizedWidth).be.greaterThan(initialWidth); + }); - expect(newWidth).be.greaterThan(initialWidth); - expect(newHeight).be.greaterThan(initialHeight); + cy.get("[ui5-popover]").invoke("prop", "open", false); + cy.get("[ui5-popover]").should("not.be.visible"); + + cy.get("[ui5-popover]").invoke("prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); + + cy.get("[ui5-popover]").then($popover => { + const reopenedWidth = $popover[0].getBoundingClientRect().width; + expect(Math.abs(reopenedWidth - initialWidth)).to.be.lessThan(5); }); }); }); diff --git a/packages/main/test/pages/PopoverResize.html b/packages/main/test/pages/PopoverResize.html index 0227c47fcd22..ec809227f483 100644 --- a/packages/main/test/pages/PopoverResize.html +++ b/packages/main/test/pages/PopoverResize.html @@ -4,7 +4,7 @@ - Popover + Popover Resize + + + + + + +
+ Popover Resize +
+
+ Placement + + Start + End + Top + Bottom + +
+
+ Horizontal Align + + Center + Start + End + Stretch + +
+
+ Vertical Align + + Center + Top + Bottom + Stretch + +
+
+ Hide Arrow + +
+
+
+ Open Popover + +
+ This is a Popover control. +
+ + OK + +
+
+
+ + From a3dbd62efa91c206cf5c89e2f85883a6d27ddaa7 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 20 Nov 2025 13:55:14 +0200 Subject: [PATCH 19/22] chore: override styles like in the Dialog --- packages/main/src/themes/Dialog.css | 16 ---------------- packages/main/src/themes/Popover.css | 2 +- packages/main/src/themes/PopupsCommon.css | 16 ++++++++++++++++ .../main/src/themes/base/sizes-parameters.css | 8 ++++++-- packages/main/test/pages/PopoverResize.html | 3 +-- packages/main/test/pages/PopoverResizeRTL.html | 3 +-- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/main/src/themes/Dialog.css b/packages/main/src/themes/Dialog.css index 0e3026922293..ee4490054197 100644 --- a/packages/main/src/themes/Dialog.css +++ b/packages/main/src/themes/Dialog.css @@ -139,22 +139,6 @@ color: var(--sapButton_Lite_TextColor); } -::slotted([slot="footer"]) { - height: var(--_ui5_dialog_footer_height); -} - -::slotted([slot="footer"][ui5-bar][design="Footer"]) { - border-top: none; -} - -::slotted([slot="header"][ui5-bar]) { - box-shadow: none; -} - -::slotted([slot="footer"][ui5-toolbar]) { - border: 0; -} - :host::backdrop { background-color: var(--_ui5_popup_block_layer_background); opacity: var(--_ui5_popup_block_layer_opacity); diff --git a/packages/main/src/themes/Popover.css b/packages/main/src/themes/Popover.css index 6da2bc2ed983..a85fa99ab4dd 100644 --- a/packages/main/src/themes/Popover.css +++ b/packages/main/src/themes/Popover.css @@ -166,4 +166,4 @@ .ui5-popover-resizing, .ui5-popover-resizing * { user-select: none !important; -} +} \ No newline at end of file diff --git a/packages/main/src/themes/PopupsCommon.css b/packages/main/src/themes/PopupsCommon.css index 450f6b3bde82..d4aab5d184f0 100644 --- a/packages/main/src/themes/PopupsCommon.css +++ b/packages/main/src/themes/PopupsCommon.css @@ -127,3 +127,19 @@ padding-left: var(--_ui5_popup_header_footer_padding_xl); padding-right: var(--_ui5_popup_header_footer_padding_xl); } + +::slotted([slot="footer"]) { + height: var(--_ui5_popup_footer_height); +} + +::slotted([slot="footer"][ui5-bar][design="Footer"]) { + border-top: none; +} + +::slotted([slot="header"][ui5-bar]) { + box-shadow: none; +} + +::slotted([slot="footer"][ui5-toolbar]) { + border: 0; +} \ No newline at end of file diff --git a/packages/main/src/themes/base/sizes-parameters.css b/packages/main/src/themes/base/sizes-parameters.css index 20eab2e85cca..3e9c5e1f7a64 100644 --- a/packages/main/src/themes/base/sizes-parameters.css +++ b/packages/main/src/themes/base/sizes-parameters.css @@ -64,9 +64,11 @@ --_ui5_datetime_timeview_phonemode_clocks_width: 24.5rem; --_ui5_datetime_dateview_phonemode_margin_bottom: 0; + /* Popup */ + --_ui5_popup_footer_height: 2.75rem; + /* Dialog */ --_ui5_dialog_content_min_height: 2.75rem; - --_ui5_dialog_footer_height: 2.75rem; --_ui5_input_inner_padding: 0 0.625rem; --_ui5_input_inner_padding_with_icon: 0 0.25rem 0 0.625rem; @@ -265,9 +267,11 @@ --_ui5_datetime_timeview_phonemode_clocks_width: 21.125rem; --_ui5_datetime_dateview_phonemode_margin_bottom: 3.125rem; + /* Popup */ + --_ui5_popup_footer_height: 2.5rem; + /* Dialog */ --_ui5_dialog_content_min_height: 2.5rem; - --_ui5_dialog_footer_height: 2.5rem; /* Form */ --_ui5_form_item_min_height: 2rem; diff --git a/packages/main/test/pages/PopoverResize.html b/packages/main/test/pages/PopoverResize.html index ec809227f483..64b13e3e7381 100644 --- a/packages/main/test/pages/PopoverResize.html +++ b/packages/main/test/pages/PopoverResize.html @@ -85,8 +85,7 @@ This is a Popover control.
- OK + OK
diff --git a/packages/main/test/pages/PopoverResizeRTL.html b/packages/main/test/pages/PopoverResizeRTL.html index 8279ec739b17..223b33489951 100644 --- a/packages/main/test/pages/PopoverResizeRTL.html +++ b/packages/main/test/pages/PopoverResizeRTL.html @@ -85,8 +85,7 @@ This is a Popover control.
- OK + OK From 6d543d9bf341bde1d4e64694fcf60fa517689ccb Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 20 Nov 2025 14:27:58 +0200 Subject: [PATCH 20/22] chore: add public sample --- packages/main/src/Popover.ts | 44 ++++++++++--------- .../docs/_components_pages/main/Popover.mdx | 6 ++- .../main/Popover/Resizable/Resizable.md | 4 ++ .../_samples/main/Popover/Resizable/main.js | 21 +++++++++ .../main/Popover/Resizable/sample.html | 28 ++++++++++++ 5 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 packages/website/docs/_samples/main/Popover/Resizable/Resizable.md create mode 100644 packages/website/docs/_samples/main/Popover/Resizable/main.js create mode 100644 packages/website/docs/_samples/main/Popover/Resizable/sample.html diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index ca6a26ded677..8542c0f4255e 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -240,11 +240,15 @@ class Popover extends Popup { _minHeight?: number; _resized = false; - _currDeltaX?: number; - _currDeltaY?: number; + _currentResizeDeltaX?: number; + _currentResizeDeltaY?: number; - _accumulatedDeltaX?: number; - _accumulatedDeltaY?: 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. + _totalResizeDeltaX?: number; + _totalResizeDeltaY?: number; _initialWidth?: string; _initialHeight?: string; @@ -320,11 +324,13 @@ class Popover extends Popup { if (this._resized) { this._resized = false; - delete this._currDeltaX; - delete this._currDeltaY; + delete this._currentResizeDeltaX; + delete this._currentResizeDeltaY; - delete this._accumulatedDeltaX; - delete this._accumulatedDeltaY; + delete this._totalResizeDeltaX; + delete this._totalResizeDeltaY; + + delete this._resizeHandlePlacement; } super.closePopup(escPressed, preventRegistryUpdate, preventFocusRestore); @@ -871,7 +877,7 @@ class Popover extends Popup { case PopoverActualHorizontalAlign.Stretch: left = targetRect.left - (popoverSize.width - targetRect.width) / 2; if (this._resized) { - left -= this._currDeltaX || 0; + left -= this._currentResizeDeltaX || 0; } break; case PopoverActualHorizontalAlign.Left: @@ -893,7 +899,7 @@ class Popover extends Popup { case PopoverVerticalAlign.Stretch: top = targetRect.top - (popoverSize.height - targetRect.height) / 2; if (this._resized) { - top -= this._currDeltaY || 0; + top -= this._currentResizeDeltaY || 0; } break; case PopoverVerticalAlign.Top: @@ -1055,8 +1061,8 @@ class Popover extends Popup { this._resized = true; this._initialBoundingRect = this.getBoundingClientRect(); - this._accumulatedDeltaX = this._currDeltaX; - this._accumulatedDeltaY = this._currDeltaY; + this._totalResizeDeltaX = this._currentResizeDeltaX; + this._totalResizeDeltaY = this._currentResizeDeltaY; const { minWidth, @@ -1116,7 +1122,7 @@ class Popover extends Popup { // Recalculate width based on actual left position to stay within viewport with margin newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft); - this._currDeltaX = (initialBoundingRect.x - newLeft) / 2; + this._currentResizeDeltaX = (initialBoundingRect.x - newLeft) / 2; } else { // Resizing from right edge - width increases when moving right (positive delta) const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin; @@ -1127,7 +1133,7 @@ class Popover extends Popup { maxWidthFromRight, ); - this._currDeltaX = (initialBoundingRect.width - newWidth) / 2; + this._currentResizeDeltaX = (initialBoundingRect.width - newWidth) / 2; } // Calculate height changes @@ -1152,7 +1158,7 @@ class Popover extends Popup { // Recalculate height based on actual top position to stay within viewport with margin newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop); - this._currDeltaY = (initialBoundingRect.y - newTop) / 2; + this._currentResizeDeltaY = (initialBoundingRect.y - newTop) / 2; } else { // Resizing from bottom edge - height increases when moving down (positive delta) const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin; @@ -1163,11 +1169,11 @@ class Popover extends Popup { maxHeightFromBottom, ); - this._currDeltaY = (initialBoundingRect.height - newHeight) / 2; + this._currentResizeDeltaY = (initialBoundingRect.height - newHeight) / 2; } - this._currDeltaX += this._accumulatedDeltaX || 0; - this._currDeltaY += this._accumulatedDeltaY || 0; + this._currentResizeDeltaX += this._totalResizeDeltaX || 0; + this._currentResizeDeltaY += this._totalResizeDeltaY || 0; Object.assign(this.style, { height: `${newHeight}px`, @@ -1182,8 +1188,6 @@ class Popover extends Popup { delete this._minWidth; delete this._minHeight; - delete this._resizeHandlePlacement; - this._detachMouseResizeHandlers(); } diff --git a/packages/website/docs/_components_pages/main/Popover.mdx b/packages/website/docs/_components_pages/main/Popover.mdx index 8c03cf5479ab..986c61d7d366 100644 --- a/packages/website/docs/_components_pages/main/Popover.mdx +++ b/packages/website/docs/_components_pages/main/Popover.mdx @@ -4,6 +4,7 @@ slug: ../Popover import Basic from "../../_samples/main/Popover/Basic/Basic.md"; import Placement from "../../_samples/main/Popover/Placement/Placement.md"; +import Resizable from "../../_samples/main/Popover/Resizable/Resizable.md"; <%COMPONENT_OVERVIEW%> @@ -18,4 +19,7 @@ import Placement from "../../_samples/main/Popover/Placement/Placement.md"; You can influence the placement of the popup. Note: if there is not enough space for the popup on the defined side, it will open on the side with enough space. - \ No newline at end of file + + +### Resizable + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Popover/Resizable/Resizable.md b/packages/website/docs/_samples/main/Popover/Resizable/Resizable.md new file mode 100644 index 000000000000..17798ecc59ab --- /dev/null +++ b/packages/website/docs/_samples/main/Popover/Resizable/Resizable.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + diff --git a/packages/website/docs/_samples/main/Popover/Resizable/main.js b/packages/website/docs/_samples/main/Popover/Resizable/main.js new file mode 100644 index 000000000000..28e046936663 --- /dev/null +++ b/packages/website/docs/_samples/main/Popover/Resizable/main.js @@ -0,0 +1,21 @@ +import "@ui5/webcomponents/dist/Dialog.js"; +import "@ui5/webcomponents/dist/Button.js"; +import "@ui5/webcomponents/dist/Toolbar.js"; +import "@ui5/webcomponents/dist/ToolbarButton.js"; + +var popoverOpener = document.getElementById("popoverOpener"); +var popover = document.getElementById("popover"); +var popoverClosers = popover.querySelector(".popoverCloser"); + +popoverOpener.accessibilityAttributes = { + hasPopup: "dialog", + controls: popover.id, +}; +popoverOpener.addEventListener("click", () => { + popover.open = true; +}); +popoverClosers.forEach(btn => { + btn.addEventListener("click", () => { + popover.open = false; + }); +}) \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Popover/Resizable/sample.html b/packages/website/docs/_samples/main/Popover/Resizable/sample.html new file mode 100644 index 000000000000..8d91cbca738f --- /dev/null +++ b/packages/website/docs/_samples/main/Popover/Resizable/sample.html @@ -0,0 +1,28 @@ + + + + + + + + Sample + + + + + + Open Popover + + +

Resize this popover by dragging it by its resize handle.

+

This featur is available only on Desktop.

+ + + +
+ + + + + + From 51612179ec1515a910783cf13d5e9a9e27819486 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 20 Nov 2025 14:48:07 +0200 Subject: [PATCH 21/22] chore: fix typo --- .../website/docs/_samples/main/Popover/Resizable/sample.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/docs/_samples/main/Popover/Resizable/sample.html b/packages/website/docs/_samples/main/Popover/Resizable/sample.html index 8d91cbca738f..a19d445b4155 100644 --- a/packages/website/docs/_samples/main/Popover/Resizable/sample.html +++ b/packages/website/docs/_samples/main/Popover/Resizable/sample.html @@ -15,7 +15,7 @@

Resize this popover by dragging it by its resize handle.

-

This featur is available only on Desktop.

+

This feature is available only on Desktop.

From 5bb57bf0cea622853ae55df3638d3d25a0b1d1cf Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Thu, 20 Nov 2025 15:28:47 +0200 Subject: [PATCH 22/22] chore: improve public sample --- packages/website/docs/_components_pages/main/Popover.mdx | 3 +++ .../website/docs/_samples/main/Popover/Resizable/sample.html | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/website/docs/_components_pages/main/Popover.mdx b/packages/website/docs/_components_pages/main/Popover.mdx index 986c61d7d366..5f1220911767 100644 --- a/packages/website/docs/_components_pages/main/Popover.mdx +++ b/packages/website/docs/_components_pages/main/Popover.mdx @@ -22,4 +22,7 @@ Note: if there is not enough space for the popup on the defined side, it will op ### Resizable +The Resizable sample demonstrates how the Popover component can be resized by dragging its edges. +This allows users to adjust the popup's width and height interactively, providing greater flexibility for content display. + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Popover/Resizable/sample.html b/packages/website/docs/_samples/main/Popover/Resizable/sample.html index a19d445b4155..f56e3f5ce0b6 100644 --- a/packages/website/docs/_samples/main/Popover/Resizable/sample.html +++ b/packages/website/docs/_samples/main/Popover/Resizable/sample.html @@ -13,13 +13,13 @@ Open Popover - +

Resize this popover by dragging it by its resize handle.

This feature is available only on Desktop.

-
+