Make floating dialog drag compositor-friendly and avoid forced style reads

This commit is contained in:
Dariusz L
2026-07-27 19:33:19 +02:00
parent f6db340116
commit c62a88f86a
4 changed files with 249 additions and 17 deletions
+141 -15
View File
@@ -171,16 +171,22 @@ export const dialogShellMethods = {
isVisible() {
if (!this.element?.isConnected) return false;
return getComputedStyle(this.element).display !== 'none';
return this.element.style.display === 'flex';
},
dockTo(container) {
if (!container || !this.element) return;
if (!this.docked) {
this.captureFloatingRect();
if (this._pendingDragDockRect) {
this._floatingRectBeforeDock = this._pendingDragDockRect;
this._pendingDragDockRect = null;
} else {
this.captureFloatingRect();
}
}
this.setDockDropPreviewActive(false);
if (this.fullscreen) {
this.setFullScreen(false);
}
@@ -298,6 +304,8 @@ export const dialogShellMethods = {
undockToFloating({ persist = true, closeSidebar = true } = {}) {
if (!this.element) return;
this.setDockDropPreviewActive(false);
this._pendingDragDockRect = null;
const wasDocked = this.docked;
const dockContainer = this.dockContainer;
this.docked = false;
@@ -498,20 +506,33 @@ export const dialogShellMethods = {
return { top, left };
},
saveModalPosition() {
saveModalPosition(position = null) {
if (this.docked) return;
const el = this.element;
if (!el) return;
const top = Number(position?.top);
const left = Number(position?.left);
if (Number.isFinite(top) && Number.isFinite(left)) {
safeStorage.setItem('model_resolver_modal_pos', JSON.stringify({
top: Math.round(top),
left: Math.round(left)
}));
return;
}
const rect = el.getBoundingClientRect();
safeStorage.setItem('model_resolver_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) }));
safeStorage.setItem('model_resolver_modal_pos', JSON.stringify({
top: Math.round(rect.top),
left: Math.round(rect.left)
}));
},
ensureModalHandleInViewport({ persist = false } = {}) {
if (this.docked) return;
if (this.fullscreen) return;
const el = this.element;
if (!el || getComputedStyle(el).display === 'none') return;
if (!el || !this.isVisible()) return;
const rect = el.getBoundingClientRect();
const { top, left } = this.getViewportClampedModalPosition(rect.top, rect.left);
@@ -538,6 +559,20 @@ export const dialogShellMethods = {
});
},
getDockSnapThreshold() {
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
return Math.max(40, Math.min(64, viewportWidth * 0.045));
},
setDockDropPreviewActive(active) {
const nextActive = Boolean(active && !this.docked && !this.fullscreen);
if (this._dragDockCandidate === nextActive) return;
this._dragDockCandidate = nextActive;
this.dockDropPreview?.classList.toggle('is-active', nextActive);
this.dockDropPreview?.setAttribute('aria-hidden', nextActive ? 'false' : 'true');
},
// Begin window drag
startDrag(e) {
if (this.docked) return;
@@ -545,21 +580,58 @@ export const dialogShellMethods = {
try {
const el = this.element;
if (!el) return;
e.preventDefault?.();
if (this._dragLayerCleanupTimer) {
clearTimeout(this._dragLayerCleanupTimer);
this._dragLayerCleanupTimer = null;
}
this._pendingDragDockRect = null;
this.setDockDropPreviewActive(false);
const rect = el.getBoundingClientRect();
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
const vh = window.innerHeight || document.documentElement.clientHeight || 0;
const pad = 4;
const handle = document.getElementById('model-resolver-drag-handle');
if (handle) {
const handleRect = handle.getBoundingClientRect();
const handleOffsetLeft = handleRect.left - rect.left;
const handleOffsetTop = handleRect.top - rect.top;
const handleWidth = handleRect.width || handle.offsetWidth;
const handleHeight = handleRect.height || handle.offsetHeight;
this._dragBounds = {
minLeft: pad - handleOffsetLeft,
maxLeft: vw - pad - handleOffsetLeft - handleWidth,
minTop: pad - handleOffsetTop,
maxTop: vh - pad - handleOffsetTop - handleHeight
};
} else {
this._dragBounds = {
minLeft: -rect.width + pad,
maxLeft: vw - pad,
minTop: -rect.height + pad,
maxTop: vh - pad
};
}
// Switch to absolute top/left (no transform) before dragging
el.style.top = `${rect.top}px`;
el.style.left = `${rect.left}px`;
el.style.transform = 'none';
el.style.willChange = 'transform';
this._dragging = true;
this._dragStart = {
x: e.clientX,
y: e.clientY,
top: rect.top,
left: rect.left
left: rect.left,
width: rect.width,
height: rect.height
};
this._dragPendingPosition = {
top: Math.round(rect.top),
left: Math.round(rect.left)
};
// Prevent text selection while dragging
this._prevUserSelect = document.body.style.userSelect;
document.body.style.userSelect = 'none';
// Attach listeners
this._onMouseMove = (ev) => this.onDrag(ev);
this._onMouseUp = () => this.endDrag();
@@ -576,19 +648,73 @@ export const dialogShellMethods = {
const dy = e.clientY - this._dragStart.y;
let top = this._dragStart.top + dy;
let left = this._dragStart.left + dx;
({ top, left } = this.getViewportClampedModalPosition(top, left));
el.style.top = `${Math.round(top)}px`;
el.style.left = `${Math.round(left)}px`;
const bounds = this._dragBounds;
if (bounds) {
left = Math.max(bounds.minLeft, Math.min(bounds.maxLeft, left));
top = Math.max(bounds.minTop, Math.min(bounds.maxTop, top));
}
this._dragPendingPosition = {
top: Math.round(top),
left: Math.round(left)
};
this.setDockDropPreviewActive(e.clientX <= this.getDockSnapThreshold());
if (this._dragAnimationFrame) return;
this._dragAnimationFrame = requestAnimationFrame(() => {
this._dragAnimationFrame = null;
if (!this._dragging || !this._dragStart || !this._dragPendingPosition) return;
const translateX = this._dragPendingPosition.left - this._dragStart.left;
const translateY = this._dragPendingPosition.top - this._dragStart.top;
el.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`;
});
},
endDrag() {
if (!this._dragging) return;
const shouldDock = this._dragDockCandidate;
this._dragging = false;
document.removeEventListener('mousemove', this._onMouseMove);
this.setDockDropPreviewActive(false);
if (this._dragAnimationFrame) {
cancelAnimationFrame(this._dragAnimationFrame);
this._dragAnimationFrame = null;
}
const el = this.element;
const finalPosition = this._dragPendingPosition || this._dragStart;
if (shouldDock && finalPosition && this._dragStart) {
this._pendingDragDockRect = {
top: Math.round(finalPosition.top),
left: Math.round(finalPosition.left),
width: this._dragStart.width,
height: this._dragStart.height
};
}
if (el && finalPosition) {
el.style.top = `${Math.round(finalPosition.top)}px`;
el.style.left = `${Math.round(finalPosition.left)}px`;
el.style.transform = 'none';
}
this._dragStart = null;
this._dragBounds = null;
this._dragPendingPosition = null;
// Persist position
this.saveModalPosition();
// Restore selection
try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) {}
this.saveModalPosition(finalPosition);
this._dragLayerCleanupTimer = setTimeout(() => {
this._dragLayerCleanupTimer = null;
if (!this._dragging && this.element) {
this.element.style.willChange = '';
}
}, 120);
if (shouldDock) {
this.dockToSidebar();
}
},
/**