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

fix: updates on existing toasts and displaying of icons. #16

Merged
Merged
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
30 changes: 29 additions & 1 deletion dev/components/Types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}),
Expand All @@ -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(<div>A custom toast with default styling</div>)',
Expand Down
114 changes: 58 additions & 56 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* https://github.com/emilkowalski/sonner/blob/main/src/index.tsx
*/
import './styles.css'
import type { Component, ValidComponent } from 'solid-js'
import { Dynamic } from 'solid-js/web'
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'
import type { ExternalToast, HeightT, Position, ToastProps, ToastT, ToastToDismiss, ToasterProps } from './types'
import { ToastState, toast } from './state'
Expand Down Expand Up @@ -63,7 +63,7 @@
const [pointerStartRef, setPointerStartRef] = createSignal<{ x: number; y: number } | null>(null)
const coords = () => props.position.split('-')
const toastsHeightBefore = () => {
return props.heights.reduce((prev, curr, reducerIndex) => {

Check warning on line 66 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored
// Calculate offset up untill current toast
if (reducerIndex >= heightIndex())
return prev
Expand Down Expand Up @@ -103,7 +103,7 @@
setInitialHeight(newHeight)

createEffect(() => {
props.setHeights((heights) => {

Check warning on line 106 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored
const alreadyExists = heights.find(height => height.toastId === props.toast.id)
if (!alreadyExists)
return [{ toastId: props.toast.id, height: newHeight, position: props.toast.position }, ...heights]
Expand All @@ -117,7 +117,7 @@
// Save the offset for the exit swipe animation
setRemoved(true)
setOffsetBeforeRemove(offset())
props.setHeights(h => h.filter(height => height.toastId !== props.toast.id))

Check warning on line 120 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored

setTimeout(() => {
props.removeToast(props.toast)
Expand Down Expand Up @@ -189,7 +189,7 @@

// Add toast height tot heights array after the toast is mounted
setInitialHeight(height)
props.setHeights(h => [{ toastId, height, position: props.toast.position }, ...h])

Check warning on line 192 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored

onCleanup(() => {
props.setHeights(h => h.filter(height => height.toastId !== toastId))
Expand Down Expand Up @@ -309,9 +309,9 @@
data-disabled={disabled()}
data-close-button
onClick={
disabled()

Check warning on line 312 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

The reactive variable 'disabled' should be wrapped in a function for reactivity. This includes event handler bindings on native elements, which are not reactive like other JSX props
? undefined
: () => {

Check warning on line 314 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored
deleteToast()
props.toast.onDismiss?.(props.toast)
}
Expand Down Expand Up @@ -340,12 +340,8 @@
<>
<Show when={toastType() || props.toast.icon || props.toast.promise}>
<div data-icon="">
<Show
when={props.toast.icon || props.toast.promise || toastType() === 'loading'}
fallback={<Dynamic component={getAsset(toastType()!)!} />}
>
<Dynamic component={((props.toast.icon && (() => props.toast.icon)) || props.icons?.loading || getLoadingIcon()) as ValidComponent} />
</Show>
{props.toast.promise || (props.toast.type === 'loading' && !props.toast.icon) ? props.toast.icon || getLoadingIcon() : null}
{props.toast.type !== 'loading' ? props.toast.icon || props.icons?.[toastType() as unknown as keyof typeof props.icons] || getAsset(toastType()!)!() : null}
</div>
</Show>

Expand Down Expand Up @@ -427,10 +423,16 @@
dir: getDocumentDirection(),
}, props) as ToasterProps & { position: Position; hotkey: string[]; visibleToasts: number }

const [toasts, setToasts] = createSignal<ToastT[]>([])
/**
* 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<HeightT[]>([])
Expand All @@ -441,37 +443,39 @@
const [lastFocusedElementRef, setLastFocusedElementRef] = createSignal<HTMLElement | null>(null)
const [isFocusedWithinRef, setIsFocusedWithinRef] = createSignal(false)
const [actualTheme, setActualTheme] = createSignal(
propsWithDefaults.theme !== 'system'

Check warning on line 446 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

The reactive variable 'propsWithDefaults.theme' should be used within JSX, a tracked scope (like createEffect), or inside an event handler function, or else changes will be ignored
? propsWithDefaults.theme

Check warning on line 447 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

The reactive variable 'propsWithDefaults.theme' should be used within JSX, a tracked scope (like createEffect), or inside an event handler function, or else changes will be ignored
: typeof window !== 'undefined'
? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: '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) => {

Check warning on line 457 in src/index.tsx

View workflow job for this annotation

GitHub Actions / lint

This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored
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(() => {
Expand Down Expand Up @@ -504,7 +508,7 @@

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)
})

Expand Down Expand Up @@ -549,7 +553,7 @@
)

return (
<Show when={toasts().length > 0}>
<Show when={toastsStore.toasts.length > 0}>
{/* Remove item from normal navigation flow, only available via hotkey */}
<section aria-label={`Notifications ${hotkeyLabel()}`} tabIndex={-1}>
<For each={possiblePositions()}>
Expand Down Expand Up @@ -603,36 +607,34 @@
onPointerUp={() => setInteracting(false)}
>
<For each={
toasts()
.filter(toast => (!toast.position && index() === 0) || toast.position === position)
}>
toastsStore.toasts.filter(toast => (!toast.position && index() === 0) || toast.position === position)}>
{(toast, index) => (
<Toast
index={index()}
icons={propsWithDefaults.icons}
toast={toast}
duration={propsWithDefaults.toastOptions?.duration ?? props.duration}
class={propsWithDefaults.toastOptions?.class}
classes={propsWithDefaults.toastOptions?.classes}
cancelButtonStyle={propsWithDefaults.toastOptions?.cancelButtonStyle}
actionButtonStyle={propsWithDefaults.toastOptions?.actionButtonStyle}
descriptionClass={propsWithDefaults.toastOptions?.descriptionClass}
invert={Boolean(propsWithDefaults.invert)}
visibleToasts={propsWithDefaults.visibleToasts}
closeButton={Boolean(propsWithDefaults.closeButton)}
interacting={interacting()}
position={propsWithDefaults.position}
style={propsWithDefaults.toastOptions?.style}
unstyled={propsWithDefaults.toastOptions?.unstyled}
removeToast={removeToast}
toasts={toasts()}
heights={heights()}
setHeights={setHeights}
expandByDefault={Boolean(propsWithDefaults.expand)}
gap={propsWithDefaults.gap}
expanded={expanded()}
pauseWhenPageIsHidden={propsWithDefaults.pauseWhenPageIsHidden}
/>
<Toast
index={index()}
icons={propsWithDefaults.icons}
toast={toast}
duration={propsWithDefaults.toastOptions?.duration ?? props.duration}
class={propsWithDefaults.toastOptions?.class}
classes={propsWithDefaults.toastOptions?.classes}
cancelButtonStyle={propsWithDefaults.toastOptions?.cancelButtonStyle}
actionButtonStyle={propsWithDefaults.toastOptions?.actionButtonStyle}
descriptionClass={propsWithDefaults.toastOptions?.descriptionClass}
invert={Boolean(propsWithDefaults.invert)}
visibleToasts={propsWithDefaults.visibleToasts}
closeButton={Boolean(propsWithDefaults.closeButton)}
interacting={interacting()}
position={propsWithDefaults.position}
style={propsWithDefaults.toastOptions?.style}
unstyled={propsWithDefaults.toastOptions?.unstyled}
removeToast={removeToast}
toasts={toastsStore.toasts}
heights={heights()}
setHeights={setHeights}
expandByDefault={Boolean(propsWithDefaults.expand)}
gap={propsWithDefaults.gap}
expanded={expanded()}
pauseWhenPageIsHidden={propsWithDefaults.pauseWhenPageIsHidden}
/>
)}
</For>
</ol>
Expand Down
Loading