From b59bc40379b5af23c680e51b41e9470ef719b801 Mon Sep 17 00:00:00 2001 From: VINEETH ASOK KUMAR Date: Thu, 14 Sep 2023 15:56:27 +0200 Subject: [PATCH] Add Toast primitives --- package-lock.json | 55 ++++++ package.json | 1 + src/components/Toast/Toast.stories.tsx | 38 ++++ src/components/Toast/Toast.tsx | 232 +++++++++++++++++++++++++ src/components/Toast/useToast.tsx | 9 + src/components/index.ts | 2 + src/components/types.ts | 1 + src/styles/variables.json | 10 +- src/styles/variables.light.json | 10 +- 9 files changed, 348 insertions(+), 10 deletions(-) create mode 100644 src/components/Toast/Toast.stories.tsx create mode 100644 src/components/Toast/Toast.tsx create mode 100644 src/components/Toast/useToast.tsx diff --git a/package-lock.json b/package-lock.json index bf3704b9..073917d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", "cmdk": "^0.2.0", "lodash": "^4.17.21", @@ -5101,6 +5102,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.4.tgz", + "integrity": "sha512-wf+fc8DOywrpRK3jlPlWVe+ELYGHdKDaaARJZNuUTWyWYq7+ANCFLp4rTjZ/mcGkJJQ/vZ949Zis9xxEpfq9OA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", @@ -23561,6 +23596,26 @@ "@radix-ui/react-use-controllable-state": "1.0.1" } }, + "@radix-ui/react-toast": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.4.tgz", + "integrity": "sha512-wf+fc8DOywrpRK3jlPlWVe+ELYGHdKDaaARJZNuUTWyWYq7+ANCFLp4rTjZ/mcGkJJQ/vZ949Zis9xxEpfq9OA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + } + }, "@radix-ui/react-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", diff --git a/package.json b/package.json index e4b04afc..bf8d20f4 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", "cmdk": "^0.2.0", "lodash": "^4.17.21", diff --git a/src/components/Toast/Toast.stories.tsx b/src/components/Toast/Toast.stories.tsx new file mode 100644 index 00000000..8e0c4e0a --- /dev/null +++ b/src/components/Toast/Toast.stories.tsx @@ -0,0 +1,38 @@ +import { Button, useToast, ToastProvider, ToastProps } from "@/components"; +const ToastTrigger = (props: ToastProps) => { + const { createToast } = useToast(); + return ( + + ); +}; +const ToastExample = (props: ToastProps) => { + return ( + + + + ); +}; +export default { + component: ToastExample, + title: "Display/Toast", + tags: ["form-field", "toast", "autodocs"], + argTypes: { + type: { + control: "inline-radio", + options: [undefined, "default", "danger", "warning", "success"], + }, + }, +}; + +export const Playground = { + args: { + description: "description", + title: "title", + }, +}; diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx new file mode 100644 index 00000000..c81683ed --- /dev/null +++ b/src/components/Toast/Toast.tsx @@ -0,0 +1,232 @@ +import { ReactNode, createContext, useState } from "react"; +import * as RadixUIToast from "@radix-ui/react-toast"; +import { Button, ButtonProps, Icon, IconButton, IconName } from "@/components"; +import styled, { keyframes } from "styled-components"; + +interface ContextProps { + createToast: (toast: ToastProps) => void; +} +export const ToastContext = createContext({ + createToast: () => null, +}); + +type ToastType = "danger" | "warning" | "default" | "success"; +export interface ToastProps { + id?: string; + type?: ToastType; + title: string; + description?: ReactNode; + actions?: Array; +} + +const ToastIcon = styled(Icon)<{ $type?: ToastType }>` + ${({ theme, $type = "default" }) => ` + width: ${theme.click.toast.icon.size.width}; + height: ${theme.click.toast.icon.size.height}; + color: ${theme.click.toast.color.icon[$type]} +`} +`; +const hide = keyframes` + from { + opacity: 1; + } + to { + opacity: 0; + } +`; +const slideIn = keyframes` + from { + transform: translateX(calc(100% + var(--viewport-padding))); + } + to { + transform: translateX(0); + } +`; +const swipeOut = keyframes` + from { + transform: translateX(var(--radix-toast-swipe-end-x)); + } + to { + transform: translateX(calc(100% + var(--viewport-padding))); + } +`; + +const ToastRoot = styled(RadixUIToast.Root)` + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + ${({ theme }) => ` + padding: ${theme.click.toast.space.y} ${theme.click.toast.space.x}; + gap: ${theme.click.toast.space.gap}; + border-radius: ${theme.click.toast.radii.all}; + border: 1px solid ${theme.click.toast.color.stroke.default}; + background: ${theme.click.global.color.background.default}; + box-shadow: ${theme.click.toast.shadow}; + `} + &[data-state='open'] { + animation: ${slideIn} 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + &[data-state="closed"] { + animation: ${hide} 100ms ease-in; + } + &[data-swipe="move"] { + transform: translateX(var(--radix-toast-swipe-move-x)); + } + &[data-swipe="cancel"] { + transform: translateX(0); + transition: transform 200ms ease-out; + } + &[data-swipe="end"] { + animation: ${swipeOut} 100ms ease-out; + } +`; + +const ToastHeader = styled(RadixUIToast.Title)` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + gap: inherit; + ${({ theme }) => ` + font: ${theme.click.toast.typography.title.default}; + color: ${theme.click.toast.color.title.default}; + `} +`; + +const ToastDescriptionContainer = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + align-items: flex-end; + gap: inherit; + ${({ theme }) => ` + font: ${theme.click.toast.typography.title.default}; + color: ${theme.click.toast.color.title.default}; + `} +`; + +const ToastDescriptionContent = styled.div` + display: flex; + align-self: stretch; + gap: inherit; + ${({ theme }) => ` + font: ${theme.click.toast.typography.description.default}; + color: ${theme.click.toast.color.description.default}; + `} +`; + +const Title = styled.div` + flex: 1; +`; + +const Toast = ({ + type, + title, + description, + actions = [], + onClose, +}: ToastProps & { onClose: (open: boolean) => void }) => { + let iconName = ""; + if (type === "default") { + iconName = "info-in-circle"; + } else if (type === "success") { + iconName = "check"; + } else if (type && ["danger", "warning"].includes(type)) { + iconName = "warning"; + } + return ( + + + {iconName.length > 0 && ( + + )} + {title} + + + + + {(description || actions.length > 0) && ( + + + {description} + + {actions.length > 0 && ( + + {actions.map(({ altText, ...btnProps }) => ( + +