diff --git a/src/NotificationList/Content.tsx b/src/NotificationList/Content.tsx index 700fd2f..7d51a0a 100644 --- a/src/NotificationList/Content.tsx +++ b/src/NotificationList/Content.tsx @@ -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 { listPrefixCls: string; height: number; + topNoticeBounds?: NoticeBounds; } const Content = React.forwardRef((props, ref) => { - const { listPrefixCls, height, className, style, ...restProps } = props; + const { listPrefixCls, height, topNoticeBounds, className, style, ...restProps } = props; const contentPrefixCls = `${listPrefixCls}-content`; @@ -18,15 +20,26 @@ const Content = React.forwardRef((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 (
); diff --git a/src/NotificationList/index.tsx b/src/NotificationList/index.tsx index 08c3d68..00d3b1a 100644 --- a/src/NotificationList/index.tsx +++ b/src/NotificationList/index.tsx @@ -210,12 +210,26 @@ const NotificationList: React.FC = (props) => { // ====================== List Measure ====================== const [gap, setGap] = React.useState(0); const contentRef = React.useRef(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; @@ -259,6 +273,7 @@ const NotificationList: React.FC = (props) => { { + const [notificationPosition, totalHeight, topNoticeBounds] = React.useMemo(() => { let offsetY = 0; let nextTotalHeight = 0; const stackThreshold = stack?.threshold ?? 0; const nextNotificationPosition = new Map(); + let nextTopNoticeBounds: NoticeBounds | undefined; configList .slice() @@ -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); } @@ -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; } diff --git a/tests/index.test.tsx b/tests/index.test.tsx index e45e5d5..4230a4c 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -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();