Skip to content

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

Merged
merged 16 commits into from
Feb 14, 2024
266 changes: 266 additions & 0 deletions packages/blade/src/components/Toast/_decisions/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# 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
- Positioning
- `informational` or `promotional` types

## Anatomy

<img src="./toast-anatomy.png" width="100%" alt="Toast anatomy" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

show promotional type here as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anatomy is the same. leading, button, close button & content.


## Simple Usage

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 { showToast } = useToast();

return (
<Button
onClick={() => {
showToast({
type: 'informational',
color: 'success',
content: 'Payment Successful',
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" />
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 useToast can decide the toast position?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can only be 1.

Can there not be a single ToastProvider that is part of BladeProvider and useToast can decide the toast position?

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

### ToastContainer

```ts
type ToastContainer = {
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
}
```

```ts
type Toast = {
/**
* @default `informational`
*/
type: 'informational' | 'promotional';

/**
* @default `neutral`
*/
color: 'neutral' | 'positive' | 'negative' | 'warning' | 'information'

/**
* If the type is `promotional`, the content will be `React.ReactNode`
*/
content: string | React.ReactNode;

/**
* Can be used to render an icon
*/
leading?: IconComponent;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we don't have trailing counterpart and we know that icon will always be leading wouldn't it be better to name it just icon? so it will also be consistent with rest of the components?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Lets just call it icon similar to how we do it for button

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In promotional toast the leading can also become an asset, so leading maybe more flexible naming here.

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we mean by asset? It can be an image or text or video? Any restrictions or is it a slot?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No restrictions, it's a slot. we should atleast keep the prop name generic to cater to future usecase.

Same thing we did for BottomSheetHeader too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can have icon and asset as a naming then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two props? Will cause impossible states.


/**
* If true, the toast will be dismissed after few seconds
*
* @default true - for informational toast
* @default false - for promotional toast
*/
autoDismiss?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a use case for promotional toast to be auto dismissed?

Copy link
Member Author

Choose a reason for hiding this comment

The 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;

/**
* Called when the toast is dismissed or duration runs out
*/
onDismissButtonClick?: () => void;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onDismissButtonClick?: () => void;
onDismiss?: () => void;

to avoid confusion?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to have onDismissClicked & onAutoDismiss?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah maybe we need this separated out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps single onDismiss with type = 'auto' | 'onclick'?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onAutoDismiss & onManualDismiss


/**
* Primary action of toast
*/
action?: {
text: string;
onClick?: () => void;
isLoading? boolean;
}

/**
* 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 = {
/**
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
*/
showToast: (toast: Toast) => string;

/**
* id of the toast to be dismissed
*
* if id is not provided, all the toasts will be dismissed
*/
dismissToast: (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 { showToast, dismissToast } = useToast();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toast will have a context at the app level right? can't see that in the example?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, I'm not sure right now actually, so react-hot-toast works in a way that it doesn't need ReactContext.
In the current POC that I've done that also didn't need a toast context.


return (
<Box>
<Button
onClick={() => {
toastId.current = showToast({
color: 'success',
content: 'Payment Successful',
});
}}
>
Show Toast
</Button>
<Button onClick={() => dismissToast(toastId.current)}>Dismiss Toast</Button>
</Box>
);
};
```

### Promotional Toast

```jsx
import { BladeProvider, ToastContainer, useToast } from "@razorpay/blade/components"

const Example = () => {
const { showToast } = useToast();

return (
<Button
onClick={() => {
showToast({
type: 'promotional',
content: (
<Box>
<Heading>Payment Successful</Heading>
<Text>Amount: ₹100</Text>
</Box>
),
leading: <DollarIcon />,
action: {
text: 'Okay'
}
});
}}
>
Do payment
</Button>
);
};
```

<img src="./example-promotional-toast.png" width="380" alt="Promotional toast example" />


## Motion

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.


## References

- https://react-hot-toast.com/docs
- https://chakra-ui.com/docs/components/toast/usage

## Open Questions

- Q. What should be the default duration for auto dismissable toasts?
- A. 4s for informational toasts and 6s for promotional toasts

- Q. Should we call it `onDismissButtonClick` or `onDismiss`?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onManualDismiss

Copy link
Member Author

Choose a reason for hiding this comment

The 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`)

- 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();
}
```

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

- 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).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.