Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/toast-priority-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@zag-js/toast": minor
---

Add priority-based queue system for toasts inspired by Adobe Spectrum's design guidelines. Toasts now support priority
levels (1-8, where 1 is highest priority) with automatic priority assignment:

- **Error toasts**: Priority 1 (actionable) or 2 (non-actionable) - always shown first
- **Warning toasts**: Priority 2 (actionable) or 6 (non-actionable)
- **Loading toasts**: Priority 3 (actionable) or 4 (non-actionable)
- **Success toasts**: Priority 4 (actionable) or 7 (non-actionable)
- **Info toasts**: Priority 5 (actionable) or 8 (non-actionable)

When the maximum number of toasts is reached, new high-priority toasts are queued and displayed as soon as space becomes
available, ensuring critical notifications are never missed. Custom priorities can be set using the `priority` option.

Additionally, when using `toast.promise` to track a promise, the `success` options can now specify `type` as either
`success` or `warning` for more flexible promise result handling.
1 change: 1 addition & 0 deletions packages/machines/toast/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type {
StatusChangeDetails,
ToastStore as Store,
ToastStoreProps as StoreProps,
ToastQueuePriority,
Type,
} from "./toast.types"

Expand Down
59 changes: 49 additions & 10 deletions packages/machines/toast/src/toast.store.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import type { Required } from "@zag-js/types"
import { compact, runIfFn, uuid, warn } from "@zag-js/utils"
import type { Options, PromiseOptions, ToastProps, ToastStore, ToastStoreProps } from "./toast.types"
import type {
Options,
ToastQueuePriority,
PromiseOptions,
ToastProps,
ToastStore,
ToastStoreProps,
Type,
} from "./toast.types"

const withDefaults = <T extends object, D extends Partial<T>>(options: T, defaults: D): T & Required<D> => {
return { ...defaults, ...compact(options as any) }
}

const priorities: Record<string, [ToastQueuePriority, ToastQueuePriority]> = {
error: [1, 2],
warning: [2, 6],
loading: [3, 4],
success: [4, 7],
info: [5, 8],
}

const DEFAULT_TYPE: Type = "info"

const getPriorityForType = (type?: Type, hasAction?: boolean): ToastQueuePriority => {
const [actionable, nonActionable] = priorities[type ?? DEFAULT_TYPE] ?? [5, 8]
return hasAction ? actionable : nonActionable
}

const sortToastsByPriority = <V>(toastArray: Partial<ToastProps<V>>[]): Partial<ToastProps<V>>[] => {
return toastArray.sort((a, b) => {
const priorityA = a.priority ?? getPriorityForType(a.type, !!a.action)
const priorityB = b.priority ?? getPriorityForType(b.type, !!b.action)
return priorityA - priorityB
})
}

export function createToastStore<V = any>(props: ToastStoreProps): ToastStore<V> {
const attrs = withDefaults(props, {
placement: "bottom",
Expand Down Expand Up @@ -47,6 +78,7 @@ export function createToastStore<V = any>(props: ToastStoreProps): ToastStore<V>

const processQueue = () => {
while (toastQueue.length > 0 && toasts.length < attrs.max) {
toastQueue = sortToastsByPriority(toastQueue)
const nextToast = toastQueue.shift()
if (nextToast) {
publish(nextToast)
Expand All @@ -70,15 +102,17 @@ export function createToastStore<V = any>(props: ToastStoreProps): ToastStore<V>
return toast
})
} else {
addToast({
const newToast = {
id,
duration: attrs.duration,
removeDelay: attrs.removeDelay,
type: "info",
type: DEFAULT_TYPE,
...data,
stacked: !attrs.overlap,
gap: attrs.gap,
})
}
const priority = newToast.priority ?? getPriorityForType(newToast.type, !!newToast.action)
addToast({ ...newToast, priority })
}

return id
Expand All @@ -102,23 +136,28 @@ export function createToastStore<V = any>(props: ToastStoreProps): ToastStore<V>
}

const error = (data?: Omit<Options<V>, "type">) => {
return create({ ...data, type: "error" })
const priority = data?.priority ?? getPriorityForType("error", !!data?.action)
return create({ ...data, type: "error", priority })
}

const success = (data?: Omit<Options<V>, "type">) => {
return create({ ...data, type: "success" })
const priority = data?.priority ?? getPriorityForType("success", !!data?.action)
return create({ ...data, type: "success", priority })
}

const info = (data?: Omit<Options<V>, "type">) => {
return create({ ...data, type: "info" })
const priority = data?.priority ?? getPriorityForType("info", !!data?.action)
return create({ ...data, type: "info", priority })
}

const warning = (data?: Omit<Options<V>, "type">) => {
return create({ ...data, type: "warning" })
const priority = data?.priority ?? getPriorityForType("warning", !!data?.action)
return create({ ...data, type: "warning", priority })
}

const loading = (data?: Omit<Options<V>, "type">) => {
return create({ ...data, type: "loading" })
const priority = data?.priority ?? getPriorityForType("loading", !!data?.action)
return create({ ...data, type: "loading", priority })
}

const getVisibleToasts = () => {
Expand Down Expand Up @@ -161,7 +200,7 @@ export function createToastStore<V = any>(props: ToastStoreProps): ToastStore<V>
} else if (options.success !== undefined) {
removable = false
const successOptions = runIfFn(options.success, response)
create({ ...shared, ...successOptions, id, type: "success" })
create({ ...shared, ...successOptions, id, type: successOptions.type ?? "success" })
}
})
.catch(async (error) => {
Expand Down
10 changes: 8 additions & 2 deletions packages/machines/toast/src/toast.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type { EventObject, Machine, Service } from "@zag-js/core"
* Base types
* -----------------------------------------------------------------------------*/

export type Type = "success" | "error" | "loading" | "info" | (string & {})
export type Type = "success" | "error" | "loading" | "info" | "warning" | (string & {})

export type ToastQueuePriority = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8

export type Placement = "top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end"

Expand Down Expand Up @@ -74,6 +76,10 @@ export interface Options<T = any> {
* The type of the toast
*/
type?: Type | undefined
/**
* The priority of the toast (1 = highest, 8 = lowest)
*/
priority?: ToastQueuePriority | undefined
/**
* Function called when the toast is visible
*/
Expand Down Expand Up @@ -358,7 +364,7 @@ type MaybeFunction<Value, Args> = Value | ((arg: Args) => Value)

export interface PromiseOptions<V, O = any> {
loading: Omit<Options<O>, "type">
success?: MaybeFunction<Omit<Options<O>, "type">, V> | undefined
success?: MaybeFunction<Omit<Options<O>, "type"> & { type?: "success" | "warning" }, V> | undefined
error?: MaybeFunction<Omit<Options<O>, "type">, unknown> | undefined
finally?: (() => void | Promise<void>) | undefined
}
Expand Down
1 change: 1 addition & 0 deletions packages/machines/toast/src/toast.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const defaultTimeouts: Record<Type, number> = {
error: 5000,
success: 2000,
loading: Infinity,
warning: 5000,
DEFAULT: 5000,
}

Expand Down
Loading