From 102c50318bdcf82c26a8ae1df0a48e12be6551cd Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Fri, 10 Jul 2026 16:44:03 -0700 Subject: [PATCH] fix(studio): support drag on FlatSlider, not just click-to-set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only onPointerDown was wired, so dragging the knob/track only ever committed the initial click position — nothing tracked the pointer after that. Uses the Pointer Capture API (setPointerCapture on pointerdown, onPointerMove while captured, release on pointerup) so the value follows the cursor continuously during a drag, matching how the legacy native control behaves for free. Co-Authored-By: Claude Sonnet 5 --- .../propertyPanelFlatPrimitives.test.tsx | 76 +++++++++++++++++++ .../editor/propertyPanelFlatPrimitives.tsx | 10 +++ 2 files changed, 86 insertions(+) diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx index dcb8dd6dd4..0e3d5eb928 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx @@ -280,6 +280,82 @@ describe("FlatSlider", () => { expect(onCommit).toHaveBeenCalledWith(10); act(() => root.unmount()); }); + + it("commits continuously while dragging, not just on the initial pointerdown", () => { + const onCommit = vi.fn(); + const { host, root } = renderInto( + , + ); + const track = host.querySelector('[data-flat-slider-track="true"]'); + if (!track) throw new Error("expected a track element"); + Object.defineProperty(track, "getBoundingClientRect", { + value: () => ({ left: 0, width: 200, top: 0, height: 20, right: 200, bottom: 20 }), + }); + act(() => { + track.dispatchEvent( + new PointerEvent("pointerdown", { bubbles: true, clientX: 20, pointerId: 1 }), + ); + }); + expect(onCommit).toHaveBeenLastCalledWith(10); + act(() => { + track.dispatchEvent( + new PointerEvent("pointermove", { bubbles: true, clientX: 160, pointerId: 1 }), + ); + }); + expect(onCommit).toHaveBeenLastCalledWith(80); + act(() => { + track.dispatchEvent( + new PointerEvent("pointermove", { bubbles: true, clientX: 100, pointerId: 1 }), + ); + }); + expect(onCommit).toHaveBeenLastCalledWith(50); + act(() => { + track.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, pointerId: 1 })); + }); + act(() => root.unmount()); + }); + + it("ignores pointermove once a drag has ended (pointer capture released)", () => { + const onCommit = vi.fn(); + const { host, root } = renderInto( + , + ); + const track = host.querySelector('[data-flat-slider-track="true"]'); + if (!track) throw new Error("expected a track element"); + Object.defineProperty(track, "getBoundingClientRect", { + value: () => ({ left: 0, width: 200, top: 0, height: 20, right: 200, bottom: 20 }), + }); + act(() => { + track.dispatchEvent( + new PointerEvent("pointerdown", { bubbles: true, clientX: 20, pointerId: 1 }), + ); + track.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, pointerId: 1 })); + }); + onCommit.mockClear(); + act(() => { + track.dispatchEvent( + new PointerEvent("pointermove", { bubbles: true, clientX: 160, pointerId: 1 }), + ); + }); + expect(onCommit).not.toHaveBeenCalled(); + act(() => root.unmount()); + }); }); describe("FlatSlider — Grade extensions", () => { diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx index f2cdf6220b..52eba79a3e 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx @@ -262,8 +262,18 @@ export function FlatSlider({ className={`relative h-5 flex-1 ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`} onPointerDown={(e) => { if (disabled) return; + e.currentTarget.setPointerCapture(e.pointerId); commitFromClientX(e.clientX, e.currentTarget.getBoundingClientRect()); }} + onPointerMove={(e) => { + if (disabled || !e.currentTarget.hasPointerCapture(e.pointerId)) return; + commitFromClientX(e.clientX, e.currentTarget.getBoundingClientRect()); + }} + onPointerUp={(e) => { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } + }} >
{centerTick && (