Skip to content

Commit ce0498d

Browse files
zombieJclaude
andcommitted
refactor: improve stack position calculation using bottom edge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 15a0a2f commit ce0498d

File tree

2 files changed

+65
-43
lines changed

2 files changed

+65
-43
lines changed

src/hooks/useListPosition/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,25 @@ export default function useListPosition(
1616

1717
const notificationPosition = React.useMemo(() => {
1818
let offsetY = 0;
19+
let offsetBottom = 0;
1920
const nextNotificationPosition = new Map<string, NodePosition>();
2021

2122
configList
2223
.slice()
2324
.reverse()
24-
.forEach((config) => {
25+
.forEach((config, index) => {
2526
const key = String(config.key);
27+
const height = sizeMap[key]?.height ?? 0;
2628
const nodePosition = {
2729
x: 0,
28-
y: offsetY,
30+
y: stack && index > 0 ? offsetBottom + (stack.offset ?? 0) - height : offsetY,
2931
};
3032

3133
nextNotificationPosition.set(key, nodePosition);
32-
offsetY += (stack ? stack.offset : sizeMap[key]?.height) ?? 0;
33-
34-
if (!stack) {
35-
offsetY += gap;
34+
if (stack) {
35+
offsetBottom = nodePosition.y + height;
36+
} else {
37+
offsetY += height + gap;
3638
}
3739
});
3840

tests/stack.test.tsx

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -86,57 +86,77 @@ describe('stack', () => {
8686
expect(document.querySelector('.rc-notification-stack-expanded')).toBeFalsy();
8787
});
8888

89-
it('passes stack offset to list position when collapsed', () => {
90-
const Demo = () => {
91-
const countRef = React.useRef(0);
92-
const [api, holder] = useNotification({
93-
stack: { threshold: 1, offset: 12 },
94-
});
95-
96-
return (
97-
<>
98-
<button
99-
type="button"
100-
onClick={() => {
101-
const index = countRef.current;
102-
countRef.current += 1;
89+
it('passes stack offset to list position by bottom edge when collapsed', () => {
90+
const offsetHeightSpy = vi
91+
.spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
92+
.mockImplementation(function mockOffsetHeight() {
93+
if (this.classList?.contains('rc-notification-notice')) {
94+
if (this.querySelector('.context-content-first')) {
95+
return 80;
96+
}
97+
98+
if (this.querySelector('.context-content-second')) {
99+
return 40;
100+
}
101+
}
103102

104-
api.open({
105-
description: <div className={`context-content-${index}`}>Test {index}</div>,
106-
duration: false,
107-
});
108-
}}
109-
/>
110-
{holder}
111-
</>
112-
);
113-
};
103+
return 0;
104+
});
114105

115-
const { container } = render(<Demo />);
106+
render(
107+
<NotificationList
108+
placement="topRight"
109+
stack={{ threshold: 1, offset: 12 }}
110+
configList={[
111+
{
112+
key: 'first',
113+
description: <div className="context-content-first">First</div>,
114+
duration: false,
115+
},
116+
{
117+
key: 'second',
118+
description: <div className="context-content-second">Second</div>,
119+
duration: false,
120+
},
121+
]}
122+
/>,
123+
);
116124

117-
for (let i = 0; i < 2; i++) {
118-
fireEvent.click(container.querySelector('button'));
119-
}
125+
const firstNotice = document
126+
.querySelector('.context-content-first')
127+
?.closest<HTMLElement>('.rc-notification-notice');
128+
const secondNotice = document
129+
.querySelector('.context-content-second')
130+
?.closest<HTMLElement>('.rc-notification-notice');
120131

121-
const notices = Array.from(document.querySelectorAll<HTMLElement>('.rc-notification-notice'));
122-
const offsetList = notices.map((notice) => notice.style.getPropertyValue('--notification-y'));
132+
const getBottom = (notice: HTMLElement | undefined | null) =>
133+
(notice ? parseFloat(notice.style.getPropertyValue('--notification-y')) : 0) +
134+
(notice?.offsetHeight ?? 0);
123135

124-
expect(notices[0].querySelector('.context-content-0')).toBeTruthy();
125-
expect(notices[1].querySelector('.context-content-1')).toBeTruthy();
126-
expect(offsetList).toEqual(['12px', '0px']);
136+
expect(firstNotice?.style.getPropertyValue('--notification-y')).toBe('-28px');
137+
expect(secondNotice?.style.getPropertyValue('--notification-y')).toBe('0px');
138+
expect(getBottom(firstNotice) - getBottom(secondNotice)).toBe(12);
127139

128140
fireEvent.mouseEnter(document.querySelector('.rc-notification-list'));
129141

130-
expect(
131-
notices.every((notice) => notice.style.getPropertyValue('--notification-y') === '0px'),
132-
).toBeTruthy();
142+
expect(firstNotice?.style.getPropertyValue('--notification-y')).toBe('40px');
143+
expect(secondNotice?.style.getPropertyValue('--notification-y')).toBe('0px');
144+
145+
offsetHeightSpy.mockRestore();
133146
});
134147

135148
it('passes list css gap to list position when expanded', () => {
136149
const offsetHeightSpy = vi
137150
.spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
138151
.mockImplementation(function mockOffsetHeight() {
139-
return this.classList?.contains('rc-notification-notice-wrapper') ? 50 : 0;
152+
if (
153+
this.classList?.contains('rc-notification-notice-wrapper') ||
154+
this.classList?.contains('rc-notification-notice')
155+
) {
156+
return 50;
157+
}
158+
159+
return 0;
140160
});
141161
const originGetComputedStyle = window.getComputedStyle;
142162
const getComputedStyleSpy = vi

0 commit comments

Comments
 (0)