From 0446c764696e08f53d915505bd587b5439a317b3 Mon Sep 17 00:00:00 2001 From: yanuaraditia Date: Wed, 3 Jun 2026 15:20:26 +0800 Subject: [PATCH] feat(Toast): allow action without dismissing toast --- docs/content/docs/components/toast.md | 14 +++++++ docs/content/meta/ToastAction.md | 12 +++++- packages/core/src/Toast/Toast.test.ts | 50 ++++++++++++++++++++++++- packages/core/src/Toast/ToastAction.vue | 23 +++++++++++- 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/components/toast.md b/docs/content/docs/components/toast.md index 9e02da6ada..019be716f7 100644 --- a/docs/content/docs/components/toast.md +++ b/docs/content/docs/components/toast.md @@ -139,6 +139,20 @@ An action that is safe to ignore to ensure users are not expected to complete ta When obtaining a user response is necessary, portal an ["AlertDialog"](/docs/components/alert-dialog) styled as a toast into the viewport instead. +By default, clicking `ToastAction` dismisses the toast. Set `closeOnClick` to `false` when the action should run while keeping the toast visible. + +```vue + +``` + ### Close diff --git a/docs/content/meta/ToastAction.md b/docs/content/meta/ToastAction.md index af14f5f73c..22854291d9 100644 --- a/docs/content/meta/ToastAction.md +++ b/docs/content/meta/ToastAction.md @@ -13,13 +13,20 @@ 'description': '

The element or component this component should render as. Can be overwritten by asChild.

\n', 'type': 'AsTag | Component', 'required': false, - 'default': '\'div\'' + 'default': '\'button\'' }, { 'name': 'asChild', 'description': '

Change the default rendered element for the one passed as a child, merging their props and behavior.

\n

Read our Composition guide for more details.

\n', 'type': 'boolean', 'required': false + }, + { + 'name': 'closeOnClick', + 'description': '

Whether the action should close the toast when clicked.

\n', + 'type': 'boolean', + 'required': false, + 'default': 'true' } ]" /> @@ -31,7 +38,8 @@ | Name | Description | Type | Required | Default | | --- | --- | --- | --- | --- | | `altText` | A short description for an alternate way to carry out the action. For screen reader users who will not be able to navigate to the button easily/quickly. | `string` | Yes | - | -| `as` | The element or component this component should render as. Can be overwritten by asChild. | `AsTag \| Component` | No | `"div"` | +| `as` | The element or component this component should render as. Can be overwritten by asChild. | `AsTag \| Component` | No | `"button"` | | `asChild` | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | `boolean` | No | - | +| `closeOnClick` | Whether the action should close the toast when clicked. | `boolean` | No | `true` | diff --git a/packages/core/src/Toast/Toast.test.ts b/packages/core/src/Toast/Toast.test.ts index bd34a8ba70..3afc7c4aa9 100644 --- a/packages/core/src/Toast/Toast.test.ts +++ b/packages/core/src/Toast/Toast.test.ts @@ -3,11 +3,48 @@ import { findByText, fireEvent } from '@testing-library/vue' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it } from 'vitest' import { axe } from 'vitest-axe' -import { nextTick } from 'vue' +import { defineComponent, h, nextTick, ref } from 'vue' +import { ToastAction, ToastDescription, ToastProvider, ToastRoot, ToastViewport } from '.' import Toast from './story/_Toast.vue' const CLOSE_TEXT = 'Close' +const ToastWithPersistentAction = defineComponent({ + setup() { + const open = ref(true) + const actionClicks = ref(0) + + return { + open, + actionClicks, + } + }, + render() { + return h(ToastProvider, null, { + default: () => [ + h(ToastRoot, { + 'open': this.open, + 'onUpdate:open': (value: boolean) => { + this.open = value + }, + }, { + default: () => [ + h(ToastDescription, null, { default: () => 'Action available' }), + h(ToastAction, { + altText: 'Keep toast open after action', + closeOnClick: false, + onClick: () => { + this.actionClicks += 1 + }, + }, { default: () => 'Undo' }), + ], + }), + h(ToastViewport), + ], + }) + }, +}) + describe('given a default Toast', () => { let wrapper: VueWrapper> let trigger: DOMWrapper @@ -66,6 +103,17 @@ describe('given a default Toast', () => { expect(text).toContain('Scheduled: Catch up') }) + it('should keep the toast open when action closeOnClick is false', async () => { + const wrapper = mount(ToastWithPersistentAction, { attachTo: document.body }) + + const action = await findByText(document.body, 'Undo') + await fireEvent.click(action) + + expect(wrapper.vm.actionClicks).toBe(1) + expect(wrapper.vm.open).toBe(true) + expect(document.body.innerHTML).toContain('Action available') + }) + describe('after clicking the trigger', () => { beforeEach(async () => { fireEvent.click(trigger.element) diff --git a/packages/core/src/Toast/ToastAction.vue b/packages/core/src/Toast/ToastAction.vue index edc3abcac2..3f7945f0a0 100644 --- a/packages/core/src/Toast/ToastAction.vue +++ b/packages/core/src/Toast/ToastAction.vue @@ -9,15 +9,25 @@ export interface ToastActionProps extends ToastCloseProps { * @example Undo */ altText: string + /** + * Whether the action should close the toast when clicked. + * + * @defaultValue true + */ + closeOnClick?: boolean }