Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { clsx } from 'clsx';
import useNoticeTimer from './hooks/useNoticeTimer';
import { isNonNullable, isReactRenderable, useEvent } from '@rc-component/util';
import { isNonNullable, isReactRenderable, useEvent, useLayoutEffect } from '@rc-component/util';
import useClosable, { type ClosableType } from './hooks/useClosable';
import DefaultProgress from './Progress';
import type { NotificationProgressProps } from './Progress';
Expand Down Expand Up @@ -106,6 +106,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
} = props;

const [percent, setPercent] = React.useState(0);
const percentRef = React.useRef(0);
const noticePrefixCls = `${prefixCls}-notice`;

// ========================= Close ==========================
Expand All @@ -118,7 +119,19 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
// ======================== Duration ========================
const [hovering, setHovering] = React.useState(false);

const [onResume, onPause] = useNoticeTimer(duration, onInternalClose, setPercent);
const [onResume, onPause] = useNoticeTimer(duration, onInternalClose, (nextPercent) => {
percentRef.current = nextPercent;
if (showProgress) {
setPercent(nextPercent);
}
});

// Progress can be enabled while paused, when no timer tick will update state.
useLayoutEffect(() => {
if (showProgress) {
setPercent(percentRef.current);
}
}, [showProgress]);

const validPercent = 100 - Math.min(Math.max(percent * 100, 0), 100);
const Progress = components?.progress || DefaultProgress;
Expand Down
119 changes: 119 additions & 0 deletions tests/notification-rendering.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { act, render } from '@testing-library/react';
import React from 'react';
import Notification from '../src/Notification';

function advanceFrames(count: number) {
for (let frame = 0; frame < count; frame += 1) {
act(() => vi.advanceTimersByTime(16));
}
}

describe('Notification timer rendering', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it.each([undefined, false])(
'does not render hidden progress on timer ticks (%s)',
(showProgress) => {
const onRender = vi.fn();
const onClose = vi.fn();
const { queryByRole, unmount } = render(
<React.Profiler id="notification" onRender={onRender}>
<Notification
prefixCls="rc-notification"
description="Notice"
duration={1}
showProgress={showProgress}
onClose={onClose}
/>
</React.Profiler>,
);
const initialRenderCount = onRender.mock.calls.length;

advanceFrames(32);

expect(onRender).toHaveBeenCalledTimes(initialRenderCount);
expect(queryByRole('progressbar')).toBeNull();
expect(onClose).not.toHaveBeenCalled();

advanceFrames(32);
expect(onClose).toHaveBeenCalledTimes(1);
unmount();
},
);

it('continues rendering visible progress and closes at the original duration', () => {
const onClose = vi.fn();
const { getByRole, unmount } = render(
<Notification prefixCls="rc-notification" duration={1} showProgress onClose={onClose} />,
);

advanceFrames(32);
expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(48.8, 1);
expect(onClose).not.toHaveBeenCalled();

advanceFrames(32);
expect(onClose).toHaveBeenCalledTimes(1);
unmount();
});

it('updates progress when enabled during an existing countdown without restarting it', () => {
const onClose = vi.fn();
const onRender = vi.fn();
const notice = (showProgress: boolean) => (
<React.Profiler id="notification" onRender={onRender}>
<Notification
prefixCls="rc-notification"
duration={1}
showProgress={showProgress}
onClose={onClose}
/>
</React.Profiler>
);
const { rerender, getByRole, unmount } = render(notice(false));

advanceFrames(16);
rerender(notice(true));
advanceFrames(16);
expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(48.8, 1);

rerender(notice(false));
const renderCount = onRender.mock.calls.length;
advanceFrames(16);
expect(onRender).toHaveBeenCalledTimes(renderCount);
expect(onClose).not.toHaveBeenCalled();

advanceFrames(16);
expect(onClose).toHaveBeenCalledTimes(1);
unmount();
});

it('shows elapsed progress when enabled while the countdown is paused', () => {
const onClose = vi.fn();
const notice = (showProgress: boolean, hovering: boolean) => (
<Notification
prefixCls="rc-notification"
duration={1}
showProgress={showProgress}
hovering={hovering}
onClose={onClose}
/>
);
const { rerender, getByRole, unmount } = render(notice(false, false));

advanceFrames(16);
rerender(notice(false, true));
advanceFrames(32);
rerender(notice(true, true));

expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(74.4, 1);
advanceFrames(16);
expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(74.4, 1);
expect(onClose).not.toHaveBeenCalled();

rerender(notice(true, false));
advanceFrames(48);
expect(onClose).toHaveBeenCalledTimes(1);
unmount();
});
});