Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 274 additions & 1 deletion packages/main/src/Popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ type CalculatedPlacement = {
actualPlacement: `${PopoverActualPlacement}`,
}

enum ResizeHandlePlacement {
TopLeft = "TopLeft",
TopRight = "TopRight",
BottomLeft = "BottomLeft",
BottomRight = "BottomRight",
}

/**
* @class
*
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -211,12 +228,32 @@ 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;
_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);
}

/**
Expand Down Expand Up @@ -850,6 +887,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;
}

Expand Down Expand Up @@ -884,6 +938,225 @@ 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();

const {
top,
left,
} = 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);

this._resizeHandlePlacement = this._getResizeHandlePlacement();

Object.assign(this.style, {
top: `${top}px`,
left: `${left}px`,
});

this._resized = true;
this._attachMouseResizeHandlers();
}

_onResizeMouseMove(e: MouseEvent) {
const { clientX, clientY } = e;
const placement = this._resizeHandlePlacement;
const margin = Popover.VIEWPORT_MARGIN;

let newWidth,
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;

newWidth = clamp(
this._initialWidth! - 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(
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!,
maxWidthFromRight,
);
}

// 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: newLeft !== undefined ? `${newLeft}px` : undefined,
top: newTop !== undefined ? `${newTop}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;

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 = () => {
Object.assign(this.style, {
top: "",
left: "",
width: "",
height: "",
});
}
}

const instanceOfPopover = (object: any): object is Popover => {
Expand Down
10 changes: 10 additions & 0 deletions packages/main/src/PopoverTemplate.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -32,5 +34,13 @@ function afterContent(this: Popover) {
<slot name="footer"></slot>
</footer>
}

{this._showResizeHandle &&
<div class="ui5-popover-resize-handle"
onMouseDown={this._onResizeMouseDown}
>
<Icon name={resizeCorner} />
</div>
}
</>);
}
Loading
Loading