Skip to content
Merged
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
3 changes: 1 addition & 2 deletions src/components/Notification/Notification.scss
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,7 @@ $notificationSideActionsOffset: $notificationSideActionsWidth + 8px;
}

#{$block}:hover &__actions_side-actions,
#{$block}:focus-within &__actions_side-actions,
&__actions_side-actions:focus-within {
#{$block}:has(:focus-visible) &__actions_side-actions {
opacity: 1;
}

Expand Down
28 changes: 21 additions & 7 deletions src/components/Notification/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import './Notification.scss';

const b = block('notification');

type Props = {notification: NotificationProps};
type Props = {
wrapperRef?: React.RefObject<HTMLDivElement>;
notification: NotificationProps;
};

interface ClickableElementProps {
notification: NotificationProps;
Expand All @@ -20,9 +23,10 @@ interface ClickableElementProps {
}

export const Notification = React.memo(function Notification(props: Props) {
const ref = React.useRef<HTMLDivElement>(null);
const {t} = i18n.useTranslation();
const mobile = useMobile();
const {notification} = props;
const {wrapperRef, notification} = props;
const {
title,
content,
Expand Down Expand Up @@ -58,11 +62,20 @@ export const Notification = React.memo(function Notification(props: Props) {
<div className={b('actions', {'bottom-actions': true})}>{notification.bottomActions}</div>
) : null;

const renderedContent = (
<div className={b('content-wrapper')}>
<div className={b('content')}>{content}</div>
</div>
);
let renderedContent;
if (typeof content === 'function') {
renderedContent = (
<div className={b('content-wrapper')}>
<div className={b('content')}>{content({wrapperRef})}</div>
</div>
);
} else {
renderedContent = (
<div className={b('content-wrapper')}>
<div className={b('content')}>{content}</div>
</div>
);
}

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

return (
<div
ref={ref}
className={b(layoutModifiers, notification.className)}
onMouseEnter={notification.onMouseEnter}
onMouseLeave={notification.onMouseLeave}
Expand Down
10 changes: 7 additions & 3 deletions src/components/Notification/NotificationWithSwipe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ const b = block('notification');
const notificationWrapperCls = b('notification-wrapper');
const swipeActionContainerCls = b('swipe-action-container');

type Props = {notification: NotificationProps; swipeThreshold?: number};
type Props = {
notification: NotificationProps;
swipeThreshold?: number;
wrapperRef?: React.RefObject<HTMLDivElement>;
};

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

const ref = React.useRef<HTMLDivElement>(null);
const notification = props.notification;
const {notification, wrapperRef} = props;
const swipeActions = notification.swipeActions;
const leftAction = swipeActions && 'left' in swipeActions ? swipeActions.left : undefined;
const rightAction = swipeActions && 'right' in swipeActions ? swipeActions.right : undefined;
Expand Down Expand Up @@ -132,7 +136,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p
>
{leftAction ? renderAction(leftAction) : null}
<div className={notificationWrapperCls}>
<Notification {...props} />
<Notification notification={notification} wrapperRef={wrapperRef} />
</div>
{rightAction ? renderAction(rightAction) : null}
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Notification/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export type NotificationSwipeActionsProps =

export type NotificationProps = {
id: string;
content: React.ReactNode;
content:
| React.ReactNode
| ((props: {wrapperRef?: React.RefObject<HTMLDivElement>}) => React.ReactNode);

title?: React.ReactNode;
formattedDate?: React.ReactNode;
Expand Down
49 changes: 24 additions & 25 deletions src/components/Notifications/NotificationWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ export const NotificationWrapper = (props: {

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

React.useEffect(() => {
if (!ref.current) {
const element = ref.current;

if (!element) {
if (!notification.archived && isRemoved) {
setIsRemoved(false);
}
Expand All @@ -34,36 +35,34 @@ export const NotificationWrapper = (props: {
const listener = (event: TransitionEvent) => {
if (event.propertyName === 'max-height') {
setIsRemoved(true);
ref.current?.removeEventListener('transitionend', listener);
element.removeEventListener('transitionend', listener);
}
};

ref.current.addEventListener('transitionend', listener);
element.addEventListener('transitionend', listener);

element.style.maxHeight = `${element.scrollHeight}px`;
element.style.transition = 'max-height 0.3s';

ref.current.style.transition = 'max-height 0.3s';
setWrapperMaxHeight(0);
// Firefox batches style changes made within a single frame, so setting maxHeight
// to scrollHeight and then to 0px in the same frame skips the transition entirely.
// Two nested requestAnimationFrame calls guarantee the browser commits the initial
// maxHeight in one frame before applying 0px in the next, so the animation runs.
requestAnimationFrame(() => {
requestAnimationFrame(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave an explanation comment for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the comment here

element.style.maxHeight = '0px';
});
});

return () => {
ref.current?.removeEventListener('transitionend', listener);
element.removeEventListener('transitionend', listener);
};
} else {
setIsRemoved(false);

setTimeout(() => {
if (!ref.current) return;

ref.current.style.transition = 'none';
ref.current.style.maxHeight = 'none';

const maxHeight = ref.current?.getBoundingClientRect().height ?? 0;
setWrapperMaxHeight(maxHeight);
}, 0);

return () => {};
}
}, [ref, notification.archived, isRemoved]);

const style = wrapperMaxHeight === undefined ? {} : {maxHeight: `${wrapperMaxHeight}px`};
setIsRemoved(false);

return () => {};
}, [notification.archived, isRemoved]);

if (isRemoved) {
return null;
Expand All @@ -78,15 +77,15 @@ export const NotificationWrapper = (props: {
active: Boolean(notification.onClick),
})}
ref={ref}
style={style}
>
{mobile && notification.swipeActions ? (
<NotificationWithSwipe
notification={notification}
swipeThreshold={swipeThreshold}
wrapperRef={ref}
/>
) : (
<Notification notification={notification} />
<Notification notification={notification} wrapperRef={ref} />
)}
</div>
</li>
Expand Down
30 changes: 29 additions & 1 deletion src/components/Notifications/__stories__/mockData.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';

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

import {NotificationAction} from '../../Notification/NotificationAction';
import {NotificationSwipeAction} from '../../Notification/NotificationSwipeAction';
Expand Down Expand Up @@ -89,6 +89,28 @@ export const notificationBottomActions: JSX.Element = (
</React.Fragment>
);

export const LongNotificationContent = (props: {wrapperRef?: React.RefObject<HTMLDivElement>}) => {
const {wrapperRef} = props;

const handleUpdate = (expanded: boolean) => {
if (!expanded) {
requestAnimationFrame(() => {
wrapperRef?.current?.scrollIntoView({block: 'nearest', behavior: 'smooth'});
});
}
};

return (
<Disclosure summary="Collapsed content" onUpdate={handleUpdate}>
<Flex direction="column" gap={1}>
{Array.from({length: 20}, (_, index) => (
<i key={index}>{'Long expanded content. '}</i>
))}
</Flex>
</Disclosure>
);
};

export const mockNotifications: NotificationProps[] = [
{
id: 'tracker',
Expand Down Expand Up @@ -153,6 +175,12 @@ export const mockNotifications: NotificationProps[] = [
swipeActions: notificationsMockSwipeActions,
href: 'https://ya.ru',
},
{
id: 'looooong-content',
content: (contentProps) => <LongNotificationContent {...contentProps} />,
formattedDate: '29 seconds ago',
swipeActions: notificationsMockSwipeActions,
},
{
id: 'yandex',
content: (
Expand Down
Loading