Skip to content

Commit c65ca1e

Browse files
zombieJclaude
andcommitted
refactor: reorganize Notifications props and simplify useNotification types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 95968fe commit c65ca1e

7 files changed

Lines changed: 130 additions & 94 deletions

File tree

docs/examples/NotificationList.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,23 @@ const motion: CSSMotionProps = {
1717

1818
const Demo = () => {
1919
const [configList, setConfigList] = React.useState<NotificationListConfig[]>([]);
20+
const [stack, setStack] = React.useState(false);
2021
const keyRef = React.useRef(0);
22+
const createNotification = React.useCallback(
23+
(key: number): NotificationListConfig => ({
24+
key,
25+
duration: false,
26+
content: `Config ${key + 1}`,
27+
}),
28+
[],
29+
);
2130

2231
const createConfig = React.useCallback(() => {
2332
const key = keyRef.current;
2433
keyRef.current += 1;
2534

26-
setConfigList((prevConfigList) => [
27-
...prevConfigList,
28-
{
29-
key,
30-
duration: false,
31-
content: `Config ${key + 1}`,
32-
},
33-
]);
34-
}, []);
35+
setConfigList((prevConfigList) => [...prevConfigList, createNotification(key)]);
36+
}, [createNotification]);
3537

3638
const createFiveConfigs = React.useCallback(() => {
3739
setConfigList((prevConfigList) => {
@@ -40,18 +42,10 @@ const Demo = () => {
4042

4143
return [
4244
...prevConfigList,
43-
...Array.from({ length: 5 }, (_, index) => {
44-
const key = startKey + index;
45-
46-
return {
47-
key,
48-
duration: false,
49-
content: `Config ${key + 1}`,
50-
};
51-
}),
45+
...Array.from({ length: 5 }, (_, index) => createNotification(startKey + index)),
5246
];
5347
});
54-
}, []);
48+
}, [createNotification]);
5549

5650
const removeLastConfig = React.useCallback(() => {
5751
setConfigList((prevConfigList) => prevConfigList.slice(0, -1));
@@ -91,6 +85,16 @@ const Demo = () => {
9185
<button type="button" onClick={removeMiddleConfig}>
9286
Remove Middle Config
9387
</button>
88+
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
89+
<input
90+
type="checkbox"
91+
checked={stack}
92+
onChange={(event) => {
93+
setStack(event.target.checked);
94+
}}
95+
/>
96+
Enable Stack
97+
</label>
9498
</div>
9599

96100
<NotificationList
@@ -99,6 +103,7 @@ const Demo = () => {
99103
classNames={{ root: 'notification-notice' }}
100104
motion={motion}
101105
placement="topRight"
106+
stack={stack ? { threshold: 3, offset: 20 } : undefined}
102107
/>
103108
</>
104109
);

src/NotificationList.tsx

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CSSMotionList } from '@rc-component/motion';
22
import type { CSSMotionProps } from '@rc-component/motion';
33
import { clsx } from 'clsx';
44
import * as React from 'react';
5+
import type { StackConfig } from './interface';
56
import { NotificationContext } from './legacy/NotificationProvider';
67
import useStack from './legacy/hooks/useStack';
78
import Notification, {
@@ -13,14 +14,7 @@ import useListPosition from './hooks/useListPosition';
1314
import useListScroll from './hooks/useListScroll';
1415

1516
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
16-
17-
export type StackConfig =
18-
| boolean
19-
| {
20-
threshold?: number;
21-
offset?: number;
22-
gap?: number;
23-
};
17+
export type { StackConfig } from './interface';
2418

2519
export interface NotificationListConfig extends NotificationProps {
2620
key: React.Key;
@@ -35,7 +29,7 @@ export interface NotificationListProps {
3529
pauseOnHover?: boolean;
3630
classNames?: NotificationClassNames;
3731
styles?: NotificationStyles;
38-
stack?: StackConfig;
32+
stack?: boolean | StackConfig;
3933
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
4034
className?: string;
4135
style?: React.CSSProperties;
@@ -83,28 +77,26 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
8377

8478
// ========================= Motion =========================
8579
const placementMotion = typeof motion === 'function' ? motion(placement) : motion;
86-
const [stack, { threshold }] = useStack(stackConfig);
87-
const [hoverKeys, setHoverKeys] = React.useState<string[]>([]);
88-
const expanded = stack && (hoverKeys.length > 0 || keys.length <= threshold);
80+
const [stackEnabled, { offset, threshold }] = useStack(stackConfig);
81+
const [listHovering, setListHovering] = React.useState(false);
82+
const expanded = stackEnabled && (listHovering || keys.length <= threshold);
83+
const stackPosition = React.useMemo<StackConfig | undefined>(() => {
84+
if (!stackEnabled || expanded) {
85+
return undefined;
86+
}
87+
88+
return {
89+
offset,
90+
threshold,
91+
};
92+
}, [expanded, offset, stackEnabled, threshold]);
8993

90-
const [notificationPosition, setNodeSize] = useListPosition(mergedConfigList);
94+
const [notificationPosition, setNodeSize] = useListPosition(mergedConfigList, stackPosition);
9195
const { contentRef, onWheel, scrollOffset, viewportRef } = useListScroll(
9296
keyList,
9397
notificationPosition,
9498
);
9599

96-
React.useEffect(() => {
97-
if (stack && hoverKeys.length > 1) {
98-
setHoverKeys((originKeys) => {
99-
const nextKeys = originKeys.filter((key) =>
100-
keyList.some((existingKey) => existingKey === key),
101-
);
102-
103-
return nextKeys.length === originKeys.length ? originKeys : nextKeys;
104-
});
105-
}
106-
}, [hoverKeys, keyList, stack]);
107-
108100
// ========================= Render =========================
109101
const listPrefixCls = `${prefixCls}-list`;
110102
const itemPrefixCls = `${listPrefixCls}-item`;
@@ -120,11 +112,17 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
120112
contextClassNames?.list,
121113
className,
122114
{
123-
[`${prefixCls}-stack`]: stack,
115+
[`${prefixCls}-stack`]: stackEnabled,
124116
[`${prefixCls}-stack-expanded`]: expanded,
125117
},
126118
)}
127119
onWheel={onWheel}
120+
onMouseEnter={() => {
121+
setListHovering(true);
122+
}}
123+
onMouseLeave={() => {
124+
setListHovering(false);
125+
}}
128126
ref={viewportRef}
129127
style={style}
130128
>
@@ -169,14 +167,6 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
169167
...styles?.wrapper,
170168
...config.styles?.wrapper,
171169
}}
172-
onMouseEnter={() =>
173-
setHoverKeys((originKeys) =>
174-
originKeys.includes(strKey) ? originKeys : [...originKeys, strKey],
175-
)
176-
}
177-
onMouseLeave={() =>
178-
setHoverKeys((originKeys) => originKeys.filter((key) => key !== strKey))
179-
}
180170
>
181171
<Notification
182172
key={config.times}
@@ -209,7 +199,7 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
209199
...config.styles?.progress,
210200
},
211201
}}
212-
hovering={stack && hoverKeys.length > 0}
202+
hovering={stackEnabled && listHovering}
213203
pauseOnHover={config.pauseOnHover ?? pauseOnHover}
214204
onCloseInternal={() => {
215205
onNoticeClose?.(key);

src/Notifications.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@ import NotificationList, {
1111
} from './NotificationList';
1212

1313
export interface NotificationsProps {
14+
// Style
1415
prefixCls?: string;
15-
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
16-
container?: HTMLElement | ShadowRoot;
17-
maxCount?: number;
18-
pauseOnHover?: boolean;
1916
classNames?: NotificationClassNames;
2017
styles?: NotificationStyles;
2118
className?: (placement: Placement) => string;
2219
style?: (placement: Placement) => React.CSSProperties;
20+
21+
// UI
22+
container?: HTMLElement | ShadowRoot;
23+
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
24+
25+
// Behavior
26+
maxCount?: number;
27+
pauseOnHover?: boolean;
28+
stack?: boolean | StackConfig;
29+
30+
// Function
2331
onAllRemoved?: VoidFunction;
24-
stack?: StackConfig;
2532
renderNotifications?: (
2633
node: ReactElement,
2734
info: { prefixCls: string; key: React.Key },
@@ -34,9 +41,11 @@ export interface NotificationsRef {
3441
destroy: () => void;
3542
}
3643

44+
// ========================= Types ==========================
3745
type Placements = Partial<Record<Placement, NotificationListConfig[]>>;
3846

3947
const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((props, ref) => {
48+
// ========================= Props ==========================
4049
const {
4150
prefixCls = 'rc-notification',
4251
container,
@@ -51,8 +60,13 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
5160
stack,
5261
renderNotifications,
5362
} = props;
63+
64+
// ========================= State ==========================
5465
const [configList, setConfigList] = React.useState<NotificationListConfig[]>([]);
66+
const [placements, setPlacements] = React.useState<Placements>({});
67+
const emptyRef = React.useRef(false);
5568

69+
// ========================== Ref ===========================
5670
React.useImperativeHandle(ref, () => ({
5771
open: (config) => {
5872
setConfigList((list) => {
@@ -84,8 +98,7 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
8498
},
8599
}));
86100

87-
const [placements, setPlacements] = React.useState<Placements>({});
88-
101+
// ======================== Effect =========================
89102
React.useEffect(() => {
90103
const nextPlacements: Placements = {};
91104

@@ -102,6 +115,7 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
102115
setPlacements(nextPlacements);
103116
}, [configList]);
104117

118+
// ======================== Callback =======================
105119
const onAllNoticeRemoved = React.useCallback((placement: Placement) => {
106120
setPlacements((originPlacements) => {
107121
const clone = {
@@ -116,7 +130,7 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
116130
});
117131
}, []);
118132

119-
const emptyRef = React.useRef(false);
133+
// ======================== Effect =========================
120134
React.useEffect(() => {
121135
if (Object.keys(placements).length > 0) {
122136
emptyRef.current = true;
@@ -126,6 +140,7 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
126140
}
127141
}, [placements, onAllRemoved]);
128142

143+
// ======================== Render =========================
129144
if (!container) {
130145
return null;
131146
}

src/hooks/useListPosition/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as React from 'react';
2+
import type { StackConfig } from '../../interface';
23
import useSizes from './useSizes';
34

45
export type NodePosition = {
56
x: number;
67
y: number;
78
};
89

9-
export default function useListPosition(configList: { key: React.Key }[]) {
10+
export default function useListPosition(configList: { key: React.Key }[], stack?: StackConfig) {
1011
const [sizeMap, setNodeSize] = useSizes();
1112

1213
const notificationPosition = React.useMemo(() => {
@@ -17,15 +18,15 @@ export default function useListPosition(configList: { key: React.Key }[]) {
1718
const key = String(config.key);
1819
const nodePosition = {
1920
x: 0,
20-
y: offsetY,
21+
y: stack ? offsetY + (stack.offset ?? 0) : offsetY,
2122
};
2223

2324
nextNotificationPosition.set(key, nodePosition);
2425
offsetY += sizeMap[key]?.height ?? 0;
2526
});
2627

2728
return nextNotificationPosition;
28-
}, [configList, sizeMap]);
29+
}, [configList, sizeMap, stack]);
2930

3031
return [notificationPosition, setNodeSize] as const;
3132
}

src/hooks/useNotification.tsx

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,27 @@
1-
import type { CSSMotionProps } from '@rc-component/motion';
21
import { useEvent } from '@rc-component/util';
32
import * as React from 'react';
43
import Notifications, { type NotificationsProps, type NotificationsRef } from '../Notifications';
5-
import type {
6-
NotificationClassNames,
7-
NotificationListConfig,
8-
NotificationStyles,
9-
} from '../NotificationList';
10-
import type { Placement, StackConfig } from '../NotificationList';
4+
import type { NotificationListConfig } from '../NotificationList';
5+
import type { Placement } from '../NotificationList';
116

127
const defaultGetContainer = () => document.body;
138

149
// ========================= Types ==========================
1510
type OptionalConfig = Partial<NotificationListConfig>;
16-
type SharedConfig = Pick<NotificationListConfig, 'placement' | 'closable' | 'duration'>;
17-
18-
export interface NotificationConfig {
19-
// Style
20-
prefixCls?: string;
21-
className?: (placement: Placement) => string;
22-
style?: (placement: Placement) => React.CSSProperties;
23-
classNames?: NotificationClassNames;
24-
styles?: NotificationStyles;
11+
type SharedConfig = Pick<
12+
NotificationListConfig,
13+
'placement' | 'closable' | 'duration' | 'showProgress'
14+
>;
2515

16+
export interface NotificationConfig extends Omit<NotificationsProps, 'container'> {
2617
// UI
2718
placement?: Placement;
2819
getContainer?: () => HTMLElement | ShadowRoot;
29-
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
3020

3121
// Behavior
3222
closable?: NotificationListConfig['closable'];
3323
duration?: number | false | null;
34-
pauseOnHover?: boolean;
35-
maxCount?: number;
36-
stack?: StackConfig;
37-
38-
// Function
39-
onAllRemoved?: VoidFunction;
40-
renderNotifications?: NotificationsProps['renderNotifications'];
24+
showProgress?: NotificationListConfig['showProgress'];
4125
}
4226

4327
export interface NotificationAPI {
@@ -96,6 +80,7 @@ export default function useNotification(
9680
placement,
9781
closable,
9882
duration,
83+
showProgress,
9984
pauseOnHover,
10085
classNames,
10186
styles,
@@ -110,6 +95,7 @@ export default function useNotification(
11095
placement,
11196
closable,
11297
duration,
98+
showProgress,
11399
};
114100

115101
// ========================= Holder =========================

src/interface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface StackConfig {
2+
threshold?: number;
3+
offset?: number;
4+
}

0 commit comments

Comments
 (0)