diff --git a/README.md b/README.md
index bc059f5..43984c6 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,12 @@ props details:
1.5 |
after duration of time, this notice will disappear.(seconds) |
+
+ | showProgress |
+ boolean |
+ false |
+ show with progress bar for auto-closing notification |
+
| style |
Object |
diff --git a/assets/index.less b/assets/index.less
index 441ed46..5fbbf71 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -90,6 +90,34 @@
filter: alpha(opacity=100);
}
}
+
+ // Progress
+ &-progress {
+ position: absolute;
+ left: 3px;
+ right: 3px;
+ border-radius: 1px;
+ overflow: hidden;
+ appearance: none;
+ -webkit-appearance: none;
+ display: block;
+ inline-size: 100%;
+ block-size: 2px;
+ border: 0;
+
+ &,
+ &::-webkit-progress-bar {
+ background-color: rgba(0, 0, 0, 0.04);
+ }
+
+ &::-moz-progress-bar {
+ background-color: #31afff;
+ }
+
+ &::-webkit-progress-value {
+ background-color: #31afff;
+ }
+ }
}
&-fade {
diff --git a/docs/demo/showProgress.md b/docs/demo/showProgress.md
new file mode 100644
index 0000000..e895cd9
--- /dev/null
+++ b/docs/demo/showProgress.md
@@ -0,0 +1,8 @@
+---
+title: showProgress
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/examples/showProgress.tsx b/docs/examples/showProgress.tsx
new file mode 100644
index 0000000..50e80eb
--- /dev/null
+++ b/docs/examples/showProgress.tsx
@@ -0,0 +1,24 @@
+/* eslint-disable no-console */
+import React from 'react';
+import '../../assets/index.less';
+import { useNotification } from '../../src';
+import motion from './motion';
+
+export default () => {
+ const [notice, contextHolder] = useNotification({ motion, showProgress: true });
+
+ return (
+ <>
+
+ {contextHolder}
+ >
+ );
+};
diff --git a/src/Notice.tsx b/src/Notice.tsx
index 84adcb6..69bb940 100644
--- a/src/Notice.tsx
+++ b/src/Notice.tsx
@@ -21,6 +21,7 @@ const Notify = React.forwardRef 0 && showProgress;
// ======================== Close =========================
const onInternalClose = () => {
@@ -50,17 +54,48 @@ const Notify = React.forwardRef {
if (!mergedHovering && duration > 0) {
- const timeout = setTimeout(() => {
- onInternalClose();
- }, duration * 1000);
+ const start = Date.now() - spentTime;
+ const timeout = setTimeout(
+ () => {
+ onInternalClose();
+ },
+ duration * 1000 - spentTime,
+ );
return () => {
clearTimeout(timeout);
+ setSpentTime(Date.now() - start);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [duration, mergedHovering, times]);
+ React.useEffect(() => {
+ if (!mergedHovering && mergedShowProgress) {
+ const start = performance.now();
+ let animationFrame: number;
+
+ const calculate = () => {
+ cancelAnimationFrame(animationFrame);
+ animationFrame = requestAnimationFrame((timestamp) => {
+ const runtime = timestamp + spentTime - start;
+ const progress = Math.min(runtime / (duration * 1000), 1);
+ setPercent(progress * 100);
+ if (progress < 1) {
+ calculate();
+ }
+ });
+ };
+
+ calculate();
+
+ return () => {
+ cancelAnimationFrame(animationFrame);
+ };
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [duration, mergedHovering, mergedShowProgress, times]);
+
// ======================== Closable ========================
const closableObj = React.useMemo(() => {
if (typeof closable === 'object' && closable !== null) {
@@ -74,6 +109,9 @@ const Notify = React.forwardRef 100 ? 100 : percent);
+
// ======================== Render ========================
const noticePrefixCls = `${prefixCls}-notice`;
@@ -115,6 +153,13 @@ const Notify = React.forwardRef
)}
+
+ {/* Progress Bar */}
+ {mergedShowProgress && (
+
+ )}
);
});
diff --git a/src/hooks/useNotification.tsx b/src/hooks/useNotification.tsx
index ad79905..31f5d81 100644
--- a/src/hooks/useNotification.tsx
+++ b/src/hooks/useNotification.tsx
@@ -17,6 +17,7 @@ export interface NotificationConfig {
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
maxCount?: number;
duration?: number;
+ showProgress?: boolean;
/** @private. Config for notification holder style. Safe to remove if refactor */
className?: (placement: Placement) => string;
/** @private. Config for notification holder style. Safe to remove if refactor */
diff --git a/src/interface.ts b/src/interface.ts
index b9de386..b5d48f1 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -7,6 +7,7 @@ type NoticeSemanticProps = 'wrapper';
export interface NoticeConfig {
content?: React.ReactNode;
duration?: number | null;
+ showProgress?: boolean;
closeIcon?: React.ReactNode;
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
className?: string;
diff --git a/tests/index.test.tsx b/tests/index.test.tsx
index 6fa75ee..5dbbeb5 100644
--- a/tests/index.test.tsx
+++ b/tests/index.test.tsx
@@ -270,6 +270,45 @@ describe('Notification.Basic', () => {
expect(document.querySelectorAll('.freeze')).toHaveLength(0);
});
+ it('continue timing after hover', () => {
+ const { instance } = renderDemo({
+ duration: 1,
+ });
+
+ act(() => {
+ instance.open({
+ content: 1
,
+ });
+ });
+
+ expect(document.querySelector('.test')).toBeTruthy();
+
+ // Wait for 500ms
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+ expect(document.querySelector('.test')).toBeTruthy();
+
+ // Mouse in should not remove
+ fireEvent.mouseEnter(document.querySelector('.rc-notification-notice'));
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+ expect(document.querySelector('.test')).toBeTruthy();
+
+ // Mouse out should not remove until 500ms later
+ fireEvent.mouseLeave(document.querySelector('.rc-notification-notice'));
+ act(() => {
+ vi.advanceTimersByTime(450);
+ });
+ expect(document.querySelector('.test')).toBeTruthy();
+
+ act(() => {
+ vi.advanceTimersByTime(100);
+ });
+ expect(document.querySelector('.test')).toBeFalsy();
+ });
+
describe('maxCount', () => {
it('remove work when maxCount set', () => {
const { instance } = renderDemo({
@@ -688,6 +727,7 @@ describe('Notification.Basic', () => {
fireEvent.keyDown(document.querySelector('.rc-notification-notice-close'), { key: 'Enter' }); // origin latest
expect(closeCount).toEqual(1);
});
+
it('Support aria-* in closable', () => {
const { instance } = renderDemo({
closable: {
@@ -712,4 +752,33 @@ describe('Notification.Basic', () => {
document.querySelector('.rc-notification-notice-close').getAttribute('aria-labelledby'),
).toEqual('close');
});
+
+ describe('showProgress', () => {
+ it('show with progress', () => {
+ const { instance } = renderDemo({
+ duration: 1,
+ showProgress: true,
+ });
+
+ act(() => {
+ instance.open({
+ content: 1
,
+ });
+ });
+
+ expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ expect(document.querySelector('.rc-notification-notice-progress')).toBeFalsy();
+ });
+ });
});