Skip to content

Commit 95968fe

Browse files
committed
fix: forward notification hook defaults
1 parent 2a03246 commit 95968fe

5 files changed

Lines changed: 93 additions & 29 deletions

File tree

src/Notifications.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import type { ReactElement } from 'react';
33
import { createPortal } from 'react-dom';
44
import type { CSSMotionProps } from '@rc-component/motion';
55
import NotificationList, {
6+
type NotificationClassNames,
67
type NotificationListConfig,
8+
type NotificationStyles,
79
type Placement,
810
type StackConfig,
911
} from './NotificationList';
@@ -13,6 +15,9 @@ export interface NotificationsProps {
1315
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
1416
container?: HTMLElement | ShadowRoot;
1517
maxCount?: number;
18+
pauseOnHover?: boolean;
19+
classNames?: NotificationClassNames;
20+
styles?: NotificationStyles;
1621
className?: (placement: Placement) => string;
1722
style?: (placement: Placement) => React.CSSProperties;
1823
onAllRemoved?: VoidFunction;
@@ -37,6 +42,9 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
3742
container,
3843
motion,
3944
maxCount,
45+
pauseOnHover,
46+
classNames,
47+
styles,
4048
className,
4149
style,
4250
onAllRemoved,
@@ -133,6 +141,9 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
133141
configList={placements[placement]}
134142
placement={placement}
135143
prefixCls={prefixCls}
144+
pauseOnHover={pauseOnHover}
145+
classNames={classNames}
146+
styles={styles}
136147
className={className?.(placement)}
137148
style={style?.(placement)}
138149
motion={motion}

src/hooks/useNotification.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,32 @@ import type { Placement, StackConfig } from '../NotificationList';
1111

1212
const defaultGetContainer = () => document.body;
1313

14+
// ========================= Types ==========================
1415
type OptionalConfig = Partial<NotificationListConfig>;
16+
type SharedConfig = Pick<NotificationListConfig, 'placement' | 'closable' | 'duration'>;
1517

1618
export interface NotificationConfig {
19+
// Style
1720
prefixCls?: string;
21+
className?: (placement: Placement) => string;
22+
style?: (placement: Placement) => React.CSSProperties;
23+
classNames?: NotificationClassNames;
24+
styles?: NotificationStyles;
25+
26+
// UI
27+
placement?: Placement;
1828
getContainer?: () => HTMLElement | ShadowRoot;
1929
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
20-
placement?: Placement;
30+
31+
// Behavior
2132
closable?: NotificationListConfig['closable'];
2233
duration?: number | false | null;
23-
showProgress?: boolean;
2434
pauseOnHover?: boolean;
25-
classNames?: NotificationClassNames;
26-
styles?: NotificationStyles;
2735
maxCount?: number;
28-
className?: (placement: Placement) => string;
29-
style?: (placement: Placement) => React.CSSProperties;
30-
onAllRemoved?: VoidFunction;
3136
stack?: StackConfig;
37+
38+
// Function
39+
onAllRemoved?: VoidFunction;
3240
renderNotifications?: NotificationsProps['renderNotifications'];
3341
}
3442

@@ -54,6 +62,7 @@ interface DestroyTask {
5462

5563
type Task = OpenTask | CloseTask | DestroyTask;
5664

65+
// ======================== Helper ==========================
5766
let uniqueKey = 0;
5867

5968
function mergeConfig<T>(...objList: Partial<T>[]): T {
@@ -79,28 +88,45 @@ function mergeConfig<T>(...objList: Partial<T>[]): T {
7988
export default function useNotification(
8089
rootConfig: NotificationConfig = {},
8190
): [NotificationAPI, React.ReactElement] {
91+
// ========================= Config =========================
8292
const {
8393
getContainer = defaultGetContainer,
8494
motion,
8595
prefixCls,
96+
placement,
97+
closable,
98+
duration,
99+
pauseOnHover,
100+
classNames,
101+
styles,
86102
maxCount,
87103
className,
88104
style,
89105
onAllRemoved,
90106
stack,
91107
renderNotifications,
92-
...shareConfig
93108
} = rootConfig;
109+
const shareConfig: SharedConfig = {
110+
placement,
111+
closable,
112+
duration,
113+
};
94114

115+
// ========================= Holder =========================
95116
const [container, setContainer] = React.useState<HTMLElement | ShadowRoot>();
96117
const notificationsRef = React.useRef<NotificationsRef | null>(null);
118+
const [taskQueue, setTaskQueue] = React.useState<Task[]>([]);
119+
97120
const contextHolder = (
98121
<Notifications
99122
container={container}
100123
ref={notificationsRef}
101124
prefixCls={prefixCls}
102125
motion={motion}
103126
maxCount={maxCount}
127+
pauseOnHover={pauseOnHover}
128+
classNames={classNames}
129+
styles={styles}
104130
className={className}
105131
style={style}
106132
onAllRemoved={onAllRemoved}
@@ -109,8 +135,7 @@ export default function useNotification(
109135
/>
110136
);
111137

112-
const [taskQueue, setTaskQueue] = React.useState<Task[]>([]);
113-
138+
// ========================== API ==========================
114139
const open = useEvent<NotificationAPI['open']>((config) => {
115140
const mergedConfig = mergeConfig<NotificationListConfig>(shareConfig, config);
116141

@@ -135,6 +160,7 @@ export default function useNotification(
135160
[open],
136161
);
137162

163+
// ======================== Effect =========================
138164
React.useEffect(() => {
139165
setContainer(getContainer());
140166
});
@@ -163,5 +189,6 @@ export default function useNotification(
163189
}
164190
}, [taskQueue]);
165191

192+
// ======================== Return =========================
166193
return [api, contextHolder];
167194
}

src/legacy/hooks/useNotification.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,14 @@ export default function useNotification(
135135
);
136136

137137
// ======================= Container ======================
138-
// React 18 should all in effect that we will check container in each render
139-
// Which means getContainer should be stable.
138+
// `getContainer` should be stable.
140139
React.useEffect(() => {
141140
setContainer(getContainer());
142141
});
143142

144143
// ======================== Effect ========================
145144
React.useEffect(() => {
146-
// Flush task when node ready
145+
// Flush queued tasks once the holder is ready.
147146
if (notificationsRef.current && taskQueue.length) {
148147
taskQueue.forEach((task) => {
149148
switch (task.type) {
@@ -163,23 +162,11 @@ export default function useNotification(
163162

164163
// https://github.com/ant-design/ant-design/issues/52590
165164
// React `startTransition` will run once `useEffect` but many times `setState`,
166-
// So `setTaskQueue` with filtered array will cause infinite loop.
167-
// We cache the first match queue instead.
168-
let oriTaskQueue: Task[];
169-
let tgtTaskQueue: Task[];
170-
171-
// React 17 will mix order of effect & setState in async
172-
// - open: setState[0]
173-
// - effect[0]
174-
// - open: setState[1]
175-
// - effect setState([]) * here will clean up [0, 1] in React 17
165+
// so we only publish a new queue when something was actually removed.
176166
setTaskQueue((oriQueue) => {
177-
if (oriTaskQueue !== oriQueue || !tgtTaskQueue) {
178-
oriTaskQueue = oriQueue;
179-
tgtTaskQueue = oriQueue.filter((task) => !taskQueue.includes(task));
180-
}
167+
const tgtTaskQueue = oriQueue.filter((task) => !taskQueue.includes(task));
181168

182-
return tgtTaskQueue;
169+
return tgtTaskQueue.length === oriQueue.length ? oriQueue : tgtTaskQueue;
183170
});
184171
}
185172
}, [taskQueue]);

tests/hooks.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,43 @@ describe('Notification.Hooks', () => {
180180
expect(document.querySelector('.rc-notification')).toHaveClass('banana');
181181
expect(document.querySelector('.custom-notice')).toHaveClass('apple');
182182
});
183+
184+
it('support root classNames defaults', () => {
185+
const { instance } = renderDemo({
186+
classNames: {
187+
wrapper: 'hook-wrapper',
188+
close: 'hook-close',
189+
},
190+
});
191+
192+
act(() => {
193+
instance.open({
194+
content: <div className="bamboo" />,
195+
duration: 0,
196+
closable: true,
197+
classNames: {
198+
root: 'notice-root',
199+
},
200+
});
201+
});
202+
203+
expect(document.querySelector('.rc-notification-notice-wrapper')).toHaveClass('hook-wrapper');
204+
expect(document.querySelector('.rc-notification-notice')).toHaveClass('notice-root');
205+
expect(document.querySelector('.rc-notification-notice-close')).toHaveClass('hook-close');
206+
});
207+
208+
it('support root placement defaults', () => {
209+
const { instance } = renderDemo({
210+
placement: 'bottomLeft',
211+
});
212+
213+
act(() => {
214+
instance.open({
215+
content: <div className="bamboo" />,
216+
duration: 0,
217+
});
218+
});
219+
220+
expect(document.querySelector('.rc-notification')).toHaveClass('rc-notification-bottomLeft');
221+
});
183222
});

tests/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,12 +923,12 @@ describe('Notification.Basic', () => {
923923
it('show with progress', () => {
924924
const { instance } = renderDemo({
925925
duration: 1,
926-
showProgress: true,
927926
});
928927

929928
act(() => {
930929
instance.open({
931930
content: <p className="test">1</p>,
931+
showProgress: true,
932932
});
933933
});
934934

0 commit comments

Comments
 (0)