From 3109895a58fdf32d580613021316999b3da213b2 Mon Sep 17 00:00:00 2001 From: Blankeos Date: Sat, 18 May 2024 02:47:30 +0800 Subject: [PATCH 1/3] fix: updates on existing toasts and displaying of icons. --- dev/components/Types/index.tsx | 30 ++++++++- src/index.tsx | 113 +++++++++++++++++---------------- 2 files changed, 87 insertions(+), 56 deletions(-) diff --git a/dev/components/Types/index.tsx b/dev/components/Types/index.tsx index aed28ae..96ca679 100644 --- a/dev/components/Types/index.tsx +++ b/dev/components/Types/index.tsx @@ -72,8 +72,11 @@ toast.promise(promise, { action: () => toast.promise<{ name: string }>( () => - new Promise((resolve) => { + new Promise((resolve, reject) => { setTimeout(() => { + const random50Percent = Math.floor(Math.random() * 2) + if (random50Percent > 0) + reject(new Error('Something\'s not right!')) resolve({ name: 'Solid Sonner' }) }, 1500) }), @@ -86,6 +89,31 @@ toast.promise(promise, { }, ), }, + { + name: 'Loading', + snippet: `const promise = () => new Promise((resolve) => setTimeout(resolve, 800)); + +toast.loading('Uploading...', { id: 'form' }); +await promise(); +toast.loading('Saving...', { id: 'form'}); +await promise(); +toast.success('Success!', { id: 'form' }); +`, + action: async () => { + const idAlphabet = 'abcdefghijklmnopqrstuvwxyz1234567890' + const getRandomIndex = () => Math.floor(Math.random() * idAlphabet.length) + const toastId = idAlphabet[getRandomIndex()]! + idAlphabet[getRandomIndex()]! + idAlphabet[getRandomIndex()]! + + const promise = () => new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + toast.loading('Uploading...', { id: toastId }) + await promise() + toast.loading('Saving...', { id: toastId }) + await promise() + toast.success('Success!', { id: toastId }) + }, + }, { name: 'Custom', snippet: 'toast(
A custom toast with default styling
)', diff --git a/src/index.tsx b/src/index.tsx index db476d7..8d5d9f2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,8 +7,8 @@ */ import './styles.css' import type { Component, ValidComponent } from 'solid-js' -import { Dynamic } from 'solid-js/web' import { For, Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount } from 'solid-js' +import { createStore, produce, reconcile } from 'solid-js/store' import { Loader, getAsset } from './assets' import type { ExternalToast, HeightT, Position, ToastProps, ToastT, ToastToDismiss, ToasterProps } from './types' import { ToastState, toast } from './state' @@ -340,12 +340,9 @@ const Toast: Component = (props) => { <>
- } - > - props.toast.icon)) || props.icons?.loading || getLoadingIcon()) as ValidComponent} /> - + {props.toast.promise || (props.toast.type === 'loading' && !props.toast.icon) ? props.toast.icon || getLoadingIcon() : null} + {/* @ts-expect-error Some icons don't display when toast type updates existing toasts. TODO: Can be improved to be written in the "solid-way". This removes the bug. */} + {props.toast.type !== 'loading' ? props.toast.icon || props.icons?.[toastType() as unknown as keyof typeof props.icons] || getAsset(toastType()!)! : null}
@@ -427,10 +424,16 @@ const Toaster: Component = (props) => { dir: getDocumentDirection(), }, props) as ToasterProps & { position: Position; hotkey: string[]; visibleToasts: number } - const [toasts, setToasts] = createSignal([]) + /** + * Use a store instead of a signal for fine-grained reactivity. + * All the setters only have to change the deepest part of the tree + * to maintain referential integrity when rendered in the DOM. + */ + const [toastsStore, setToastsStore] = createStore<{ toasts: ToastT[] }>({ toasts: [] }) + const possiblePositions = () => { return Array.from( - new Set([propsWithDefaults.position].concat(toasts().filter(toast => toast.position).map(toast => toast.position as Position))), + new Set([propsWithDefaults.position].concat(toastsStore.toasts.filter(toast => toast.position).map(toast => toast.position as Position))), ) } const [heights, setHeights] = createSignal([]) @@ -449,29 +452,31 @@ const Toaster: Component = (props) => { : 'light' : 'light', ) - const removeToast = (toast: ToastT) => setToasts(toasts => toasts.filter(({ id }) => id !== toast.id)) + const removeToast = (toast: ToastT) => setToastsStore('toasts', toasts => toasts.filter(({ id }) => id !== toast.id)) onMount(() => { const unsub = ToastState.subscribe((toast) => { if ((toast as ToastToDismiss).dismiss) { - setToasts(toasts => toasts.map(t => (t.id === toast.id ? { ...t, delete: true } : t))) + setToastsStore('toasts', produce((_toasts) => { + _toasts.forEach((t) => { + if (t.id === toast.id) + t.delete = true + }) + })) return } - setToasts((toasts) => { - const indexOfExistingToast = toasts.findIndex(t => t.id === toast.id) - - // Update the toast if it already exists - if (indexOfExistingToast !== -1) { - return [ - ...toasts.slice(0, indexOfExistingToast), - { ...toasts[indexOfExistingToast], ...toast }, - ...toasts.slice(indexOfExistingToast + 1), - ] - } + // Update (Fine-grained) + const changedIndex = toastsStore.toasts.findIndex(t => t.id === toast.id) + if (changedIndex !== -1) { + setToastsStore('toasts', [changedIndex], reconcile(toast)) + return + } - return [toast, ...toasts] - }) + // Insert (Fine-grained) + setToastsStore('toasts', produce((_toasts) => { + _toasts.unshift(toast) + })) }) onCleanup(() => { @@ -504,7 +509,7 @@ const Toaster: Component = (props) => { createEffect(() => { // Ensure expanded is always false when no toasts are present / only one left - if (toasts().length <= 1) + if (toastsStore.toasts.length <= 1) setExpanded(false) }) @@ -549,7 +554,7 @@ const Toaster: Component = (props) => { ) return ( - 0}> + 0}> {/* Remove item from normal navigation flow, only available via hotkey */}
@@ -603,36 +608,34 @@ const Toaster: Component = (props) => { onPointerUp={() => setInteracting(false)} > (!toast.position && index() === 0) || toast.position === position) - }> + toastsStore.toasts.filter(toast => (!toast.position && index() === 0) || toast.position === position)}> {(toast, index) => ( - + )} From e85a99afef5320829ac5e82e6567c3c29e4204fb Mon Sep 17 00:00:00 2001 From: Blankeos Date: Sat, 18 May 2024 14:29:19 +0800 Subject: [PATCH 2/3] fix: ts-error for getAsset. --- src/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 8d5d9f2..2f611a0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -341,8 +341,7 @@ const Toast: Component = (props) => {
{props.toast.promise || (props.toast.type === 'loading' && !props.toast.icon) ? props.toast.icon || getLoadingIcon() : null} - {/* @ts-expect-error Some icons don't display when toast type updates existing toasts. TODO: Can be improved to be written in the "solid-way". This removes the bug. */} - {props.toast.type !== 'loading' ? props.toast.icon || props.icons?.[toastType() as unknown as keyof typeof props.icons] || getAsset(toastType()!)! : null} + {props.toast.type !== 'loading' ? props.toast.icon || props.icons?.[toastType() as unknown as keyof typeof props.icons] || getAsset(toastType()!)!() : null}
From 466d3ff33b546ed2979cd994738c4dc8ae3b8c52 Mon Sep 17 00:00:00 2001 From: Blankeos Date: Sun, 9 Jun 2024 14:34:37 +0800 Subject: [PATCH 3/3] refactor: Removed ValidComponent type. --- src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 2f611a0..9e808da 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,7 @@ * https://github.com/emilkowalski/sonner/blob/main/src/index.tsx */ import './styles.css' -import type { Component, ValidComponent } from 'solid-js' +import type { Component } from 'solid-js' import { For, Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount } from 'solid-js' import { createStore, produce, reconcile } from 'solid-js/store' import { Loader, getAsset } from './assets'