Colour widget: commit the value being released

The brightness column changes and commits in one handler, so the commit
read the render's draft (still null) and sent nothing; the queued draft then
stuck and rode along with the next release. The pending colour lives in a ref
now. color-drag.spec gains a column drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Melvin Strobl
2026-10-14 08:44:38 +02:00
co-authored by Claude Opus 5.5
parent 340b4f3447
commit 28a44853e3
2 changed files with 59 additions and 9 deletions
@@ -1,4 +1,4 @@
import { useState } from "react"
import { useRef, useState } from "react"
import { usePublish } from "./publish"
import { useUi } from "./ui"
@@ -46,6 +46,10 @@ export function ColorWidget({ widget, dashboard }: WidgetProps) {
const { target, value, send, pulse, locked } = usePublish(widget, dashboard)
// While dragging, the disc follows the finger rather than the engine.
const [draft, setDraft] = useState<Triple | null>(null)
// What a release sends. The brightness column changes and commits in one
// handler, before `setDraft` has rendered, so the state would still be the
// last render's — read the value from here, which is set the moment it moves.
const pending = useRef<Triple | null>(null)
if (!target) return <p className="text-muted-foreground">Pick a message.</p>
const format = colorFormatOf(widget)
@@ -59,10 +63,14 @@ export function ColorWidget({ widget, dashboard }: WidgetProps) {
name={widget.title || target}
hsv={current}
disabled={locked}
onChange={setDraft}
onChange={(hsv) => {
pending.current = hsv
setDraft(hsv)
}}
onCommit={() => {
if (draft === null) return
send(encodeColor(draft, format))
if (pending.current === null) return
send(encodeColor(pending.current, format))
pending.current = null
setDraft(null)
}}
/>
+47 -5
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test"
import { expect, type Page, test } from "@playwright/test"
import { api, apiPage, deleteAll } from "./utils/api"
@@ -19,6 +19,7 @@ const dashboard = `${flowName}_d`
const target = `${flowName}.tint`
test.use({ storageState: "playwright/.auth/user.json" })
test.describe.configure({ mode: "serial" })
test.beforeAll(async ({ browser }) => {
const page = await apiPage(browser)
@@ -86,12 +87,12 @@ test.afterAll(async ({ browser }) => {
await deleteAll(browser, [`/dashboards/${dashboard}`, `/flows/${flowName}`])
})
test("a dragged disc publishes once, where it was let go", async ({ page }) => {
/** Open the panel, settled, and collect every publish the widget makes. */
async function openBoard(page: Page) {
await page.goto(`/view/${dashboard}`)
const disc = page.getByTestId("color-wheel")
await disc.waitFor({ timeout: 20000 })
await page.getByTestId("color-wheel").waitFor({ timeout: 20000 })
// A tile fades and scales in, so where the disc is drawn is only settled
// once that has finished — and the gesture below is aimed in pixels.
// once that has finished — and the gestures below are aimed in pixels.
await page.waitForFunction(
() =>
[...document.querySelectorAll(".widget-cell")].every(
@@ -111,7 +112,12 @@ test("a dragged disc publishes once, where it was let go", async ({ page }) => {
)
sent.push(request.postDataJSON()?.value)
})
return sent
}
test("a dragged disc publishes once, where it was let go", async ({ page }) => {
const sent = await openBoard(page)
const disc = page.getByTestId("color-wheel")
const box = (await disc.boundingBox())!
const middle = { x: box.x + box.width / 2, y: box.y + box.height / 2 }
@@ -169,3 +175,39 @@ test("a dragged disc publishes once, where it was let go", async ({ page }) => {
)
.toEqual(sent[0])
})
test("a dragged brightness column publishes where it was let go", async ({
page,
}) => {
const sent = await openBoard(page)
const column = page.getByRole("slider", { name: "Tint brightness" })
const before = Number(await column.inputValue())
const box = (await column.boundingBox())!
const x = box.x + box.width / 2
const bottom = box.y + box.height - 2
const end = box.y + box.height * 0.3
// From the foot of the column up to three tenths from its top. The column
// reports nothing while it moves, so the release alone decides what is
// sent — and it has to send what the column was let go at. That is read
// off the column rather than worked out from the pixels: Playwright's
// WebKit reads a `writing-mode` range across, not along.
await page.mouse.move(x, bottom)
await page.mouse.down()
for (let step = 1; step <= 8; step++) {
await page.mouse.move(x, bottom - ((bottom - end) * step) / 8, {
steps: 2,
})
}
const letGo = Number(await column.inputValue())
expect(letGo, "the drag never moved the column").not.toBe(before)
await page.mouse.up()
await expect
.poll(() => sent.length, {
timeout: 10000,
message: "letting go of the column published nothing",
})
.toBe(1)
expect(sent[0][2], "the release sent another brightness").toBe(letGo)
})