diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx
index 09b1e1038..dda93de0d 100644
--- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx
+++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx
@@ -774,6 +774,54 @@ describe("FlatSlider — Grade extensions", () => {
expect(track.getAttribute("aria-valuenow")).toBe("80");
act(() => root.unmount());
});
+
+ it("resets the dragging state on lostpointercapture even without a prior pointerup/pointercancel", () => {
+ 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: 100, top: 0, height: 20, right: 100, bottom: 20 }),
+ });
+ act(() => {
+ track.dispatchEvent(
+ new PointerEvent("pointerdown", { bubbles: true, clientX: 30, pointerId: 1 }),
+ );
+ });
+ expect(track.getAttribute("aria-valuenow")).toBe("30");
+ act(() => {
+ // Capture lost WITHOUT a pointerup/pointercancel first — e.g. another
+ // element steals it, or the browser reclaims it for a scroll gesture.
+ track.dispatchEvent(new Event("lostpointercapture", { bubbles: true }));
+ });
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ // If lostpointercapture hadn't cleared the dragging flag, this external
+ // value change would be silently ignored (mid-drag echo suppression)
+ // forever — the knob would be stuck at 30.
+ expect(track.getAttribute("aria-valuenow")).toBe("99");
+ act(() => root.unmount());
+ });
});
describe("FlatSelectRow", () => {
@@ -813,6 +861,28 @@ describe("FlatSelectRow", () => {
act(() => root.unmount());
});
+ it("disables the reset button (and gives the select an accessible name) when the row itself is disabled", () => {
+ const onReset = vi.fn();
+ const { host, root } = renderInto(
+ ,
+ );
+ const select = host.querySelector("select");
+ expect(select?.getAttribute("aria-label")).toBe("Shadow");
+ const reset = host.querySelector('[data-flat-select-reset="true"]');
+ expect(reset?.disabled).toBe(true);
+ act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
+ expect(onReset).not.toHaveBeenCalled();
+ act(() => root.unmount());
+ });
+
it("fires onChange when the select value changes", () => {
const onChange = vi.fn();
const { host, root } = renderInto(
diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx
index df7120b2c..7e5afca27 100644
--- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx
+++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx
@@ -404,6 +404,14 @@ export function FlatSlider({
e.currentTarget.releasePointerCapture(e.pointerId);
}
}}
+ onLostPointerCapture={() => {
+ // Capture can be lost without either pointerup or pointercancel
+ // firing first (e.g. another element steals it, or the browser
+ // reclaims it for a scroll/touch gesture) — without this,
+ // draggingRef stays stuck true and the knob permanently stops
+ // syncing to the committed value prop.
+ draggingRef.current = false;
+ }}
onKeyDown={(e) => {
if (disabled) return;
const next = sliderKeyTarget(e.key, draft, min, max, step);
@@ -507,6 +515,7 @@ export function FlatSelectRow({