Skip to content

Commit f06e70d

Browse files
authored
feat: able to use dynamic notification in notifications (#389)
Co-authored-by: Bair Buldaev <qbit982@ya.ru>
1 parent 69b273f commit f06e70d

6 files changed

Lines changed: 85 additions & 39 deletions

File tree

src/components/Notification/Notification.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,7 @@ $notificationSideActionsOffset: $notificationSideActionsWidth + 8px;
182182
}
183183

184184
#{$block}:hover &__actions_side-actions,
185-
#{$block}:focus-within &__actions_side-actions,
186-
&__actions_side-actions:focus-within {
185+
#{$block}:has(:focus-visible) &__actions_side-actions {
187186
opacity: 1;
188187
}
189188

src/components/Notification/Notification.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import './Notification.scss';
1111

1212
const b = block('notification');
1313

14-
type Props = {notification: NotificationProps};
14+
type Props = {
15+
wrapperRef?: React.RefObject<HTMLDivElement>;
16+
notification: NotificationProps;
17+
};
1518

1619
interface ClickableElementProps {
1720
notification: NotificationProps;
@@ -20,9 +23,10 @@ interface ClickableElementProps {
2023
}
2124

2225
export const Notification = React.memo(function Notification(props: Props) {
26+
const ref = React.useRef<HTMLDivElement>(null);
2327
const {t} = i18n.useTranslation();
2428
const mobile = useMobile();
25-
const {notification} = props;
29+
const {wrapperRef, notification} = props;
2630
const {
2731
title,
2832
content,
@@ -58,11 +62,20 @@ export const Notification = React.memo(function Notification(props: Props) {
5862
<div className={b('actions', {'bottom-actions': true})}>{notification.bottomActions}</div>
5963
) : null;
6064

61-
const renderedContent = (
62-
<div className={b('content-wrapper')}>
63-
<div className={b('content')}>{content}</div>
64-
</div>
65-
);
65+
let renderedContent;
66+
if (typeof content === 'function') {
67+
renderedContent = (
68+
<div className={b('content-wrapper')}>
69+
<div className={b('content')}>{content({wrapperRef})}</div>
70+
</div>
71+
);
72+
} else {
73+
renderedContent = (
74+
<div className={b('content-wrapper')}>
75+
<div className={b('content')}>{content}</div>
76+
</div>
77+
);
78+
}
6679

6780
const renderedSourceText =
6881
source?.title || formattedDate ? (
@@ -121,6 +134,7 @@ export const Notification = React.memo(function Notification(props: Props) {
121134

122135
return (
123136
<div
137+
ref={ref}
124138
className={b(layoutModifiers, notification.className)}
125139
onMouseEnter={notification.onMouseEnter}
126140
onMouseLeave={notification.onMouseLeave}

src/components/Notification/NotificationWithSwipe.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ const b = block('notification');
1414
const notificationWrapperCls = b('notification-wrapper');
1515
const swipeActionContainerCls = b('swipe-action-container');
1616

17-
type Props = {notification: NotificationProps; swipeThreshold?: number};
17+
type Props = {
18+
notification: NotificationProps;
19+
swipeThreshold?: number;
20+
wrapperRef?: React.RefObject<HTMLDivElement>;
21+
};
1822

1923
export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(props: Props) {
2024
const swipeThreshold = props.swipeThreshold ?? 0.4;
@@ -24,7 +28,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p
2428
}
2529

2630
const ref = React.useRef<HTMLDivElement>(null);
27-
const notification = props.notification;
31+
const {notification, wrapperRef} = props;
2832
const swipeActions = notification.swipeActions;
2933
const leftAction = swipeActions && 'left' in swipeActions ? swipeActions.left : undefined;
3034
const rightAction = swipeActions && 'right' in swipeActions ? swipeActions.right : undefined;
@@ -132,7 +136,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p
132136
>
133137
{leftAction ? renderAction(leftAction) : null}
134138
<div className={notificationWrapperCls}>
135-
<Notification {...props} />
139+
<Notification notification={notification} wrapperRef={wrapperRef} />
136140
</div>
137141
{rightAction ? renderAction(rightAction) : null}
138142
</div>

src/components/Notification/definitions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export type NotificationSwipeActionsProps =
2424

2525
export type NotificationProps = {
2626
id: string;
27-
content: React.ReactNode;
27+
content:
28+
| React.ReactNode
29+
| ((props: {wrapperRef?: React.RefObject<HTMLDivElement>}) => React.ReactNode);
2830

2931
title?: React.ReactNode;
3032
formattedDate?: React.ReactNode;

src/components/Notifications/NotificationWrapper.tsx

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ export const NotificationWrapper = (props: {
1919

2020
const {notification, swipeThreshold} = props;
2121
const mobile = useMobile();
22-
const [wrapperMaxHeight, setWrapperMaxHeight] = React.useState<number | undefined>(undefined);
2322
const [isRemoved, setIsRemoved] = React.useState(false);
2423

2524
React.useEffect(() => {
26-
if (!ref.current) {
25+
const element = ref.current;
26+
27+
if (!element) {
2728
if (!notification.archived && isRemoved) {
2829
setIsRemoved(false);
2930
}
@@ -34,36 +35,34 @@ export const NotificationWrapper = (props: {
3435
const listener = (event: TransitionEvent) => {
3536
if (event.propertyName === 'max-height') {
3637
setIsRemoved(true);
37-
ref.current?.removeEventListener('transitionend', listener);
38+
element.removeEventListener('transitionend', listener);
3839
}
3940
};
4041

41-
ref.current.addEventListener('transitionend', listener);
42+
element.addEventListener('transitionend', listener);
43+
44+
element.style.maxHeight = `${element.scrollHeight}px`;
45+
element.style.transition = 'max-height 0.3s';
4246

43-
ref.current.style.transition = 'max-height 0.3s';
44-
setWrapperMaxHeight(0);
47+
// Firefox batches style changes made within a single frame, so setting maxHeight
48+
// to scrollHeight and then to 0px in the same frame skips the transition entirely.
49+
// Two nested requestAnimationFrame calls guarantee the browser commits the initial
50+
// maxHeight in one frame before applying 0px in the next, so the animation runs.
51+
requestAnimationFrame(() => {
52+
requestAnimationFrame(() => {
53+
element.style.maxHeight = '0px';
54+
});
55+
});
4556

4657
return () => {
47-
ref.current?.removeEventListener('transitionend', listener);
58+
element.removeEventListener('transitionend', listener);
4859
};
49-
} else {
50-
setIsRemoved(false);
51-
52-
setTimeout(() => {
53-
if (!ref.current) return;
54-
55-
ref.current.style.transition = 'none';
56-
ref.current.style.maxHeight = 'none';
57-
58-
const maxHeight = ref.current?.getBoundingClientRect().height ?? 0;
59-
setWrapperMaxHeight(maxHeight);
60-
}, 0);
61-
62-
return () => {};
6360
}
64-
}, [ref, notification.archived, isRemoved]);
6561

66-
const style = wrapperMaxHeight === undefined ? {} : {maxHeight: `${wrapperMaxHeight}px`};
62+
setIsRemoved(false);
63+
64+
return () => {};
65+
}, [notification.archived, isRemoved]);
6766

6867
if (isRemoved) {
6968
return null;
@@ -78,15 +77,15 @@ export const NotificationWrapper = (props: {
7877
active: Boolean(notification.onClick),
7978
})}
8079
ref={ref}
81-
style={style}
8280
>
8381
{mobile && notification.swipeActions ? (
8482
<NotificationWithSwipe
8583
notification={notification}
8684
swipeThreshold={swipeThreshold}
85+
wrapperRef={ref}
8786
/>
8887
) : (
89-
<Notification notification={notification} />
88+
<Notification notification={notification} wrapperRef={ref} />
9089
)}
9190
</div>
9291
</li>

src/components/Notifications/__stories__/mockData.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22

33
import {Archive, ArrowRotateLeft, CircleCheck, Funnel, TrashBin} from '@gravity-ui/icons';
4-
import {DropdownMenu, Icon, Link} from '@gravity-ui/uikit';
4+
import {Disclosure, DropdownMenu, Flex, Icon, Link} from '@gravity-ui/uikit';
55

66
import {NotificationAction} from '../../Notification/NotificationAction';
77
import {NotificationSwipeAction} from '../../Notification/NotificationSwipeAction';
@@ -89,6 +89,28 @@ export const notificationBottomActions: JSX.Element = (
8989
</React.Fragment>
9090
);
9191

92+
export const LongNotificationContent = (props: {wrapperRef?: React.RefObject<HTMLDivElement>}) => {
93+
const {wrapperRef} = props;
94+
95+
const handleUpdate = (expanded: boolean) => {
96+
if (!expanded) {
97+
requestAnimationFrame(() => {
98+
wrapperRef?.current?.scrollIntoView({block: 'nearest', behavior: 'smooth'});
99+
});
100+
}
101+
};
102+
103+
return (
104+
<Disclosure summary="Collapsed content" onUpdate={handleUpdate}>
105+
<Flex direction="column" gap={1}>
106+
{Array.from({length: 20}, (_, index) => (
107+
<i key={index}>{'Long expanded content. '}</i>
108+
))}
109+
</Flex>
110+
</Disclosure>
111+
);
112+
};
113+
92114
export const mockNotifications: NotificationProps[] = [
93115
{
94116
id: 'tracker',
@@ -153,6 +175,12 @@ export const mockNotifications: NotificationProps[] = [
153175
swipeActions: notificationsMockSwipeActions,
154176
href: 'https://ya.ru',
155177
},
178+
{
179+
id: 'looooong-content',
180+
content: (contentProps) => <LongNotificationContent {...contentProps} />,
181+
formattedDate: '29 seconds ago',
182+
swipeActions: notificationsMockSwipeActions,
183+
},
156184
{
157185
id: 'yandex',
158186
content: (

0 commit comments

Comments
 (0)