Skip to content

Commit

Permalink
Allow toast updates and listen to height changes (#75)
Browse files Browse the repository at this point in the history
* Write initial update logic

* Progress

* Calculate height correctly

* Update

* Update readme

* Revert hero changes

* Fix logic
  • Loading branch information
emilkowalski committed May 23, 2023
1 parent 277e605 commit fe03e34
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 14 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ You can pass jsx as the first argument instead of a string to render custom jsx
toast(<div>A custom toast with default styling</div>);
```

### Updating a toast

You can update a toast by using the `toast` function and passing it the id of the toast you want to update, the rest stays the same.

```jsx
const toastId = toast('Sonner');

toast.success('Toast has been updated', {
id: toastId,
});
```

## Customization

### Headless
Expand Down
36 changes: 35 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,33 @@ const Toast = (props: ToastProps) => {
}, [heights, heightIndex]);
const invert = toast.invert || ToasterInvert;
const disabled = promiseStatus === 'loading';

offset.current = React.useMemo(() => heightIndex * GAP + toastsHeightBefore, [heightIndex, toastsHeightBefore]);

React.useEffect(() => {
// Trigger enter animation without using CSS animation
setMounted(true);
}, []);

React.useLayoutEffect(() => {
if (!mounted) return;
const toastNode = toastRef.current;
const originalHeight = toastNode.style.height;
toastNode.style.height = 'auto';
const newHeight = toastNode.getBoundingClientRect().height;
toastNode.style.height = originalHeight;

setInitialHeight(newHeight);

const alreadyExists = heights.find((height) => height.toastId === toast.id);

if (!alreadyExists) {
setHeights((h) => [{ toastId: toast.id, height: newHeight }, ...h]);
} else {
setHeights((h) => h.map((height) => (height.toastId === toast.id ? { ...height, height: newHeight } : height)));
}
}, [toast.title, toast.description]);

React.useEffect(() => {
if (isPromise(toast)) {
setPromiseStatus('loading');
Expand Down Expand Up @@ -195,6 +215,7 @@ const Toast = (props: ToastProps) => {

if (toastNode) {
const height = toastNode.getBoundingClientRect().height;

// Add toast height tot heights array after the toast is mounted
setInitialHeight(height);
setHeights((h) => [{ toastId: toast.id, height }, ...h]);
Expand Down Expand Up @@ -420,7 +441,20 @@ const Toaster = (props: ToasterProps) => {
// Prevent batching, temp solution.
setTimeout(() => {
ReactDOM.flushSync(() => {
setToasts((toasts) => [toast, ...toasts]);
setToasts((toasts) => {
const indexOfExistingToast = toasts.findIndex((t) => t.id === toast.id);

// Upadte the toast if it already exists
if (indexOfExistingToast !== -1) {
return [
...toasts.slice(0, indexOfExistingToast),
{ ...toasts[indexOfExistingToast], ...toast },
...toasts.slice(indexOfExistingToast + 1),
];
}

return [toast, ...toasts];
});
});
});
});
Expand Down
41 changes: 28 additions & 13 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { ExternalToast, ToastT, PromiseData, PromiseT, ToastToDismiss } from './types';
import { ExternalToast, ToastT, PromiseData, PromiseT, ToastToDismiss, ToastTypes } from './types';

let toastsCounter = 0;

Expand Down Expand Up @@ -27,6 +27,29 @@ class Observer {
this.toasts = [...this.toasts, data];
};

create = (data: ExternalToast & { message?: string | React.ReactNode; type?: ToastTypes }) => {
const { message, ...rest } = data;
const id = typeof data?.id === 'number' || data.id?.length > 0 ? data.id : toastsCounter++;
const alreadyExists = this.toasts.find((toast) => {
return toast.id === id;
});

if (alreadyExists) {
this.toasts.map((toast) => {
if (toast.id === id) {
this.publish({ ...toast, ...data, id });
return { ...toast, ...data };
}

return toast;
});
} else {
this.publish({ title: message, ...rest, id });
}

return id;
};

dismiss = (id?: number | string) => {
if (!id) {
this.toasts.forEach((toast) => {
Expand All @@ -39,27 +62,19 @@ class Observer {
};

message = (message: string | React.ReactNode, data?: ExternalToast) => {
const id = data?.id || toastsCounter++;
this.publish({ ...data, id, title: message });
return id;
return this.create({ ...data, message });
};

error = (message: string | React.ReactNode, data?: ExternalToast) => {
const id = data?.id || toastsCounter++;
this.publish({ ...data, id, type: 'error', title: message });
return id;
return this.create({ ...data, message, type: 'error' });
};

success = (message: string | React.ReactNode, data?: ExternalToast) => {
const id = data?.id || toastsCounter++;
this.publish({ ...data, id, type: 'success', title: message });
return id;
return this.create({ ...data, type: 'success', message });
};

promise = (promise: PromiseT, data?: PromiseData) => {
const id = data?.id || toastsCounter++;
this.publish({ ...data, promise, id });
return id;
return this.create({ ...data, promise });
};

// We can't provide the toast we just created as a prop as we didn't creat it yet, so we can create a default toast object, I just don't know how to use function in argument when calling()?
Expand Down

0 comments on commit fe03e34

Please sign in to comment.