Skip to content

Commit 06a471c

Browse files
authored
feat(show-progress): add show progress support (#322)
* feat(show-progress): add show progress support * feat(show-progress): update readme changes * feat(show-progress): move the progress bar to the bottom * fix: remove rc-progress and resolve conversation * feat(show-progress): using standard progress element * fix: also define the standard property 'appearance' for compatibility * fix: remove progress wrapper * docs: revert hooks add showProgress demo * fix(show-progress): keep on hover and reverse * fix: add spent time to continue the progress * fix: add test case for spentTime * fix: fixing the start time for animation
1 parent 31ca871 commit 06a471c

File tree

8 files changed

+185
-3
lines changed

8 files changed

+185
-3
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ props details:
130130
<td>1.5</td>
131131
<td>after duration of time, this notice will disappear.(seconds)</td>
132132
</tr>
133+
<tr>
134+
<td>showProgress</td>
135+
<td>boolean</td>
136+
<td>false</td>
137+
<td>show with progress bar for auto-closing notification</td>
138+
</tr>
133139
<tr>
134140
<td>style</td>
135141
<td>Object</td>

assets/index.less

+28
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,34 @@
9090
filter: alpha(opacity=100);
9191
}
9292
}
93+
94+
// Progress
95+
&-progress {
96+
position: absolute;
97+
left: 3px;
98+
right: 3px;
99+
border-radius: 1px;
100+
overflow: hidden;
101+
appearance: none;
102+
-webkit-appearance: none;
103+
display: block;
104+
inline-size: 100%;
105+
block-size: 2px;
106+
border: 0;
107+
108+
&,
109+
&::-webkit-progress-bar {
110+
background-color: rgba(0, 0, 0, 0.04);
111+
}
112+
113+
&::-moz-progress-bar {
114+
background-color: #31afff;
115+
}
116+
117+
&::-webkit-progress-value {
118+
background-color: #31afff;
119+
}
120+
}
93121
}
94122

95123
&-fade {

docs/demo/showProgress.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: showProgress
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/showProgress.tsx"></code>

docs/examples/showProgress.tsx

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import '../../assets/index.less';
4+
import { useNotification } from '../../src';
5+
import motion from './motion';
6+
7+
export default () => {
8+
const [notice, contextHolder] = useNotification({ motion, showProgress: true });
9+
10+
return (
11+
<>
12+
<button
13+
onClick={() => {
14+
notice.open({
15+
content: `${new Date().toISOString()}`,
16+
});
17+
}}
18+
>
19+
Show With Progress
20+
</button>
21+
{contextHolder}
22+
</>
23+
);
24+
};

src/Notice.tsx

+48-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const Notify = React.forwardRef<HTMLDivElement, NoticeProps & { times?: number }
2121
style,
2222
className,
2323
duration = 4.5,
24+
showProgress,
2425

2526
eventKey,
2627
content,
@@ -34,7 +35,10 @@ const Notify = React.forwardRef<HTMLDivElement, NoticeProps & { times?: number }
3435
hovering: forcedHovering,
3536
} = props;
3637
const [hovering, setHovering] = React.useState(false);
38+
const [percent, setPercent] = React.useState(0);
39+
const [spentTime, setSpentTime] = React.useState(0);
3740
const mergedHovering = forcedHovering || hovering;
41+
const mergedShowProgress = duration > 0 && showProgress;
3842

3943
// ======================== Close =========================
4044
const onInternalClose = () => {
@@ -50,17 +54,48 @@ const Notify = React.forwardRef<HTMLDivElement, NoticeProps & { times?: number }
5054
// ======================== Effect ========================
5155
React.useEffect(() => {
5256
if (!mergedHovering && duration > 0) {
53-
const timeout = setTimeout(() => {
54-
onInternalClose();
55-
}, duration * 1000);
57+
const start = Date.now() - spentTime;
58+
const timeout = setTimeout(
59+
() => {
60+
onInternalClose();
61+
},
62+
duration * 1000 - spentTime,
63+
);
5664

5765
return () => {
5866
clearTimeout(timeout);
67+
setSpentTime(Date.now() - start);
5968
};
6069
}
6170
// eslint-disable-next-line react-hooks/exhaustive-deps
6271
}, [duration, mergedHovering, times]);
6372

73+
React.useEffect(() => {
74+
if (!mergedHovering && mergedShowProgress) {
75+
const start = performance.now();
76+
let animationFrame: number;
77+
78+
const calculate = () => {
79+
cancelAnimationFrame(animationFrame);
80+
animationFrame = requestAnimationFrame((timestamp) => {
81+
const runtime = timestamp + spentTime - start;
82+
const progress = Math.min(runtime / (duration * 1000), 1);
83+
setPercent(progress * 100);
84+
if (progress < 1) {
85+
calculate();
86+
}
87+
});
88+
};
89+
90+
calculate();
91+
92+
return () => {
93+
cancelAnimationFrame(animationFrame);
94+
};
95+
}
96+
// eslint-disable-next-line react-hooks/exhaustive-deps
97+
}, [duration, mergedHovering, mergedShowProgress, times]);
98+
6499
// ======================== Closable ========================
65100
const closableObj = React.useMemo(() => {
66101
if (typeof closable === 'object' && closable !== null) {
@@ -74,6 +109,9 @@ const Notify = React.forwardRef<HTMLDivElement, NoticeProps & { times?: number }
74109

75110
const ariaProps = pickAttrs(closableObj, true);
76111

112+
// ======================== Progress ========================
113+
const validPercent = 100 - (!percent || percent < 0 ? 0 : percent > 100 ? 100 : percent);
114+
77115
// ======================== Render ========================
78116
const noticePrefixCls = `${prefixCls}-notice`;
79117

@@ -115,6 +153,13 @@ const Notify = React.forwardRef<HTMLDivElement, NoticeProps & { times?: number }
115153
{closableObj.closeIcon}
116154
</a>
117155
)}
156+
157+
{/* Progress Bar */}
158+
{mergedShowProgress && (
159+
<progress className={`${noticePrefixCls}-progress`} max="100" value={validPercent}>
160+
{validPercent + '%'}
161+
</progress>
162+
)}
118163
</div>
119164
);
120165
});

src/hooks/useNotification.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface NotificationConfig {
1717
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
1818
maxCount?: number;
1919
duration?: number;
20+
showProgress?: boolean;
2021
/** @private. Config for notification holder style. Safe to remove if refactor */
2122
className?: (placement: Placement) => string;
2223
/** @private. Config for notification holder style. Safe to remove if refactor */

src/interface.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type NoticeSemanticProps = 'wrapper';
77
export interface NoticeConfig {
88
content?: React.ReactNode;
99
duration?: number | null;
10+
showProgress?: boolean;
1011
closeIcon?: React.ReactNode;
1112
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
1213
className?: string;

tests/index.test.tsx

+69
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,45 @@ describe('Notification.Basic', () => {
270270
expect(document.querySelectorAll('.freeze')).toHaveLength(0);
271271
});
272272

273+
it('continue timing after hover', () => {
274+
const { instance } = renderDemo({
275+
duration: 1,
276+
});
277+
278+
act(() => {
279+
instance.open({
280+
content: <p className="test">1</p>,
281+
});
282+
});
283+
284+
expect(document.querySelector('.test')).toBeTruthy();
285+
286+
// Wait for 500ms
287+
act(() => {
288+
vi.advanceTimersByTime(500);
289+
});
290+
expect(document.querySelector('.test')).toBeTruthy();
291+
292+
// Mouse in should not remove
293+
fireEvent.mouseEnter(document.querySelector('.rc-notification-notice'));
294+
act(() => {
295+
vi.advanceTimersByTime(1000);
296+
});
297+
expect(document.querySelector('.test')).toBeTruthy();
298+
299+
// Mouse out should not remove until 500ms later
300+
fireEvent.mouseLeave(document.querySelector('.rc-notification-notice'));
301+
act(() => {
302+
vi.advanceTimersByTime(450);
303+
});
304+
expect(document.querySelector('.test')).toBeTruthy();
305+
306+
act(() => {
307+
vi.advanceTimersByTime(100);
308+
});
309+
expect(document.querySelector('.test')).toBeFalsy();
310+
});
311+
273312
describe('maxCount', () => {
274313
it('remove work when maxCount set', () => {
275314
const { instance } = renderDemo({
@@ -688,6 +727,7 @@ describe('Notification.Basic', () => {
688727
fireEvent.keyDown(document.querySelector('.rc-notification-notice-close'), { key: 'Enter' }); // origin latest
689728
expect(closeCount).toEqual(1);
690729
});
730+
691731
it('Support aria-* in closable', () => {
692732
const { instance } = renderDemo({
693733
closable: {
@@ -712,4 +752,33 @@ describe('Notification.Basic', () => {
712752
document.querySelector('.rc-notification-notice-close').getAttribute('aria-labelledby'),
713753
).toEqual('close');
714754
});
755+
756+
describe('showProgress', () => {
757+
it('show with progress', () => {
758+
const { instance } = renderDemo({
759+
duration: 1,
760+
showProgress: true,
761+
});
762+
763+
act(() => {
764+
instance.open({
765+
content: <p className="test">1</p>,
766+
});
767+
});
768+
769+
expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy();
770+
771+
act(() => {
772+
vi.advanceTimersByTime(500);
773+
});
774+
775+
expect(document.querySelector('.rc-notification-notice-progress')).toBeTruthy();
776+
777+
act(() => {
778+
vi.advanceTimersByTime(500);
779+
});
780+
781+
expect(document.querySelector('.rc-notification-notice-progress')).toBeFalsy();
782+
});
783+
});
715784
});

0 commit comments

Comments
 (0)