-
Notifications
You must be signed in to change notification settings - Fork 151
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
feat(blade): add toast component #2000
Changes from 12 commits
7051454
dfa7939
2323145
39fb2bb
efa548f
d1672e4
7eedb4e
101094b
5cbbc61
3319520
5c23c83
3331954
5185747
469e8f5
a0dfac3
0120aed
ffe9e69
45f535f
a51eada
8c77908
4354e65
13cba57
cc4d1e3
6ff58ad
9b56142
feefb6e
53f308a
5ba4e5c
87bff48
c819313
1b62f74
b380ff5
f7e9b65
5a51c09
78340b8
d83cf0f
8c1b236
9a472f6
6cb1941
19953d7
5980893
62e45aa
ff8de33
e90ac1c
0a90bd6
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 |
---|---|---|
|
@@ -138,6 +138,7 @@ | |
"use-presence": "1.1.0", | ||
"@use-gesture/react": "10.2.24", | ||
"@floating-ui/react": "0.25.4", | ||
"react-hot-toast": "2.4.1", | ||
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. Would RN consumers need to install this too even though they won't be using it at all? 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. Dependencies will anyways be installed regardless, since most of your deps are inside "dependencies" |
||
"@emotion/react": "11.11.1", | ||
"@table-library/react-table-library": "4.1.7", | ||
"tinycolor2": "1.6.0" | ||
|
@@ -282,6 +283,7 @@ | |
"react-native-pager-view": "^6.2.1", | ||
"react-native-svg": "^12.3.0", | ||
"react-native-gesture-handler": "^2.9.0", | ||
"react-hot-toast": "2.4.1", | ||
"@gorhom/bottom-sheet": "^4.4.6", | ||
"@gorhom/portal": "^1.0.14" | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* eslint-disable jsx-a11y/label-has-associated-control */ | ||
/* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||
import { Title } from '@storybook/addon-docs'; | ||
import type { StoryFn, Meta } from '@storybook/react'; | ||
import React from 'react'; | ||
import { useToasterStore } from 'react-hot-toast'; | ||
import { useToast } from './useToast'; | ||
import type { ToastProps } from './'; | ||
import { Toast as ToastComponent, ToastContainer } from './'; | ||
import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; | ||
import { Sandbox } from '~utils/storybook/Sandbox'; | ||
import { Box } from '~components/Box'; | ||
import { Button } from '~components/Button'; | ||
import { Heading, Text } from '~components/Typography'; | ||
|
||
const Page = (): React.ReactElement => { | ||
return ( | ||
<StoryPageWrapper | ||
componentName="Toast" | ||
componentDescription="A switch component is used to quickly switch between two possible states. These are only used for binary actions that occur immediately after the user turn the switch on/off." | ||
figmaURL="https://www.figma.com/file/jubmQL9Z8V7881ayUD95ps/Blade---Payment-Light?node-id=13227%3A163026" | ||
> | ||
<Title>Usage</Title> | ||
<Sandbox> | ||
{` | ||
import { Switch } from '@razorpay/blade/components'; | ||
|
||
function App(): React.ReactElement { | ||
return ( | ||
// Check console | ||
<Switch | ||
onChange={(e) => console.log(e.isChecked)} | ||
accessibilityLabel="Toggle DarkMode" | ||
/> | ||
); | ||
} | ||
|
||
export default App; | ||
`} | ||
</Sandbox> | ||
</StoryPageWrapper> | ||
); | ||
}; | ||
|
||
export default { | ||
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. The basic story is a bit misleading. Changing props on it doesn't change toast visual instead it changes properties for the next toast. Not sure if we should keep it this way or not. Regardless, we need to communicate it better that whatever props they change on storybook would be applied to the next toast and not the existing one 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. Rather lets add some gap below the button so that the toasts collapse when they reach a certain stack height and button is never hidden 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. Sure, added a note in the top. |
||
title: 'Components/Toast', | ||
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. The content is fully configurable via the storybook controls nah? Can't really change it dynamically based on info/promo toasts. |
||
component: ToastComponent, | ||
tags: ['autodocs'], | ||
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.
Yes exactly, defaults which can be overridden depending on product usecase. |
||
parameters: { | ||
docs: { | ||
page: Page, | ||
}, | ||
}, | ||
} as Meta<ToastProps>; | ||
|
||
const ToastTemplate: StoryFn<typeof ToastComponent> = () => { | ||
const toast = useToast(); | ||
const { toasts } = useToasterStore(); | ||
const promoToasts = React.useMemo( | ||
// @ts-expect-error | ||
() => toasts.filter((toast) => toast.type === 'promotional' && toast.visible), | ||
[toasts], | ||
); | ||
const hasPromoToast = promoToasts.length > 0; | ||
return ( | ||
<Box> | ||
<ToastContainer /> | ||
<Button | ||
onClick={() => | ||
toast.show({ | ||
content: | ||
Math.random() > 0.5 | ||
? 'Payment Successful' | ||
: 'Razorpay Turbo UPI streamlines all the friction points of previous flows. Businesses can now manage the UPI checkout experience within their app, without the user ever leaving the app', | ||
// @ts-expect-error | ||
color: ['positive', 'negative', 'warning', 'information', 'neutral'][ | ||
Math.floor(Math.random() * 5) | ||
], | ||
duration: Infinity, | ||
action: { | ||
text: 'Okay', | ||
onClick: () => console.log(1), | ||
}, | ||
onDismissButtonClick: () => console.log(1), | ||
}) | ||
} | ||
> | ||
show info | ||
</Button> | ||
<Button | ||
marginLeft="spacing.3" | ||
isDisabled={hasPromoToast} | ||
onClick={() => | ||
toast.show({ | ||
type: 'promotional', | ||
content: ( | ||
<Box display="flex" gap="spacing.3" flexDirection="column"> | ||
<Heading>Introducing TurboUPI</Heading> | ||
<img | ||
loading="lazy" | ||
src="https://d6xcmfyh68wv8.cloudfront.net/blog-content/uploads/2023/05/Features-blog.png" | ||
width="100%" | ||
height="100px" | ||
alt="Promotional Toast" | ||
style={{ objectFit: 'cover', borderRadius: '8px' }} | ||
/> | ||
<Text weight="semibold"> | ||
Lightning-fast payments with the new Razorpay Turbo UPI | ||
</Text> | ||
<Text size="xsmall"> | ||
Turbo UPI allows end-users to complete their payment in-app, with no redirections | ||
or dependence on third-party UPI apps. With Turbo UPI, payments will be 5x faster | ||
with a significantly-improved success rate of 10%! | ||
</Text> | ||
</Box> | ||
), | ||
action: { | ||
text: 'Try TurboUPI', | ||
onClick: () => console.log(1), | ||
}, | ||
// @ts-expect-error | ||
duration: Infinity, | ||
onDismissButtonClick: () => console.log(1), | ||
}) | ||
} | ||
> | ||
show promo | ||
</Button> | ||
</Box> | ||
); | ||
}; | ||
|
||
export const Default = ToastTemplate.bind({}); | ||
Default.storyName = 'Default'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import React from 'react'; | ||
import toast from 'react-hot-toast'; | ||
import type { FlattenSimpleInterpolation } from 'styled-components'; | ||
import styled, { css, keyframes } from 'styled-components'; | ||
import type { ToastProps } from './types'; | ||
import { Box } from '~components/Box'; | ||
import { Button } from '~components/Button'; | ||
import { IconButton } from '~components/Button/IconButton'; | ||
import { | ||
AlertOctagonIcon, | ||
AlertTriangleIcon, | ||
CheckCircleIcon, | ||
CloseIcon, | ||
InfoIcon, | ||
} from '~components/Icons'; | ||
import BaseBox from '~components/Box/BaseBox'; | ||
import { Text } from '~components/Typography'; | ||
import { makeMotionTime, useTheme } from '~utils'; | ||
|
||
const iconMap = { | ||
positive: CheckCircleIcon, | ||
negative: AlertOctagonIcon, | ||
information: InfoIcon, | ||
neutral: InfoIcon, | ||
warning: AlertTriangleIcon, | ||
}; | ||
|
||
const slideIn = keyframes` | ||
from { | ||
opacity: 0; | ||
transform: translateY(100%); | ||
} | ||
|
||
to { | ||
opacity: 1; | ||
transform: translateY(0); | ||
} | ||
`; | ||
|
||
const slideOut = keyframes` | ||
from { | ||
opacity: 1; | ||
transform: translateY(0); | ||
} | ||
|
||
to { | ||
opacity: 0; | ||
transform: translateY(100%); | ||
} | ||
`; | ||
|
||
const AnimatedFade = styled(BaseBox)<{ animationType: FlattenSimpleInterpolation | null }>( | ||
({ animationType }) => { | ||
return css` | ||
overflow: hidden; | ||
${animationType} | ||
`; | ||
}, | ||
); | ||
|
||
const Toast = ({ | ||
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. Wrap it with assignWithoutSideEffects? 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's only needed when you use React.forwardRef |
||
type, | ||
color = 'neutral', | ||
leading, | ||
action, | ||
content, | ||
onDismissButtonClick, | ||
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. Shouldn't we also expose 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. react-hot-toast doesn't have a callback that it calls when a toast gets auto dismissed, and I'm not sure if we even need that. There is a workaround but doing that means you need to keep track of each and every toast's timer manually. |
||
isVisible, | ||
id, | ||
}: ToastProps): React.ReactElement => { | ||
const { theme } = useTheme(); | ||
const Icon = leading || iconMap[color]; | ||
|
||
const colorMap = { | ||
positive: 'positive', | ||
negative: 'negative', | ||
warning: 'notice', | ||
information: 'information', | ||
neutral: 'neutral', | ||
} as const; | ||
|
||
const isPromotional = type === 'promotional'; | ||
const actionButton = action ? ( | ||
<Box> | ||
<Button | ||
size="xsmall" | ||
variant={isPromotional ? 'secondary' : 'tertiary'} | ||
color={isPromotional ? 'primary' : 'white'} | ||
onClick={(e) => { | ||
e.stopPropagation(); | ||
action?.onClick?.(e as never); | ||
}} | ||
isLoading={action?.isLoading} | ||
> | ||
{action?.text} | ||
</Button> | ||
</Box> | ||
) : null; | ||
|
||
const enter = css` | ||
opacity: 0; | ||
animation: ${slideIn} ${makeMotionTime(theme.motion.duration.gentle)} | ||
${theme.motion.easing.entrance.effective as string} forwards; | ||
anuraghazra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`; | ||
|
||
const exit = css` | ||
opacity: 1; | ||
animation: ${slideOut} ${makeMotionTime(theme.motion.duration.moderate)} | ||
${theme.motion.easing.exit.effective as string} forwards; | ||
`; | ||
|
||
return ( | ||
<AnimatedFade | ||
animationType={isVisible ? enter : exit} | ||
width="100%" | ||
display="flex" | ||
gap="spacing.3" | ||
paddingX="spacing.4" | ||
paddingY={isPromotional ? 'spacing.4' : 'spacing.3'} | ||
borderRadius="medium" | ||
alignItems="center" | ||
// TODO: fix border color | ||
border="2px solid white" | ||
backgroundColor={ | ||
isPromotional | ||
? 'surface.background.gray.intense' | ||
: `feedback.background.${colorMap[color]}.intense` | ||
} | ||
> | ||
{Icon ? ( | ||
<Box | ||
flexShrink={0} | ||
display="flex" | ||
alignItems="center" | ||
alignSelf={isPromotional ? 'start' : 'center'} | ||
> | ||
<Icon | ||
color={isPromotional ? 'surface.icon.gray.normal' : 'surface.icon.staticWhite.normal'} | ||
/> | ||
</Box> | ||
) : null} | ||
<Box display="flex" flexDirection="column" gap="spacing.3"> | ||
{isPromotional ? ( | ||
content | ||
) : ( | ||
<Text as="span" size="small" color="surface.text.staticWhite.normal"> | ||
{content} | ||
</Text> | ||
)} | ||
{isPromotional && actionButton} | ||
</Box> | ||
<Box alignSelf="start" marginLeft="auto" display="flex" gap="spacing.4"> | ||
{!isPromotional && actionButton} | ||
{onDismissButtonClick ? ( | ||
<IconButton | ||
emphasis={isPromotional ? 'intense' : 'subtle'} | ||
accessibilityLabel="Dismiss toast" | ||
onClick={(e: any) => { | ||
e.stopPropagation(); | ||
onDismissButtonClick?.(); | ||
toast.dismiss(id); | ||
}} | ||
icon={CloseIcon} | ||
/> | ||
) : null} | ||
</Box> | ||
</AnimatedFade> | ||
); | ||
}; | ||
|
||
export { Toast }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update documentation to mention that users need to add this too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users won't need to install this separately, since it's in
dependencies
If users are using react-hot-toast separately in their own codebase then they need to install it.