Skip to content
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

Merged
merged 45 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7051454
chore: wip toast
anuraghazra Jan 28, 2024
dfa7939
chore: wip toast stacking
anuraghazra Feb 1, 2024
2323145
chore: update stacking animation
anuraghazra Feb 5, 2024
39fb2bb
chore: update toast stacking animation
anuraghazra Feb 6, 2024
efa548f
chore: revise stacking
anuraghazra Feb 7, 2024
d1672e4
feat: promo toast stacking
anuraghazra Feb 7, 2024
7eedb4e
chore: fix pointer event bug
anuraghazra Feb 7, 2024
101094b
chore: update
anuraghazra Feb 7, 2024
5cbbc61
chore: cleanup & mobile ux fixes
anuraghazra Feb 7, 2024
3319520
chore: fix promo toast stack bug
anuraghazra Feb 8, 2024
5c23c83
chore: fix exit animation
anuraghazra Feb 9, 2024
3331954
chore: fix abrupt exit animation
anuraghazra Feb 9, 2024
5185747
chore: add duration,autoDismiss logic
anuraghazra Feb 11, 2024
469e8f5
chore: add docs
anuraghazra Feb 11, 2024
a0dfac3
chore: update mobile check
anuraghazra Feb 12, 2024
0120aed
chore: update timing
anuraghazra Feb 12, 2024
ffe9e69
chore: fix build
anuraghazra Feb 12, 2024
45f535f
chore: update spacing tokens
anuraghazra Feb 12, 2024
a51eada
chore: update border color
anuraghazra Feb 13, 2024
8c77908
chore: update
anuraghazra Feb 13, 2024
4354e65
chore: review comments
anuraghazra Feb 13, 2024
13cba57
chore: fix timers starting/pausing repeatedly
anuraghazra Feb 13, 2024
cc4d1e3
fix: first toast height calculation going wrong with invisible toast
anuraghazra Feb 13, 2024
6ff58ad
refactor: stacking logic and added video for understanding
anuraghazra Feb 13, 2024
9b56142
chore: add dev warning for 1 promo toast
anuraghazra Feb 14, 2024
feefb6e
chore: fix toast prop drilling
anuraghazra Feb 14, 2024
53f308a
chore: fix ts
anuraghazra Feb 14, 2024
5ba4e5c
chore: update peek gutter
anuraghazra Feb 14, 2024
87bff48
chore: update zindex
anuraghazra Feb 14, 2024
c819313
chore: move to constants
anuraghazra Feb 14, 2024
1b62f74
chore: update height
anuraghazra Feb 14, 2024
b380ff5
chore: review update
anuraghazra Feb 14, 2024
f7e9b65
chore: change warning to notice
anuraghazra Feb 14, 2024
5a51c09
Merge remote-tracking branch 'origin' into anu/toast
anuraghazra Feb 14, 2024
78340b8
Merge branch 'master' into anu/toast
anuraghazra Feb 14, 2024
d83cf0f
chore: update warning to notive
anuraghazra Feb 14, 2024
8c1b236
chore: update
anuraghazra Feb 14, 2024
9a472f6
chore: update height calculation bug
anuraghazra Feb 14, 2024
6cb1941
chore: update mouse over container
anuraghazra Feb 15, 2024
19953d7
Merge remote-tracking branch 'origin' into anu/toast
anuraghazra Feb 15, 2024
5980893
chore: update lock
anuraghazra Feb 15, 2024
62e45aa
test(e2e): add toast interaction tests (#2012)
anuraghazra Feb 15, 2024
ff8de33
Create lovely-eggs-juggle.md
anuraghazra Feb 15, 2024
e90ac1c
chore: update texts
anuraghazra Feb 15, 2024
0a90bd6
chore: update
anuraghazra Feb 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/blade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Collaborator

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?

Copy link
Member Author

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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"
Expand Down Expand Up @@ -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"
},
Expand Down
134 changes: 134 additions & 0 deletions packages/blade/src/components/Toast/Toast.stories.tsx
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lets move the show toast button to the right side corner so it doesn't overlap in the basic story?
Screenshot 2024-02-13 at 12 41 36 PM

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, added a note in the top.

title: 'Components/Toast',
Copy link
Collaborator

Choose a reason for hiding this comment

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

The default promotional toast that shows up seems too dull & basic? Should add some content to it by default?

Screenshot 2024-02-13 at 12 47 21 PM

Copy link
Member Author

Choose a reason for hiding this comment

The 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'],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we exposing duration as a prop? If yes, then it means "4000 for informational toast 8000 for promotional toast" is just a default that can be overridden?
Screenshot 2024-02-13 at 12 49 10 PM

Copy link
Member Author

Choose a reason for hiding this comment

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

is just a default that can be overridden?

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';
172 changes: 172 additions & 0 deletions packages/blade/src/components/Toast/Toast.web.tsx
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 = ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wrap it with assignWithoutSideEffects?

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's only needed when you use React.forwardRef

type,
color = 'neutral',
leading,
action,
content,
onDismissButtonClick,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't we also expose onDismiss?

Copy link
Member Author

@anuraghazra anuraghazra Feb 14, 2024

Choose a reason for hiding this comment

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