fix(ui): resolve toast icon inside the toast component (#34874)

This commit is contained in:
Luke Parker 2026-07-02 17:53:47 +10:00 committed by GitHub
commit f52424e05f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 65 additions and 37 deletions

View file

@ -0,0 +1,26 @@
import { describe, expect, test } from "bun:test"
import { createSignal, type JSX } from "solid-js"
import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
describe("showToastV2", () => {
test("creates no reactive computations at call time", () => {
const [tick, setTick] = createSignal(0)
let reads = 0
const icon = (() => {
reads++
tick()
return undefined
}) as unknown as JSX.Element
const id = showToastV2({ description: "test", icon })
// Resolving the icon at call time creates an ownerless computation that is
// never disposed and tracks its dependencies forever; it must only resolve
// once the toast component renders.
expect(reads).toBe(0)
setTick(1)
expect(reads).toBe(0)
toasterV2.dismiss(id)
})
})

View file

@ -97,44 +97,46 @@ export interface ToastV2Options {
export function showToastV2(options: ToastV2Options | string) {
const opts = typeof options === "string" ? { description: options } : options
const resolvedIcon = children(() => opts.icon)
return toaster.show((props) => (
<ToastV2 toastId={props.toastId} duration={opts.duration} persistent={opts.persistent}>
<div data-slot="toast-v2-header">
<Show when={resolvedIcon()}>
<ToastV2.Icon>{resolvedIcon()}</ToastV2.Icon>
return toaster.show((props) => {
const resolvedIcon = children(() => opts.icon)
return (
<ToastV2 toastId={props.toastId} duration={opts.duration} persistent={opts.persistent}>
<div data-slot="toast-v2-header">
<Show when={resolvedIcon()}>
<ToastV2.Icon>{resolvedIcon()}</ToastV2.Icon>
</Show>
<ToastV2.Content>
<Show when={opts.title}>
<ToastV2.Title>{opts.title}</ToastV2.Title>
</Show>
<Show when={opts.description}>
<ToastV2.Description>{opts.description}</ToastV2.Description>
</Show>
</ToastV2.Content>
<ToastV2.CloseButton />
</div>
<Show when={opts.actions?.length}>
<ToastV2.Actions>
{opts.actions!.map((action) => (
<ButtonV2
variant={action.variant === "secondary" ? "ghost" : "neutral"}
size="small"
data-action-variant={action.variant ?? "primary"}
onClick={() => {
if (typeof action.onClick === "function") {
action.onClick()
}
toaster.dismiss(props.toastId)
}}
>
{action.label}
</ButtonV2>
))}
</ToastV2.Actions>
</Show>
<ToastV2.Content>
<Show when={opts.title}>
<ToastV2.Title>{opts.title}</ToastV2.Title>
</Show>
<Show when={opts.description}>
<ToastV2.Description>{opts.description}</ToastV2.Description>
</Show>
</ToastV2.Content>
<ToastV2.CloseButton />
</div>
<Show when={opts.actions?.length}>
<ToastV2.Actions>
{opts.actions!.map((action) => (
<ButtonV2
variant={action.variant === "secondary" ? "ghost" : "neutral"}
size="small"
data-action-variant={action.variant ?? "primary"}
onClick={() => {
if (typeof action.onClick === "function") {
action.onClick()
}
toaster.dismiss(props.toastId)
}}
>
{action.label}
</ButtonV2>
))}
</ToastV2.Actions>
</Show>
</ToastV2>
))
</ToastV2>
)
})
}
export interface ToastV2PromiseOptions<T, U = unknown> {