vue-sonner icon indicating copy to clipboard operation
vue-sonner copied to clipboard

Allowing different descriptions based on promise state (loading/success/fail)

Open bw1984 opened this issue 1 year ago • 6 comments

Im relying on the title+description format to add additional details to my toasts, and i cant see a way to change the description field based on the promise. Could this be possible?

bw1984 avatar Sep 24 '24 10:09 bw1984

+1

RoyvanEmpel avatar Oct 22 '24 09:10 RoyvanEmpel

For me it works when passing a ref as the description and adjusting it in the error or success callbacks.

const description = ref('Long description wrapping over multiple lines. Long description wrapping over multiple lines.')

toast.promise(
	doWork(),
	{
		richColors: true,
		loading: 'Doing work...',
		description,
		success: () => 'Success',
		error: (err) => {
			description.value = 'Short Description.'
			return 'Failure'
		},
	},
)

HOWEVER: When doing this the height of the toast is not adjusted accordingly and the toast gets a wrong height when hovering over with the mouse (see video). :/

https://github.com/user-attachments/assets/a369c028-c65e-4f86-b84e-808c4c91e505

aa-ndrej avatar Jan 24 '25 11:01 aa-ndrej

For me it works when passing a ref as the description and adjusting it in the error or success callbacks.

const description = ref('Long description wrapping over multiple lines. Long description wrapping over multiple lines.')

toast.promise( doWork(), { richColors: true, loading: 'Doing work...', description, success: () => 'Success', error: (err) => { description.value = 'Short Description.' return 'Failure' }, }, ) HOWEVER: When doing this the height of the toast is not adjusted accordingly and the toast gets a wrong height when hovering over with the mouse (see video). :/

Bildschirmaufnahme.2025-01-24.um.12.26.57.mov

Have you found any workarounds for this? I found the same issue if the description is null before the promise and then we add the description after.

lahdekorpi avatar Sep 01 '25 06:09 lahdekorpi

@lahdekorpi my workaround was to use two different toasts like this:

try {
    await showPromiseToast(
        doWork(),
        {
            title: 'Doing work'
        },
    )
    toast.success('Success', { richColors: true })
}
catch (err) {
    toast.error('Error', { richColors: true })
}

Where showPromiseToast is a custom function that shows a loading toast (toast.loading()), but only shows it after to promise is still not resolved after ~100ms. So if the work is finished fast the user only sees the success or error toast. And if the loading toast is shown it is at least show for ~500ms so that it does not feel like the UI is flickering too much.

showPromiseToast Function
import { toast } from 'vue-sonner'


const LOADING_TOAST_MIN_DURATION = 500
const LOADING_TOAST_DISMISS_DURATION = 200


type LoadingToastOpts = NonNullable<Parameters<typeof toast.loading>[1]>

export async function showPromiseToast<ResultT>(
    promise: Promise<ResultT>,
    opts: LoadingToastOpts & {
        title: string,
    },
) {
    let loadingToastId: string | number = -1
    let loadingToastShowedAt = -1
    let isPromisePending = true


    async function waitAndDismissLoadingToast() {
        if (loadingToastId === -1) return
        // else: The loading toast is currently showing.
        //
        // Wait at least 500ms before dismissing the loading toast.
        // This prevents the loading toast from flickering if the promise resolves quickly.

        const remainingWaitingTime = LOADING_TOAST_MIN_DURATION - (Date.now() - loadingToastShowedAt)
        await new Promise(resolve => setTimeout(resolve, remainingWaitingTime))

        toast.dismiss(loadingToastId)

        // Wait again for the dismiss animation to finish. This prevents UI flickering when
        // the potentially new toast is shown.
        await new Promise(resolve => setTimeout(resolve, LOADING_TOAST_DISMISS_DURATION))
    }


    const returnPromise = promise
        .finally(() => {
            isPromisePending = false
        })
        .then(async (result) => {
            await waitAndDismissLoadingToast()
            return result
        })
        .catch(async (err) => {
            await waitAndDismissLoadingToast()
            throw err
        })

    // Wait at least 100ms before showing the loading toast.
    // This allows us to avoid showing the loading toast for promises that resolve quickly.
    //
    // 100ms is persieved as instant by the user.
    // Source: https://www.pubnub.com/blog/how-fast-is-realtime-human-perception-and-technology
    //
    await new Promise(resolve => setTimeout(resolve, 100))
    if (isPromisePending) {
        loadingToastId = toast.loading(opts.title, opts)
        loadingToastShowedAt = Date.now()
    }

    return returnPromise
}

aa-ndrej avatar Sep 01 '25 10:09 aa-ndrej

@lahdekorpi my workaround was to use two different toasts like this:

I tested this as well, but the issue with this is, it triggers a new animation instead of replacing the existing one, and is distracting.

lahdekorpi avatar Sep 01 '25 10:09 lahdekorpi

@lahdekorpi thats true. I specifically build the showPromiseToast function to minimize these distractions if the promise resolves quickly, but this is not a perfect workaround and I think currently there is none.

aa-ndrej avatar Sep 01 '25 15:09 aa-ndrej