Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(toast): remove removeDelay #936

Closed
wants to merge 3 commits into from
Closed
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
49 changes: 36 additions & 13 deletions .xstate/toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ const {
} = actions;
const fetchMachine = createMachine({
id,
entry: "invokeOnOpen",
entry: ["invokeOnOpen", "checkAnimation"],
exit: "notifyParentToRemove",
initial: type === "loading" ? "persist" : "active",
context: {
"hasTypeChanged && isChangingToLoading": false,
"hasDurationChanged || hasTypeChanged": false,
"!isLoadingType": false
"!isLoadingType": false,
"hasAnimation": false,
"hasAnimation": false,
"hasAnimation": false
},
on: {
UPDATE: [{
Expand Down Expand Up @@ -52,17 +56,35 @@ const fetchMachine = createMachine({
target: "active",
actions: ["setCreatedAt"]
},
DISMISS: "dismissing"
DISMISS: [{
cond: "hasAnimation",
target: "dismissing"
}, {
target: "inactive",
actions: "invokeOnClosing"
}]
}
},
active: {
tags: ["visible"],
activities: "trackDocumentVisibility",
after: {
VISIBLE_DURATION: "dismissing"
},
after: [{
delay: "VISIBLE_DURATION",
cond: "hasAnimation",
target: "dismissing"
}, {
delay: "VISIBLE_DURATION",
target: "inactive",
actions: "invokeOnClosing"
}],
on: {
DISMISS: "dismissing",
DISMISS: [{
cond: "hasAnimation",
target: "dismissing"
}, {
target: "inactive",
actions: "invokeOnClosing"
}],
PAUSE: {
target: "persist",
actions: "setRemainingDuration"
Expand All @@ -71,15 +93,15 @@ const fetchMachine = createMachine({
},
dismissing: {
entry: "invokeOnClosing",
after: {
REMOVE_DELAY: {
target: "inactive",
actions: "notifyParentToRemove"
activities: ["trackAnimationEvents"],
on: {
ANIMATION_END: {
target: "inactive"
}
}
},
inactive: {
entry: "invokeOnClose",
entry: ["invokeOnClose"],
type: "final"
}
}
Expand All @@ -94,6 +116,7 @@ const fetchMachine = createMachine({
guards: {
"hasTypeChanged && isChangingToLoading": ctx => ctx["hasTypeChanged && isChangingToLoading"],
"hasDurationChanged || hasTypeChanged": ctx => ctx["hasDurationChanged || hasTypeChanged"],
"!isLoadingType": ctx => ctx["!isLoadingType"]
"!isLoadingType": ctx => ctx["!isLoadingType"],
"hasAnimation": ctx => ctx["hasAnimation"]
}
});
11 changes: 6 additions & 5 deletions examples/next-ts/pages/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ function ToastItem({ actor }: { actor: toast.Service }) {
},
}

if (state.context.render) {
return state.context.render(api)
}

return (
<pre {...api.rootProps}>
<div {...progressbarProps} />
Expand All @@ -41,9 +45,6 @@ export default function Page() {
const [state, send] = useMachine(
toast.group.machine({
id: useId(),
defaultOptions: {
placement: "top-start",
},
}),
{
context: controls.context,
Expand Down Expand Up @@ -79,7 +80,7 @@ export default function Page() {
})
}}
>
Notify (Error)
Custom Render Notify (Error)
</button>
<button
onClick={() => {
Expand All @@ -104,7 +105,7 @@ export default function Page() {
</div>
))}
</main>
<Toolbar controls={controls.ui}>
<Toolbar controls={controls.ui} viz>
<StateVisualizer state={state} />
</Toolbar>
</>
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,6 @@ export class Machine<
// exit current state
this.performExitEffects(this.state.value!, toEvent<TEvent>(ActionTypes.Stop))

// execute root stop or exit actions
this.executeActions(this.config.exit, toEvent<TEvent>(ActionTypes.Stop))

this.setState("")
this.setEvent(ActionTypes.Stop)

Expand All @@ -228,6 +225,9 @@ export class Machine<
this.stopEventListeners()
this.stopContextListeners()

// execute root stop or exit actions
this.executeActions(this.config.exit, toEvent<TEvent>(ActionTypes.Stop))

this.status = MachineStatus.Stopped
return this
}
Expand Down
10 changes: 6 additions & 4 deletions packages/machines/toast/src/toast-group.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export function groupMachine(userContext: UserDefinedGroupContext) {
},

on: {
SETUP: {},

PAUSE_TOAST: {
actions: (_ctx, evt, { self }) => {
self.sendChild("PAUSE", evt.id)
Expand Down Expand Up @@ -56,7 +54,9 @@ export function groupMachine(userContext: UserDefinedGroupContext) {
guard: (ctx) => ctx.toasts.length < ctx.max,
actions: (ctx, evt, { self }) => {
const options = {
...ctx.defaultOptions,
placement: ctx.placement,
duration: ctx.duration,
render: ctx.render,
...evt.toast,
pauseOnPageIdle: ctx.pauseOnPageIdle,
pauseOnInteraction: ctx.pauseOnInteraction,
Expand Down Expand Up @@ -89,7 +89,9 @@ export function groupMachine(userContext: UserDefinedGroupContext) {

REMOVE_TOAST: {
actions: (ctx, evt, { self }) => {
self.stopChild(evt.id)
try {
self.stopChild(evt.id)
} catch {}
const index = ctx.toasts.findIndex((toast) => toast.id === evt.id)
ctx.toasts.splice(index, 1)
},
Expand Down
15 changes: 0 additions & 15 deletions packages/machines/toast/src/toast.connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,6 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
send("DISMISS")
},

render() {
return state.context.render?.({
id: state.context.id,
type: state.context.type,
duration: state.context.duration,
title: state.context.title,
placement: state.context.placement,
description: state.context.description,
dismiss() {
send("DISMISS")
},
})
},

rootProps: normalize.element({
...parts.root.attrs,
dir: state.context.dir,
Expand All @@ -59,7 +45,6 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
position: "relative",
pointerEvents: "auto",
margin: "calc(var(--toast-gutter) / 2)",
"--remove-delay": `${state.context.removeDelay}ms`,
"--duration": `${state.context.duration}ms`,
},
onKeyDown(event) {
Expand Down
1 change: 1 addition & 0 deletions packages/machines/toast/src/toast.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export const dom = createScope({
getDescriptionId: (ctx: Ctx) => `toast:${ctx.id}:description`,
getCloseTriggerId: (ctx: Ctx) => `toast${ctx.id}:close`,
getPortalId: (ctx: GroupCtx) => `toast-portal:${ctx.id}`,
getRootEl: (ctx: Ctx) => dom.getById(ctx, dom.getRootId(ctx)),
})
67 changes: 51 additions & 16 deletions packages/machines/toast/src/toast.machine.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMachine, guards } from "@zag-js/core"
import { addDomEvent } from "@zag-js/dom-event"
import { raf } from "@zag-js/dom-query"
import { compact } from "@zag-js/utils"
import { dom } from "./toast.dom"
import type { MachineContext, MachineState, Options } from "./toast.types"
Expand All @@ -8,25 +9,26 @@ import { getToastDuration } from "./toast.utils"
const { not, and, or } = guards

export function createToastMachine(options: Options = {}) {
const { type = "info", duration, id = "toast", placement = "bottom", removeDelay = 0, ...restProps } = options
const { type = "info", duration, id = "toast", placement = "bottom", ...restProps } = options
const ctx = compact(restProps)

const computedDuration = getToastDuration(duration, type)

return createMachine<MachineContext, MachineState>(
{
id,
entry: "invokeOnOpen",
entry: ["invokeOnOpen", "checkAnimation"],
exit: "notifyParentToRemove",
initial: type === "loading" ? "persist" : "active",
context: {
id,
type,
remaining: computedDuration,
duration: computedDuration,
removeDelay,
createdAt: Date.now(),
placement,
...ctx,
createdAt: Date.now(),
remaining: computedDuration,
hasAnimation: true,
},

on: {
Expand Down Expand Up @@ -64,18 +66,25 @@ export function createToastMachine(options: Options = {}) {
target: "active",
actions: ["setCreatedAt"],
},
DISMISS: "dismissing",
DISMISS: [
{ guard: "hasAnimation", target: "dismissing" },
{ target: "inactive", actions: "invokeOnClosing" },
],
},
},

active: {
tags: ["visible"],
activities: "trackDocumentVisibility",
after: {
VISIBLE_DURATION: "dismissing",
},
after: [
{ delay: "VISIBLE_DURATION", guard: "hasAnimation", target: "dismissing" },
{ delay: "VISIBLE_DURATION", target: "inactive", actions: "invokeOnClosing" },
],
on: {
DISMISS: "dismissing",
DISMISS: [
{ guard: "hasAnimation", target: "dismissing" },
{ target: "inactive", actions: "invokeOnClosing" },
],
PAUSE: {
target: "persist",
actions: "setRemainingDuration",
Expand All @@ -85,22 +94,40 @@ export function createToastMachine(options: Options = {}) {

dismissing: {
entry: "invokeOnClosing",
after: {
REMOVE_DELAY: {
activities: ["trackAnimationEvents"],
on: {
ANIMATION_END: {
target: "inactive",
actions: "notifyParentToRemove",
},
},
},

inactive: {
entry: "invokeOnClose",
entry: ["invokeOnClose"],
type: "final",
},
},
},
{
activities: {
trackAnimationEvents(ctx, _evt, { send }) {
const node = dom.getRootEl(ctx)
if (!node) return

const onEnd = (event: AnimationEvent) => {
if (event.currentTarget === node) {
send("ANIMATION_END")
}
}

node.addEventListener("animationcancel", onEnd)
node.addEventListener("animationend", onEnd)

return () => {
node.removeEventListener("animationcancel", onEnd)
node.removeEventListener("animationend", onEnd)
}
},
trackDocumentVisibility(ctx, _evt, { send }) {
if (!ctx.pauseOnPageIdle) return
const doc = dom.getDoc(ctx)
Expand All @@ -115,11 +142,11 @@ export function createToastMachine(options: Options = {}) {
isLoadingType: (ctx) => ctx.type === "loading",
hasTypeChanged: (ctx, evt) => evt.toast?.type !== ctx.type,
hasDurationChanged: (ctx, evt) => evt.toast?.duration !== ctx.duration,
hasAnimation: (ctx) => ctx.hasAnimation,
},

delays: {
VISIBLE_DURATION: (ctx) => ctx.remaining,
REMOVE_DELAY: (ctx) => ctx.removeDelay,
},

actions: {
Expand All @@ -130,7 +157,9 @@ export function createToastMachine(options: Options = {}) {
ctx.createdAt = Date.now()
},
notifyParentToRemove(_ctx, _evt, { self }) {
self.sendParent({ type: "REMOVE_TOAST", id: self.id })
setTimeout(() => {
self.sendParent({ type: "REMOVE_TOAST", id: self.id })
}, 100)
},
invokeOnClosing(ctx) {
ctx.onClosing?.()
Expand All @@ -144,6 +173,12 @@ export function createToastMachine(options: Options = {}) {
invokeOnUpdate(ctx) {
ctx.onUpdate?.()
},
checkAnimation(ctx) {
raf(() => {
const animations = dom.getRootEl(ctx)?.getAnimations() ?? []
ctx.hasAnimation = animations.length > 0
})
},
setContext(ctx, evt) {
const { duration, type } = evt.toast
const time = getToastDuration(duration, type)
Expand Down
Loading
Loading