diff --git a/ui/src/components/resizable-divider.test.ts b/ui/src/components/resizable-divider.test.ts index c8eb066cf41f..263b4d66119c 100644 --- a/ui/src/components/resizable-divider.test.ts +++ b/ui/src/components/resizable-divider.test.ts @@ -74,14 +74,14 @@ async function renderDivider() { return divider; } -function dispatchPointer(target: EventTarget, type: string, clientX: number) { +function dispatchPointer(target: EventTarget, type: string, clientX: number, pointerId = 7) { target.dispatchEvent( new PointerEvent(type, { bubbles: true, button: 0, cancelable: true, clientX, - pointerId: 7, + pointerId, pointerType: "touch", }), ); @@ -204,7 +204,7 @@ describe("resizable-divider", () => { expectLastResizeRatio(resized, 0.65); }); - it("uses pointer events for mouse, pen, and touch dragging", async () => { + it("keeps dragging owned by the initiating pointer", async () => { const divider = await renderDivider(); const resized = vi.fn(); const resizeEnded = vi.fn(); @@ -222,17 +222,29 @@ describe("resizable-divider", () => { expect([...divider.classList]).toEqual(["dragging"]); expect(setPointerCapture).toHaveBeenCalledWith(7); - dispatchPointer(document, "pointermove", 220); + dispatchPointer(divider, "pointerdown", 180, 8); + dispatchPointer(document, "pointermove", 220, 8); + dispatchPointer(document, "pointercancel", 220, 8); + dispatchPointer(document, "pointerup", 220, 8); + + expect(setPointerCapture).toHaveBeenCalledTimes(1); + expect(resized).not.toHaveBeenCalled(); + expect(resizeEnded).not.toHaveBeenCalled(); + expect([...divider.classList]).toEqual(["dragging"]); + + dispatchPointer(document, "pointermove", 220, 7); expectLastResizeRatio(resized, 0.7); expect(resizeEnded).not.toHaveBeenCalled(); - dispatchPointer(document, "pointerup", 220); + dispatchPointer(document, "pointerup", 220, 7); const endEvent = resizeEnded.mock.lastCall?.[0] as | CustomEvent<{ splitRatio: number }> | undefined; expect(endEvent?.detail).toEqual({ splitRatio: 0.7 }); + expect(resizeEnded).toHaveBeenCalledTimes(1); expect([...divider.classList]).toEqual([]); expect(releasePointerCapture).toHaveBeenCalledWith(7); + expect(releasePointerCapture).toHaveBeenCalledTimes(1); }); it("stops dragging when the window loses focus", async () => { diff --git a/ui/src/components/resizable-divider.ts b/ui/src/components/resizable-divider.ts index 00b7662706ab..75d2c3d0525d 100644 --- a/ui/src/components/resizable-divider.ts +++ b/ui/src/components/resizable-divider.ts @@ -126,7 +126,7 @@ class ResizableDivider extends OpenClawLitElement { } private handlePointerDown = (e: PointerEvent) => { - if (e.button !== 0) { + if (e.button !== 0 || this.activePointerId !== null) { return; } this.startPosition = this.orientation === "horizontal" ? e.clientY : e.clientX; @@ -144,7 +144,7 @@ class ResizableDivider extends OpenClawLitElement { }; private handlePointerMove = (e: PointerEvent) => { - if (this.activePointerId === null) { + if (e.pointerId !== this.activePointerId) { return; } @@ -205,7 +205,10 @@ class ResizableDivider extends OpenClawLitElement { this.emitResizeEnd(nextRatio); }; - private readonly finishDragging = () => { + private readonly finishDragging = (event: Event) => { + if ("pointerId" in event && event.pointerId !== this.activePointerId) { + return; + } if (this.activePointerId !== null) { this.emitResizeEnd(this.dragRatio); } @@ -213,11 +216,12 @@ class ResizableDivider extends OpenClawLitElement { }; private stopDragging() { - if (this.activePointerId === null) { + const pointerId = this.activePointerId; + if (pointerId === null) { return; } this.classList.remove("dragging"); - this.releaseActivePointer(); + this.releaseActivePointer(pointerId); window.removeEventListener("pointermove", this.handlePointerMove); for (const type of DRAG_END_EVENTS) { @@ -281,10 +285,9 @@ class ResizableDivider extends OpenClawLitElement { this.setPointerCapture(pointerId); } - private releaseActivePointer() { - const pointerId = this.activePointerId; + private releaseActivePointer(pointerId: number) { this.activePointerId = null; - if (pointerId == null || typeof this.releasePointerCapture !== "function") { + if (typeof this.releasePointerCapture !== "function") { return; } if (typeof this.hasPointerCapture === "function" && !this.hasPointerCapture(pointerId)) {