Skip to content

Commit 54b3e48

Browse files
committed
refactor: add useNotification hook
1 parent 71219ac commit 54b3e48

8 files changed

Lines changed: 635 additions & 45 deletions

File tree

assets/geek.less

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,39 @@
4949
color: #111;
5050
font-size: 14px;
5151
line-height: 1.5;
52+
53+
&-content {
54+
white-space: pre-wrap;
55+
padding-right: 36px;
56+
}
57+
58+
&-close {
59+
position: absolute;
60+
top: 12px;
61+
right: 12px;
62+
display: inline-flex;
63+
align-items: center;
64+
justify-content: center;
65+
width: 28px;
66+
height: 28px;
67+
border: 2px solid #111;
68+
border-radius: 999px;
69+
background: #fff;
70+
color: #111;
71+
font-size: 14px;
72+
line-height: 1;
73+
cursor: pointer;
74+
transition:
75+
transform @notificationMotionDuration @notificationMotionEase,
76+
box-shadow @notificationMotionDuration @notificationMotionEase,
77+
background-color @notificationMotionDuration @notificationMotionEase;
78+
79+
&:hover {
80+
background: #f4f9ff;
81+
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.12);
82+
transform: translate3d(-1px, -1px, 0);
83+
}
84+
}
5285
}
5386
}
5487

docs/examples/hooks.tsx

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
/* eslint-disable no-console */
22
import React from 'react';
3-
import '../../assets/index.less';
3+
import type { CSSMotionProps } from '@rc-component/motion';
4+
import '../../assets/geek.less';
45
import { useNotification } from '../../src';
5-
import motion from './motion';
6+
7+
const motion: CSSMotionProps = {
8+
motionName: 'notification-fade',
9+
motionAppear: true,
10+
motionEnter: true,
11+
motionLeave: true,
12+
onLeaveStart: (ele) => {
13+
const { offsetHeight } = ele;
14+
return { height: offsetHeight };
15+
},
16+
onLeaveActive: () => ({ height: 0, opacity: 0, margin: 0 }),
17+
};
618

719
const App = () => {
8-
const [notice, contextHolder] = useNotification({ motion, closable: true });
20+
const [notice, contextHolder] = useNotification({
21+
motion,
22+
closable: true,
23+
prefixCls: 'notification',
24+
});
925

1026
return (
1127
<>
12-
<div>
13-
<div>
28+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
29+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
1430
{/* Default */}
1531
<button
32+
type="button"
1633
onClick={() => {
1734
notice.open({
1835
content: `${new Date().toISOString()}`,
@@ -24,6 +41,7 @@ const App = () => {
2441

2542
{/* Not Close */}
2643
<button
44+
type="button"
2745
onClick={() => {
2846
notice.open({
2947
content: `${Array(Math.round(Math.random() * 5) + 1)
@@ -34,11 +52,12 @@ const App = () => {
3452
});
3553
}}
3654
>
37-
Not Auto Close
55+
Not Auto Close (Random)
3856
</button>
3957

4058
{/* Not Close */}
4159
<button
60+
type="button"
4261
onClick={() => {
4362
notice.open({
4463
content: `${Array(5)
@@ -49,13 +68,14 @@ const App = () => {
4968
});
5069
}}
5170
>
52-
Not Auto Close
71+
Not Auto Close (5 Items)
5372
</button>
5473
</div>
5574

56-
<div>
75+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
5776
{/* No Closable */}
5877
<button
78+
type="button"
5979
onClick={() => {
6080
notice.open({
6181
content: `No Close! ${new Date().toISOString()}`,
@@ -73,24 +93,26 @@ const App = () => {
7393

7494
{/* Force Close */}
7595
<button
96+
type="button"
7697
onClick={() => {
7798
notice.close('No Close');
7899
}}
79100
>
80101
Force Close No Closable
81102
</button>
82103
</div>
83-
</div>
84104

85-
<div>
86-
{/* Destroy All */}
87-
<button
88-
onClick={() => {
89-
notice.destroy();
90-
}}
91-
>
92-
Destroy All
93-
</button>
105+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
106+
{/* Destroy All */}
107+
<button
108+
type="button"
109+
onClick={() => {
110+
notice.destroy();
111+
}}
112+
>
113+
Destroy All
114+
</button>
115+
</div>
94116
</div>
95117

96118
{contextHolder}

src/Notification.tsx

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
11
import * as React from 'react';
22
import { clsx } from 'clsx';
3+
import pickAttrs from '@rc-component/util/lib/pickAttrs';
34
import useNoticeTimer from './hooks/useNoticeTimer';
45
import { useEvent } from '@rc-component/util';
56

67
export interface NotificationClassNames {
8+
wrapper?: string;
79
root?: string;
10+
content?: string;
811
close?: string;
12+
progress?: string;
913
}
1014

1115
export interface NotificationStyles {
16+
wrapper?: React.CSSProperties;
1217
root?: React.CSSProperties;
18+
content?: React.CSSProperties;
1319
close?: React.CSSProperties;
20+
progress?: React.CSSProperties;
1421
}
1522

1623
export interface NotificationProps {
24+
prefixCls?: string;
1725
content?: React.ReactNode;
1826
actions?: React.ReactNode;
1927
close?: React.ReactNode;
20-
duration?: number | false;
28+
closable?:
29+
| boolean
30+
| ({ closeIcon?: React.ReactNode; onClose?: VoidFunction } & React.AriaAttributes);
31+
duration?: number | false | null;
32+
showProgress?: boolean;
33+
times?: number;
34+
hovering?: boolean;
2135
offset?: {
2236
x: number;
2337
y: number;
@@ -27,45 +41,91 @@ export interface NotificationProps {
2741
style?: React.CSSProperties;
2842
classNames?: NotificationClassNames;
2943
styles?: NotificationStyles;
44+
props?: React.HTMLAttributes<HTMLDivElement> & Record<string, any>;
3045
onClick?: React.MouseEventHandler<HTMLDivElement>;
31-
/** Callback when notification is closed by timeout */
3246
onClose?: () => void;
47+
onCloseInternal?: VoidFunction;
3348
}
3449

3550
const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props, ref) => {
3651
const {
52+
prefixCls = 'rc-notification',
3753
content,
3854
actions,
3955
close,
56+
closable,
4057
duration = 4.5,
58+
showProgress,
59+
hovering: forcedHovering,
4160
offset,
4261
pauseOnHover = true,
4362
className,
4463
style,
4564
classNames,
4665
styles,
66+
props: divProps,
4767
onClick,
4868
onClose,
69+
onCloseInternal,
4970
} = props;
71+
const [hovering, setHovering] = React.useState(false);
72+
const [percent, setPercent] = React.useState(0);
73+
const noticePrefixCls = `${prefixCls}-notice`;
5074

5175
// ========================= Close ==========================
5276
const onEventClose = useEvent(onClose);
77+
const onEventCloseInternal = useEvent(onCloseInternal);
5378
const offsetRef = React.useRef(offset);
79+
const closableObj = React.useMemo(() => {
80+
if (typeof closable === 'object' && closable !== null) {
81+
return closable;
82+
}
83+
84+
return {};
85+
}, [closable]);
86+
const closeContent = close === undefined ? (closableObj.closeIcon ?? 'x') : close;
87+
const mergedClosable = close !== undefined ? close !== null : !!closable;
88+
const ariaProps = pickAttrs(closableObj, true);
5489

5590
if (offset) {
5691
offsetRef.current = offset;
5792
}
5893

5994
// ======================== Duration ========================
60-
const [onResume, onPause] = useNoticeTimer(duration, onEventClose, () => {});
95+
const [onResume, onPause] = useNoticeTimer(
96+
duration,
97+
() => {
98+
closableObj.onClose?.();
99+
onEventClose();
100+
onEventCloseInternal();
101+
},
102+
setPercent,
103+
!!showProgress,
104+
);
61105

62106
const mergedOffset = offset ?? offsetRef.current;
107+
const validPercent = 100 - Math.min(Math.max(percent * 100, 0), 100);
108+
109+
React.useEffect(() => {
110+
if (!pauseOnHover) {
111+
return;
112+
}
113+
114+
if (forcedHovering) {
115+
onPause();
116+
} else if (!hovering) {
117+
onResume();
118+
}
119+
}, [forcedHovering, hovering, onPause, onResume, pauseOnHover]);
63120

64121
// ========================= Render =========================
65122
return (
66123
<div
124+
{...divProps}
67125
ref={ref}
68-
className={clsx(className, classNames?.root)}
126+
className={clsx(noticePrefixCls, className, classNames?.root, {
127+
[`${noticePrefixCls}-closable`]: mergedClosable,
128+
})}
69129
style={{
70130
...styles?.root,
71131
...(mergedOffset
@@ -77,23 +137,64 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
77137
...style,
78138
}}
79139
onClick={onClick}
80-
onMouseEnter={pauseOnHover ? onPause : undefined}
81-
onMouseLeave={pauseOnHover ? onResume : undefined}
140+
onMouseEnter={(event) => {
141+
setHovering(true);
142+
if (pauseOnHover) {
143+
onPause();
144+
}
145+
divProps?.onMouseEnter?.(event);
146+
}}
147+
onMouseLeave={(event) => {
148+
setHovering(false);
149+
if (pauseOnHover && !forcedHovering) {
150+
onResume();
151+
}
152+
divProps?.onMouseLeave?.(event);
153+
}}
82154
>
83-
{content}
84-
{close && (
155+
<div
156+
className={clsx(`${noticePrefixCls}-content`, classNames?.content)}
157+
style={styles?.content}
158+
>
159+
{content}
160+
</div>
161+
162+
{mergedClosable && (
85163
<button
86-
className={clsx('close', classNames?.close)}
164+
className={clsx(`${noticePrefixCls}-close`, classNames?.close)}
87165
aria-label="Close"
166+
{...ariaProps}
88167
style={styles?.close}
168+
onKeyDown={(event) => {
169+
if (event.key === 'Enter' || event.code === 'Enter') {
170+
closableObj.onClose?.();
171+
onEventClose();
172+
onEventCloseInternal();
173+
}
174+
}}
89175
onClick={(e) => {
176+
e.preventDefault();
90177
e.stopPropagation();
178+
closableObj.onClose?.();
91179
onEventClose();
180+
onEventCloseInternal();
92181
}}
93182
>
94-
{close}
183+
{closeContent}
95184
</button>
96185
)}
186+
187+
{showProgress && typeof duration === 'number' && duration > 0 && (
188+
<progress
189+
className={clsx(`${noticePrefixCls}-progress`, classNames?.progress)}
190+
max="100"
191+
value={validPercent}
192+
style={styles?.progress}
193+
>
194+
{validPercent}%
195+
</progress>
196+
)}
197+
97198
{actions && <div className="actions">{actions}</div>}
98199
</div>
99200
);

0 commit comments

Comments
 (0)