fix(ui): keep resize drag owned by one pointer (#127760)

Amp-Thread-ID: https://ampcode.com/threads/T-01a021f4-b547-7788-a916-d4a94cbd3e3b

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-21 21:36:10 -07:00
committed by GitHub
parent 6cb08cb8ee
commit 1bd5f22b13
2 changed files with 28 additions and 13 deletions
+17 -5
View File
@@ -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 () => {
+11 -8
View File
@@ -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)) {