Skip to content

Commit c7cc318

Browse files
committed
fix(webapp): restore floating rect on zoned drop; client coords for dock zones
1 parent d2e20f5 commit c7cc318

3 files changed

Lines changed: 47 additions & 24 deletions

File tree

apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ function fakePanInfo(dx: number, dy: number): PanInfo {
8686
};
8787
}
8888

89-
function fakePanInfoAt(point: { x: number; y: number }): PanInfo {
90-
return { delta: { x: 0, y: 0 }, offset: { x: 0, y: 0 }, point, velocity: { x: 0, y: 0 } };
89+
// Dock zones read the pointer event's client coordinates, not PanInfo.point (which is page
90+
// coordinates), so drag-to-dock tests build events carrying clientX/clientY.
91+
function fakePointerEventAt(target: PointerEvent["target"], x: number, y: number): PointerEvent {
92+
return { target, clientX: x, clientY: y } as unknown as PointerEvent;
9193
}
9294

9395
describe("the floating window's rect, wired with panel-layout's own constants", () => {
@@ -198,32 +200,34 @@ describe("FloatingAgentWindow's drag-to-dock zones", () => {
198200
const target = view.titleEl() as unknown as PointerEvent["target"];
199201

200202
act(() => {
201-
view.dragHandleProps.onPanStart!({ target } as PointerEvent, fakePanInfoAt({ x: 0, y: 0 }));
202-
view.dragHandleProps.onPan!({ target } as PointerEvent, fakePanInfoAt({ x: 1190, y: 400 }));
203+
view.dragHandleProps.onPanStart!(fakePointerEventAt(target, 0, 0), fakePanInfo(0, 0));
204+
view.dragHandleProps.onPan!(fakePointerEventAt(target, 1190, 400), fakePanInfo(-20, 0));
203205
});
204206

205207
expect(document.body.textContent).toContain("Dock right");
206208
});
207209

208-
it("calls onRequestModeChange with rightPanel on release in the right zone, without a rect jump", () => {
210+
it("restores the pre-drag rect and calls onRequestModeChange(rightPanel) on release in the right zone", () => {
209211
stubViewport(1200, 900);
210212
const onRequestModeChange = vi.fn();
211213
const view = renderFloatingAgentWindow(onRequestModeChange);
212214
const target = view.titleEl() as unknown as PointerEvent["target"];
215+
const leftBeforeDrag = view.outerLeft();
213216

214217
act(() => {
215-
view.dragHandleProps.onPanStart!({ target } as PointerEvent, fakePanInfoAt({ x: 0, y: 0 }));
216-
view.dragHandleProps.onPan!({ target } as PointerEvent, fakePanInfoAt({ x: 1190, y: 400 }));
218+
view.dragHandleProps.onPanStart!(fakePointerEventAt(target, 0, 0), fakePanInfo(0, 0));
219+
view.dragHandleProps.onPan!(fakePointerEventAt(target, 1190, 400), fakePanInfo(-200, 0));
217220
});
218-
const leftBeforeRelease = view.outerLeft();
221+
// The drag really did move the rect, so the restore below undoes a real change.
222+
expect(view.outerLeft()).not.toBe(leftBeforeDrag);
219223

220224
act(() => {
221-
view.dragHandleProps.onPanEnd!({ target } as PointerEvent, fakePanInfoAt({ x: 1190, y: 400 }));
225+
view.dragHandleProps.onPanEnd!(fakePointerEventAt(target, 1190, 400), fakePanInfo(0, 0));
222226
});
223227

224228
expect(onRequestModeChange).toHaveBeenCalledTimes(1);
225229
expect(onRequestModeChange).toHaveBeenCalledWith("rightPanel");
226-
expect(view.outerLeft()).toBe(leftBeforeRelease);
230+
expect(view.outerLeft()).toBe(leftBeforeDrag);
227231
expect(document.body.textContent).not.toContain("Dock right");
228232
});
229233

@@ -234,13 +238,13 @@ describe("FloatingAgentWindow's drag-to-dock zones", () => {
234238
const target = view.titleEl() as unknown as PointerEvent["target"];
235239

236240
act(() => {
237-
view.dragHandleProps.onPanStart!({ target } as PointerEvent, fakePanInfoAt({ x: 0, y: 0 }));
238-
view.dragHandleProps.onPan!({ target } as PointerEvent, fakePanInfoAt({ x: 500, y: 5 }));
241+
view.dragHandleProps.onPanStart!(fakePointerEventAt(target, 0, 0), fakePanInfo(0, 0));
242+
view.dragHandleProps.onPan!(fakePointerEventAt(target, 500, 5), fakePanInfo(0, -20));
239243
});
240244
expect(document.body.textContent).toContain("Fullscreen");
241245

242246
act(() => {
243-
view.dragHandleProps.onPanEnd!({ target } as PointerEvent, fakePanInfoAt({ x: 500, y: 5 }));
247+
view.dragHandleProps.onPanEnd!(fakePointerEventAt(target, 500, 5), fakePanInfo(0, 0));
244248
});
245249

246250
expect(onRequestModeChange).toHaveBeenCalledTimes(1);
@@ -255,11 +259,11 @@ describe("FloatingAgentWindow's drag-to-dock zones", () => {
255259
const startLeft = view.outerLeft();
256260

257261
act(() => {
258-
view.dragHandleProps.onPanStart!({ target } as PointerEvent, fakePanInfo(0, 0));
259-
view.dragHandleProps.onPan!({ target } as PointerEvent, fakePanInfo(-20, 0));
262+
view.dragHandleProps.onPanStart!(fakePointerEventAt(target, 0, 0), fakePanInfo(0, 0));
263+
view.dragHandleProps.onPan!(fakePointerEventAt(target, 500, 400), fakePanInfo(-20, 0));
260264
});
261265
act(() => {
262-
view.dragHandleProps.onPanEnd!({ target } as PointerEvent, fakePanInfoAt({ x: 500, y: 400 }));
266+
view.dragHandleProps.onPanEnd!(fakePointerEventAt(target, 500, 400), fakePanInfo(0, 0));
263267
});
264268

265269
expect(onRequestModeChange).not.toHaveBeenCalled();

apps/webapp/app/components/dashboard-agent/panel-layout.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
dockZoneForPoint,
1414
type DockZone,
1515
type Point,
16+
type Rect,
1617
} from "~/components/primitives/draggableResizableMath";
1718
import { cn } from "~/utils/cn";
1819

@@ -134,24 +135,30 @@ export function FloatingAgentWindow({
134135
const fullscreen = mode === "fullscreen";
135136
const docked = mode === "rightPanel";
136137
const initial = useMemo(() => initialFloatingRect(), []);
137-
const { style, dragHandleProps, resizeHandleProps } = useDraggableResizable({
138-
initial,
139-
minSize: FLOATING_MIN_SIZE,
140-
viewportPadding: FLOATING_MARGIN,
141-
});
138+
const { style, dragHandleProps, resizeHandleProps, position, size, setRect } =
139+
useDraggableResizable({
140+
initial,
141+
minSize: FLOATING_MIN_SIZE,
142+
viewportPadding: FLOATING_MARGIN,
143+
});
142144
const [dragging, setDragging] = useState(false);
143145
const [dockZone, setDockZone] = useState<DockZone | null>(null);
144146
// onPan can arrive before onPanStart, so the no-drag check runs once, on whichever fires first.
145147
const gestureClassified = useRef(false);
146148
const ignoringGesture = useRef(false);
149+
// The rect as it stood before this gesture, so a zoned drop can restore it — dragHandleProps.onPan
150+
// (called below) has already folded the drop point's delta into the hook's own state by then.
151+
const preDragRect = useRef<Rect | null>(null);
147152

148153
const classifyGesture = (event: PointerEvent) => {
149154
if (gestureClassified.current) return;
150155
gestureClassified.current = true;
151156
ignoringGesture.current = !!(event.target as HTMLElement | null)?.closest(NO_DRAG_SELECTOR);
152157
};
153158

154-
const zoneForPoint = (point: Point) =>
159+
// PanInfo.point is page coordinates; the dock zones compare against the viewport, so use the
160+
// pointer event's client coordinates instead.
161+
const zoneForClientPoint = (point: Point) =>
155162
dockZoneForPoint(point, { width: window.innerWidth, height: window.innerHeight });
156163

157164
// Same shape as `dragHandleProps` below empty, so a mode with no drag doesn't change types.
@@ -162,13 +169,17 @@ export function FloatingAgentWindow({
162169
onPanStart: (event: PointerEvent, info: PanInfo) => {
163170
classifyGesture(event);
164171
if (ignoringGesture.current) return;
172+
preDragRect.current = { ...position, ...size };
165173
setDragging(true);
166174
dragHandleProps.onPanStart?.(event, info);
167175
},
168176
onPan: (event: PointerEvent, info: PanInfo) => {
169177
classifyGesture(event);
170178
if (ignoringGesture.current) return;
171-
setDockZone(zoneForPoint(info.point));
179+
// onPan can arrive before onPanStart, so both capture the pre-drag rect and flip dragging.
180+
if (!preDragRect.current) preDragRect.current = { ...position, ...size };
181+
setDragging(true);
182+
setDockZone(zoneForClientPoint({ x: event.clientX, y: event.clientY }));
172183
dragHandleProps.onPan?.(event, info);
173184
},
174185
onPanEnd: (event: PointerEvent, info: PanInfo) => {
@@ -177,8 +188,13 @@ export function FloatingAgentWindow({
177188
ignoringGesture.current = false;
178189
setDragging(false);
179190
setDockZone(null);
180-
const droppedZone = wasIgnoring ? null : zoneForPoint(info.point);
191+
const droppedZone = wasIgnoring
192+
? null
193+
: zoneForClientPoint({ x: event.clientX, y: event.clientY });
194+
const rectBeforeDrag = preDragRect.current;
195+
preDragRect.current = null;
181196
if (droppedZone) {
197+
if (rectBeforeDrag) setRect(rectBeforeDrag);
182198
onRequestModeChange?.(droppedZone);
183199
return;
184200
}

apps/webapp/app/components/primitives/DraggableResizable.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export type UseDraggableResizableResult = {
3838
resizeHandleProps: (edge: ResizeEdge) => PanHandlerProps;
3939
position: Point;
4040
size: Size;
41+
/** Escape hatch to force the rect, e.g. restoring it after a drag that ended somewhere else (a dock zone). */
42+
setRect: (rect: Rect) => void;
4143
};
4244

4345
function getViewport(): Viewport {
@@ -111,6 +113,7 @@ export function useDraggableResizable({
111113
resizeHandleProps,
112114
position: { x: rect.x, y: rect.y },
113115
size: { w: rect.w, h: rect.h },
116+
setRect,
114117
};
115118
}
116119

0 commit comments

Comments
 (0)