Skip to content

Commit c59cf0e

Browse files
committed
fix(webapp): classify a drag gesture on whichever pan event lands first
onPan can arrive before onPanStart, so a gesture starting on a data-agent-no-drag element could leak its first delta(s) before ignoringGesture was set. Both handlers now classify the gesture once (gestureClassified) using whichever event's target shows up first. New test drives onPan before onPanStart from a no-drag target and asserts zero movement. Also drop a gratuitous useMemo over a 25-item array in storybook.ai-agent's DotGridEditor (CodeRabbit nitpick).
1 parent 8ce45c3 commit c59cf0e

3 files changed

Lines changed: 32 additions & 13 deletions

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,20 @@ describe("FloatingAgentWindow's drag-vs-click filter", () => {
158158

159159
expect(view.outerLeft()).toBe(startLeft);
160160
});
161+
162+
it("does not leak a delta when onPan for a no-drag target lands before its onPanStart", () => {
163+
stubViewport(1200, 900);
164+
const view = renderFloatingAgentWindow();
165+
const startLeft = view.outerLeft();
166+
167+
act(() => {
168+
const target = view.actionEl() as unknown as PointerEvent["target"];
169+
// Framer-motion's real ordering: onPan can arrive first.
170+
view.dragHandleProps.onPan!({ target } as PointerEvent, fakePanInfo(-20, 0));
171+
view.dragHandleProps.onPanStart!({ target } as PointerEvent, fakePanInfo(0, 0));
172+
view.dragHandleProps.onPan!({ target } as PointerEvent, fakePanInfo(-20, 0));
173+
});
174+
175+
expect(view.outerLeft()).toBe(startLeft);
176+
});
161177
});

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ export function FloatingAgentWindow({
8989
viewportPadding: FLOATING_MARGIN,
9090
});
9191
const [dragging, setDragging] = useState(false);
92-
// Set for the rest of a gesture that started on a no-drag element, since framer-motion
93-
// can deliver onPan before onPanStart and the target is only known at start.
92+
// Framer-motion can deliver onPan before onPanStart, so the no-drag check runs on
93+
// whichever event lands first; `gestureClassified` makes sure it only runs once per
94+
// gesture (a late onPanStart must not re-decide after onPan already classified it).
95+
const gestureClassified = useRef(false);
9496
const ignoringGesture = useRef(false);
9597

9698
if (fullscreen) {
@@ -101,21 +103,26 @@ export function FloatingAgentWindow({
101103
);
102104
}
103105

106+
const classifyGesture = (event: PointerEvent) => {
107+
if (gestureClassified.current) return;
108+
gestureClassified.current = true;
109+
ignoringGesture.current = !!(event.target as HTMLElement | null)?.closest(NO_DRAG_SELECTOR);
110+
};
111+
104112
const filteredDragHandleProps: Partial<PanHandlerProps> = {
105113
onPanStart: (event: PointerEvent, info: PanInfo) => {
106-
if ((event.target as HTMLElement | null)?.closest(NO_DRAG_SELECTOR)) {
107-
ignoringGesture.current = true;
108-
return;
109-
}
110-
ignoringGesture.current = false;
114+
classifyGesture(event);
115+
if (ignoringGesture.current) return;
111116
setDragging(true);
112117
dragHandleProps.onPanStart?.(event, info);
113118
},
114119
onPan: (event: PointerEvent, info: PanInfo) => {
120+
classifyGesture(event);
115121
if (ignoringGesture.current) return;
116122
dragHandleProps.onPan?.(event, info);
117123
},
118124
onPanEnd: (event: PointerEvent, info: PanInfo) => {
125+
gestureClassified.current = false;
119126
ignoringGesture.current = false;
120127
setDragging(false);
121128
dragHandleProps.onPanEnd?.(event, info);

apps/webapp/app/routes/storybook.ai-agent/route.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,8 @@ function DotGridEditor() {
255255
setLit((current) => current.map((value, i) => (i === index ? !value : value)));
256256
};
257257

258-
const rows = useMemo(
259-
() =>
260-
Array.from({ length: MATRIX }, (_, r) =>
261-
Array.from({ length: MATRIX }, (_, c) => (lit[r * MATRIX + c] ? "#" : ".")).join("")
262-
),
263-
[lit]
258+
const rows = Array.from({ length: MATRIX }, (_, r) =>
259+
Array.from({ length: MATRIX }, (_, c) => (lit[r * MATRIX + c] ? "#" : ".")).join("")
264260
);
265261

266262
return (

0 commit comments

Comments
 (0)