mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 11:39:41 +03:00
fix(app): stop question dock motion when its knob turns off (#59407)
This commit is contained in:
committed by
GitHub
Unverified
parent
a0dca06854
commit
8eb272c8df
@@ -1,5 +1,6 @@
|
||||
import { expect, story } from "../../stories/playwright/story"
|
||||
import type { Locator } from "@playwright/test"
|
||||
import type { MotionKnob } from "@opencode/ui/motion-tuning"
|
||||
import { expect, sourceURL, story } from "../../stories/playwright/story"
|
||||
|
||||
story("resizes the dock to fit different answer counts", async ({ mount }) => {
|
||||
const component = await mount("app-current-session-surface--question-answer-counts")
|
||||
@@ -639,6 +640,42 @@ story.describe("with the question dock's motion on", () => {
|
||||
expect(leave.at(-1)).toMatchObject({ next: 1, submit: 0, footer: false })
|
||||
await expect(back).toHaveAttribute("inert", "")
|
||||
})
|
||||
|
||||
story("stops a switch where it is when the knob turns off", async ({ mount }) => {
|
||||
const component = await mount("session-question-switch--question-series")
|
||||
const question = component.locator('[data-component="dock-prompt"][data-kind="question"]')
|
||||
await expect(question.getByRole("radio").first()).toBeFocused()
|
||||
|
||||
const frames = await question.evaluate(
|
||||
async (root, tuning) => {
|
||||
const { motionTuning } = await import(tuning)
|
||||
const knob = motionTuning.list().find((item: MotionKnob) => item.id === "question.motion")
|
||||
const frame = () => new Promise((resolve) => requestAnimationFrame(resolve))
|
||||
|
||||
const sample = () => ({
|
||||
animations: document.getAnimations().length,
|
||||
heights: [...root.querySelectorAll<HTMLElement>("*")].filter((el) => el.style.height).length,
|
||||
ghosts: root.querySelectorAll("[data-ghost]").length,
|
||||
})
|
||||
|
||||
document.activeElement!.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }),
|
||||
)
|
||||
await frame()
|
||||
await frame()
|
||||
const moving = sample()
|
||||
motionTuning.set(knob, false)
|
||||
await frame()
|
||||
await frame()
|
||||
|
||||
return { moving, after: sample() }
|
||||
},
|
||||
sourceURL(new URL("../../ui/src/motion-tuning.ts", import.meta.url)),
|
||||
)
|
||||
|
||||
expect(frames.moving.heights + frames.moving.ghosts).toBeGreaterThan(0)
|
||||
expect(frames.after).toEqual({ animations: 0, heights: 0, ghosts: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
story("switches questions, folds and submits on the same frame with the dock's motion off", async ({ mount, page }) => {
|
||||
|
||||
@@ -267,6 +267,13 @@ export function ComposerMorph<T extends { key: string }>(props: {
|
||||
),
|
||||
)
|
||||
|
||||
// A face that turns instant mid-flight lands at once.
|
||||
createEffect(() => {
|
||||
const list = untrack(faces)
|
||||
|
||||
if (stop && list.some((item) => props.instant?.(item.value()))) settle(list)
|
||||
})
|
||||
|
||||
onCleanup(observeReducedMotion(() => prefersReducedMotion() && stop && settle(untrack(faces))))
|
||||
onCleanup(() => stop?.())
|
||||
|
||||
@@ -357,6 +364,16 @@ export function Grow<T>(props: {
|
||||
let el: HTMLDivElement | undefined
|
||||
let entering = !!props.when && !!props.appear
|
||||
|
||||
const closed = () => {
|
||||
setPresent(false)
|
||||
props.onClosed?.()
|
||||
}
|
||||
|
||||
// Turning instant mid-grow finishes the grow where it was going.
|
||||
createEffect(() => {
|
||||
if (props.instant && el) growing.get(el)?.end?.()
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [!!props.when, present()] as const,
|
||||
@@ -370,18 +387,22 @@ export function Grow<T>(props: {
|
||||
if (!el || (!entering && (!prev || prev[0] === when))) return
|
||||
entering = false
|
||||
|
||||
if (props.instant && !growing.has(el)) {
|
||||
if (props.instant) {
|
||||
const current = growing.get(el)
|
||||
|
||||
if (current) {
|
||||
current.open = when
|
||||
current.done = closed
|
||||
|
||||
return current.end?.()
|
||||
}
|
||||
|
||||
if (when) return
|
||||
|
||||
setPresent(false)
|
||||
|
||||
return props.onClosed?.()
|
||||
return closed()
|
||||
}
|
||||
|
||||
grow(el, when, props.fade, props.card, props.close?.(), () => {
|
||||
setPresent(false)
|
||||
props.onClosed?.()
|
||||
})
|
||||
grow(el, when, props.fade, props.card, props.close?.(), closed)
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -409,7 +430,9 @@ export function Grow<T>(props: {
|
||||
)
|
||||
}
|
||||
|
||||
const growing = new WeakMap<HTMLElement, { spring: Spring; presence: Spring; open: boolean; done?: () => void }>()
|
||||
type Growth = { spring: Spring; presence: Spring; open: boolean; done?: () => void; end?: () => void }
|
||||
|
||||
const growing = new WeakMap<HTMLElement, Growth>()
|
||||
|
||||
function grow(
|
||||
el: HTMLElement,
|
||||
@@ -436,7 +459,7 @@ function grow(
|
||||
const prevMb =
|
||||
(!mb && el.previousElementSibling ? parseFloat(getComputedStyle(el.previousElementSibling).marginBottom) : 0) || 0
|
||||
|
||||
const state = {
|
||||
const state: Growth = {
|
||||
spring: Spring.fromDuration(
|
||||
open ? 0 : el.getBoundingClientRect().height,
|
||||
reduced ? fade : open ? morph() : (close ?? morph()),
|
||||
@@ -475,7 +498,8 @@ function grow(
|
||||
})
|
||||
|
||||
if (child) observer.observe(child)
|
||||
frameLoop.add((dt) => {
|
||||
|
||||
const stop = frameLoop.add((dt) => {
|
||||
if (!el.isConnected) {
|
||||
observer.disconnect()
|
||||
growing.delete(el)
|
||||
@@ -527,6 +551,13 @@ function grow(
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
state.end = () => {
|
||||
stop()
|
||||
observer.disconnect()
|
||||
growing.delete(el)
|
||||
finish(el, state.open, state.done)
|
||||
}
|
||||
}
|
||||
|
||||
function finish(el: HTMLElement, open: boolean, done?: () => void) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { DockPrompt } from "@opencode/session-ui/dock-prompt"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { frameLoop, observeReducedMotion, Spring, type DurationOptions } from "@opencode/ui/motion"
|
||||
import { frameLoop, Spring, type DurationOptions } from "@opencode/ui/motion"
|
||||
import { motionTuning } from "@opencode/ui/motion-tuning"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import type { FormAnswer, FormInfo, FormMultiselectField, FormStringField } from "@opencode/client/promise"
|
||||
@@ -893,11 +893,12 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
|
||||
paint()
|
||||
}
|
||||
|
||||
onCleanup(
|
||||
observeReducedMotion(() => {
|
||||
if (!questionMotion() && stop) still()
|
||||
}),
|
||||
)
|
||||
// Turning the knob off also lands any running pick or dash transition.
|
||||
createEffect(() => {
|
||||
if (questionMotion()) return
|
||||
still()
|
||||
root?.getAnimations({ subtree: true }).forEach((animation) => animation.cancel())
|
||||
})
|
||||
onCleanup(() => stop?.())
|
||||
|
||||
const jump = (tab: number) => {
|
||||
|
||||
Reference in New Issue
Block a user