@zag-js/toast 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,21 @@
1
1
  import { createMachine, guards } from "@zag-js/core"
2
- import { addDomEvent } from "@zag-js/dom-event"
3
- import { compact } from "@zag-js/utils"
2
+ import { queryAll, raf } from "@zag-js/dom-query"
3
+ import { compact, warn } from "@zag-js/utils"
4
4
  import { dom } from "./toast.dom"
5
- import type { DefaultGenericOptions, MachineContext, MachineState, Options, GenericOptions } from "./toast.types"
5
+ import type { MachineContext, MachineState, Options } from "./toast.types"
6
6
  import { getToastDuration } from "./toast.utils"
7
7
 
8
8
  const { not, and, or } = guards
9
9
 
10
- export function createToastMachine<T extends GenericOptions = DefaultGenericOptions>(options: Options<T> = {}) {
11
- const { type = "info", duration, id = "toast", placement = "bottom", removeDelay = 0, ...restProps } = options
10
+ export function createToastMachine<T>(options: Options<T>) {
11
+ const { type = "info", duration, id = "1", placement = "bottom", removeDelay = 200, ...restProps } = options
12
12
  const ctx = compact(restProps)
13
13
 
14
14
  const computedDuration = getToastDuration(duration, type)
15
15
 
16
- return createMachine<MachineContext, MachineState>(
16
+ return createMachine<MachineContext<T>, MachineState>(
17
17
  {
18
18
  id,
19
- entry: "invokeOnOpen",
20
- initial: type === "loading" ? "persist" : "active",
21
19
  context: {
22
20
  id,
23
21
  type,
@@ -27,86 +25,128 @@ export function createToastMachine<T extends GenericOptions = DefaultGenericOpti
27
25
  createdAt: Date.now(),
28
26
  placement,
29
27
  ...ctx,
28
+ height: 0,
29
+ offset: 0,
30
+ frontmost: false,
31
+ mounted: false,
32
+ index: -1,
33
+ zIndex: 0,
30
34
  },
31
35
 
36
+ initial: type === "loading" ? "visible:persist" : "visible",
37
+
32
38
  on: {
33
39
  UPDATE: [
34
40
  {
35
41
  guard: and("hasTypeChanged", "isChangingToLoading"),
36
- target: "persist",
37
- actions: ["setContext", "invokeOnUpdate"],
42
+ target: "visible:persist",
43
+ actions: ["setContext"],
38
44
  },
39
45
  {
40
46
  guard: or("hasDurationChanged", "hasTypeChanged"),
41
- target: "active:temp",
42
- actions: ["setContext", "invokeOnUpdate"],
47
+ target: "visible:updating",
48
+ actions: ["setContext"],
43
49
  },
44
50
  {
45
- actions: ["setContext", "invokeOnUpdate"],
51
+ actions: ["setContext"],
46
52
  },
47
53
  ],
54
+ MEASURE: {
55
+ actions: ["measureHeight"],
56
+ },
48
57
  },
49
58
 
59
+ entry: ["invokeOnVisible"],
60
+
61
+ activities: ["trackHeight"],
62
+
50
63
  states: {
51
- "active:temp": {
64
+ "visible:updating": {
52
65
  tags: ["visible", "updating"],
53
66
  after: {
54
- 0: "active",
67
+ 0: "visible",
55
68
  },
56
69
  },
57
70
 
58
- persist: {
71
+ "visible:persist": {
59
72
  tags: ["visible", "paused"],
60
- activities: "trackDocumentVisibility",
61
73
  on: {
62
74
  RESUME: {
63
75
  guard: not("isLoadingType"),
64
- target: "active",
76
+ target: "visible",
65
77
  actions: ["setCreatedAt"],
66
78
  },
67
79
  DISMISS: "dismissing",
68
80
  },
69
81
  },
70
82
 
71
- active: {
83
+ visible: {
72
84
  tags: ["visible"],
73
- activities: "trackDocumentVisibility",
74
85
  after: {
75
86
  VISIBLE_DURATION: "dismissing",
76
87
  },
77
88
  on: {
78
89
  DISMISS: "dismissing",
79
90
  PAUSE: {
80
- target: "persist",
91
+ target: "visible:persist",
81
92
  actions: "setRemainingDuration",
82
93
  },
83
94
  },
84
95
  },
85
96
 
86
97
  dismissing: {
87
- entry: "invokeOnClosing",
98
+ entry: "invokeOnDismiss",
88
99
  after: {
89
100
  REMOVE_DELAY: {
90
- target: "inactive",
101
+ target: "unmounted",
91
102
  actions: "notifyParentToRemove",
92
103
  },
93
104
  },
94
105
  },
95
106
 
96
- inactive: {
97
- entry: "invokeOnClose",
107
+ unmounted: {
108
+ entry: "invokeOnUnmount",
98
109
  type: "final",
99
110
  },
100
111
  },
101
112
  },
102
113
  {
103
114
  activities: {
104
- trackDocumentVisibility(ctx, _evt, { send }) {
105
- if (!ctx.pauseOnPageIdle) return
106
- const doc = dom.getDoc(ctx)
107
- return addDomEvent(doc, "visibilitychange", () => {
108
- send(doc.visibilityState === "hidden" ? "PAUSE" : "RESUME")
115
+ trackHeight(ctx, _evt, { self }) {
116
+ let cleanup: VoidFunction
117
+ raf(() => {
118
+ const rootEl = dom.getRootEl(ctx)
119
+ if (!rootEl) return
120
+ ctx.mounted = true
121
+
122
+ const ghosts = queryAll(rootEl, "[data-part=ghost]")
123
+
124
+ warn(
125
+ ghosts.length !== 2,
126
+ "[toast] No ghost element found in toast. Render the `ghostBefore` and `ghostAfter` elements",
127
+ )
128
+
129
+ const syncHeight = () => {
130
+ const originalHeight = rootEl.style.height
131
+ rootEl.style.height = "auto"
132
+ const newHeight = rootEl.offsetHeight
133
+ rootEl.style.height = originalHeight
134
+
135
+ ctx.height = newHeight
136
+ self.sendParent({ type: "UPDATE_HEIGHT", id: self.id, height: newHeight, placement: ctx.placement })
137
+ }
138
+
139
+ syncHeight()
140
+
141
+ const win = dom.getWin(ctx)
142
+
143
+ const observer = new win.MutationObserver(syncHeight)
144
+ observer.observe(rootEl, { childList: true, subtree: true, characterData: true })
145
+
146
+ cleanup = () => observer.disconnect()
109
147
  })
148
+
149
+ return () => cleanup?.()
110
150
  },
111
151
  },
112
152
 
@@ -123,6 +163,23 @@ export function createToastMachine<T extends GenericOptions = DefaultGenericOpti
123
163
  },
124
164
 
125
165
  actions: {
166
+ measureHeight(ctx, _evt, { self }) {
167
+ raf(() => {
168
+ const rootEl = dom.getRootEl(ctx)
169
+ if (!rootEl) return
170
+
171
+ ctx.mounted = true
172
+
173
+ const originalHeight = rootEl.style.height
174
+ rootEl.style.height = "auto"
175
+
176
+ const newHeight = rootEl.offsetHeight
177
+ rootEl.style.height = originalHeight
178
+ ctx.height = newHeight
179
+
180
+ self.sendParent({ type: "UPDATE_HEIGHT", id: self.id, height: newHeight, placement: ctx.placement })
181
+ })
182
+ },
126
183
  setRemainingDuration(ctx) {
127
184
  ctx.remaining -= Date.now() - ctx.createdAt
128
185
  },
@@ -132,22 +189,23 @@ export function createToastMachine<T extends GenericOptions = DefaultGenericOpti
132
189
  notifyParentToRemove(_ctx, _evt, { self }) {
133
190
  self.sendParent({ type: "REMOVE_TOAST", id: self.id })
134
191
  },
135
- invokeOnClosing(ctx) {
136
- ctx.onClosing?.()
137
- },
138
- invokeOnClose(ctx) {
139
- ctx.onClose?.()
192
+ invokeOnDismiss(ctx) {
193
+ ctx.onStatusChange?.({ status: "dismissing" })
140
194
  },
141
- invokeOnOpen(ctx) {
142
- ctx.onOpen?.()
195
+ invokeOnUnmount(ctx) {
196
+ ctx.onStatusChange?.({ status: "unmounted" })
143
197
  },
144
- invokeOnUpdate(ctx) {
145
- ctx.onUpdate?.()
198
+ invokeOnVisible(ctx) {
199
+ ctx.onStatusChange?.({ status: "visible" })
146
200
  },
147
201
  setContext(ctx, evt) {
148
202
  const { duration, type } = evt.toast
149
- const time = getToastDuration(duration, type)
150
- Object.assign(ctx, { ...evt.toast, duration: time, remaining: time })
203
+ if (duration == null && type == null) {
204
+ Object.assign(ctx, evt.toast)
205
+ } else {
206
+ const time = getToastDuration(duration, type)
207
+ Object.assign(ctx, { ...evt.toast, duration: time, remaining: time })
208
+ }
151
209
  },
152
210
  },
153
211
  },
@@ -1,44 +1,41 @@
1
- import type { Machine, StateMachine as S } from "@zag-js/core"
1
+ import type { Machine, Ref, StateMachine as S } from "@zag-js/core"
2
2
  import type { CommonProperties, Direction, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types"
3
3
 
4
4
  /* -----------------------------------------------------------------------------
5
5
  * Base types
6
6
  * -----------------------------------------------------------------------------*/
7
7
 
8
- export type Type = "success" | "error" | "loading" | "info" | "custom"
8
+ export type Type = "success" | "error" | "loading" | "info" | (string & {})
9
9
 
10
10
  export type Placement = "top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end"
11
11
 
12
- export interface GenericOptions {
13
- render?: (api: any) => any
14
- title?: any
15
- description?: any
16
- }
12
+ export type Status = "visible" | "dismissing" | "unmounted"
17
13
 
18
- export interface DefaultGenericOptions {
19
- /**
20
- * Custom function to render the toast element.
21
- */
22
- render?: (api: MachineApi<any, DefaultGenericOptions>) => any
14
+ export interface GenericOptions<T = string> {
23
15
  /**
24
16
  * The title of the toast.
25
17
  */
26
- title?: string
18
+ title?: T
27
19
  /**
28
20
  * The description of the toast.
29
21
  */
30
- description?: string
22
+ description?: T
31
23
  }
32
24
 
33
- export type GlobalToastOptions<T extends GenericOptions> = Pick<T, "render"> & {
34
- /**
35
- * Whether to pause toast when the user leaves the browser tab
36
- */
37
- pauseOnPageIdle?: boolean
38
- /**
39
- * Whether to pause the toast when interacted with
40
- */
41
- pauseOnInteraction?: boolean
25
+ export interface StatusChangeDetails {
26
+ status: Status
27
+ }
28
+
29
+ /**
30
+ * @internal
31
+ */
32
+ export interface ToastHeightDetails {
33
+ id: string
34
+ height: number
35
+ placement: Placement
36
+ }
37
+
38
+ export interface Options<T> extends GenericOptions<T> {
42
39
  /**
43
40
  * The duration the toast will be visible
44
41
  */
@@ -52,96 +49,145 @@ export type GlobalToastOptions<T extends GenericOptions> = Pick<T, "render"> & {
52
49
  * The placement of the toast
53
50
  */
54
51
  placement?: Placement
55
- }
56
-
57
- export type ToastOptions<T extends GenericOptions = DefaultGenericOptions> = T & {
58
52
  /**
59
53
  * The unique id of the toast
60
54
  */
61
- id: string
55
+ id?: string
62
56
  /**
63
57
  * The type of the toast
64
58
  */
65
59
  type: Type
66
60
  /**
67
- * Function called when the toast has been closed and removed
68
- */
69
- onClose?: VoidFunction
70
- /**
71
- * Function called when the toast is leaving
72
- */
73
- onClosing?: VoidFunction
74
- /**
75
- * Function called when the toast is shown
61
+ * Function called when the toast is visible
76
62
  */
77
- onOpen?: VoidFunction
63
+ onStatusChange?(details: StatusChangeDetails): void
78
64
  /**
79
- * Function called when the toast is updated
65
+ * The metadata of the toast
80
66
  */
81
- onUpdate?: VoidFunction
67
+ meta?: Record<string, unknown>
82
68
  }
83
69
 
84
- export type Options<T extends GenericOptions> = Partial<ToastOptions<T> & GlobalToastOptions<T>>
85
-
86
70
  /* -----------------------------------------------------------------------------
87
71
  * Machine context
88
72
  * -----------------------------------------------------------------------------*/
89
73
 
90
- export type MachineContext<T extends GenericOptions = DefaultGenericOptions> = GlobalToastOptions<T> &
91
- CommonProperties &
92
- Omit<ToastOptions<T>, "removeDelay"> & {
93
- /**
94
- * The duration for the toast to kept alive before it is removed.
95
- * Useful for exit transitions.
96
- */
97
- removeDelay: number
98
- /**
99
- * The document's text/writing direction.
100
- */
101
- dir?: Direction
102
- /**
103
- * The time the toast was created
104
- */
105
- createdAt: number
106
- /**
107
- * The time left before the toast is removed
108
- */
109
- remaining: number
110
- }
74
+ export interface MachineContext<T = any>
75
+ extends Omit<CommonProperties, "id">,
76
+ MachinePrivateContext,
77
+ Omit<Options<T>, "removeDelay"> {
78
+ /**
79
+ * The duration for the toast to kept alive before it is removed.
80
+ * Useful for exit transitions.
81
+ */
82
+ removeDelay: number
83
+ /**
84
+ * The document's text/writing direction.
85
+ */
86
+ dir?: Direction
87
+ /**
88
+ * The time the toast was created
89
+ */
90
+ createdAt: number
91
+ /**
92
+ * The time left before the toast is removed
93
+ */
94
+ remaining: number
95
+ }
96
+
97
+ interface MachinePrivateContext {
98
+ /**
99
+ * @internal
100
+ * The height of the toast
101
+ */
102
+ height: number
103
+ /**
104
+ * @internal
105
+ * The absolute height of the toast relative to other toasts
106
+ */
107
+ offset: number
108
+ /**
109
+ * @internal
110
+ * Whether the toast is in the front
111
+ */
112
+ frontmost: boolean
113
+ /**
114
+ * @internal
115
+ * The index of the toast in the group
116
+ */
117
+ index: number
118
+ /**
119
+ * @internal
120
+ * Whether the toast is mounted
121
+ */
122
+ mounted: boolean
123
+ /**
124
+ * @internal
125
+ * The z-index of the toast
126
+ */
127
+ zIndex: number
128
+ /**
129
+ * @internal
130
+ * Whether the toast is stacked
131
+ */
132
+ stacked?: boolean
133
+ }
111
134
 
112
135
  export interface MachineState {
113
- value: "active" | "active:temp" | "dismissing" | "inactive" | "persist"
136
+ value: "visible" | "visible:updating" | "dismissing" | "unmounted" | "visible:persist"
114
137
  tags: "visible" | "paused" | "updating"
115
138
  }
116
139
 
117
- export type State<T extends GenericOptions = DefaultGenericOptions> = S.State<MachineContext<T>, MachineState>
140
+ export type State<T = any> = S.State<MachineContext<T>, MachineState>
118
141
 
119
142
  export type Send = S.Send
120
143
 
121
- export type Service<T extends GenericOptions = DefaultGenericOptions> = Machine<MachineContext<T>, MachineState>
122
-
123
- type GroupPublicContext<T extends GenericOptions> = GlobalToastOptions<T> &
124
- DirectionProperty &
125
- CommonProperties & {
126
- /**
127
- * The gutter or spacing between toasts
128
- */
129
- gutter: string
130
- /**
131
- * The z-index applied to each toast group
132
- */
133
- zIndex: number
134
- /**
135
- * The maximum number of toasts that can be shown at once
136
- */
137
- max: number
138
- /**
139
- * The offset from the safe environment edge of the viewport
140
- */
141
- offsets: string | Record<"left" | "right" | "bottom" | "top", string>
142
- }
143
-
144
- export type UserDefinedGroupContext<T extends GenericOptions> = RequiredBy<GroupPublicContext<T>, "id">
144
+ export type Service<T = any> = Machine<MachineContext<T>, MachineState>
145
+
146
+ /* -----------------------------------------------------------------------------
147
+ * Group machine context
148
+ * -----------------------------------------------------------------------------*/
149
+
150
+ interface GroupPublicContext extends DirectionProperty, CommonProperties {
151
+ /**
152
+ * Whether to pause toast when the user leaves the browser tab
153
+ */
154
+ pauseOnPageIdle: boolean
155
+ /**
156
+ * The gap or spacing between toasts
157
+ */
158
+ gap: number
159
+ /**
160
+ * The maximum number of toasts that can be shown at once
161
+ */
162
+ max: number
163
+ /**
164
+ * The offset from the safe environment edge of the viewport
165
+ */
166
+ offsets: string | Record<"left" | "right" | "bottom" | "top", string>
167
+ /**
168
+ * The hotkey that will move focus to the toast group
169
+ */
170
+ hotkey: string[]
171
+ /**
172
+ * Whether the toasts should overlap each other
173
+ */
174
+ overlap?: boolean
175
+ /**
176
+ * The placement of the toast
177
+ */
178
+ placement: Placement
179
+ /**
180
+ * The duration for the toast to kept alive before it is removed.
181
+ * Useful for exit transitions.
182
+ */
183
+ removeDelay: number
184
+ /**
185
+ * The duration the toast will be visible
186
+ */
187
+ duration?: number
188
+ }
189
+
190
+ export interface UserDefinedGroupContext extends RequiredBy<GroupPublicContext, "id"> {}
145
191
 
146
192
  type GroupComputedContext = Readonly<{
147
193
  /**
@@ -151,53 +197,83 @@ type GroupComputedContext = Readonly<{
151
197
  count: number
152
198
  }>
153
199
 
154
- interface GroupPrivateContext<T extends GenericOptions> {
200
+ interface GroupPrivateContext<T> extends GenericOptions<T> {
155
201
  /**
156
202
  * @internal
157
203
  * The child toast machines (spawned by the toast group)
158
204
  */
159
205
  toasts: Service<T>[]
206
+ /**
207
+ * @internal
208
+ * The height of each toast
209
+ */
210
+ heights: ToastHeightDetails[]
211
+ /**
212
+ * @internal
213
+ */
214
+ _cleanup?: VoidFunction
215
+ /**
216
+ * @internal
217
+ */
218
+ lastFocusedEl: Ref<HTMLElement> | null
219
+ /**
220
+ * @internal
221
+ */
222
+ isFocusWithin: boolean
160
223
  }
161
224
 
162
- export interface GroupMachineContext<T extends GenericOptions = DefaultGenericOptions>
163
- extends GroupPublicContext<T>,
164
- GroupComputedContext,
165
- GroupPrivateContext<T> {}
225
+ export interface GroupMachineContext<T = any>
226
+ extends GroupPublicContext,
227
+ GroupPrivateContext<T>,
228
+ GroupComputedContext {}
229
+
230
+ export interface GroupMachineState {
231
+ value: "stack" | "overlap"
232
+ }
166
233
 
167
- export type GroupState<T extends GenericOptions = DefaultGenericOptions> = S.State<GroupMachineContext<T>>
234
+ export type GroupState<T = any> = S.State<GroupMachineContext<T>>
168
235
 
169
236
  export type GroupSend = S.Send
170
237
 
238
+ export type GroupService<T = any> = Machine<GroupMachineContext<T>, GroupMachineState>
239
+
171
240
  /* -----------------------------------------------------------------------------
172
241
  * Component API
173
242
  * -----------------------------------------------------------------------------*/
174
243
 
175
244
  type MaybeFunction<Value, Args> = Value | ((arg: Args) => Value)
176
245
 
177
- export interface PromiseOptions<V, O extends GenericOptions = DefaultGenericOptions> {
178
- loading: ToastOptions<O>
179
- success: MaybeFunction<ToastOptions<O>, V>
180
- error: MaybeFunction<ToastOptions<O>, Error>
246
+ export interface PromiseOptions<V, O = any> {
247
+ loading: Options<O>
248
+ success: MaybeFunction<Options<O>, V>
249
+ error: MaybeFunction<Options<O>, Error>
250
+ finally?: () => void | Promise<void>
181
251
  }
182
252
 
183
253
  export interface GroupProps {
254
+ /**
255
+ * The placement of the toast region
256
+ */
184
257
  placement: Placement
258
+ /**
259
+ * The human-readable label for the toast region
260
+ */
185
261
  label?: string
186
262
  }
187
263
 
188
- export interface GroupMachineApi<T extends PropTypes = PropTypes, O extends GenericOptions = DefaultGenericOptions> {
264
+ export interface GroupMachineApi<T extends PropTypes = PropTypes, O = any> {
189
265
  /**
190
266
  * The total number of toasts
191
267
  */
192
- count: number
268
+ getCount(): number
193
269
  /**
194
270
  * The active toasts
195
271
  */
196
- toasts: Service<O>[]
272
+ getToasts(): Service<O>[]
197
273
  /**
198
274
  * The active toasts by placement
199
275
  */
200
- toastsByPlacement: Partial<Record<Placement, Service<O>[]>>
276
+ getToastsByPlacement(): Partial<Record<Placement, Service<O>[]>>
201
277
  /**
202
278
  * Returns whether the toast id is visible
203
279
  */
@@ -253,18 +329,19 @@ export interface GroupMachineApi<T extends PropTypes = PropTypes, O extends Gene
253
329
  * - When the promise resolves, the toast will be updated with the success options.
254
330
  * - When the promise rejects, the toast will be updated with the error options.
255
331
  */
256
- promise<T>(promise: Promise<T>, options: PromiseOptions<T, O>, shared?: Partial<ToastOptions<O>>): Promise<T>
332
+ promise<T>(
333
+ promise: Promise<T> | (() => Promise<T>),
334
+ options: PromiseOptions<T, O>,
335
+ shared?: Partial<Options<O>>,
336
+ ): string
257
337
  /**
258
338
  * Function to subscribe to the toast group.
259
339
  */
260
- subscribe(callback: (toasts: Service<O>[]) => void): VoidFunction
340
+ subscribe(callback: (toasts: Options<O>[]) => void): VoidFunction
261
341
  getGroupProps(options: GroupProps): T["element"]
262
342
  }
263
343
 
264
- export type MachineApi<T extends PropTypes = PropTypes, O extends GenericOptions = DefaultGenericOptions> = Pick<
265
- O,
266
- "title" | "description"
267
- > & {
344
+ export interface MachineApi<T extends PropTypes = PropTypes, O = any> extends GenericOptions<O> {
268
345
  /**
269
346
  * The type of the toast.
270
347
  */
@@ -300,6 +377,9 @@ export type MachineApi<T extends PropTypes = PropTypes, O extends GenericOptions
300
377
 
301
378
  rootProps: T["element"]
302
379
  titleProps: T["element"]
380
+ ghostBeforeProps: T["element"]
381
+ ghostAfterProps: T["element"]
303
382
  descriptionProps: T["element"]
304
383
  closeTriggerProps: T["button"]
384
+ actionTriggerProps: T["button"]
305
385
  }