Skip to content

Commit 3ac4947

Browse files
committed
refactor: extract notification list scroll hooks
1 parent f95d633 commit 3ac4947

7 files changed

Lines changed: 255 additions & 158 deletions

File tree

assets/geek.less

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
box-sizing: border-box;
1616
pointer-events: none;
1717
overflow: hidden;
18+
overscroll-behavior: contain;
1819
}
1920

2021
&-list-content {
@@ -32,18 +33,14 @@
3233
left: 0;
3334
box-sizing: border-box;
3435
width: 100%;
35-
transform: translate3d(var(--notification-x, 0), var(--notification-y, 0), 0);
36-
transition: transform @notificationMotionDuration @notificationMotionEase;
37-
}
38-
39-
&-list-item-motion {
40-
width: 100%;
4136
}
4237

4338
&-notice {
4439
pointer-events: auto;
4540
box-sizing: border-box;
4641
width: 100%;
42+
transform: translate3d(var(--notification-x, 0), var(--notification-y, 0), 0);
43+
transition: transform @notificationMotionDuration @notificationMotionEase;
4744
padding: 14px 16px;
4845
border: 2px solid #111;
4946
border-radius: 14px;

docs/examples/NotificationList.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ const Demo = () => {
3737
setConfigList((prevConfigList) => prevConfigList.slice(0, -1));
3838
}, []);
3939

40+
const removeFirstConfig = React.useCallback(() => {
41+
setConfigList((prevConfigList) => prevConfigList.slice(1));
42+
}, []);
43+
44+
const removeMiddleConfig = React.useCallback(() => {
45+
setConfigList((prevConfigList) => {
46+
if (!prevConfigList.length) {
47+
return prevConfigList;
48+
}
49+
50+
const middleIndex = Math.floor(prevConfigList.length / 2);
51+
52+
return prevConfigList.filter((_, index) => index !== middleIndex);
53+
});
54+
}, []);
55+
4056
return (
4157
<>
4258
<div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
@@ -46,6 +62,12 @@ const Demo = () => {
4662
<button type="button" onClick={removeLastConfig}>
4763
Remove Last Config
4864
</button>
65+
<button type="button" onClick={removeFirstConfig}>
66+
Remove First Config
67+
</button>
68+
<button type="button" onClick={removeMiddleConfig}>
69+
Remove Middle Config
70+
</button>
4971
</div>
5072

5173
<NotificationList

src/Notification.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface NotificationProps {
1818
actions?: React.ReactNode;
1919
close?: React.ReactNode;
2020
duration?: number | false;
21+
offset?: {
22+
x: number;
23+
y: number;
24+
};
2125
pauseOnHover?: boolean;
2226
className?: string;
2327
style?: React.CSSProperties;
@@ -34,6 +38,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
3438
actions,
3539
close,
3640
duration = 4.5,
41+
offset,
3742
pauseOnHover = true,
3843
className,
3944
style,
@@ -45,17 +50,30 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
4550

4651
// ========================= Close ==========================
4752
const onEventClose = useEvent(onClose);
53+
const offsetRef = React.useRef(offset);
54+
55+
if (offset) {
56+
offsetRef.current = offset;
57+
}
4858

4959
// ======================== Duration ========================
5060
const [onResume, onPause] = useNoticeTimer(duration, onEventClose, () => {});
5161

62+
const mergedOffset = offset ?? offsetRef.current;
63+
5264
// ========================= Render =========================
5365
return (
5466
<div
5567
ref={ref}
5668
className={clsx(className, classNames?.root)}
5769
style={{
5870
...styles?.root,
71+
...(mergedOffset
72+
? {
73+
'--notification-x': `${mergedOffset.x}px`,
74+
'--notification-y': `${mergedOffset.y}px`,
75+
}
76+
: null),
5977
...style,
6078
}}
6179
onClick={onClick}

src/NotificationList.tsx

Lines changed: 28 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Notification, {
88
type NotificationStyles,
99
} from './Notification';
1010
import useListPosition from './hooks/useListPosition';
11+
import useListScroll from './hooks/useListScroll';
1112

1213
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
1314

@@ -32,14 +33,9 @@ export interface NotificationListProps {
3233
classNames?: NotificationClassNames;
3334
styles?: NotificationStyles;
3435
stack?: StackConfig;
35-
maxCount?: number;
3636
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
3737
}
3838

39-
function clampScrollOffset(offset: number, maxScroll: number) {
40-
return Math.min(0, Math.max(-maxScroll, offset));
41-
}
42-
4339
function assignRef<T>(ref: React.Ref<T>, value: T | null) {
4440
if (typeof ref === 'function') {
4541
ref(value);
@@ -48,36 +44,19 @@ function assignRef<T>(ref: React.Ref<T>, value: T | null) {
4844
}
4945
}
5046

51-
function getNoticeStyle(nodePosition?: { x: number; y: number }): React.CSSProperties | undefined {
52-
if (!nodePosition) {
53-
return undefined;
54-
}
55-
56-
return {
57-
'--notification-x': `${nodePosition.x}px`,
58-
'--notification-y': `${nodePosition.y}px`,
59-
} as React.CSSProperties;
60-
}
61-
6247
const NotificationList: React.FC<NotificationListProps> = (props) => {
6348
const {
6449
configList = [],
6550
prefixCls = 'rc-notification',
6651
pauseOnHover,
6752
classNames,
6853
styles,
69-
maxCount,
7054
motion,
7155
placement,
7256
} = props;
7357

7458
// ========================== Data ==========================
75-
const mergedConfigList = React.useMemo(() => {
76-
const list =
77-
typeof maxCount === 'number' && maxCount > 0 ? configList.slice(-maxCount) : configList;
78-
79-
return list.slice().reverse();
80-
}, [configList, maxCount]);
59+
const mergedConfigList = React.useMemo(() => configList.slice().reverse(), [configList]);
8160

8261
const keys = React.useMemo(
8362
() =>
@@ -92,107 +71,15 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
9271
// ========================= Motion =========================
9372
const placementMotion = typeof motion === 'function' ? motion(placement) : motion;
9473

95-
// ========================= Scroll =========================
96-
const viewportRef = React.useRef<HTMLDivElement>(null);
97-
const contentRef = React.useRef<HTMLDivElement>(null);
98-
const prevKeyListRef = React.useRef<string[]>(keyList);
99-
const prevNotificationPositionRef = React.useRef<Map<string, { x: number; y: number }>>(
100-
new Map(),
101-
);
102-
const notificationPositionCacheRef = React.useRef<Map<string, { x: number; y: number }>>(
103-
new Map(),
104-
);
105-
const scrollOffsetRef = React.useRef(0);
106-
const [scrollOffset, setScrollOffset] = React.useState(0);
10774
const [notificationPosition, setNodeSize] = useListPosition(mergedConfigList);
108-
109-
const syncScrollOffset = React.useCallback((nextOffset: number) => {
110-
const viewportHeight = viewportRef.current?.clientHeight ?? 0;
111-
const measuredContentHeight = contentRef.current?.scrollHeight ?? 0;
112-
const maxScroll = Math.max(measuredContentHeight - viewportHeight, 0);
113-
const mergedOffset = clampScrollOffset(nextOffset, maxScroll);
114-
115-
scrollOffsetRef.current = mergedOffset;
116-
setScrollOffset((prev) => (prev === mergedOffset ? prev : mergedOffset));
117-
}, []);
118-
119-
React.useLayoutEffect(() => {
120-
notificationPosition.forEach((position, key) => {
121-
notificationPositionCacheRef.current.set(key, position);
122-
});
123-
}, [notificationPosition]);
124-
125-
React.useLayoutEffect(() => {
126-
const prevKeyList = prevKeyListRef.current;
127-
const prevNotificationPosition = prevNotificationPositionRef.current;
128-
129-
if (scrollOffsetRef.current < 0) {
130-
const prependCount = prevKeyList.length
131-
? keyList.findIndex((key) => key === prevKeyList[0])
132-
: -1;
133-
const removedCount = keyList.length ? prevKeyList.findIndex((key) => key === keyList[0]) : -1;
134-
135-
if (prependCount > 0) {
136-
const prependHeight = notificationPosition.get(prevKeyList[0])?.y ?? 0;
137-
syncScrollOffset(scrollOffsetRef.current - prependHeight);
138-
} else if (removedCount > 0) {
139-
const removedHeight = keyList[0] ? (prevNotificationPosition.get(keyList[0])?.y ?? 0) : 0;
140-
syncScrollOffset(scrollOffsetRef.current + removedHeight);
141-
} else {
142-
syncScrollOffset(scrollOffsetRef.current);
143-
}
144-
} else {
145-
syncScrollOffset(scrollOffsetRef.current);
146-
}
147-
148-
prevKeyListRef.current = keyList;
149-
prevNotificationPositionRef.current = new Map(notificationPosition);
150-
}, [keyList, notificationPosition, syncScrollOffset]);
151-
152-
React.useLayoutEffect(() => {
153-
const viewportNode = viewportRef.current;
154-
const contentNode = contentRef.current;
155-
156-
if (!viewportNode || !contentNode || typeof ResizeObserver === 'undefined') {
157-
return;
158-
}
159-
160-
const resizeObserver = new ResizeObserver(() => {
161-
syncScrollOffset(scrollOffsetRef.current);
162-
});
163-
164-
resizeObserver.observe(viewportNode);
165-
resizeObserver.observe(contentNode);
166-
167-
return () => {
168-
resizeObserver.disconnect();
169-
};
170-
}, [syncScrollOffset]);
171-
172-
const onWheel = React.useCallback(
173-
(event: React.WheelEvent<HTMLDivElement>) => {
174-
const viewportHeight = viewportRef.current?.clientHeight ?? 0;
175-
const measuredContentHeight = contentRef.current?.scrollHeight ?? 0;
176-
const maxScroll = Math.max(measuredContentHeight - viewportHeight, 0);
177-
178-
if (!maxScroll) {
179-
return;
180-
}
181-
182-
const nextOffset = clampScrollOffset(scrollOffsetRef.current - event.deltaY, maxScroll);
183-
184-
if (nextOffset !== scrollOffsetRef.current) {
185-
event.preventDefault();
186-
syncScrollOffset(nextOffset);
187-
}
188-
},
189-
[syncScrollOffset],
75+
const { contentRef, onWheel, scrollOffset, viewportRef } = useListScroll(
76+
keyList,
77+
notificationPosition,
19078
);
19179

19280
// ========================= Render =========================
19381
const listPrefixCls = `${prefixCls}-list`;
19482
const itemPrefixCls = `${listPrefixCls}-item`;
195-
const motionPrefixCls = `${itemPrefixCls}-motion`;
19683

19784
return (
19885
<div
@@ -215,45 +102,34 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
215102
return (
216103
<div
217104
key={key}
218-
className={itemPrefixCls}
105+
className={clsx(itemPrefixCls, className)}
219106
ref={(node) => {
107+
assignRef(nodeRef, node);
220108
setNodeSize(strKey, node);
221109
}}
222-
style={{
223-
...getNoticeStyle(
224-
notificationPosition.get(strKey) ??
225-
notificationPositionCacheRef.current.get(strKey),
226-
),
227-
}}
110+
style={style}
228111
>
229-
<div
230-
ref={(node) => {
231-
assignRef(nodeRef, node);
112+
<Notification
113+
{...notificationConfig}
114+
offset={notificationPosition.get(strKey)}
115+
className={config.className}
116+
style={config.style}
117+
classNames={{
118+
root: clsx(classNames?.root, config.classNames?.root),
119+
close: clsx(classNames?.close, config.classNames?.close),
120+
}}
121+
styles={{
122+
root: {
123+
...styles?.root,
124+
...config.styles?.root,
125+
},
126+
close: {
127+
...styles?.close,
128+
...config.styles?.close,
129+
},
232130
}}
233-
className={clsx(motionPrefixCls, className)}
234-
style={style}
235-
>
236-
<Notification
237-
{...notificationConfig}
238-
className={config.className}
239-
style={config.style}
240-
classNames={{
241-
root: clsx(classNames?.root, config.classNames?.root),
242-
close: clsx(classNames?.close, config.classNames?.close),
243-
}}
244-
styles={{
245-
root: {
246-
...styles?.root,
247-
...config.styles?.root,
248-
},
249-
close: {
250-
...styles?.close,
251-
...config.styles?.close,
252-
},
253-
}}
254-
pauseOnHover={config.pauseOnHover ?? pauseOnHover}
255-
/>
256-
</div>
131+
pauseOnHover={config.pauseOnHover ?? pauseOnHover}
132+
/>
257133
</div>
258134
);
259135
}}

src/hooks/useListPosition.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react';
2+
import useSizes from './useSizes';
3+
4+
export type NodePosition = {
5+
x: number;
6+
y: number;
7+
};
8+
9+
export default function useListPosition(configList: { key: React.Key }[]) {
10+
const [sizeMap, setNodeSize] = useSizes();
11+
12+
const notificationPosition = React.useMemo(() => {
13+
let offsetY = 0;
14+
const nextNotificationPosition = new Map<string, NodePosition>();
15+
16+
configList.forEach((config) => {
17+
const key = String(config.key);
18+
const nodePosition = {
19+
x: 0,
20+
y: offsetY,
21+
};
22+
23+
nextNotificationPosition.set(key, nodePosition);
24+
offsetY += sizeMap[key]?.height ?? 0;
25+
});
26+
27+
return nextNotificationPosition;
28+
}, [configList, sizeMap]);
29+
30+
return [notificationPosition, setNodeSize] as const;
31+
}

0 commit comments

Comments
 (0)