feat: Update toast design (#8018)

**What**
- Updates the Toast component and `toast` util to match the latest design. 
- Updates every usage of `toast` as `dismissableLabel` is no longer a valid prop, as we now render a X mark instead of a text button.
This commit is contained in:
Kasper Fabricius Kristensen
2024-07-09 18:14:19 +02:00
committed by GitHub
parent 24fc6befd2
commit 6e613f4f50
88 changed files with 811 additions and 1182 deletions

View File

@@ -105,7 +105,7 @@
"react-aria": "^3.33.1",
"react-currency-input-field": "^3.6.11",
"react-stately": "^3.31.1",
"sonner": "^1.4.41",
"sonner": "^1.5.0",
"tailwind-merge": "^2.2.1",
"upgrade": "^1.1.0",
"yarn": "^1.22.22"

View File

@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react"
import * as React from "react"
import { Keyboard } from "@medusajs/icons"
import { Kbd } from "../kbd"
import { Toast } from "./toast"
const meta: Meta<typeof Toast> = {
@@ -58,6 +60,13 @@ export const Loading: Story = {
},
}
export const Message: Story = {
args: {
title: <span>Next time hit <Kbd>G</Kbd> then <Kbd>O</Kbd> to go to orders.</span>,
icon: <Keyboard className="text-ui-fg-subtle" />,
}
}
export const WithAction: Story = {
args: {
title: "Scheduled meeting",
@@ -70,3 +79,10 @@ export const WithAction: Story = {
},
},
}
export const NoDescription: Story = {
args: {
title: "Label",
variant: "info",
},
}

View File

@@ -1,17 +1,26 @@
import { CheckCircleSolid, ExclamationCircleSolid, InformationCircleSolid, Spinner, XCircleSolid } from "@medusajs/icons"
import {
CheckCircleSolid,
ExclamationCircleSolid,
InformationCircleSolid,
Spinner,
XCircleSolid,
XMark,
} from "@medusajs/icons"
import * as React from "react"
import type { ToastAction, ToastVariant } from "@/types"
import { clx } from "@/utils/clx"
import { toast } from "sonner"
import { IconButton } from "../icon-button"
interface ToastComponentProps {
id: string | number
variant?: ToastVariant
title: string
description?: string
title: React.ReactNode
description?: React.ReactNode
action?: ToastAction
onDismiss?: (id?: string | number) => void
dismissLabel?: string
icon?: React.ReactNode
dismissable?: boolean
}
/**
@@ -24,7 +33,7 @@ export const Toast = ({
id,
/**
* @ignore
*
*
* @privateRemarks
* As the Toast component is created using
* the `toast` utility functions, the variant is inferred
@@ -33,11 +42,18 @@ export const Toast = ({
variant = "info",
/**
* @ignore
*
*
* @privateRemarks
* The `toast` utility functions accept this as a parameter.
*/
title,
/**
* @ignore
*
* @privateRemarks
* The `toast` utility functions accept this as a parameter.
*/
icon: _icon,
/**
* The toast's text.
*/
@@ -48,48 +64,43 @@ export const Toast = ({
action,
/**
* @ignore
*
*
* @privateRemarks
* The `toast` utility functions don't allow
* passing this prop.
* The `toast` utility functions accept this as a parameter.
*/
onDismiss,
/**
* The label of the dismiss button, if available.
*/
dismissLabel = "Close",
dismissable = true,
}: ToastComponentProps) => {
const hasActionables = !!action || onDismiss
let icon = undefined
let icon = _icon
switch (variant) {
case "success":
icon = <CheckCircleSolid className="text-ui-tag-green-icon" />
break
case "warning":
icon = <ExclamationCircleSolid className="text-ui-tag-orange-icon" />
break
case "error":
icon = <XCircleSolid className="text-ui-tag-red-icon" />
break
case "loading":
icon = <Spinner className="text-ui-tag-blue-icon animate-spin" />
break
case "info":
icon = <InformationCircleSolid className="text-ui-fg-base" />
break
default:
break
if (!_icon) {
switch (variant) {
case "success":
icon = <CheckCircleSolid className="text-ui-tag-green-icon" />
break
case "warning":
icon = <ExclamationCircleSolid className="text-ui-tag-orange-icon" />
break
case "error":
icon = <XCircleSolid className="text-ui-tag-red-icon" />
break
case "loading":
icon = <Spinner className="text-ui-tag-blue-icon animate-spin" />
break
case "info":
icon = <InformationCircleSolid className="text-ui-fg-base" />
break
default:
break
}
}
return (
<div className="shadow-elevation-flyout bg-ui-bg-base flex w-fit min-w-[360px] max-w-[440px] overflow-hidden rounded-lg">
<div className="shadow-elevation-flyout bg-ui-bg-component flex w-fit min-w-[360px] max-w-[440px] gap-x-3 overflow-hidden rounded-lg p-3">
<div
className={clx("grid flex-1 items-start gap-3 py-3 pl-3", {
"border-r pr-3": hasActionables,
"pr-6": !hasActionables,
className={clx("grid flex-1 items-center gap-x-2", {
"grid-cols-[20px_1fr]": !!icon,
"grid-cols-1": !icon,
"items-start": !!description,
})}
>
{!!icon && (
@@ -97,29 +108,26 @@ export const Toast = ({
{icon}
</span>
)}
<div className="flex flex-col">
<span className="txt-compact-small-plus text-ui-fg-base">
{title}
</span>
<span className="txt-small text-ui-fg-subtle text-pretty">
{description}
</span>
</div>
</div>
{hasActionables && (
<div
className={clx("grid grid-cols-1", {
"grid-rows-2": !!action && onDismiss,
"grid-rows-1": !action || !onDismiss,
})}
>
<div className="flex flex-col gap-y-3">
<div className="flex flex-col">
<span className="txt-compact-small-plus text-ui-fg-base">
{title}
</span>
{description && (
<span className="txt-small text-ui-fg-subtle text-pretty">
{description}
</span>
)}
</div>
{!!action && (
<button
type="button"
className={clx(
"txt-compact-small-plus text-ui-fg-base bg-ui-bg-base hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed flex items-center justify-center px-4 transition-colors",
"txt-compact-small-plus text-ui-fg-base bg-ui-bg-base flex w-fit items-center rounded-[4px] transition-colors",
"focus-visible:shadow-borders-focus",
"hover:text-ui-fg-subtle",
"disabled:text-ui-fg-disabled",
{
"border-ui-border-base border-b": onDismiss,
"text-ui-fg-error": action.variant === "destructive",
}
)}
@@ -128,19 +136,18 @@ export const Toast = ({
{action.label}
</button>
)}
{onDismiss && (
<button
type="button"
onClick={() => onDismiss(id)}
className={clx(
"txt-compact-small-plus text-ui-fg-subtle bg-ui-bg-base hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed flex items-center justify-center px-4 transition-colors"
)}
>
{dismissLabel}
</button>
)}
</div>
</div>
{!!dismissable && (
<IconButton
size="2xsmall"
variant="transparent"
type="button"
onClick={() => toast.dismiss(id)}
>
<XMark />
</IconButton>
)}
</div>
)
}
}

View File

@@ -1,44 +1,45 @@
import { Toast } from "@/components/toast"
import { ToastAction, ToastVariant, ToasterPosition } from "@/types"
import * as React from "react"
import { toast as toastFn } from "sonner"
import { ExternalToast, toast as toastFn } from "sonner"
interface BaseToastProps {
id?: string | number
position?: ToasterPosition
dismissable?: boolean
dismissLabel?: string
duration?: number
dismissable?: boolean
icon?: React.ReactNode
}
interface ToastProps extends BaseToastProps {
description?: string
description?: React.ReactNode
action?: ToastAction
}
function create(variant: ToastVariant, title: string, props: ToastProps = {}) {
return toastFn.custom(
(t) => {
return (
<Toast
id={props.id || t}
title={title}
description={props.description}
onDismiss={
props.dismissable ? () => toastFn.dismiss(props.id || t) : undefined
}
dismissLabel={props.dismissLabel}
variant={variant}
action={props.action}
/>
)
},
{
id: props.id,
position: props.position,
dismissible: props.dismissable,
}
)
function create(variant: ToastVariant, title: React.ReactNode, props: ToastProps = {}) {
const external: ExternalToast = {
position: props.position,
duration: props.duration,
dismissible: props.dismissable,
}
if (props.id) {
external.id = props.id
}
return toastFn.custom((t) => {
return (
<Toast
id={t}
title={title}
description={props.description}
dismissable={props.dismissable}
variant={variant}
action={props.action}
icon={props.icon}
/>
)
}, external)
}
function message(
@@ -54,6 +55,12 @@ function message(
return create("message", title, props)
}
function custom() {
return create("message", "Custom",)
}
interface VariantToastProps extends Omit<ToastProps, "icon"> {}
function info(
/**
* The title of the toast.
@@ -61,7 +68,7 @@ function info(
/**
* The props of the toast.
*/
props: ToastProps = {}
props: VariantToastProps = {}
) {
return create("info", title, props)
}
@@ -73,7 +80,7 @@ function error(
/**
* The props of the toast.
*/
props: ToastProps = {}
props: VariantToastProps = {}
) {
return create("error", title, props)
}
@@ -85,7 +92,7 @@ function success(
/**
* The props of the toast.
*/
props: ToastProps = {}
props: VariantToastProps = { }
) {
return create("success", title, props)
}
@@ -97,22 +104,19 @@ function warning(
/**
* The props of the toast.
*/
props: ToastProps = {}
props: VariantToastProps = {}
) {
return create("warning", title, props)
}
type LoadingToastProps = Omit<ToastProps, "dismissable" | "dismissLabel">
function loading
/**
* The title of the toast.
*/(
title: string,
function loading(
/**
* The title of the toast.
*/ title: string,
/**
* The props of the toast.
*/
props: ToastProps = {}
props: VariantToastProps = {}
) {
return create("loading", title, { ...props, dismissable: false })
}
@@ -124,7 +128,7 @@ type PromiseStateProps =
description?: string
}
interface PromiseToastProps extends BaseToastProps {
interface PromiseToastProps extends Omit<BaseToastProps, "icon"> {
loading: PromiseStateProps
success: PromiseStateProps
error: PromiseStateProps
@@ -158,6 +162,7 @@ async function promise<TData>(
? undefined
: props.loading.description,
duration: Infinity,
dismissable: false,
}
)