-
Notifications
You must be signed in to change notification settings - Fork 154
docs(blade): Toast API decisions #1990
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
Changes from all commits
daf91a6
b5bc7ac
06cfd69
d1bda28
69e8204
f17adab
ee20396
0c441bb
d01baef
7a08645
f957687
ec2db65
74a03f1
80132a1
34b29e8
e24adc6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,267 @@ | ||||||
# Toast 🏷️ | ||||||
|
||||||
Toasts are used to provide feedback to the user after an important action has been performed. | ||||||
Toasts can also be used to provide feedback to the user when a system event occurs, such as when a file is saved. | ||||||
|
||||||
<img width="426" alt="image" src="./toast-thumbnail.png" /> | ||||||
|
||||||
## Design | ||||||
|
||||||
- [Toast - Figma Design](https://www.figma.com/file/jubmQL9Z8V7881ayUD95ps/Blade-DSL?type=design&node-id=7665-27414&mode=design&t=UNInCMmP1iFCu9je-0) | ||||||
|
||||||
## Features | ||||||
|
||||||
- Stackable | ||||||
- Auto dismissable / manual dismissable | ||||||
- `informational` or `promotional` types | ||||||
|
||||||
## Anatomy | ||||||
|
||||||
### Informational Toast | ||||||
|
||||||
<img src="./toast-anatomy.png" width="100%" alt="Toast anatomy" /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. show promotional type here as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. anatomy is the same. leading, button, close button & content. |
||||||
|
||||||
### Promotional Toast | ||||||
|
||||||
<img src="./example-promotional-toast.png" width="380" alt="Promotional toast example" /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add anatomy wala screenshot here? primarily because icon is asset in promotional There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Design hasn't created anatomy illustration for promo toast. // @saurav12
There is no difference of anatomy between info/promo toast. Both are 1:1
|
||||||
|
||||||
## Simple Usage | ||||||
|
||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
The blade toasts will use [react-hot-toast](https://react-hot-toast.com/) under the hood with a similar imperative API to show, dismiss and create new toasts without needing for consumer to handle positional or stacking logic. | ||||||
|
||||||
```jsx | ||||||
import { BladeProvider, ToastContainer, useToast } from "@razorpay/blade/components" | ||||||
|
||||||
const HomePage = () => { | ||||||
const toast = useToast(); | ||||||
|
||||||
return ( | ||||||
<Button | ||||||
onClick={() => { | ||||||
toast.show({ | ||||||
type: 'informational', | ||||||
color: 'success', | ||||||
content: 'Payment Successful', | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
leading: <DollarIcon />, | ||||||
autoDismiss: true, | ||||||
onDismissButtonClick: () => { | ||||||
console.log('Toast dismissed'); | ||||||
}, | ||||||
action: { | ||||||
text: 'View', | ||||||
onClick: () => { | ||||||
console.log('Toast action clicked'); | ||||||
} | ||||||
} | ||||||
}); | ||||||
}} | ||||||
> | ||||||
Show Toast | ||||||
</Button> | ||||||
); | ||||||
}; | ||||||
|
||||||
const App = () => { | ||||||
return ( | ||||||
<BladeProvider> | ||||||
<ToastContainer position="bottom-left" /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will there be multiple toast containers in an app? Can there not be a single ToastProvider that is part of BladeProvider and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There can only be 1.
That should be possible, we can keep the ToastContainer inside BladeProvider but the thing is we also may need to pass some props to ToastContainer, how should we handle that? |
||||||
<HomePage /> | ||||||
</BladeProvider> | ||||||
) | ||||||
} | ||||||
``` | ||||||
|
||||||
## Props | ||||||
|
||||||
```ts | ||||||
type ToastProps = { | ||||||
/** | ||||||
* @default `informational` | ||||||
*/ | ||||||
type?: 'informational' | 'promotional'; | ||||||
|
||||||
/** | ||||||
* @default `neutral` | ||||||
*/ | ||||||
color?: 'neutral' | 'positive' | 'negative' | 'warning' | 'information' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in case of promotional we omit this prop? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We just ignore this prop in case of promo. |
||||||
|
||||||
/** | ||||||
* Content of the toast | ||||||
*/ | ||||||
content: React.ReactNode; | ||||||
|
||||||
/** | ||||||
* Can be used to render an icon or asset | ||||||
*/ | ||||||
leading?: React.ReactElement; | ||||||
|
||||||
/** | ||||||
* If true, the toast will be dismissed after few seconds | ||||||
* | ||||||
* @default true - for informational toast | ||||||
* @default false - for promotional toast | ||||||
*/ | ||||||
autoDismiss?: boolean; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a use case for promotional toast to be auto dismissed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be a product decision. We do not have a defined usecase from design side. |
||||||
|
||||||
/** | ||||||
* Duration after which the toast will be dismissed (in ms) | ||||||
* | ||||||
* Duration for promotional toast is 8s | ||||||
* Duration for informational toast is 4s | ||||||
*/ | ||||||
duration?: 8000 | 4000; | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
snitin315 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
/** | ||||||
* Called when the toast is dismissed or duration runs out | ||||||
*/ | ||||||
onDismissButtonClick?: () => void; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
to avoid confusion? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we need to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah maybe we need this separated out. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps single onDismiss with type = 'auto' | 'onclick'? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
||||||
/** | ||||||
* Primary action of toast | ||||||
*/ | ||||||
action?: { | ||||||
text: string; | ||||||
onClick?: (e: Event) => void; | ||||||
isLoading? boolean; | ||||||
} | ||||||
|
||||||
parichay28 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
/** | ||||||
* Forwarded to react-hot-toast | ||||||
* | ||||||
* This can be used to programatically update toasts by providing an id | ||||||
*/ | ||||||
toastId?: string; | ||||||
} | ||||||
``` | ||||||
|
||||||
### useToast | ||||||
|
||||||
The useToast hook will return few modified methods from [react-hot-toast](https://react-hot-toast.com/docs/toast): | ||||||
|
||||||
```ts | ||||||
type useToastReturnType = { | ||||||
/** | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we shall also give an option to know the current id of the toast that is opened? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "current" can also be multiple toasts, we can give an option like: const { toasts } = useToasts(); but not sure about the usecase. |
||||||
* @returns id of the toast | ||||||
*/ | ||||||
show: (toast: Toast) => string; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will return id of which toast since there could be many? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Id of the toast just being created via this show function. |
||||||
|
||||||
/** | ||||||
* id of the toast to be dismissed | ||||||
* | ||||||
* if id is not provided, all the toasts will be dismissed | ||||||
*/ | ||||||
dismiss: (toastId?: string) => void; | ||||||
} | ||||||
``` | ||||||
|
||||||
## Examples | ||||||
|
||||||
#### Removing a toast | ||||||
|
||||||
react-hot-toast provides this functionality, for more info see [react-hot-toast docs](https://react-hot-toast.com/docs/toast#dismiss-toast-programmatically) | ||||||
|
||||||
```jsx | ||||||
import { BladeProvider, ToastContainer, useToast } from "@razorpay/blade/components" | ||||||
|
||||||
const Example = () => { | ||||||
const toastId = React.useRef(null); | ||||||
const toast = useToast(); | ||||||
|
||||||
return ( | ||||||
<Box> | ||||||
<Button | ||||||
onClick={() => { | ||||||
toastId.current = toast.show({ | ||||||
color: 'success', | ||||||
content: 'Payment Successful', | ||||||
}); | ||||||
}} | ||||||
> | ||||||
Show Toast | ||||||
</Button> | ||||||
<Button onClick={() => toast.dismiss(toastId.current)}>Dismiss Toast</Button> | ||||||
</Box> | ||||||
); | ||||||
}; | ||||||
``` | ||||||
|
||||||
### Promotional Toast | ||||||
|
||||||
```jsx | ||||||
import { BladeProvider, ToastContainer, useToast } from "@razorpay/blade/components" | ||||||
|
||||||
const Example = () => { | ||||||
const toast = useToast(); | ||||||
|
||||||
return ( | ||||||
<Button | ||||||
onClick={() => { | ||||||
toast.show({ | ||||||
type: 'promotional', | ||||||
content: ( | ||||||
<Box> | ||||||
<Heading>Payment Successful</Heading> | ||||||
<Text>Amount: ₹100</Text> | ||||||
</Box> | ||||||
kamleshchandnani marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
), | ||||||
leading: <DollarIcon />, | ||||||
action: { | ||||||
text: 'Okay' | ||||||
} | ||||||
}); | ||||||
}} | ||||||
> | ||||||
Do payment | ||||||
</Button> | ||||||
); | ||||||
}; | ||||||
``` | ||||||
|
||||||
<img src="./example-promotional-toast.png" width="380" alt="Promotional toast example" /> | ||||||
|
||||||
|
||||||
## Motion | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
You can checkout the toast motion [here](https://www.figma.com/proto/jubmQL9Z8V7881ayUD95ps/Blade-DSL?type=design&node-id=75848-1063056&t=QZkki0ZrlcG4sKzw-0&scaling=min-zoom&page-id=7665%3A27414). | ||||||
|
||||||
## Accessibility | ||||||
|
||||||
- Toast components will follow the WAI-ARIA guidelines for [alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/examples/alert/). For error toast we will use the `alert` role and for informational toast we will use the `status` role. | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
## References | ||||||
|
||||||
- https://react-hot-toast.com/docs | ||||||
- https://chakra-ui.com/docs/components/toast/usage | ||||||
|
||||||
## Open Questions | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
- Q. What should be the default duration for auto dismissable toasts? | ||||||
- A. 4s for informational toasts and 8s for promotional toasts | ||||||
|
||||||
- Q. Should we call it `onDismissButtonClick` or `onDismiss`? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going ahead with onDismissButtonClick, let's not have onManual, onAutoDismiss because there is no controlled state that users can manage for toasts so they don't really need to know if a toast is auto dismissed or not. |
||||||
|
||||||
- Q. Should the dismiss handler be called even when the toast is auto dismissed? Or should we have different handlers for auto dismiss and manual dismiss? (eg: `onAutoDismiss` `onDismissButtonClick`) | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
- A. We will call it `onDismissButtonClick` and it will only be called when the user clicks on the dismiss button. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if product needs to track the event on auto dismiss then how can consumer handle that?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if tracking auto dismiss makes sense. if 2 toasts are opened with They could just track if 2 toasts are opened or not. onClick={() => {
toast.show();
analytics.track();
}} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can do multiple things like for eg: a user was shown a toast and they navigate to a different page then when the toast is dismissed you can track the URL of the page. this way you can identify a pattern that after performing some actions what are the pages a user navigates to and get some insights around user behavior There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is possible.
But don't think it will be a very good idea for toast to automatically navigate the page after dismiss. Navigation should happen after action. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, react-hot-toast doesn't have a callback that it calls when a toast gets auto dismissed, so it's not even possible to have an There is a workaround but doing that means you need to keep track of each and every toast's timer manually, too messy. |
||||||
|
||||||
- Q. In the `useToast` hook should we call the returned functions `showToast`/`dismissToast` or `show`/`dismiss`? | ||||||
|
||||||
If we call them `show` & `dismiss` consumer can do this and might look more cleaner: | ||||||
|
||||||
```jsx | ||||||
const App = () => { | ||||||
const toast = useToast(); | ||||||
|
||||||
toast.show(); // <-- they will also get auto complete when writing `toast.` | ||||||
toast.dismiss(); | ||||||
} | ||||||
``` | ||||||
- A. We will call them `show` & `dismiss` | ||||||
|
||||||
- Q. In design we are restricting the Toast position to be bottom-left. In that case should we also do the same or should we allow all the positions & set the default to bottom-left? (If we allow all the positions, we will have to add some additional logic to handle stacking/animation of toasts coming from top instead of bottom) | ||||||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
- A. We will only allow bottom-left position for now. | ||||||
|
||||||
- Q. Should we keep the ToastContainer inside BladeProvider? | ||||||
|
||||||
The problem is if we keep it inside BladeProvider, given our new light/dark mode setup where consumers will need to nest BladeProviders it would cause ToastContainer to render [multiple times](https://github.com/razorpay/blade/pull/1990#discussion_r1470796627). | ||||||
- A. We will expose ToastContainer, so that users can render it at the root level of their app regardless of where BladeProvider is. |
Uh oh!
There was an error while loading. Please reload this page.