Only clip/scroll once the dialog has been explicitly sized via resize

This commit is contained in:
2026-06-20 17:31:00 +02:00
parent 68fa2427dc
commit cb888bbbb8
2 changed files with 151 additions and 11 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@tp/tp-dialog", "name": "@tp/tp-dialog",
"version": "1.5.0", "version": "1.5.1",
"description": "", "description": "",
"main": "tp-dialog.js", "main": "tp-dialog.js",
"scripts": { "scripts": {
+150 -10
View File
@@ -45,6 +45,7 @@ class TpDialog extends EventHelpers(LitElement) {
dialog { dialog {
position: relative; position: relative;
box-sizing: border-box;
border-radius: var(--tp-dialog-border-radius); border-radius: var(--tp-dialog-border-radius);
background-color: var(--tp-dialog-bg); background-color: var(--tp-dialog-bg);
color: var(--text); color: var(--text);
@@ -52,6 +53,28 @@ class TpDialog extends EventHelpers(LitElement) {
padding: var(--tp-dialog-padding); padding: var(--tp-dialog-padding);
pointer-events: all; pointer-events: all;
transform: translate(var(--tp-dialog-offset-x, 0px), var(--tp-dialog-offset-y, 0px)); transform: translate(var(--tp-dialog-offset-x, 0px), var(--tp-dialog-offset-y, 0px));
display: flex;
flex-direction: column;
}
dialog:not([open]) {
display: none;
}
/* Only clip/scroll once the dialog has been explicitly sized via resize. */
:host([constrained]) dialog {
overflow: hidden;
}
.scroll-wrapper {
flex: 1;
box-sizing: border-box;
min-height: 0;
overflow: visible;
}
:host([constrained]) .scroll-wrapper {
overflow: auto;
} }
.close-icon { .close-icon {
@@ -72,12 +95,26 @@ class TpDialog extends EventHelpers(LitElement) {
cursor: move; cursor: move;
z-index: 2; z-index: 2;
} }
.resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 14px;
height: 14px;
cursor: se-resize;
z-index: 4;
background-image: linear-gradient(135deg, transparent 0%, transparent 50%, var(--tp-dialog-resize-handle-color, #888) 50%, var(--tp-dialog-resize-handle-color, #888) 60%, transparent 60%, transparent 70%, var(--tp-dialog-resize-handle-color, #888) 70%, var(--tp-dialog-resize-handle-color, #888) 80%, transparent 80%);
background-size: 8px 8px;
background-repeat: no-repeat;
background-position: bottom right;
}
` `
]; ];
} }
render() { render() {
const { showClose, unmovable } = this; const { showClose, unmovable, resizable } = this;
return html` return html`
<dialog part="dialog"> <dialog part="dialog">
${!unmovable ? html` ${!unmovable ? html`
@@ -88,7 +125,12 @@ class TpDialog extends EventHelpers(LitElement) {
<tp-icon .icon=${this.icon ? this.icon : TpDialog.closeIcon} dialog-dismiss></tp-icon> <tp-icon .icon=${this.icon ? this.icon : TpDialog.closeIcon} dialog-dismiss></tp-icon>
</div> </div>
` : null} ` : null}
<slot></slot> <div class="scroll-wrapper" part="scroll-wrapper">
<slot></slot>
</div>
${resizable ? html`
<div class="resize-handle" part="resize-handle" @pointerdown=${this._onResizeStart}></div>
` : null}
</dialog> </dialog>
`; `;
} }
@@ -102,6 +144,7 @@ class TpDialog extends EventHelpers(LitElement) {
closeOnOutsideClick: { type: Boolean }, closeOnOutsideClick: { type: Boolean },
modal: { type: Boolean, reflect: true }, modal: { type: Boolean, reflect: true },
unmovable: { type: Boolean, reflect: true }, unmovable: { type: Boolean, reflect: true },
resizable: { type: Boolean, reflect: true },
}; };
} }
@@ -119,6 +162,7 @@ class TpDialog extends EventHelpers(LitElement) {
this._resolvePromise = null; this._resolvePromise = null;
this.modal = false; this.modal = false;
this.unmovable = false; this.unmovable = false;
this.resizable = false;
this._offsetX = 0; this._offsetX = 0;
this._offsetY = 0; this._offsetY = 0;
} }
@@ -138,14 +182,8 @@ class TpDialog extends EventHelpers(LitElement) {
this.unlisten(this, 'confirmed', '_handleConfirmed'); this.unlisten(this, 'confirmed', '_handleConfirmed');
this.unlisten(this, 'dismissed', '_handleDismissed'); this.unlisten(this, 'dismissed', '_handleDismissed');
// Clean up pointer drag listeners // Clean up pointer drag/resize listeners
if (this._boundDragMove) { this._cleanupActiveListeners();
window.removeEventListener('pointermove', this._boundDragMove);
}
if (this._boundDragEnd) {
window.removeEventListener('pointerup', this._boundDragEnd);
window.removeEventListener('pointercancel', this._boundDragEnd);
}
// Remove this dialog from the stack // Remove this dialog from the stack
this._removeFromDialogStack(); this._removeFromDialogStack();
@@ -209,6 +247,13 @@ class TpDialog extends EventHelpers(LitElement) {
// Remove from dialog stack // Remove from dialog stack
this._removeFromDialogStack(); this._removeFromDialogStack();
// Clean up pointer drag/resize listeners if they are currently active
this._cleanupActiveListeners();
if (this.closeOnOutsideClick) {
this.removeEventListener('click', this._handleOutsideClick);
}
// If closed without explicit confirm/dismiss (like ESC key), treat as dismissed // If closed without explicit confirm/dismiss (like ESC key), treat as dismissed
if (this._currentPromise && this._resolvePromise) { if (this._currentPromise && this._resolvePromise) {
this._resolvePromise('dismissed'); this._resolvePromise('dismissed');
@@ -217,11 +262,38 @@ class TpDialog extends EventHelpers(LitElement) {
} }
} }
_cleanupActiveListeners() {
if (this._boundDragMove) {
window.removeEventListener('pointermove', this._boundDragMove);
this._boundDragMove = null;
}
if (this._boundDragEnd) {
window.removeEventListener('pointerup', this._boundDragEnd);
window.removeEventListener('pointercancel', this._boundDragEnd);
this._boundDragEnd = null;
}
if (this._boundResizeMove) {
window.removeEventListener('pointermove', this._boundResizeMove);
this._boundResizeMove = null;
}
if (this._boundResizeEnd) {
window.removeEventListener('pointerup', this._boundResizeEnd);
window.removeEventListener('pointercancel', this._boundResizeEnd);
this._boundResizeEnd = null;
}
}
_resetPosition() { _resetPosition() {
this._offsetX = 0; this._offsetX = 0;
this._offsetY = 0; this._offsetY = 0;
this.style.removeProperty('--tp-dialog-offset-x'); this.style.removeProperty('--tp-dialog-offset-x');
this.style.removeProperty('--tp-dialog-offset-y'); this.style.removeProperty('--tp-dialog-offset-y');
this.removeAttribute('constrained');
const dialogEl = this.dialog;
if (dialogEl) {
dialogEl.style.removeProperty('width');
dialogEl.style.removeProperty('height');
}
} }
_onDragStart(e) { _onDragStart(e) {
@@ -269,6 +341,74 @@ class TpDialog extends EventHelpers(LitElement) {
window.removeEventListener('pointercancel', this._boundDragEnd); window.removeEventListener('pointercancel', this._boundDragEnd);
} }
_onResizeStart(e) {
if (!this.resizable) return;
if (e.button !== 0) return; // Only resize with left/main pointer button
e.preventDefault();
e.stopPropagation();
// Once the user starts resizing, content must scroll inside the fixed box.
this.setAttribute('constrained', '');
const rect = this.dialog.getBoundingClientRect();
this._startWidth = rect.width;
this._startHeight = rect.height;
this._startX = e.clientX;
this._startY = e.clientY;
this._startOffsetX = this._offsetX || 0;
this._startOffsetY = this._offsetY || 0;
const resizeHandle = this.shadowRoot.querySelector('.resize-handle');
if (resizeHandle && typeof resizeHandle.setPointerCapture === 'function') {
resizeHandle.setPointerCapture(e.pointerId);
}
this._boundResizeMove = this._onResizeMove.bind(this);
this._boundResizeEnd = this._onResizeEnd.bind(this);
window.addEventListener('pointermove', this._boundResizeMove);
window.addEventListener('pointerup', this._boundResizeEnd);
window.addEventListener('pointercancel', this._boundResizeEnd);
}
_onResizeMove(e) {
const dw = e.clientX - this._startX;
const dh = e.clientY - this._startY;
const newWidth = Math.max(150, this._startWidth + dw);
const newHeight = Math.max(100, this._startHeight + dh);
const dw_actual = newWidth - this._startWidth;
const dh_actual = newHeight - this._startHeight;
this._offsetX = this._startOffsetX + dw_actual / 2;
this._offsetY = this._startOffsetY + dh_actual / 2;
const originalTop = (window.innerHeight - newHeight) / 2;
if (originalTop + this._offsetY < 0) {
this._offsetY = -originalTop;
}
this.dialog.style.width = `${newWidth}px`;
this.dialog.style.height = `${newHeight}px`;
this.style.setProperty('--tp-dialog-offset-x', `${this._offsetX}px`);
this.style.setProperty('--tp-dialog-offset-y', `${this._offsetY}px`);
}
_onResizeEnd(e) {
const resizeHandle = this.shadowRoot.querySelector('.resize-handle');
if (resizeHandle && typeof resizeHandle.releasePointerCapture === 'function') {
try {
resizeHandle.releasePointerCapture(e.pointerId);
} catch (err) {}
}
window.removeEventListener('pointermove', this._boundResizeMove);
window.removeEventListener('pointerup', this._boundResizeEnd);
window.removeEventListener('pointercancel', this._boundResizeEnd);
}
_handleConfirmed(event) { _handleConfirmed(event) {
if (this._currentPromise && this._resolvePromise) { if (this._currentPromise && this._resolvePromise) {
this._resolvePromise('confirmed'); this._resolvePromise('confirmed');