diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts
index 51751a3e44cb..fc52e7207018 100644
--- a/packages/main/src/Popover.ts
+++ b/packages/main/src/Popover.ts
@@ -53,6 +53,13 @@ type CalculatedPlacement = {
actualPlacement: `${PopoverActualPlacement}`,
}
+enum ResizeHandlePlacement {
+ TopLeft = "TopLeft",
+ TopRight = "TopRight",
+ BottomLeft = "BottomLeft",
+ BottomRight = "BottomRight",
+}
+
/**
* @class
*
@@ -135,7 +142,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
@@ -161,6 +168,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
@@ -211,12 +228,27 @@ class Popover extends Popup {
_width?: string;
_height?: string;
+ _resizeMouseMoveHandler: (e: MouseEvent) => void;
+ _resizeMouseUpHandler: (e: MouseEvent) => void;
+ _initialClientX?: number;
+ _initialClientY?: number;
+
+ _initialBoundingRect?: DOMRect;
+ _minWidth?: number;
+ _minHeight?: number;
+ _resized = false;
+
+ _resizeHandlePlacement?: `${ResizeHandlePlacement}`;
+
static get VIEWPORT_MARGIN() {
return 10; // px
}
constructor() {
super();
+
+ this._resizeMouseMoveHandler = this._onResizeMouseMove.bind(this);
+ this._resizeMouseUpHandler = this._onResizeMouseUp.bind(this);
}
/**
@@ -850,6 +882,23 @@ class Popover extends Popup {
const allClasses = super.classes;
allClasses.root["ui5-popover-root"] = true;
+ 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;
}
@@ -884,6 +933,216 @@ class Popover extends Popup {
return PopoverActualHorizontalAlign.Center;
}
}
+
+ get _showResizeHandle() {
+ 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;
+ }
+
+ e.preventDefault();
+
+ this._resized = true;
+ this._initialBoundingRect = this.getBoundingClientRect();
+
+ const {
+ minWidth,
+ minHeight,
+ } = window.getComputedStyle(this);
+
+ const domRefComputedStyle = window.getComputedStyle(this._getRealDomRef!());
+
+ this._initialClientX = e.clientX;
+ this._initialClientY = e.clientY;
+
+ this._minWidth = Math.max(Number.parseFloat(minWidth), Number.parseFloat(domRefComputedStyle.minWidth));
+ this._minHeight = Number.parseFloat(minHeight);
+
+ this._resizeHandlePlacement = this._getResizeHandlePlacement();
+
+ this._attachMouseResizeHandlers();
+ }
+
+ _onResizeMouseMove(e: MouseEvent) {
+ const margin = Popover.VIEWPORT_MARGIN;
+ const { clientX, clientY } = e;
+ const placement = this._resizeHandlePlacement;
+ 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;
+
+ // 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 maxWidthFromLeft = initialBoundingRect.x + initialBoundingRect.width - margin;
+
+ newWidth = clamp(
+ initialBoundingRect.width - deltaX,
+ this._minWidth!,
+ maxWidthFromLeft,
+ );
+
+ // Adjust left position when resizing from left
+ // Ensure the left edge respects the viewport margin and the right edge position
+ newLeft = clamp(
+ initialBoundingRect.x + deltaX,
+ margin,
+ initialBoundingRect.x + initialBoundingRect.width - this._minWidth!,
+ );
+
+ // Recalculate width based on actual left position to stay within viewport with margin
+ newWidth = Math.min(newWidth, initialBoundingRect.x + initialBoundingRect.width - newLeft);
+ } else {
+ // Resizing from right edge - width increases when moving right (positive delta)
+ const maxWidthFromRight = window.innerWidth - initialBoundingRect.x - margin;
+
+ newWidth = clamp(
+ initialBoundingRect.width + deltaX,
+ this._minWidth!,
+ maxWidthFromRight,
+ );
+ }
+
+ // Calculate height changes
+ if (isResizingFromTop) {
+ // Resizing from top edge - height increases when moving up (negative delta)
+ const maxHeightFromTop = initialBoundingRect.y + initialBoundingRect.height - margin;
+
+ newHeight = clamp(
+ 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(
+ initialBoundingRect.y + deltaY,
+ margin,
+ initialBoundingRect.y + initialBoundingRect.height - this._minHeight!,
+ );
+
+ // Recalculate height based on actual top position to stay within viewport with margin
+ newHeight = Math.min(newHeight, initialBoundingRect.y + initialBoundingRect.height - newTop);
+ } else {
+ // Resizing from bottom edge - height increases when moving down (positive delta)
+ const maxHeightFromBottom = window.innerHeight - initialBoundingRect.y - margin;
+
+ newHeight = clamp(
+ initialBoundingRect.height + deltaY,
+ this._minHeight!,
+ maxHeightFromBottom,
+ );
+ }
+
+ Object.assign(this.style, {
+ height: `${newHeight}px`,
+ width: `${newWidth}px`,
+ left: `${newLeft}px`,
+ top: `${newTop}px`,
+ });
+ }
+
+ _onResizeMouseUp() {
+ delete this._initialClientX;
+ delete this._initialClientY;
+ delete this._initialBoundingRect;
+ delete this._minWidth;
+ delete this._minHeight;
+
+ delete this._resizeHandlePlacement;
+
+ this._detachMouseResizeHandlers();
+ }
+
+ _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 = () => {
+ this._resized = false;
+
+ 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 f31cb442d788..c5f9bfbfc124 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,13 @@ function afterContent(this: Popover) {