From bd3718e179ad75c3cf9ed29b58b9d1e42dd0f27c Mon Sep 17 00:00:00 2001 From: Daniel Chae Date: Mon, 19 May 2025 04:35:55 +0900 Subject: [PATCH 1/5] Fix: Notification transitions are now properly applied on close - `close` now sets the notification's `open` prop to `false` before removing it from the queue. --- .../src/useNotifications/NotificationsProvider.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx index 0c9554e0bec..2e8b8d16607 100644 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx +++ b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx @@ -193,8 +193,15 @@ function NotificationsProvider(props: NotificationsProviderProps) { const close = React.useCallback((key) => { setState((prev) => ({ ...prev, - queue: prev.queue.filter((n) => n.notificationKey !== key), + queue: prev.queue.map((n) => (n.notificationKey === key ? { ...n, open: false } : n)), })); + + setTimeout(() => { + setState((prev) => ({ + ...prev, + queue: prev.queue.filter((n) => n.open), + })); + }, 100); }, []); const contextValue = React.useMemo(() => ({ show, close }), [show, close]); From 8d8798c25773d46def127335b749d463973d2369 Mon Sep 17 00:00:00 2001 From: Daniel Chae Date: Mon, 19 May 2025 05:21:50 +0900 Subject: [PATCH 2/5] Chore: Update useNotifications.test.tsx to wait for close timeout --- .../src/useNotifications/useNotifications.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx index 9c3d719200d..4b998968978 100644 --- a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx +++ b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { describe, test, expect } from 'vitest'; -import { renderHook, within, screen } from '@testing-library/react'; +import { renderHook, within, screen, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { useNotifications } from './useNotifications'; import { NotificationsProvider } from './NotificationsProvider'; @@ -35,6 +35,8 @@ describe('useNotifications', () => { rerender(); - expect(screen.queryByRole('alert')).toBeNull(); + await waitFor(() => { + expect(screen.queryByRole('alert')).toBeNull(); + }); }); }); From 27b6f9305f91f9e8b843c0f4885a140d84909770 Mon Sep 17 00:00:00 2001 From: Daniel Chae Date: Sat, 24 May 2025 06:34:45 +0900 Subject: [PATCH 3/5] Chore: refactor NotificationsProvider to use React.Transition.onExited to remove Notifications from Queue --- .../NotificationsProvider.tsx | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx index 2e8b8d16607..3f3454940c0 100644 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx +++ b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx @@ -22,6 +22,13 @@ import type { } from './useNotifications'; import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; +export interface RemoveClosedNotifications { + /** + * Remove all closed snackbars from the NotificationsState queue. + */ + (): void; +} + export interface NotificationsProviderSlotProps { snackbar: SnackbarProps; } @@ -55,7 +62,7 @@ interface NotificationProps { function Notification({ notificationKey, open, message, options, badge }: NotificationProps) { const globalLocaleText = useLocaleText(); const localeText = { ...defaultLocaleText, ...globalLocaleText }; - const { close } = useNonNullableContext(NotificationsContext); + const { close, removeClosed } = useNonNullableContext(NotificationsContext); const { severity, actionText, onAction, autoHideDuration } = options; @@ -69,6 +76,10 @@ function Notification({ notificationKey, open, message, options, badge }: Notifi [notificationKey, close], ); + const handleExited = React.useCallback(() => { + removeClosed(); + }, [removeClosed]); + const action = ( {onAction ? ( @@ -90,6 +101,7 @@ function Notification({ notificationKey, open, message, options, badge }: Notifi const props = React.useContext(RootPropsContext); const SnackbarComponent = props?.slots?.snackbar ?? Snackbar; + const externalTransitionProps = props?.slotProps?.snackbar?.slotProps?.transition; const snackbarSlotProps = useSlotProps({ elementType: SnackbarComponent, ownerState: props, @@ -99,6 +111,12 @@ function Notification({ notificationKey, open, message, options, badge }: Notifi autoHideDuration, onClose: handleClose, action, + slotProps: { + transition: { + ...externalTransitionProps, + onExited: handleExited, + }, + }, }, }); @@ -195,16 +213,19 @@ function NotificationsProvider(props: NotificationsProviderProps) { ...prev, queue: prev.queue.map((n) => (n.notificationKey === key ? { ...n, open: false } : n)), })); + }, []); - setTimeout(() => { - setState((prev) => ({ - ...prev, - queue: prev.queue.filter((n) => n.open), - })); - }, 100); + const removeClosed = React.useCallback(() => { + setState((prev) => ({ + ...prev, + queue: prev.queue.filter((n) => n.open), + })); }, []); - const contextValue = React.useMemo(() => ({ show, close }), [show, close]); + const contextValue = React.useMemo( + () => ({ show, close, removeClosed }), + [show, close, removeClosed], + ); return ( From d283fa1d03d204746f0337fb9c9d1cf76d630b62 Mon Sep 17 00:00:00 2001 From: Daniel Chae Date: Sat, 24 May 2025 07:56:21 +0900 Subject: [PATCH 4/5] Fix: Passing in props to NotificationsProvider no longer breaks Notifications queue - Also refactored `remove` to be consistent with the other context methods. Possible this version is easier for React to optimise? --- .../NotificationsProvider.tsx | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx index 3f3454940c0..9a5b24ef5c4 100644 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx +++ b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx @@ -22,13 +22,6 @@ import type { } from './useNotifications'; import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; -export interface RemoveClosedNotifications { - /** - * Remove all closed snackbars from the NotificationsState queue. - */ - (): void; -} - export interface NotificationsProviderSlotProps { snackbar: SnackbarProps; } @@ -62,8 +55,7 @@ interface NotificationProps { function Notification({ notificationKey, open, message, options, badge }: NotificationProps) { const globalLocaleText = useLocaleText(); const localeText = { ...defaultLocaleText, ...globalLocaleText }; - const { close, removeClosed } = useNonNullableContext(NotificationsContext); - + const { close, remove } = useNonNullableContext(NotificationsContext); const { severity, actionText, onAction, autoHideDuration } = options; const handleClose = React.useCallback( @@ -77,8 +69,8 @@ function Notification({ notificationKey, open, message, options, badge }: Notifi ); const handleExited = React.useCallback(() => { - removeClosed(); - }, [removeClosed]); + remove(notificationKey); + }, [notificationKey, remove]); const action = ( @@ -101,23 +93,31 @@ function Notification({ notificationKey, open, message, options, badge }: Notifi const props = React.useContext(RootPropsContext); const SnackbarComponent = props?.slots?.snackbar ?? Snackbar; - const externalTransitionProps = props?.slotProps?.snackbar?.slotProps?.transition; + + // Passing `onExited` through `externalSlotProps` here. + // Passing it through `additionalProps` causes it to be overwritten when + // transition slotProps are specified in RootPropsContext. + const externalSnackbarSlotProps = props?.slotProps?.snackbar?.slotProps; + const externalTransitionProps = externalSnackbarSlotProps?.transition; const snackbarSlotProps = useSlotProps({ elementType: SnackbarComponent, ownerState: props, - externalSlotProps: props?.slotProps?.snackbar, - additionalProps: { - open, - autoHideDuration, - onClose: handleClose, - action, + externalSlotProps: { + ...props?.slotProps?.snackbar, slotProps: { + ...externalSnackbarSlotProps, transition: { ...externalTransitionProps, onExited: handleExited, }, }, }, + additionalProps: { + open, + autoHideDuration, + onClose: handleClose, + action, + }, }); return ( @@ -176,6 +176,15 @@ const generateId = () => { return id; }; +export interface RemoveNotification { + /** + * Remove a snackbar from the application state (after it has been closed). + * + * @param key The key of the notification to remove. + */ + (key: string): void; +} + /** * Provider for Notifications. The subtree of this component can use the `useNotifications` hook to * access the notifications API. The notifications are shown in the same order they are requested. @@ -215,17 +224,14 @@ function NotificationsProvider(props: NotificationsProviderProps) { })); }, []); - const removeClosed = React.useCallback(() => { + const remove = React.useCallback((key) => { setState((prev) => ({ ...prev, - queue: prev.queue.filter((n) => n.open), + queue: prev.queue.filter((n) => key !== n.notificationKey), })); }, []); - const contextValue = React.useMemo( - () => ({ show, close, removeClosed }), - [show, close, removeClosed], - ); + const contextValue = React.useMemo(() => ({ show, close, remove }), [show, close, remove]); return ( From 685eebb5e84d4de627389707ce3cc3481590b7bb Mon Sep 17 00:00:00 2001 From: Daniel Chae Date: Sat, 24 May 2025 08:13:25 +0900 Subject: [PATCH 5/5] Fix: Fixed types (added RemoveNotification type to context type) --- .../src/useNotifications/NotificationsContext.ts | 3 ++- .../src/useNotifications/NotificationsProvider.tsx | 10 +--------- .../src/useNotifications/useNotifications.tsx | 9 +++++++++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/toolpad-core/src/useNotifications/NotificationsContext.ts b/packages/toolpad-core/src/useNotifications/NotificationsContext.ts index 330f4a97385..674b21ef237 100644 --- a/packages/toolpad-core/src/useNotifications/NotificationsContext.ts +++ b/packages/toolpad-core/src/useNotifications/NotificationsContext.ts @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; -import type { ShowNotification, CloseNotification } from './useNotifications'; +import type { ShowNotification, CloseNotification, RemoveNotification } from './useNotifications'; /** * @ignore - internal component. @@ -9,6 +9,7 @@ import type { ShowNotification, CloseNotification } from './useNotifications'; export interface NotificationsContextValue { show: ShowNotification; close: CloseNotification; + remove: RemoveNotification; } export const NotificationsContext = React.createContext(null); diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx index 9a5b24ef5c4..e1837940539 100644 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx +++ b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx @@ -19,6 +19,7 @@ import type { CloseNotification, ShowNotification, ShowNotificationOptions, + RemoveNotification, } from './useNotifications'; import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; @@ -176,15 +177,6 @@ const generateId = () => { return id; }; -export interface RemoveNotification { - /** - * Remove a snackbar from the application state (after it has been closed). - * - * @param key The key of the notification to remove. - */ - (key: string): void; -} - /** * Provider for Notifications. The subtree of this component can use the `useNotifications` hook to * access the notifications API. The notifications are shown in the same order they are requested. diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.tsx index 91dcb340aec..230b7da69df 100644 --- a/packages/toolpad-core/src/useNotifications/useNotifications.tsx +++ b/packages/toolpad-core/src/useNotifications/useNotifications.tsx @@ -46,6 +46,15 @@ export interface CloseNotification { (key: string): void; } +export interface RemoveNotification { + /** + * Remove a snackbar from the application state (after it has been closed). + * + * @param key The key of the notification to remove. + */ + (key: string): void; +} + interface UseNotifications { show: ShowNotification; close: CloseNotification;