diff --git a/src/packages/toast/Notification.tsx b/src/packages/toast/Notification.tsx index 4400240e71..e6467c1650 100644 --- a/src/packages/toast/Notification.tsx +++ b/src/packages/toast/Notification.tsx @@ -156,13 +156,14 @@ Notification.newInstance = (properties, callback) => { let called = false - function ref(instance: any) { + function ref(instance: Notification | null) { if (called) { return } called = true callback({ component: instance, + id, destroy() { unmount(element) element && element.parentNode && element.parentNode.removeChild(element) diff --git a/src/packages/toast/__test__/toast.spec.tsx b/src/packages/toast/__test__/toast.spec.tsx index 185f278bae..3834e051fa 100644 --- a/src/packages/toast/__test__/toast.spec.tsx +++ b/src/packages/toast/__test__/toast.spec.tsx @@ -29,6 +29,12 @@ const onClickToast = vi.fn((type, msg, options?) => { } }) +const waitTimeout = (delay: number) => { + return new Promise((resolve) => { + setTimeout(resolve, delay) + }) +} + test('event click-show-toast test', async () => { const { getByTestId } = render( { expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe('loading') }) }) + +test('manually close in strict mode', async () => { + const time = 2000 + const content = 'strict mode loading' + const { getByTestId } = render( + { + onClickToast('loading', content) + onClickToast('loading', content) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(onClickToast).toBeCalled() + expect(document.querySelectorAll('.nut-toast-text')?.length).toBe(2) + }) + + Toast.clear() + + await waitTimeout(time) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(undefined) +}) + +test('no content', async () => { + const { getByTestId } = render( + { + Toast.show({}) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(undefined) + }) +}) + +test('string option', async () => { + const content = 'string option' + const { getByTestId } = render( + { + Toast.show(content) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) +}) + +test('global config', async () => { + const content = 'global config' + const contentClassName = 'content-demo' + Toast.config({ contentClassName }) + const { getByTestId } = render( + { + onClickToast('text', content) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector(`.${contentClassName}`)).toBeTruthy() + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) +}) + +test('1s after close', async () => { + const content = '1' + let isCalledClose = false + const { getByTestId } = render( + { + onClickToast('text', content, { + duration: 1, + onClose: () => { + isCalledClose = true + }, + }) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) + + await waitTimeout(2000) + expect(document.querySelector('.nut-toast-text')).toBe(null) + expect(isCalledClose).toBeTruthy() +}) diff --git a/src/packages/toast/toast.tsx b/src/packages/toast/toast.tsx index 4942a6ab8c..43be54813e 100644 --- a/src/packages/toast/toast.tsx +++ b/src/packages/toast/toast.tsx @@ -1,8 +1,17 @@ +import { ReactNode } from 'react' import Notification from './Notification' import { WebToastProps } from '@/types' import { defaultOverlayProps } from '@/packages/overlay/overlay' +import { clone } from '@/utils' -let messageInstance: any = null +type NotificationInstance = { + component: Notification + id: string + destroy: () => void +} + +let messageInstance: NotificationInstance | null = null +const messageInstanceSet = new Set() let defaultProps: WebToastProps = { ...defaultOverlayProps, @@ -26,18 +35,18 @@ type ToastNativeProps = Partial function getInstance( props: ToastNativeProps, - callback: (notification: any) => void + callback: (notification: NotificationInstance) => void ) { if (messageInstance) { messageInstance.destroy() messageInstance = null } - Notification.newInstance(props, (notification: any) => { + Notification.newInstance(props, (notification: NotificationInstance) => { return callback && callback(notification) }) } -function notice(opts: any) { +function notice(opts: ToastNativeProps) { function close() { if (messageInstance) { messageInstance.destroy() @@ -46,12 +55,16 @@ function notice(opts: any) { } } const opts2 = { ...defaultProps, ...opts, onClose: close } - getInstance(opts2, (notification: any) => { + getInstance(opts2, (notification: NotificationInstance) => { + const oldInstance = messageInstance ? clone(messageInstance) : null + if (notification.id === oldInstance?.id) { + messageInstanceSet.add(oldInstance) + } messageInstance = notification }) } -const errorMsg = (msg: any) => { +const errorMsg = (msg: ReactNode) => { if (!msg) { console.warn('[NutUI Toast]: msg cannot be null') } @@ -79,6 +92,12 @@ export default { if (messageInstance) { messageInstance.destroy() messageInstance = null + if (messageInstanceSet?.size) { + messageInstanceSet.forEach((instance: NotificationInstance) => { + instance?.destroy() + }) + messageInstanceSet.clear() + } } }, }