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:
co-authored by
Claude Opus 5.5
parent
340b4f3447
commit
28a44853e3
@@ -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)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user