Skip to content

Commit f29b125

Browse files
zombieJclaude
andcommitted
feat: support CSS gap for notification list spacing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 91f176f commit f29b125

4 files changed

Lines changed: 85 additions & 3 deletions

File tree

assets/geek.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
position: relative;
2323
display: flex;
2424
flex-direction: column;
25+
gap: 8px;
2526
width: 100%;
2627
pointer-events: none;
2728
will-change: transform;

src/NotificationList.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,26 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
9292
};
9393
}, [expanded, offset, stackEnabled, threshold]);
9494

95-
const [notificationPosition, setNodeSize] = useListPosition(configList, stackPosition);
95+
const [gap, setGap] = React.useState(0);
96+
const [notificationPosition, setNodeSize] = useListPosition(configList, stackPosition, gap);
9697
const { contentRef, onWheel, scrollOffset, viewportRef } = useListScroll(
9798
keyList,
9899
notificationPosition,
99100
);
100101

102+
React.useEffect(() => {
103+
const listNode = contentRef.current;
104+
105+
if (!listNode) {
106+
return;
107+
}
108+
109+
const { gap: cssGap, rowGap } = window.getComputedStyle(listNode);
110+
const nextGap = parseFloat(rowGap || cssGap) || 0;
111+
112+
setGap((prevGap) => (prevGap === nextGap ? prevGap : nextGap));
113+
}, [!!configList.length]);
114+
101115
// ========================= Render =========================
102116
const listPrefixCls = `${prefixCls}-list`;
103117
const itemPrefixCls = `${listPrefixCls}-item`;

src/hooks/useListPosition/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ export type NodePosition = {
77
y: number;
88
};
99

10-
export default function useListPosition(configList: { key: React.Key }[], stack?: StackConfig) {
10+
export default function useListPosition(
11+
configList: { key: React.Key }[],
12+
stack?: StackConfig,
13+
gap = 0,
14+
) {
1115
const [sizeMap, setNodeSize] = useSizes();
1216

1317
const notificationPosition = React.useMemo(() => {
@@ -26,10 +30,14 @@ export default function useListPosition(configList: { key: React.Key }[], stack?
2630

2731
nextNotificationPosition.set(key, nodePosition);
2832
offsetY += (stack ? stack.offset : sizeMap[key]?.height) ?? 0;
33+
34+
if (!stack) {
35+
offsetY += gap;
36+
}
2937
});
3038

3139
return nextNotificationPosition;
32-
}, [configList, sizeMap, stack]);
40+
}, [configList, gap, sizeMap, stack]);
3341

3442
return [notificationPosition, setNodeSize] as const;
3543
}

tests/stack.test.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useNotification } from '../src';
22
import { fireEvent, render } from '@testing-library/react';
33
import React from 'react';
4+
import NotificationList from '../src/NotificationList';
45

56
require('../assets/index.less');
67

@@ -130,4 +131,62 @@ describe('stack', () => {
130131
notices.every((notice) => notice.style.getPropertyValue('--notification-y') === '0px'),
131132
).toBeTruthy();
132133
});
134+
135+
it('passes list css gap to list position when expanded', () => {
136+
const offsetHeightSpy = vi
137+
.spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
138+
.mockImplementation(function mockOffsetHeight() {
139+
return this.classList?.contains('rc-notification-notice-wrapper') ? 50 : 0;
140+
});
141+
const originGetComputedStyle = window.getComputedStyle;
142+
const getComputedStyleSpy = vi
143+
.spyOn(window, 'getComputedStyle')
144+
.mockImplementation((element) => {
145+
const style = originGetComputedStyle(element);
146+
147+
if ((element as HTMLElement).classList?.contains('rc-notification-list-content')) {
148+
return new Proxy(style, {
149+
get(target, prop, receiver) {
150+
if (prop === 'gap' || prop === 'rowGap') {
151+
return '8px';
152+
}
153+
154+
return Reflect.get(target, prop, receiver);
155+
},
156+
}) as CSSStyleDeclaration;
157+
}
158+
159+
return style;
160+
});
161+
162+
render(
163+
<NotificationList
164+
configList={[
165+
{
166+
key: 'first',
167+
content: <div className="context-content-first">First</div>,
168+
duration: false,
169+
},
170+
{
171+
key: 'second',
172+
content: <div className="context-content-second">Second</div>,
173+
duration: false,
174+
},
175+
]}
176+
/>,
177+
);
178+
179+
const firstNotice = document
180+
.querySelector('.context-content-first')
181+
?.closest<HTMLElement>('.rc-notification-notice');
182+
const secondNotice = document
183+
.querySelector('.context-content-second')
184+
?.closest<HTMLElement>('.rc-notification-notice');
185+
186+
expect(firstNotice?.style.getPropertyValue('--notification-y')).toBe('58px');
187+
expect(secondNotice?.style.getPropertyValue('--notification-y')).toBe('0px');
188+
189+
getComputedStyleSpy.mockRestore();
190+
offsetHeightSpy.mockRestore();
191+
});
133192
});

0 commit comments

Comments
 (0)