Fix docked dialog drag snap threshold and add queue splitter resize feedback

This commit is contained in:
Dariusz L
2026-07-28 13:30:04 +02:00
parent fee001829b
commit 3d513f6587
4 changed files with 157 additions and 3 deletions
+121
View File
@@ -172,6 +172,8 @@ test('floating dialog drag stays on the compositor without forced style reads',
assert.match(resolverDialogSource, /mr-undock-drop-preview/);
assert.match(startDockedDrag, /getRememberedFloatingSize\(\)/);
assert.match(onDockedDrag, /Math\.hypot\(dx,\s*dy\)\s*<\s*5/);
assert.match(onDockedDrag, /e\.clientX\s*>\s*this\.getDockSnapThreshold\(\)/);
assert.match(onDockedDrag, /setUndockDropPreviewActive\(shouldUndock\)/);
assert.match(onDockedDrag, /requestAnimationFrame/);
assert.match(endDockedDrag, /this\.undockToFloating\(\{\s*persist:\s*false\s*\}\)/);
assert.match(endDockedDrag, /this\._floatingRectBeforeDock\s*=\s*\{\s*\.\.\.finalRect\s*\}/);
@@ -241,6 +243,70 @@ test('floating dialog drag stays on the compositor without forced style reads',
}
});
test('docked dialog drag stays docked after returning to the dock edge', () => {
const onDockedDrag = eval(`(${extractMethod(dialogShellMethodsSource, 'onDockedDrag')})`);
const endDockedDrag = eval(`(${extractMethod(dialogShellMethodsSource, 'endDockedDrag')})`);
const previousDocument = globalThis.document;
const previousRequestAnimationFrame = globalThis.requestAnimationFrame;
const previousCancelAnimationFrame = globalThis.cancelAnimationFrame;
let undockCalls = 0;
let cancelledFrame = null;
globalThis.document = {
removeEventListener() {},
};
globalThis.requestAnimationFrame = () => 23;
globalThis.cancelAnimationFrame = frame => {
cancelledFrame = frame;
};
try {
const dialog = {
docked: true,
fullscreen: false,
element: { style: {} },
undockDropPreview: { style: {} },
_dockedDragStart: { x: 20, y: 20 },
_dockedDragPendingRect: null,
_dockedDragAnimationFrame: null,
_dragUndockCandidate: false,
getDockSnapThreshold: () => 64,
getDockedDragPreviewRect: (clientX, clientY) => ({
top: clientY,
left: clientX,
width: 800,
height: 600,
}),
setUndockDropPreviewActive(active) {
this._dragUndockCandidate = Boolean(active && this.docked && !this.fullscreen);
},
undockToFloating() {
undockCalls += 1;
},
saveModalPosition() {},
};
onDockedDrag.call(dialog, { clientX: 120, clientY: 40 });
assert.equal(dialog._dragUndockCandidate, true);
assert.equal(dialog._dockedDragAnimationFrame, 23);
onDockedDrag.call(dialog, { clientX: 20, clientY: 40 });
assert.equal(dialog._dragUndockCandidate, false);
assert.equal(dialog._dockedDragPendingRect, null);
endDockedDrag.call(dialog);
assert.equal(undockCalls, 0);
assert.equal(dialog.docked, true);
assert.equal(cancelledFrame, 23);
assert.equal(dialog._dockedDragStart, null);
} finally {
globalThis.document = previousDocument;
globalThis.requestAnimationFrame = previousRequestAnimationFrame;
globalThis.cancelAnimationFrame = previousCancelAnimationFrame;
}
});
test('missing browser splitter keeps a compositor preview between live layout frames', () => {
const previousDocument = globalThis.document;
const previousRequestAnimationFrame = globalThis.requestAnimationFrame;
@@ -419,6 +485,61 @@ test('docked ComfyUI splitter coalesces live layout while its gutter stays respo
assert.doesNotMatch(dialogShellMethodsSource, /mr-resolver-external-resizing/);
});
test('queue splitter suppresses tooltips while resizing', () => {
const activateQueueSplitUi = eval(
`(${extractMethod(queueMethodsSource, 'activateQueueSplitUi')})`
);
const deactivateQueueSplitUi = eval(
`(${extractMethod(queueMethodsSource, 'deactivateQueueSplitUi')})`
);
const classes = new Set();
const splitterClasses = new Set();
let hideTooltipCalls = 0;
const body = {
classList: {
add: value => classes.add(value),
remove: value => classes.delete(value),
},
};
const dialog = {
_queueSplitUiActive: false,
splitterElement: {
classList: {
add: value => splitterClasses.add(value),
remove: value => splitterClasses.delete(value),
},
},
hideTooltip() {
hideTooltipCalls += 1;
},
};
activateQueueSplitUi.call(dialog, body);
activateQueueSplitUi.call(dialog, body);
assert.equal(hideTooltipCalls, 1);
assert.equal(classes.has('is-resizing'), true);
assert.equal(splitterClasses.has('is-resizing'), true);
deactivateQueueSplitUi.call(dialog, body);
assert.equal(dialog._queueSplitUiActive, false);
assert.equal(classes.has('is-resizing'), false);
assert.equal(splitterClasses.has('is-resizing'), false);
assert.match(
queueMethodsSource,
/onBeforeDrag:\s*\(newW\)\s*=>\s*\{\s*this\.activateQueueSplitUi\(body\)/
);
assert.match(
queueMethodsSource,
/onEnd:\s*\(finalWidth\)\s*=>\s*\{[^]*this\.deactivateQueueSplitUi\(body\)/
);
assert.match(
resolverMainCssSource,
/#model-resolver-body\.is-resizing::after\s*\{[^}]*pointer-events:\s*auto/s
);
});
test('missing browser detail remains available at narrow widths', () => {
assert.doesNotMatch(missingBrowserMethodsSource, /updateMissingBrowserDetailCollapse/);
assert.doesNotMatch(missingBrowserMethodsSource, /is-detail-collapsed/);
+9
View File
@@ -742,6 +742,15 @@
pointer-events: auto;
}
#model-resolver-body.is-resizing::after {
content: "";
position: absolute;
z-index: 9999;
inset: 0;
cursor: col-resize;
pointer-events: auto;
}
#model-resolver-modal .mr-missing-list-row {
content-visibility: auto;
contain-intrinsic-size: 70px;
+20 -2
View File
@@ -1955,6 +1955,23 @@ export const queueMethods = {
});
},
activateQueueSplitUi(body) {
if (this._queueSplitUiActive) return;
const targetBody = body || this.getQueueSplitBody();
this._queueSplitUiActive = true;
this.hideTooltip?.();
targetBody?.classList?.add('is-resizing');
this.splitterElement?.classList?.add('is-resizing');
},
deactivateQueueSplitUi(body) {
const targetBody = body || this.getQueueSplitBody();
this._queueSplitUiActive = false;
targetBody?.classList?.remove('is-resizing');
this.splitterElement?.classList?.remove('is-resizing');
},
// Begin split drag for resizable panels
startSplitDrag(e, { startWidth = null, edgeOpen = false, startCollapsedPreview = false } = {}) {
try {
@@ -1987,12 +2004,14 @@ export const queueMethods = {
this._splitPreviewCollapsed = !!startCollapsedPreview;
this._splitEdgeOpen = !!edgeOpen;
this._splitEdgeOpenedPastThreshold = false;
const body = this.getQueueSplitBody();
this._splitInteraction = startSplitterDrag(e, {
anchor: 'right',
startWidth: initialWidth,
bounds,
onBeforeDrag: (newW) => {
this.activateQueueSplitUi(body);
if (newW < bounds.min) newW = bounds.min;
if (newW > bounds.max) newW = bounds.max;
return newW;
@@ -2041,6 +2060,7 @@ export const queueMethods = {
},
onEnd: (finalWidth) => {
this._splitDragging = false;
this.deactivateQueueSplitUi(body);
if (this.queueElement && finalWidth && !this._splitPreviewCollapsed) {
this.applyQueueSplitWidth(finalWidth, {
bounds: this._splitStart?.bounds,
@@ -2049,8 +2069,6 @@ export const queueMethods = {
});
}
const body = this.getQueueSplitBody();
if (this._splitEdgeOpen && !this._pendingSplitWidth) {
try {
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
+7 -1
View File
@@ -906,8 +906,14 @@ export const dialogShellMethods = {
const dy = e.clientY - start.y;
if (!this._dragUndockCandidate && Math.hypot(dx, dy) < 5) return;
const shouldUndock = e.clientX > this.getDockSnapThreshold();
this.setUndockDropPreviewActive(shouldUndock);
if (!shouldUndock) {
this._dockedDragPendingRect = null;
return;
}
this._dockedDragPendingRect = this.getDockedDragPreviewRect(e.clientX, e.clientY);
this.setUndockDropPreviewActive(true);
if (this._dockedDragAnimationFrame || !this._dockedDragPendingRect) return;
this._dockedDragAnimationFrame = requestAnimationFrame(() => {