diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-16 13:12:46 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-20 14:12:48 +0200 |
| commit | f594762360906b08eafaa8534a5d942cf19652ef (patch) | |
| tree | 9294ae19d5ee450d379ac70e044432ccaf98d334 | |
| parent | 860885af59cb87bb44ec1e1c87af8d6af0b0f35f (diff) | |
| download | mullvadvpn-f594762360906b08eafaa8534a5d942cf19652ef.tar.xz mullvadvpn-f594762360906b08eafaa8534a5d942cf19652ef.zip | |
Use motion for in-app notification transition
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx | 38 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx | 57 |
2 files changed, 44 insertions, 51 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx index bf61ff8003..61080939a6 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx @@ -1,3 +1,4 @@ +import { AnimatePresence } from 'motion/react'; import { useCallback, useState } from 'react'; import { messages } from '../../shared/gettext'; @@ -35,6 +36,7 @@ import { } from '../lib/notifications'; import { AppUpgradeAvailableNotificationProvider } from '../lib/notifications/app-upgrade-available'; import { useTunnelProtocol } from '../lib/relay-settings-hooks'; +import { useMounted } from '../lib/utility-hooks'; import accountActions from '../redux/account/actions'; import { convertEventTypeToStep } from '../redux/app-upgrade/helpers'; import { useAppUpgradeError, useVersionSuggestedUpgrade } from '../redux/hooks'; @@ -193,12 +195,28 @@ export default function NotificationArea(props: IProps) { notification.mayDisplay(), ); + const notification = notificationProvider?.getInAppNotification(); if (notificationProvider) { - const notification = notificationProvider.getInAppNotification(); + if (!notification) { + log.error( + `Notification providers mayDisplay() returned true but getInAppNotification() returned undefined for ${notificationProvider.constructor.name}`, + ); + } + } + + // We only want to animate notifications after first mount, + // so as to prevent an animation from animating in when the + // app has just started. + const mounted = useMounted(); + const isMounted = mounted(); - if (notification) { - return ( - <NotificationBanner className={props.className} data-testid="notificationBanner"> + return ( + <AnimatePresence> + {notification && ( + <NotificationBanner + animateIn={isMounted} + aria-hidden={!notification} + className={props.className}> <NotificationIndicator $type={notification.indicator} data-testid="notificationIndicator" @@ -220,15 +238,9 @@ export default function NotificationArea(props: IProps) { /> )} </NotificationBanner> - ); - } else { - log.error( - `Notification providers mayDisplay() returned true but getInAppNotification() returned undefined for ${notificationProvider.constructor.name}`, - ); - } - } - - return <NotificationBanner className={props.className} aria-hidden={true} />; + )} + </AnimatePresence> + ); } interface NotificationActionWrapperProps { diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx index eedbdb3de4..c22051f825 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react'; +import { motion } from 'motion/react'; +import React from 'react'; import styled from 'styled-components'; import { messages } from '../../shared/gettext'; @@ -6,7 +7,6 @@ import { InAppNotificationIndicatorType } from '../../shared/notifications/notif import { IconButton } from '../lib/components'; import { colors } from '../lib/foundations'; import { useExclusiveTask } from '../lib/hooks/use-exclusive-task'; -import { useEffectEvent, useLastDefinedValue, useStyledRef } from '../lib/utility-hooks'; import { tinyText } from './common-styles'; const NOTIFICATION_AREA_ID = 'notification-area'; @@ -106,22 +106,13 @@ export const NotificationIndicator = styled.div<INotificationIndicatorProps>((pr : colors.transparent, })); -interface ICollapsibleProps { - $alignBottom: boolean; - $height?: number; -} - -const Collapsible = styled.div<ICollapsibleProps>((props) => { - return { - display: 'flex', - flexDirection: 'column', - justifyContent: props.$alignBottom ? 'flex-end' : 'flex-start', - backgroundColor: colors.darkerBlue50, - overflow: 'hidden', - // Using auto as the initial value prevents transition if a notification is visible on mount. - height: props.$height === undefined ? 'auto' : `${props.$height}px`, - transition: 'height 250ms ease-in-out', - }; +const Collapsible = styled(motion.div)({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + translateY: '0%', + backgroundColor: colors.darkerBlue50, + overflow: 'hidden', }); const Content = styled.section({ @@ -134,30 +125,20 @@ const Content = styled.section({ interface INotificationBannerProps { children?: React.ReactNode; // Array<NotificationContent | NotificationActions>, className?: string; + animateIn: boolean; } -export function NotificationBanner(props: INotificationBannerProps) { - const [contentHeight, setContentHeight] = useState<number>(); - const [alignBottom, setAlignBottom] = useState(false); - - const contentRef = useStyledRef<HTMLDivElement>(); - - const children = useLastDefinedValue(props.children); - - const updateHeightEvent = useEffectEvent(() => { - const newHeight = - props.children !== undefined ? (contentRef.current?.getBoundingClientRect().height ?? 0) : 0; - if (newHeight !== contentHeight) { - setContentHeight(newHeight); - setAlignBottom((alignBottom) => alignBottom || contentHeight === 0 || newHeight === 0); - } - }); - - useEffect(() => updateHeightEvent()); +export function NotificationBanner({ className, children, animateIn }: INotificationBannerProps) { + const translateYInitial = animateIn ? '-100%' : '0%'; return ( - <Collapsible $height={contentHeight} className={props.className} $alignBottom={alignBottom}> - <Content ref={contentRef}>{children}</Content> + <Collapsible + animate={{ translateY: '0%' }} + className={className} + exit={{ translateY: '-100%' }} + initial={{ translateY: translateYInitial }} + transition={{ duration: 0.25 }}> + <Content>{children}</Content> </Collapsible> ); } |
