Skip to content

Commit 5be1e49

Browse files
authored
fix(design): preserve drag modifier parity (#5534)
* fix(design): preserve ignore auto layout modifier through drag * fix(design): preserve drag modifier parity * style(design): format drag bridge * fix(design): preserve nested pointer target order * fix(design): preserve host drag modifier at start * fix(design): prefer deepest nested drag target * fix(design): resolve authored nested drag targets * fix(design): forward host ignore layout shortcut * fix(design): pass host drag modifiers into bridge * perf(design): bound nested target hit testing * fix(design): mirror host modifier focus * fix(design): include control modifier in host drag start * fix(design): prioritize nested container drops * fix(design): capture host control modifier * fix(design): preserve control across iframe focus handoff * fix(design): retain control through drag focus handoff * fix(design): document cross-origin bridge fallback * fix(design): clear bridge modifiers after board drag * fix(design): clear host modifier on drag cancel * fix(design): retain generated bridge fallback rationale * fix(design): limit nested target candidates to authored nodes * fix(design): preserve bridge fallback catch * fix(design): isolate drag modifier parity * chore(design): regenerate bridge from workspace deps * fix(design): consume host modifier at drag start * fix(design): synchronize drag modifier handoff * fix(design): scope modifier listener guard per iframe * fix(design): preserve modifier state across iframe blur * fix(design): clean modifier listeners on drag cancel * fix(design): clear transient drag modifiers safely * test(design): guard held drag modifier cleanup * fix(design): refresh held-drag bridge guard * fix(design): reset cross-screen drag modifiers * fix(design): regenerate bridge with portable paths
1 parent 6b0cdca commit 5be1e49

4 files changed

Lines changed: 299 additions & 38 deletions

File tree

templates/design/.generated/bridge/editor-chrome.generated.ts

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4773,7 +4773,13 @@ export const editorChromeBridgeScript: string = `"use strict";
47734773
var activeEditorDragId = "";
47744774
var bridgeSpaceKeyPressed = false;
47754775
var bridgeIgnoreAutoLayoutKeyPressed = false;
4776+
var hostIgnoreAutoLayoutAtPointerDown = false;
47764777
var bridgeSpaceKeyConsumedByDrag = false;
4778+
function resetBridgeDragModifierStateOnCancel() {
4779+
bridgeSpaceKeyPressed = false;
4780+
bridgeSpaceKeyConsumedByDrag = false;
4781+
hostIgnoreAutoLayoutAtPointerDown = false;
4782+
}
47774783
var activeCrossScreenStyleSnapshot = void 0;
47784784
var activeCrossScreenDragIdentity = null;
47794785
var spacingDrag = null;
@@ -10602,7 +10608,6 @@ export const editorChromeBridgeScript: string = `"use strict";
1060210608
function postCrossScreenDrag(phase, el, ev, options) {
1060310609
dndLog("post:cross-screen", { phase, el: getSelector(el ?? null) });
1060410610
if (phase === "cancel") {
10605-
bridgeIgnoreAutoLayoutKeyPressed = false;
1060610611
activeCrossScreenStyleSnapshot = void 0;
1060710612
activeCrossScreenDragIdentity = null;
1060810613
window.parent.postMessage(
@@ -13238,6 +13243,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1323813243
clearActiveDragCancel(onRejectedEscape2);
1323913244
shieldOverlay.style.cursor = "default";
1324013245
}, onRejectedEscape2 = function() {
13246+
resetBridgeDragModifierStateOnCancel();
1324113247
cleanupRejectedDrag2();
1324213248
hideTransformBadge();
1324313249
suppressNextShieldClickBriefly();
@@ -13908,6 +13914,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1390813914
}, onReorderVisibilityChange2 = function() {
1390913915
if (document.visibilityState === "hidden") onReorderEscape2();
1391013916
}, onReorderEscape2 = function() {
13917+
resetBridgeDragModifierStateOnCancel();
1391113918
cleanupReorderDrag2();
1391213919
hideTransformBadge();
1391313920
hideInsertionGuide();
@@ -14259,6 +14266,11 @@ export const editorChromeBridgeScript: string = `"use strict";
1425914266
var moved = false;
1426014267
dndLog("start:free", { el: getSelector(gestureEl), isGroup: isGroupDrag });
1426114268
var currentAutoLayoutTarget = null;
14269+
var dragIgnoreAutoLayout = hostIgnoreAutoLayoutAtPointerDown || pointerStartParam?.ignoreAutoLayout === true || isIgnoreAutoLayoutChord(e);
14270+
hostIgnoreAutoLayoutAtPointerDown = false;
14271+
function ignoreAutoLayoutHeld(ev) {
14272+
return dragIgnoreAutoLayout || isIgnoreAutoLayoutChordForDragPoint(ev);
14273+
}
1426214274
var autoLayoutTargetFrame = 0;
1426314275
var pendingAutoLayoutTargetPoint = null;
1426414276
var snapCandidateRects = collectSnapCandidateRects(dragEl, groupOthers);
@@ -14269,7 +14281,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1426914281
if (!target || target.placement !== "inside" || target.dropMode !== "flow-insert") {
1427014282
return target;
1427114283
}
14272-
if (ev && (isIgnoreAutoLayoutChordForDragPoint(ev) || isPlatformPrimaryChord(ev))) {
14284+
if (ev && (ignoreAutoLayoutHeld(ev) || isPlatformPrimaryChord(ev))) {
1427314285
return target;
1427414286
}
1427514287
var container = dropContainerForTarget(target);
@@ -14391,7 +14403,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1439114403
modifiers: {
1439214404
metaKey: !!e.metaKey,
1439314405
ctrlKey: !!e.ctrlKey,
14394-
ignoreAutoLayout: isIgnoreAutoLayoutChord(e),
14406+
ignoreAutoLayout: dragIgnoreAutoLayout,
1439514407
forceNestedAutoLayout: isPlatformPrimaryChord(e)
1439614408
}
1439714409
});
@@ -14420,7 +14432,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1442014432
clientY: ev.clientY,
1442114433
metaKey: !!ev.metaKey,
1442214434
ctrlKey: !!ev.ctrlKey,
14423-
ignoreAutoLayout: isIgnoreAutoLayoutChord(ev),
14435+
ignoreAutoLayout: ignoreAutoLayoutHeld(ev),
1442414436
forceNestedAutoLayout: isPlatformPrimaryChord(ev)
1442514437
};
1442614438
if (crossScreenDragMoveScheduled) return;
@@ -14513,7 +14525,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1451314525
var rawDy = controllerMove.gesture.canvasDelta.y;
1451414526
var nextLeft = originLeft + rawDx;
1451514527
var nextTop = originTop + rawDy;
14516-
var snapBypass = isIgnoreAutoLayoutChord(ev) || isPlatformPrimaryChord(ev);
14528+
var snapBypass = ignoreAutoLayoutHeld(ev) || isPlatformPrimaryChord(ev);
1451714529
var snapResult = !snapBypass && !duplicatedForDrag ? computeMoveSnapOffset(
1451814530
{
1451914531
// snapCandidateRects are client space; nextLeft/nextTop are
@@ -14608,6 +14620,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1460814620
suppressNextShieldClickBriefly();
1460914621
}
1461014622
function cancelMoveDrag() {
14623+
resetBridgeDragModifierStateOnCancel();
1461114624
bridgeMoveController.cancel();
1461214625
cleanupMoveDrag();
1461314626
hideTransformBadge();
@@ -14674,7 +14687,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1467414687
modifiers: {
1467514688
metaKey: !!ev.metaKey,
1467614689
ctrlKey: !!ev.ctrlKey,
14677-
ignoreAutoLayout: isIgnoreAutoLayoutChord(ev),
14690+
ignoreAutoLayout: ignoreAutoLayoutHeld(ev),
1467814691
forceNestedAutoLayout: isPlatformPrimaryChord(ev)
1467914692
}
1468014693
});
@@ -14698,7 +14711,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1469814711
groupOthers,
1469914712
isPlatformPrimaryChord(ev)
1470014713
);
14701-
if (finalAutoLayoutTarget && isIgnoreAutoLayoutChord(ev)) {
14714+
if (finalAutoLayoutTarget && ignoreAutoLayoutHeld(ev)) {
1470214715
finalAutoLayoutTarget = ignoreAutoLayoutForDropTarget(
1470314716
finalAutoLayoutTarget
1470414717
);
@@ -16001,6 +16014,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1600116014
if (e.type === "pointerdown") lastPointerDownTimestamp = Date.now();
1600216015
stopNativeInteraction(e);
1600316016
clearGridProjectionCaches();
16017+
hostIgnoreAutoLayoutAtPointerDown = false;
1600416018
pendingMoveCommitRevert = null;
1600516019
if (e.button !== 0) return;
1600616020
if (activeTextEditEl && !exitStaleTextEditSession()) return;
@@ -16081,13 +16095,22 @@ export const editorChromeBridgeScript: string = `"use strict";
1608116095
suppressNextShieldClickBriefly();
1608216096
startMove(ev, groupGestureMember, {
1608316097
clientX: startX,
16084-
clientY: startY
16098+
clientY: startY,
16099+
ignoreAutoLayout: Boolean(
16100+
e.__agentNativeIgnoreAutoLayout
16101+
) || isIgnoreAutoLayoutChord(ev)
1608516102
});
1608616103
return;
1608716104
}
1608816105
selectTarget(dragTarget, ev);
1608916106
suppressNextShieldClickBriefly();
16090-
startMove(ev, void 0, { clientX: startX, clientY: startY });
16107+
startMove(ev, void 0, {
16108+
clientX: startX,
16109+
clientY: startY,
16110+
ignoreAutoLayout: Boolean(
16111+
e.__agentNativeIgnoreAutoLayout
16112+
) || isIgnoreAutoLayoutChord(ev)
16113+
});
1609116114
}
1609216115
function onUp(ev) {
1609316116
clearPendingShieldDrag();
@@ -16339,6 +16362,43 @@ export const editorChromeBridgeScript: string = `"use strict";
1633916362
},
1634016363
true
1634116364
);
16365+
try {
16366+
var parentDocument = window.parent.document;
16367+
var modifierListenerWindows = parentDocument.__agentNativeDesignModifierListeners || /* @__PURE__ */ new WeakMap();
16368+
parentDocument.__agentNativeDesignModifierListeners = modifierListenerWindows;
16369+
modifierListenerWindows.get(window)?.cleanup();
16370+
var onParentModifierKeyDown = function(e) {
16371+
if (!isApplePlatformBridge() && String(e.key).toLowerCase() === "s") {
16372+
bridgeIgnoreAutoLayoutKeyPressed = true;
16373+
}
16374+
};
16375+
var onParentModifierKeyUp = function(e) {
16376+
if (!isApplePlatformBridge() && String(e.key).toLowerCase() === "s") {
16377+
bridgeIgnoreAutoLayoutKeyPressed = false;
16378+
}
16379+
};
16380+
var cleanupParentModifierListeners = function() {
16381+
parentDocument.removeEventListener(
16382+
"keydown",
16383+
onParentModifierKeyDown,
16384+
true
16385+
);
16386+
parentDocument.removeEventListener("keyup", onParentModifierKeyUp, true);
16387+
if (modifierListenerWindows.get(window)?.cleanup === cleanupParentModifierListeners) {
16388+
modifierListenerWindows.delete(window);
16389+
}
16390+
};
16391+
parentDocument.addEventListener("keydown", onParentModifierKeyDown, true);
16392+
parentDocument.addEventListener("keyup", onParentModifierKeyUp, true);
16393+
modifierListenerWindows.set(window, {
16394+
cleanup: cleanupParentModifierListeners
16395+
});
16396+
window.addEventListener("unload", cleanupParentModifierListeners, {
16397+
once: true
16398+
});
16399+
} catch (_err) {
16400+
void _err;
16401+
}
1634216402
document.addEventListener(
1634316403
"keyup",
1634416404
function(e) {
@@ -16365,6 +16425,7 @@ export const editorChromeBridgeScript: string = `"use strict";
1636516425
window.setTimeout(function() {
1636616426
if (!activeDragCancel) {
1636716427
bridgeIgnoreAutoLayoutKeyPressed = false;
16428+
hostIgnoreAutoLayoutAtPointerDown = false;
1636816429
}
1636916430
}, 0);
1637016431
});
@@ -17198,6 +17259,22 @@ export const editorChromeBridgeScript: string = `"use strict";
1719817259
activateProgrammaticTextEdit(resumeTarget, false, resumeBookmark);
1719917260
return;
1720017261
}
17262+
if (e.data.type === "design-hotkey") {
17263+
if (!isApplePlatformBridge() && String(e.data.key).toLowerCase() === "s") {
17264+
bridgeIgnoreAutoLayoutKeyPressed = true;
17265+
}
17266+
return;
17267+
}
17268+
if (e.data.type === "agent-native:drag-modifiers") {
17269+
hostIgnoreAutoLayoutAtPointerDown = e.data.ignoreAutoLayout === true;
17270+
return;
17271+
}
17272+
if (e.data.type === "design-hotkey-up") {
17273+
if (!isApplePlatformBridge() && String(e.data.key).toLowerCase() === "s") {
17274+
bridgeIgnoreAutoLayoutKeyPressed = false;
17275+
}
17276+
return;
17277+
}
1720117278
if (e.data.type === "text-edit-inspector-focus") {
1720217279
if (typeof e.data.focused !== "boolean") return;
1720317280
textEditInspectorFocused = e.data.focused;

templates/design/app/components/design/MultiScreenCanvas.tsx

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
12001200
// Track the host key state before then so a chord held during the source drag
12011201
// is still present when that start initializes the host payload.
12021202
const crossScreenSKeyPressedRef = useRef(false);
1203+
const crossScreenControlPressedRef = useRef(false);
12031204
/** False once this canvas unmounts. Nothing may persist a drop after that.
12041205
* Mount-scoped on purpose: the message effect's cleanup also runs on every
12051206
* dependency change, and invalidating there kills live commits. */
@@ -2706,6 +2707,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
27062707
const clearCrossScreenDrag = () => {
27072708
crossScreenPreviewGenerationRef.current += 1;
27082709
stopParentCrossScreenDrag();
2710+
// The parent key listeners can be removed before a held Control keyup
2711+
// arrives. Clear the transient handoff state at the gesture boundary so
2712+
// a later drag cannot inherit a modifier from this one.
2713+
crossScreenIgnoreAutoLayoutRef.current = false;
2714+
crossScreenControlPressedRef.current = false;
27092715
clearCrossScreenPreviewGuide();
27102716
const previousClaim = crossScreenClaimSentRef.current;
27112717
if (previousClaim?.claimed) {
@@ -3642,13 +3648,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
36423648
};
36433649
const handleParentWindowBlur = () => {
36443650
cancelPendingParentDrag();
3645-
crossScreenIgnoreAutoLayoutRef.current = false;
36463651
// An iframe-focus handoff also emits blur on some browsers, while the
36473652
// top document remains focused. Only a real window blur may discard
3648-
// the S timeline before the source end message arrives.
3653+
// the modifier timeline before the source end message arrives.
36493654
if (
36503655
shouldClearCrossScreenSKeyTimesOnWindowBlur(document.hasFocus())
36513656
) {
3657+
crossScreenIgnoreAutoLayoutRef.current = false;
3658+
crossScreenControlPressedRef.current = false;
36523659
crossScreenSKeyTimesRef.current = { downAt: null, upAt: null };
36533660
}
36543661
clearCrossScreenDrag();
@@ -3672,6 +3679,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
36723679
}
36733680
};
36743681
const handleParentKeyDown = (ev: KeyboardEvent) => {
3682+
if (isApplePlatform() && ev.key === "Control") {
3683+
crossScreenControlPressedRef.current = true;
3684+
}
36753685
if (hostUsesSForIgnoreAutoLayout() && ev.key.toLowerCase() === "s") {
36763686
syncHostIgnoreAutoLayout(true, ev.timeStamp);
36773687
ev.preventDefault();
@@ -3700,6 +3710,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
37003710
clearCrossScreenDrag();
37013711
};
37023712
const handleParentKeyUp = (ev: KeyboardEvent) => {
3713+
if (isApplePlatform() && ev.key === "Control") {
3714+
crossScreenControlPressedRef.current = false;
3715+
}
37033716
if (hostUsesSForIgnoreAutoLayout() && ev.key.toLowerCase() === "s") {
37043717
syncHostIgnoreAutoLayout(false, ev.timeStamp);
37053718
ev.preventDefault();
@@ -3714,6 +3727,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
37143727
window.removeEventListener("blur", handleParentWindowBlur, true);
37153728
window.removeEventListener("keydown", handleParentKeyDown, true);
37163729
window.removeEventListener("keyup", handleParentKeyUp, true);
3730+
// Finalization may remove this listener before Apple Control is
3731+
// released; never carry its transient state into the next drag.
3732+
crossScreenControlPressedRef.current = false;
37173733
restorePreviewPointerEvents();
37183734
if (crossScreenParentDragCleanupRef.current === cleanup) {
37193735
crossScreenParentDragCleanupRef.current = null;
@@ -8158,6 +8174,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
81588174
{ type: "agent-native:cancel-active-drag", pressedAt },
81598175
"*",
81608176
);
8177+
crossScreenControlPressedRef.current = false;
81618178
};
81628179
boardElementResizeCancel.current = cancelResize;
81638180
installDragListeners(handleMouseMove, handleMouseUp, () => {
@@ -8214,22 +8231,26 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
82148231
altKey: boolean;
82158232
metaKey: boolean;
82168233
ctrlKey: boolean;
8234+
ignoreAutoLayout?: boolean;
82178235
},
82188236
buttons: number,
82198237
) => {
8220-
target.dispatchEvent(
8221-
new MouseEvent(type, {
8222-
clientX: point.x,
8223-
clientY: point.y,
8224-
shiftKey: source.shiftKey,
8225-
altKey: source.altKey,
8226-
metaKey: source.metaKey,
8227-
ctrlKey: source.ctrlKey,
8228-
buttons,
8229-
bubbles: true,
8230-
cancelable: true,
8231-
}),
8232-
);
8238+
const event = new MouseEvent(type, {
8239+
clientX: point.x,
8240+
clientY: point.y,
8241+
shiftKey: source.shiftKey,
8242+
altKey: source.altKey,
8243+
metaKey: source.metaKey,
8244+
ctrlKey: source.ctrlKey,
8245+
buttons,
8246+
bubbles: true,
8247+
cancelable: true,
8248+
});
8249+
Object.defineProperty(event, "__agentNativeIgnoreAutoLayout", {
8250+
configurable: true,
8251+
value: source.ignoreAutoLayout === true,
8252+
});
8253+
target.dispatchEvent(event);
82338254
};
82348255

82358256
const pressModifiers = {
@@ -8242,11 +8263,24 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
82428263
const startBridgeDrag = () => {
82438264
if (bridgeDragStarted) return;
82448265
bridgeDragStarted = true;
8266+
const ignoreAutoLayout =
8267+
crossScreenIgnoreAutoLayoutRef.current ||
8268+
crossScreenControlPressedRef.current ||
8269+
(isApplePlatform() &&
8270+
pressModifiers.ctrlKey &&
8271+
!pressModifiers.metaKey);
8272+
iframe.contentWindow?.postMessage(
8273+
{
8274+
type: "agent-native:drag-modifiers",
8275+
ignoreAutoLayout,
8276+
},
8277+
"*",
8278+
);
82458279
dispatchAt(
82468280
selectionOverlay,
82478281
"mousedown",
82488282
toIframePoint(e.clientX, e.clientY),
8249-
pressModifiers,
8283+
{ ...pressModifiers, ignoreAutoLayout },
82508284
1,
82518285
);
82528286
};
@@ -8264,11 +8298,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
82648298
);
82658299
};
82668300
const cancelMove = (pressedAt: number) => {
8301+
crossScreenIgnoreAutoLayoutRef.current = false;
8302+
crossScreenControlPressedRef.current = false;
82678303
if (!bridgeDragStarted) return;
82688304
iframe.contentWindow?.postMessage(
82698305
{ type: "agent-native:cancel-active-drag", pressedAt },
82708306
"*",
82718307
);
8308+
iframe.contentWindow?.postMessage(
8309+
{ type: "agent-native:drag-modifiers", ignoreAutoLayout: false },
8310+
"*",
8311+
);
8312+
crossScreenControlPressedRef.current = false;
82728313
};
82738314
const handleMouseUp = (ev: MouseEvent) => {
82748315
if (!bridgeDragStarted) {
@@ -8301,6 +8342,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
83018342
ev,
83028343
0,
83038344
);
8345+
iframe.contentWindow?.postMessage(
8346+
{ type: "agent-native:drag-modifiers", ignoreAutoLayout: false },
8347+
"*",
8348+
);
83048349
}
83058350
finishDrag();
83068351
};

0 commit comments

Comments
 (0)