Skip to content

Commit ecc429a

Browse files
committed
fix(studio): a move keeps the route it chose at press through drop and save
1 parent ed20d67 commit ecc429a

18 files changed

Lines changed: 266 additions & 56 deletions

‎packages/studio/src/components/editor/DomEditOverlay.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type BlockedMoveState,
1111
type DomEditGroupPathOffsetCommit,
1212
type FocusableDomEditOverlay,
13+
type MoveCommitOptions,
1314
type GestureState,
1415
type GroupGestureState,
1516
focusDomEditOverlayElement,
@@ -45,7 +46,7 @@ export {
4546
hasDomEditRotationChanged,
4647
resolveDomEditRotationGesture,
4748
} from "./domEditOverlayGestures";
48-
export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
49+
export type { DomEditGroupPathOffsetCommit, MoveCommitOptions } from "./domEditOverlayGestures";
4950

5051
export interface DomEditOverlayProps {
5152
iframeRef: RefObject<HTMLIFrameElement | null>;
@@ -81,7 +82,7 @@ export interface DomEditOverlayProps {
8182
onPathOffsetCommit: (
8283
selection: DomEditSelection,
8384
next: { x: number; y: number },
84-
modifiers?: { altKey?: boolean },
85+
modifiers?: MoveCommitOptions,
8586
) => Promise<unknown> | void;
8687
onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown> | void;
8788
onBoxSizeCommit: (

‎packages/studio/src/components/editor/domEditOverlayGestures.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,15 @@ export function hasDomEditRotationChanged(initialAngle: number, nextAngle: numbe
232232
// These live here (rather than in DomEditOverlay.tsx or useDomEditOverlayGestures.ts)
233233
// to break circular imports between those files.
234234

235+
export interface MoveCommitOptions {
236+
altKey?: boolean;
237+
plainTranslate?: boolean;
238+
}
239+
235240
export interface DomEditGroupPathOffsetCommit {
236241
selection: DomEditSelection;
237242
next: { x: number; y: number };
243+
plainTranslate?: boolean;
238244
}
239245

240246
// Refs are stable across renders; values are read via .current.
@@ -259,7 +265,7 @@ export type UseDomEditOverlayGesturesOptions = {
259265
(
260266
s: DomEditSelection,
261267
n: { x: number; y: number },
262-
m?: { altKey?: boolean },
268+
m?: MoveCommitOptions,
263269
) => Promise<unknown> | void
264270
>;
265271
onGroupPathOffsetCommitRef: RefObject<

‎packages/studio/src/components/editor/domEditOverlayStartGesture.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ function pressOptions(element: HTMLElement) {
4242
rafPausedRef: ref(false),
4343
onManualDragStartRef: ref(vi.fn()),
4444
onBlockedMoveRef: ref(vi.fn()),
45+
onPathOffsetCommitRef: ref(vi.fn()),
46+
snapGuidesRef: ref(null),
47+
groupGestureRef: ref(null),
48+
blockedMoveRef: ref(null),
49+
setOverlayRect: vi.fn(),
50+
suppressNextBoxClickRef: ref(false),
51+
hoverSelectionRef: ref(null),
52+
onCanvasMouseDown: vi.fn(),
4553
};
4654
}
4755

@@ -81,3 +89,27 @@ describe("a drag press on a centred element without GSAP", () => {
8189
expect(element.getAttribute("style")).toBe(style);
8290
});
8391
});
92+
93+
describe("a drag on an element without GSAP", () => {
94+
it("drops on the route it chose at press, even if GSAP takes the element over mid-drag", () => {
95+
const element = document.createElement("div");
96+
element.style.setProperty("translate", "40px 30px");
97+
document.body.append(element);
98+
const opts = pressOptions(element);
99+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
100+
expect(handlers.startGesture("drag", PRESS as never)).toBe(true);
101+
Object.assign(element, { _gsap: { renderTransform: () => {} } });
102+
const release = {
103+
...PRESS,
104+
clientX: 110,
105+
clientY: 70,
106+
currentTarget: { releasePointerCapture() {} },
107+
};
108+
handlers.onPointerUp(release as never);
109+
expect(opts.onPathOffsetCommitRef.current).toHaveBeenCalledWith(
110+
expect.anything(),
111+
expect.anything(),
112+
expect.objectContaining({ plainTranslate: true }),
113+
);
114+
});
115+
});

‎packages/studio/src/components/editor/plainTranslate.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,6 @@ export function writeTranslatePx(el: HTMLElement, p: Point): void {
8080
}
8181

8282
/** The position the panel shows and edits: the translate a move writes, unless GSAP positions it. */
83-
export function readMoveOffset(el: HTMLElement): Point {
84-
return gsapWritesPosition(el) ? readStudioPathOffset(el) : readTranslatePx(el);
83+
export function readMoveOffset(el: HTMLElement, plainTranslate = !gsapWritesPosition(el)): Point {
84+
return plainTranslate ? readTranslatePx(el) : readStudioPathOffset(el);
8585
}

‎packages/studio/src/components/editor/propertyPanelTransformCommit.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,32 @@ describe("createTransformCommitHandlers", () => {
5555
expect(onSetManualRotation).not.toHaveBeenCalled();
5656
});
5757
});
58+
59+
describe("the panel's position fields", () => {
60+
it("read and move an element without GSAP by its CSS translate, on the route they chose", async () => {
61+
const onSetManualOffset = vi.fn();
62+
const box = document.createElement("div");
63+
box.style.setProperty("translate", "40px 30px");
64+
const element = { id: "box", selector: "#box", element: box } as unknown as DomEditSelection;
65+
const handlers = createTransformCommitHandlers({
66+
element,
67+
styles: {},
68+
hasGsapAnimation: false,
69+
gsapAnimId: null,
70+
gsapKeyframes: null,
71+
currentPct: 0,
72+
onCommitAnimatedProperty: undefined,
73+
onAddKeyframe: undefined,
74+
onSetManualOffset,
75+
onSetManualSize: vi.fn(),
76+
onSetManualRotation: vi.fn(),
77+
showToast: vi.fn(),
78+
});
79+
await handlers.commitManualOffset("x", "100px");
80+
expect(onSetManualOffset).toHaveBeenCalledWith(
81+
element,
82+
{ x: 100, y: 30 },
83+
{ plainTranslate: true },
84+
);
85+
});
86+
});

‎packages/studio/src/components/editor/propertyPanelTransformCommit.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { gsapWritesPosition } from "../../hooks/gsapRuntimeKeyframes";
12
import type { DomEditSelection } from "./domEditingTypes";
23
import { readStudioBoxSize } from "./manualEdits";
34
import { readMoveOffset } from "./plainTranslate";
@@ -74,12 +75,14 @@ export function createTransformCommitHandlers({
7475
)
7576
)
7677
return;
77-
const current = readMoveOffset(element.element);
78+
const plainTranslate = !gsapWritesPosition(element.element);
79+
const current = readMoveOffset(element.element, plainTranslate);
7880
await Promise.resolve(
79-
onSetManualOffset(element, {
80-
x: axis === "x" ? parsed : current.x,
81-
y: axis === "y" ? parsed : current.y,
82-
}),
81+
onSetManualOffset(
82+
element,
83+
{ x: axis === "x" ? parsed : current.x, y: axis === "y" ? parsed : current.y },
84+
{ plainTranslate },
85+
),
8386
);
8487
};
8588

‎packages/studio/src/components/editor/propertyPanelTypes.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface PropertyPanelProps {
7979
onSetManualOffset: (
8080
element: DomEditSelection,
8181
next: { x: number; y: number },
82+
route?: { plainTranslate: boolean },
8283
) => void | Promise<void>;
8384
onSetManualSize: (
8485
element: DomEditSelection,

‎packages/studio/src/components/editor/useDomEditNudge.test.tsx‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,36 @@ describe("useDomEditNudge — selection cleanup keyed on stable identity", () =>
226226
elementB.remove();
227227
});
228228
});
229+
230+
describe("useDomEditNudge carries the route its press chose", () => {
231+
beforeEach(() => {
232+
vi.useFakeTimers();
233+
__resetForTests();
234+
});
235+
afterEach(() => vi.useRealTimers());
236+
237+
it("commits an element without GSAP on the CSS route", () => {
238+
const host = document.createElement("div");
239+
const root = createRoot(host);
240+
const element = document.createElement("div");
241+
document.body.append(element);
242+
const commit = vi.fn();
243+
act(() => {
244+
root.render(
245+
React.createElement(Harness, {
246+
selection: makeSelection("Dot", element),
247+
onPathOffsetCommit: commit,
248+
}),
249+
);
250+
});
251+
act(() => dispatchArrowRight());
252+
act(() => vi.advanceTimersByTime(CANVAS_NUDGE_COMMIT_DEBOUNCE_MS + 10));
253+
expect(commit).toHaveBeenCalledWith(
254+
expect.anything(),
255+
expect.anything(),
256+
expect.objectContaining({ plainTranslate: true }),
257+
);
258+
act(() => root.unmount());
259+
element.remove();
260+
});
261+
});

‎packages/studio/src/components/editor/useDomEditNudge.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import type {
1919
BlockedMoveState,
2020
DomEditGroupPathOffsetCommit,
21+
MoveCommitOptions,
2122
GestureState,
2223
GroupGestureState,
2324
} from "./domEditOverlayGestures";
@@ -59,7 +60,7 @@ export interface UseDomEditNudgeParams {
5960
(
6061
s: DomEditSelection,
6162
n: { x: number; y: number },
62-
m?: { altKey?: boolean },
63+
m?: MoveCommitOptions,
6364
) => Promise<unknown> | void
6465
>;
6566
onGroupPathOffsetCommitRef: RefObject<
@@ -149,11 +150,12 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
149150
const updates: DomEditGroupPathOffsetCommit[] = session.members.map((member) => ({
150151
selection: member.selection,
151152
next: applyManualOffsetNudgeCommit(member, session.accum),
153+
plainTranslate: member.plainTranslate,
152154
}));
153155
const p = paramsRef.current;
154156
const commit = session.isGroup
155157
? p.onGroupPathOffsetCommitRef.current(updates)
156-
: p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next);
158+
: p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next, updates[0]);
157159
void Promise.resolve(commit)
158160
.catch(() => {
159161
for (const member of session.members) {

‎packages/studio/src/components/editor/useDomEditOverlayGestures.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
336336
const updates = groupG.members.map((member) => ({
337337
selection: member.selection,
338338
next: applyManualOffsetDragCommit(member, dx, dy),
339+
plainTranslate: member.plainTranslate,
339340
}));
340341
logDrag("drop", {
341342
pointer: `${Math.round(rawDx)},${Math.round(rawDy)}`,
@@ -476,7 +477,10 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
476477
box.style.top = `${nextBoxTop}px`;
477478
}
478479
void Promise.resolve(
479-
opts.onPathOffsetCommitRef.current(sel, finalOffset, { altKey: e.altKey }),
480+
opts.onPathOffsetCommitRef.current(sel, finalOffset, {
481+
altKey: e.altKey,
482+
plainTranslate: g.pathOffsetMember.plainTranslate,
483+
}),
480484
)
481485
.catch(() => {
482486
if (

0 commit comments

Comments
 (0)