feat: Add close button option to toast component

- Add `closeButton` prop to BaseToastProps interface
- Update dismissable prop description for clarity
- Add close button styling to classNames
- Pass closeButton prop to external toast configuration
- Update Storybook stories to include closeButton control
- Rename "Dismissable Control" story to "Close Button Control"
This commit is contained in:
icarus 2026-01-05 21:55:18 +08:00
parent a8801e58f3
commit d89fa3cb54
No known key found for this signature in database
GPG Key ID: D4AF089AAEC25D18
3 changed files with 27 additions and 14 deletions

View File

@ -18,12 +18,14 @@ interface BaseToastProps {
colored?: boolean colored?: boolean
/** Duration in milliseconds before auto-dismissal */ /** Duration in milliseconds before auto-dismissal */
duration?: number duration?: number
/** Whether the toast can be manually dismissed */ /** If 'false', it'll prevent the user from dismissing the toast. Defaults to false. */
dismissable?: boolean dismissable?: boolean
/** Callback function when toast is dismissed */ /** Callback function when toast is dismissed */
onDismiss?: () => void onDismiss?: () => void
/** Action button or custom React node */ /** Action button or custom React node */
button?: Action | ReactNode button?: Action | ReactNode
/** Whether to show a close button. Defaults to false */
closeButton?: boolean
/** Custom class names for toast sub-components */ /** Custom class names for toast sub-components */
classNames?: ToastClassnames classNames?: ToastClassnames
} }

View File

@ -316,12 +316,14 @@ interface BaseToastProps {
colored?: boolean colored?: boolean
/** Duration in milliseconds before auto-dismissal */ /** Duration in milliseconds before auto-dismissal */
duration?: number duration?: number
/** Whether the toast can be manually dismissed */ /** If 'false', it'll prevent the user from dismissing the toast. Defaults to false. */
dismissable?: boolean dismissable?: boolean
/** Callback function when toast is dismissed */ /** Callback function when toast is dismissed */
onDismiss?: () => void onDismiss?: () => void
/** Action button or custom React node */ /** Action button or custom React node */
button?: Action | ReactNode button?: Action | ReactNode
/** Whether to show a close button. Defaults to false */
closeButton?: boolean
/** Custom class names for toast sub-components */ /** Custom class names for toast sub-components */
classNames?: ToastClassnames classNames?: ToastClassnames
} }
@ -388,7 +390,8 @@ function toast(props: ToastProps) {
props.classNames?.actionButton props.classNames?.actionButton
), ),
icon: cn('size-6 min-w-6', props.description && 'self-start'), icon: cn('size-6 min-w-6', props.description && 'self-start'),
loader: cn('!static ![--size:24px]') loader: cn('!static ![--size:24px]'),
closeButton: cn('absolute size-5 min-w-5 top-[5px] right-1.5 [&_svg]:size-5')
} }
const { classNames: externalClassNames, ...rest } = props const { classNames: externalClassNames, ...rest } = props
delete externalClassNames?.toast delete externalClassNames?.toast
@ -400,7 +403,8 @@ function toast(props: ToastProps) {
duration: rest.duration, duration: rest.duration,
action: rest.button, action: rest.button,
dismissible: rest.dismissable, dismissible: rest.dismissable,
onDismiss: rest.onDismiss onDismiss: rest.onDismiss,
closeButton: rest.closeButton
} satisfies ExternalToast } satisfies ExternalToast
switch (props.type) { switch (props.type) {
default: default:

View File

@ -10,6 +10,7 @@ interface PlaygroundArgs {
colored: boolean colored: boolean
duration: number duration: number
dismissable: boolean dismissable: boolean
closeButton: boolean
withButton: boolean withButton: boolean
buttonLabel: string buttonLabel: string
} }
@ -49,6 +50,7 @@ export const Playground: StoryObj<PlaygroundArgs> = {
colored: false, colored: false,
duration: 4000, duration: 4000,
dismissable: true, dismissable: true,
closeButton: false,
withButton: false, withButton: false,
buttonLabel: 'Action' buttonLabel: 'Action'
}, },
@ -76,7 +78,11 @@ export const Playground: StoryObj<PlaygroundArgs> = {
}, },
dismissable: { dismissable: {
control: 'boolean', control: 'boolean',
description: 'Whether the toast can be manually dismissed' description: 'Whether the toast can be dismissed by user interaction (click, swipe)'
},
closeButton: {
control: 'boolean',
description: 'Whether to show a close button'
}, },
withButton: { withButton: {
control: 'boolean', control: 'boolean',
@ -95,6 +101,7 @@ export const Playground: StoryObj<PlaygroundArgs> = {
colored: args.colored, colored: args.colored,
duration: args.duration, duration: args.duration,
dismissable: args.dismissable, dismissable: args.dismissable,
closeButton: args.closeButton,
...(args.withButton && { ...(args.withButton && {
button: { button: {
label: args.buttonLabel || 'Action', label: args.buttonLabel || 'Action',
@ -608,30 +615,30 @@ export const CustomToast: Story = {
} }
} }
// Dismissable Control // Close Button Control
export const DismissableControl: Story = { export const CloseButtonControl: Story = {
render: () => { render: () => {
return ( return (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button <Button
onClick={() => onClick={() =>
toast.info('Dismissable toast', { toast.info('With close button', {
description: 'You can close this manually', description: 'Click the X to close this toast',
dismissable: true, closeButton: true,
duration: Number.POSITIVE_INFINITY duration: Number.POSITIVE_INFINITY
}) })
}> }>
Dismissable (Default) With Close Button
</Button> </Button>
<Button <Button
onClick={() => onClick={() =>
toast.warning('Non-dismissable toast', { toast.warning('Without close button', {
description: 'This will auto-close after 3 seconds', description: 'This will auto-close after 3 seconds',
dismissable: false, closeButton: false,
duration: 3000 duration: 3000
}) })
}> }>
Non-dismissable Without Close Button (Default)
</Button> </Button>
</div> </div>
) )