Skip to content

Commit bcba5d0

Browse files
zombieJclaude
andcommitted
refactor: add scale and clip-path effects for stack notifications
- Add notificationIndex and stackInThreshold props to Notification component - Implement CSS scale transformation based on notification index - Add clip-path animation for stacked notifications - Update tests to verify notification index attributes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce0498d commit bcba5d0

File tree

4 files changed

+73
-18
lines changed

4 files changed

+73
-18
lines changed

assets/geek.less

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@notificationPrefixCls: notification;
22
@notificationMotionDuration: 0.3s;
3+
// @notificationMotionDuration: 10s;
34
@notificationMotionEase: cubic-bezier(0.22, 1, 0.36, 1);
45
@notificationMotionOffset: 64px;
56

@@ -42,10 +43,13 @@
4243
// transform: translate3d(var(--notification-x, 0), var(--notification-y, 0), 0);
4344
right: var(--notification-x, 0);
4445
top: var(--notification-y, 0);
46+
transform: scale(var(--notification-scale, 1));
4547
transition:
4648
transform @notificationMotionDuration @notificationMotionEase,
4749
inset @notificationMotionDuration @notificationMotionEase,
50+
clip-path @notificationMotionDuration @notificationMotionEase,
4851
opacity @notificationMotionDuration @notificationMotionEase;
52+
transform-origin: center bottom;
4953
padding: 14px 16px;
5054
border: 2px solid #111;
5155
border-radius: 14px;
@@ -89,8 +93,21 @@
8993
}
9094

9195
&-stack {
96+
.@{notificationPrefixCls}-notice {
97+
clip-path: inset(-50% -50% -50% -50%);
98+
}
99+
92100
&:not(.@{notificationPrefixCls}-stack-expanded) {
93-
.@{notificationPrefixCls}-list-item:nth-last-child(n + 4) {
101+
.@{notificationPrefixCls}-notice {
102+
--notification-scale: ~'calc(1 - min(var(--notification-index, 0), 2) * 0.06)';
103+
clip-path: inset(50% -50% -50% -50%);
104+
}
105+
106+
.@{notificationPrefixCls}-notice[data-notification-index='0'] {
107+
clip-path: inset(-50% -50% -50% -50%);
108+
}
109+
110+
.@{notificationPrefixCls}-notice:not(.@{notificationPrefixCls}-notice-stack-in-threshold) {
94111
opacity: 0;
95112
pointer-events: none;
96113
}
@@ -106,28 +123,28 @@
106123
.notification-fade-appear-prepare,
107124
.notification-fade-enter-prepare {
108125
opacity: 0;
109-
transform: translateX(@notificationMotionOffset);
126+
transform: translateX(@notificationMotionOffset) scale(var(--notification-scale, 1));
110127
transition: none;
111128
}
112129

113130
.notification-fade-appear-start,
114131
.notification-fade-enter-start {
115132
opacity: 0;
116-
transform: translateX(@notificationMotionOffset);
133+
transform: translateX(@notificationMotionOffset) scale(var(--notification-scale, 1));
117134
}
118135

119136
.notification-fade-appear-active,
120137
.notification-fade-enter-active {
121138
opacity: 1;
122-
transform: translateX(0);
139+
transform: translateX(0) scale(var(--notification-scale, 1));
123140
}
124141

125142
.notification-fade-leave-start {
126143
opacity: 1;
127-
transform: translateX(0);
144+
transform: translateX(0) scale(var(--notification-scale, 1));
128145
}
129146

130147
.notification-fade-leave-active {
131148
opacity: 0;
132-
transform: translateX(@notificationMotionOffset);
149+
transform: translateX(@notificationMotionOffset) scale(var(--notification-scale, 1));
133150
}

src/Notification.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export interface NotificationProps {
4747
x: number;
4848
y: number;
4949
};
50+
notificationIndex?: number;
51+
stackInThreshold?: boolean;
5052
props?: React.HTMLAttributes<HTMLDivElement> & Record<string, any>;
5153

5254
// Behavior
@@ -81,6 +83,8 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
8183
actions,
8284
closable,
8385
offset,
86+
notificationIndex,
87+
stackInThreshold,
8488
props: rootProps,
8589

8690
// Behavior
@@ -156,6 +160,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
156160
}
157161

158162
const mergedOffset = offset ?? offsetRef.current;
163+
const mergedNotificationIndex = notificationIndex ?? 0;
159164

160165
// ======================== Content =========================
161166
const titleNode =
@@ -199,24 +204,32 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
199204
}
200205

201206
// ========================= Render =========================
207+
const mergedStyle: React.CSSProperties & {
208+
'--notification-index'?: number;
209+
'--notification-x'?: string;
210+
'--notification-y'?: string;
211+
} = {
212+
'--notification-index': mergedNotificationIndex,
213+
...styles?.root,
214+
...style,
215+
};
216+
217+
if (mergedOffset) {
218+
mergedStyle['--notification-x'] = `${mergedOffset.x}px`;
219+
mergedStyle['--notification-y'] = `${mergedOffset.y}px`;
220+
}
221+
202222
return (
203223
<div
204224
{...rootProps}
205225
ref={ref}
226+
data-notification-index={mergedNotificationIndex}
206227
// Styles
207228
className={clsx(noticePrefixCls, className, classNames?.root, {
208229
[`${noticePrefixCls}-closable`]: mergedClosable,
230+
[`${noticePrefixCls}-stack-in-threshold`]: stackInThreshold,
209231
})}
210-
style={{
211-
...styles?.root,
212-
...(mergedOffset
213-
? {
214-
'--notification-x': `${mergedOffset.x}px`,
215-
'--notification-y': `${mergedOffset.y}px`,
216-
}
217-
: null),
218-
...style,
219-
}}
232+
style={mergedStyle}
220233
// Events
221234
onClick={onClick}
222235
onMouseEnter={onInternalMouseEnter}

src/NotificationList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,11 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
152152
}
153153
}}
154154
>
155-
{({ config, className: motionClassName, style: motionStyle }, nodeRef) => {
156-
const { key, placement: _placement, ...notificationConfig } = config;
155+
{({ config, className: motionClassName, style: motionStyle, index = 0 }, nodeRef) => {
156+
const { key, placement: itemPlacement, ...notificationConfig } = config;
157157
const strKey = String(key);
158+
const notificationIndex = keyList.length - index - 1;
159+
const stackInThreshold = stackEnabled && notificationIndex < threshold;
158160

159161
const setItemRef = (node: HTMLDivElement | null) => {
160162
setNodeSize(strKey, node);
@@ -167,6 +169,8 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
167169
ref={composeRef(nodeRef, setItemRef)}
168170
prefixCls={prefixCls}
169171
offset={notificationPosition.get(strKey)}
172+
notificationIndex={notificationIndex}
173+
stackInThreshold={stackInThreshold}
170174
className={clsx(contextClassNames?.notice, config.className)}
171175
style={config.style}
172176
classNames={{

tests/stack.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ describe('stack', () => {
4141
expect(document.querySelectorAll('.rc-notification-notice')).toHaveLength(5);
4242
expect(document.querySelector('.rc-notification-stack-expanded')).toBeFalsy();
4343

44+
const notices = Array.from(document.querySelectorAll<HTMLElement>('.rc-notification-notice'));
45+
expect(notices.map((notice) => notice.getAttribute('data-notification-index'))).toEqual([
46+
'4',
47+
'3',
48+
'2',
49+
'1',
50+
'0',
51+
]);
52+
expect(
53+
notices
54+
.slice(0, 2)
55+
.every((notice) => !notice.matches('.rc-notification-notice-stack-in-threshold')),
56+
).toBeTruthy();
57+
expect(
58+
notices
59+
.slice(2)
60+
.every((notice) => notice.matches('.rc-notification-notice-stack-in-threshold')),
61+
).toBeTruthy();
62+
4463
fireEvent.mouseEnter(document.querySelector('.rc-notification-list'));
4564
expect(document.querySelector('.rc-notification-stack-expanded')).toBeTruthy();
4665
});
@@ -203,6 +222,8 @@ describe('stack', () => {
203222
.querySelector('.context-content-second')
204223
?.closest<HTMLElement>('.rc-notification-notice');
205224

225+
expect(firstNotice?.getAttribute('data-notification-index')).toBe('1');
226+
expect(secondNotice?.getAttribute('data-notification-index')).toBe('0');
206227
expect(firstNotice?.style.getPropertyValue('--notification-y')).toBe('58px');
207228
expect(secondNotice?.style.getPropertyValue('--notification-y')).toBe('0px');
208229

0 commit comments

Comments
 (0)