diff --git a/src/components/Notification/Notification.scss b/src/components/Notification/Notification.scss index 9b8f53c9..2d0f745f 100644 --- a/src/components/Notification/Notification.scss +++ b/src/components/Notification/Notification.scss @@ -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; } diff --git a/src/components/Notification/Notification.tsx b/src/components/Notification/Notification.tsx index da167b58..d4d0e0a5 100644 --- a/src/components/Notification/Notification.tsx +++ b/src/components/Notification/Notification.tsx @@ -11,7 +11,10 @@ import './Notification.scss'; const b = block('notification'); -type Props = {notification: NotificationProps}; +type Props = { + wrapperRef?: React.RefObject; + notification: NotificationProps; +}; interface ClickableElementProps { notification: NotificationProps; @@ -20,9 +23,10 @@ interface ClickableElementProps { } export const Notification = React.memo(function Notification(props: Props) { + const ref = React.useRef(null); const {t} = i18n.useTranslation(); const mobile = useMobile(); - const {notification} = props; + const {wrapperRef, notification} = props; const { title, content, @@ -58,11 +62,20 @@ export const Notification = React.memo(function Notification(props: Props) {
{notification.bottomActions}
) : null; - const renderedContent = ( -
-
{content}
-
- ); + let renderedContent; + if (typeof content === 'function') { + renderedContent = ( +
+
{content({wrapperRef})}
+
+ ); + } else { + renderedContent = ( +
+
{content}
+
+ ); + } const renderedSourceText = source?.title || formattedDate ? ( @@ -121,6 +134,7 @@ export const Notification = React.memo(function Notification(props: Props) { return (
; +}; export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(props: Props) { const swipeThreshold = props.swipeThreshold ?? 0.4; @@ -24,7 +28,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p } const ref = React.useRef(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; @@ -132,7 +136,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p > {leftAction ? renderAction(leftAction) : null}
- +
{rightAction ? renderAction(rightAction) : null}
diff --git a/src/components/Notification/definitions.ts b/src/components/Notification/definitions.ts index 5b51951a..f277a41b 100644 --- a/src/components/Notification/definitions.ts +++ b/src/components/Notification/definitions.ts @@ -24,7 +24,9 @@ export type NotificationSwipeActionsProps = export type NotificationProps = { id: string; - content: React.ReactNode; + content: + | React.ReactNode + | ((props: {wrapperRef?: React.RefObject}) => React.ReactNode); title?: React.ReactNode; formattedDate?: React.ReactNode; diff --git a/src/components/Notifications/NotificationWrapper.tsx b/src/components/Notifications/NotificationWrapper.tsx index d6f6818e..c32e3947 100644 --- a/src/components/Notifications/NotificationWrapper.tsx +++ b/src/components/Notifications/NotificationWrapper.tsx @@ -19,11 +19,12 @@ export const NotificationWrapper = (props: { const {notification, swipeThreshold} = props; const mobile = useMobile(); - const [wrapperMaxHeight, setWrapperMaxHeight] = React.useState(undefined); const [isRemoved, setIsRemoved] = React.useState(false); React.useEffect(() => { - if (!ref.current) { + const element = ref.current; + + if (!element) { if (!notification.archived && isRemoved) { setIsRemoved(false); } @@ -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(() => { + 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; @@ -78,15 +77,15 @@ export const NotificationWrapper = (props: { active: Boolean(notification.onClick), })} ref={ref} - style={style} > {mobile && notification.swipeActions ? ( ) : ( - + )} diff --git a/src/components/Notifications/__stories__/mockData.tsx b/src/components/Notifications/__stories__/mockData.tsx index 472f8a84..6ca74252 100644 --- a/src/components/Notifications/__stories__/mockData.tsx +++ b/src/components/Notifications/__stories__/mockData.tsx @@ -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'; @@ -89,6 +89,28 @@ export const notificationBottomActions: JSX.Element = ( ); +export const LongNotificationContent = (props: {wrapperRef?: React.RefObject}) => { + const {wrapperRef} = props; + + const handleUpdate = (expanded: boolean) => { + if (!expanded) { + requestAnimationFrame(() => { + wrapperRef?.current?.scrollIntoView({block: 'nearest', behavior: 'smooth'}); + }); + } + }; + + return ( + + + {Array.from({length: 20}, (_, index) => ( + {'Long expanded content. '} + ))} + + + ); +}; + export const mockNotifications: NotificationProps[] = [ { id: 'tracker', @@ -153,6 +175,12 @@ export const mockNotifications: NotificationProps[] = [ swipeActions: notificationsMockSwipeActions, href: 'https://ya.ru', }, + { + id: 'looooong-content', + content: (contentProps) => , + formattedDate: '29 seconds ago', + swipeActions: notificationsMockSwipeActions, + }, { id: 'yandex', content: (