Skip to content

Commit 5d160e6

Browse files
authored
allow to pass jsx to actions (#379)
* allow to pass jsx to actions * Allow to pass jsx as actions * update docs
1 parent 0d395c4 commit 5d160e6

File tree

4 files changed

+61
-28
lines changed

4 files changed

+61
-28
lines changed

.github/pull-request-template.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
### Issue 😱:
1+
### Issue:
22

33
Closes https://github.com/emilkowalski/sonner/issues/
44

5-
### What has been done:
5+
### What has been done:
66

77
- [ ]
88

9-
### Screenshots/Videos 🎥:
9+
### Screenshots/Videos:
1010

1111
N/A

src/index.tsx

+29-16
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
import React from 'react';
44
import ReactDOM from 'react-dom';
55

6-
import DOMPurify from "dompurify";
6+
import DOMPurify from 'dompurify';
77
import { getAsset, Loader } from './assets';
88
import { useIsDocumentHidden } from './hooks';
99
import { toast, ToastState } from './state';
1010
import './styles.css';
11-
import type { ExternalToast, HeightT, ToasterProps, ToastProps, ToastT, ToastToDismiss } from './types';
11+
import {
12+
isAction,
13+
type ExternalToast,
14+
type HeightT,
15+
type ToasterProps,
16+
type ToastProps,
17+
type ToastT,
18+
type ToastToDismiss,
19+
} from './types';
1220

1321
// Visible toasts amount
1422
const VISIBLE_TOASTS_AMOUNT = 3;
@@ -56,7 +64,7 @@ const Toast = (props: ToastProps) => {
5664
descriptionClassName = '',
5765
duration: durationFromToaster,
5866
position,
59-
gap = GAP,
67+
gap,
6068
loadingIcon: loadingIconProp,
6169
expandByDefault,
6270
classNames,
@@ -375,16 +383,16 @@ const Toast = (props: ToastProps) => {
375383
<>
376384
{toastType || toast.icon || toast.promise ? (
377385
<div data-icon="" className={cn(classNames?.icon)}>
378-
{toast.promise || (toast.type === 'loading' && !toast.icon)
379-
? toast.icon || getLoadingIcon()
380-
: null}
386+
{toast.promise || (toast.type === 'loading' && !toast.icon) ? toast.icon || getLoadingIcon() : null}
381387
{toast.type !== 'loading' ? toast.icon || icons?.[toastType] || getAsset(toastType) : null}
382388
</div>
383389
) : null}
384390

385391
<div data-content="" className={cn(classNames?.content)}>
386-
<div data-title="" className={cn(classNames?.title, toast?.classNames?.title)}
387-
dangerouslySetInnerHTML={sanitizeHTML(toast.title as string)}
392+
<div
393+
data-title=""
394+
className={cn(classNames?.title, toast?.classNames?.title)}
395+
dangerouslySetInnerHTML={sanitizeHTML(toast.title as string)}
388396
></div>
389397
{toast.description ? (
390398
<div
@@ -399,29 +407,35 @@ const Toast = (props: ToastProps) => {
399407
></div>
400408
) : null}
401409
</div>
402-
{toast.cancel ? (
410+
{React.isValidElement(toast.cancel) ? (
411+
toast.cancel
412+
) : toast.cancel && isAction(toast.cancel) ? (
403413
<button
404414
data-button
405415
data-cancel
406416
style={toast.cancelButtonStyle || cancelButtonStyle}
407417
onClick={(event) => {
418+
// We need to check twice because typescript
419+
if (!isAction(toast.cancel)) return;
408420
if (!dismissible) return;
409421
deleteToast();
410-
if (toast.cancel?.onClick) {
411-
toast.cancel.onClick(event);
412-
}
422+
toast.cancel.onClick(event);
413423
}}
414424
className={cn(classNames?.cancelButton, toast?.classNames?.cancelButton)}
415425
>
416426
{toast.cancel.label}
417427
</button>
418428
) : null}
419-
{toast.action ? (
429+
{React.isValidElement(toast.action) ? (
430+
toast.action
431+
) : toast.action && isAction(toast.action) ? (
420432
<button
421433
data-button=""
422434
style={toast.actionButtonStyle || actionButtonStyle}
423435
onClick={(event) => {
424-
toast.action?.onClick(event);
436+
// We need to check twice because typescript
437+
if (!isAction(toast.action)) return;
438+
toast.action.onClick(event);
425439
if (event.defaultPrevented) return;
426440
deleteToast();
427441
}}
@@ -465,7 +479,7 @@ const Toaster = (props: ToasterProps) => {
465479
visibleToasts = VISIBLE_TOASTS_AMOUNT,
466480
toastOptions,
467481
dir = getDocumentDirection(),
468-
gap,
482+
gap = GAP,
469483
loadingIcon,
470484
icons,
471485
containerAriaLabel = 'Notifications',
@@ -703,4 +717,3 @@ const Toaster = (props: ToasterProps) => {
703717
);
704718
};
705719
export { toast, Toaster, type ExternalToast, type ToastT };
706-

src/types.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export interface ToastIcons {
4040
loading?: React.ReactNode;
4141
}
4242

43+
interface Action {
44+
label: string;
45+
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
46+
actionButtonStyle?: React.CSSProperties;
47+
}
48+
4349
export interface ToastT {
4450
id: number | string;
4551
title?: string | React.ReactNode;
@@ -53,14 +59,8 @@ export interface ToastT {
5359
duration?: number;
5460
delete?: boolean;
5561
important?: boolean;
56-
action?: {
57-
label: React.ReactNode;
58-
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
59-
};
60-
cancel?: {
61-
label: React.ReactNode;
62-
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
63-
};
62+
action?: Action | React.ReactNode;
63+
cancel?: Action | React.ReactNode;
6464
onDismiss?: (toast: ToastT) => void;
6565
onAutoClose?: (toast: ToastT) => void;
6666
promise?: PromiseT;
@@ -74,6 +74,10 @@ export interface ToastT {
7474
position?: Position;
7575
}
7676

77+
export function isAction(action: Action | React.ReactNode): action is Action {
78+
return (action as Action).label !== undefined && typeof (action as Action).onClick === 'function';
79+
}
80+
7781
export type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center';
7882
export interface HeightT {
7983
height: number;

website/src/pages/toast.mdx

+17-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ To render a toast on initial page load it is required that the function `toast()
3434
```jsx
3535
setTimeout(() => {
3636
toast('My toast on a page load');
37-
})
37+
});
3838
```
3939

4040
## Creating toasts
@@ -68,6 +68,14 @@ toast('My action toast', {
6868
});
6969
```
7070

71+
You can also render jsx as your action.
72+
73+
```jsx
74+
toast('My action toast', {
75+
action: <Button onClick={() => console.log('Action!')}>Action</Button>,
76+
});
77+
```
78+
7179
### Cancel
7280

7381
Renders a secondary button, clicking it will close the toast and run the callback passed via `onClick`.
@@ -81,6 +89,14 @@ toast('My cancel toast', {
8189
});
8290
```
8391

92+
You can also render jsx as your action.
93+
94+
```jsx
95+
toast('My cancel toast', {
96+
action: <Button onClick={() => console.log('Cancel!')}>Cancel</Button>,
97+
});
98+
```
99+
84100
### Promise
85101

86102
Starts in a loading state and will update automatically after the promise resolves or fails.

0 commit comments

Comments
 (0)