diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx index d21adf1479..b75f773f90 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx @@ -247,6 +247,43 @@ describe('', () => { expect(queryByText('transient message three')).not.toBeInTheDocument(); jest.useRealTimers(); }); + + it('renders 3 different messages with overlapping timeout and manual removal', async () => { + jest.useFakeTimers(); + const { queryByText } = await renderInTestApp( + + + , + ); + + // 3 messages stacked with 1.5s each, display times: 0-1.5, 1.5-3, 3-4.5 + + // 0s in, only message 1 + expect(queryByText('transient message one')).toBeInTheDocument(); + + // 1s in, message 1 still shown, message 2 added in background + act(() => jest.advanceTimersByTime(1000)); + expect(queryByText('transient message one')).toBeInTheDocument(); + expect(queryByText('(1 older message)')).toBeInTheDocument(); + + // manually remove message 1 + fireEvent.click(screen.getByTestId('error-button-close')); + expect(screen.getByText('transient message two')).toBeInTheDocument(); + + // 2s in, message 2 now shown, message 3 added + act(() => jest.advanceTimersByTime(1000)); + expect(queryByText('transient message two')).toBeInTheDocument(); + expect(queryByText('(1 older message)')).toBeInTheDocument(); + + // 3s in, message 3 now shown + act(() => jest.advanceTimersByTime(1500)); + expect(queryByText('transient message three')).toBeInTheDocument(); + + // 4s in, all messages gone + act(() => jest.advanceTimersByTime(1500)); + expect(queryByText('transient message three')).not.toBeInTheDocument(); + jest.useRealTimers(); + }); }); describe('with multiple messages of mixed display', () => { diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx index fe20b1d66d..f1021b1f8b 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx @@ -19,7 +19,6 @@ import Snackbar from '@material-ui/core/Snackbar'; import Typography from '@material-ui/core/Typography'; import CloseIcon from '@material-ui/icons/Close'; import { Alert } from '@material-ui/lab'; -import { useIsMounted } from '@react-hookz/web'; import pluralize from 'pluralize'; import React, { useEffect, useState } from 'react'; @@ -63,7 +62,6 @@ export type AlertDisplayProps = { export function AlertDisplay(props: AlertDisplayProps) { const [messages, setMessages] = useState>([]); const alertApi = useApi(alertApiRef); - const isMounted = useIsMounted(); const { anchorOrigin = { vertical: 'top', horizontal: 'center' }, @@ -81,24 +79,25 @@ export function AlertDisplay(props: AlertDisplayProps) { }; }, [alertApi]); + const [firstMessage] = messages; + useEffect(() => { - const [current] = messages; - if (current && current.display === 'transient') { - setTimeout(() => { - if (isMounted()) { - setMessages(msgs => msgs.filter(msg => msg !== current)); - } + if (firstMessage && firstMessage.display === 'transient') { + const timeout = setTimeout(() => { + setMessages(msgs => { + const newMsgs = msgs.filter(msg => msg !== firstMessage); + return newMsgs.length === msgs.length ? msgs : newMsgs; + }); }, timeoutMs); + return () => clearTimeout(timeout); } return undefined; - }, [messages, timeoutMs, isMounted]); + }, [firstMessage, timeoutMs]); if (messages.length === 0) { return null; } - const [firstMessage] = messages; - const handleClose = () => { setMessages(msgs => msgs.filter(msg => msg !== firstMessage)); };