Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/NotificationList/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { clsx } from 'clsx';
import * as React from 'react';
import type { NoticeBounds } from '../hooks/useListPosition';

export interface ContentProps extends React.HTMLAttributes<HTMLDivElement> {
listPrefixCls: string;
height: number;
topNoticeBounds?: NoticeBounds;
}

const Content = React.forwardRef<HTMLDivElement, ContentProps>((props, ref) => {
const { listPrefixCls, height, className, style, ...restProps } = props;
const { listPrefixCls, height, topNoticeBounds, className, style, ...restProps } = props;

const contentPrefixCls = `${listPrefixCls}-content`;

Expand All @@ -18,15 +20,26 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>((props, ref) => {

prevHeightRef.current = height;

// ========================= Style ==========================
const contentStyle: React.CSSProperties & {
'--notification-top'?: string;
'--notification-bottom'?: string;
} = {
...style,
height,
};

if (topNoticeBounds) {
contentStyle['--notification-top'] = `${topNoticeBounds.top}px`;
contentStyle['--notification-bottom'] = `${topNoticeBounds.bottom}px`;
}

// ========================= Render =========================
return (
<div
{...restProps}
className={clsx(contentPrefixCls, `${contentPrefixCls}-${heightStatus}`, className)}
style={{
...style,
height,
}}
style={contentStyle}
ref={ref}
/>
);
Expand Down
17 changes: 16 additions & 1 deletion src/NotificationList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,26 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
// ====================== List Measure ======================
const [gap, setGap] = React.useState(0);
const contentRef = React.useRef<HTMLDivElement>(null);
const [notificationPosition, setNodeSize, totalHeight] = useListPosition(
const [notificationPosition, setNodeSize, totalHeight, topNoticeBounds] = useListPosition(
configList,
stackPosition,
gap,
);
const hasConfigList = !!configList.length;
const topNoticeContentBounds = React.useMemo(() => {
if (!topNoticeBounds) {
return undefined;
}

if (placement.startsWith('bottom')) {
return {
top: totalHeight - topNoticeBounds.bottom,
bottom: totalHeight - topNoticeBounds.top,
};
}

return topNoticeBounds;
}, [placement, topNoticeBounds, totalHeight]);

React.useEffect(() => {
const listNode = contentRef.current;
Expand Down Expand Up @@ -259,6 +273,7 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
<Content
listPrefixCls={listPrefixCls}
height={totalHeight}
topNoticeBounds={topNoticeContentBounds}
className={classNames?.listContent}
style={styles?.listContent}
ref={contentRef}
Expand Down
19 changes: 16 additions & 3 deletions src/hooks/useListPosition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import * as React from 'react';
import type { StackConfig } from '../useStack';
import useSizes from './useSizes';

export interface NoticeBounds {
top: number;
bottom: number;
}

/**
* Calculates each notification's position and the full list height.
*/
Expand All @@ -12,11 +17,12 @@ export default function useListPosition(
) {
const [sizeMap, setNodeSize] = useSizes();

const [notificationPosition, totalHeight] = React.useMemo(() => {
const [notificationPosition, totalHeight, topNoticeBounds] = React.useMemo(() => {
let offsetY = 0;
let nextTotalHeight = 0;
const stackThreshold = stack?.threshold ?? 0;
const nextNotificationPosition = new Map<string, number>();
let nextTopNoticeBounds: NoticeBounds | undefined;

configList
.slice()
Expand All @@ -29,6 +35,13 @@ export default function useListPosition(

nextNotificationPosition.set(key, y);

if (index === 0) {
nextTopNoticeBounds = {
top: y,
bottom: y + height,
};
}

if (!stack || index < stackThreshold) {
nextTotalHeight = Math.max(nextTotalHeight, y + height);
}
Expand All @@ -40,8 +53,8 @@ export default function useListPosition(
}
});

return [nextNotificationPosition, nextTotalHeight] as const;
return [nextNotificationPosition, nextTotalHeight, nextTopNoticeBounds] as const;
}, [configList, gap, sizeMap, stack]);

return [notificationPosition, setNodeSize, totalHeight] as const;
return [notificationPosition, setNodeSize, totalHeight, topNoticeBounds] as const;
}
32 changes: 32 additions & 0 deletions tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,38 @@ describe('Notification.Basic', () => {
expect(document.querySelector('.rc-notification-list-content')).toHaveClass('bamboo');
});

it('should expose top notice bounds on listContent', () => {
const offsetHeightSpy = vi
.spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
.mockImplementation(function offsetHeight(this: HTMLElement) {
if (this.classList.contains('rc-notification-notice')) {
return this.textContent === 'second' ? 18 : 10;
}

return 0;
});

const { instance } = renderDemo();

act(() => {
instance.open({
description: 'first',
duration: false,
});
instance.open({
description: 'second',
duration: false,
});
});

expect(document.querySelector('.rc-notification-list-content')).toHaveStyle({
'--notification-top': '0px',
'--notification-bottom': '18px',
});

offsetHeightSpy.mockRestore();
});

it('placement', () => {
const { instance } = renderDemo();

Expand Down
Loading