From 20521b5b3feb2554a028a13e1071df4d587b6d7a Mon Sep 17 00:00:00 2001 From: Jared Vu Date: Mon, 20 Nov 2023 18:12:38 -0500 Subject: [PATCH] Add button to stack/unstack Toast notifications (#149) * Add button to stack/unstack * Button -> Toggle button, add offset for notifs --- src/components/Toast.tsx | 3 +- src/components/ToastArea.tsx | 7 ++- src/layout/NotificationsToastArea.tsx | 90 ++++++++++++++++++++++----- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 0afbeed..6a2f3da 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -76,7 +76,7 @@ export const Toast = ({ <$CloseButton iconName={IconName.Close} - shape={ButtonShape.Square} + shape={ButtonShape.Circle} size={ButtonSize.XSmall} onClick={(e: MouseEvent) => e.stopPropagation()} /> @@ -249,7 +249,6 @@ const $CloseButton = styled(IconButton)` right: 0; border: solid var(--border-width) var(--color-border); - border-radius: 50%; ${$Root}:hover & { display: block; diff --git a/src/components/ToastArea.tsx b/src/components/ToastArea.tsx index 5f552a9..0457db9 100644 --- a/src/components/ToastArea.tsx +++ b/src/components/ToastArea.tsx @@ -1,6 +1,7 @@ +import styled from 'styled-components'; + import { Provider, Viewport } from '@radix-ui/react-toast'; -import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; type ElementProps = { @@ -12,7 +13,9 @@ type StyleProps = { className?: string; }; -export const ToastArea = ({ swipeDirection, children, className }: ElementProps & StyleProps) => ( +type ToastAreaProps = ElementProps & StyleProps; + +export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProps) => ( <$ToastArea className={className}> {children} diff --git a/src/layout/NotificationsToastArea.tsx b/src/layout/NotificationsToastArea.tsx index b48c69e..93b18f3 100644 --- a/src/layout/NotificationsToastArea.tsx +++ b/src/layout/NotificationsToastArea.tsx @@ -1,15 +1,17 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import styled, { css } from 'styled-components'; import { NotificationStatus } from '@/constants/notifications'; -import { ButtonSize } from '@/constants/buttons'; - +import { ButtonShape, ButtonSize } from '@/constants/buttons'; import { useNotifications } from '@/hooks/useNotifications'; import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { ChevronLeftIcon } from '@/icons'; +import { breakpoints } from '@/styles'; import { Button } from '@/components/Button'; import { Toast } from '@/components/Toast'; import { ToastArea } from '@/components/ToastArea'; +import { ToggleButton } from '@/components/ToggleButton'; type StyleProps = { className?: string; @@ -18,6 +20,8 @@ type StyleProps = { const MAX_TOASTS = 10; export const NotificationsToastArea = ({ className }: StyleProps) => { + const [shouldStackNotifications, setshouldStackNotifications] = useState(true); + const { notifications, getKey, @@ -43,20 +47,37 @@ export const NotificationsToastArea = ({ className }: StyleProps) => { key: getKey(notification), displayData: getDisplayData(notification), })) - .filter(({ displayData }) => displayData) + .filter( + ({ displayData, notification }) => + displayData && notification.status < NotificationStatus.Unseen + ) .slice(-MAX_TOASTS) ); }, [notifications, getKey, getDisplayData]); if (isMenuOpen) return null; + const hasMultipleToasts = notificationMap.length > 1; + return ( + {hasMultipleToasts && ( + setshouldStackNotifications(!shouldStackNotifications)} + > + + + )} + {notificationMap.map(({ notification, key, displayData }, idx) => ( { actionAltText={displayData.actionAltText} duration={displayData.toastDuration ?? Infinity} sensitivity={displayData.toastSensitivity} - setIsOpen={(isOpen, isClosedFromTimeout) => { + setIsOpen={(isOpen: boolean, isClosedFromTimeout?: boolean) => { if (!isOpen) if (isClosedFromTimeout) // Toast timer expired without user interaction @@ -97,16 +118,55 @@ const StyledToastArea = styled(ToastArea)` padding: 0.75rem 0.75rem 0.75rem 0; mask-image: linear-gradient(to left, transparent, white 0.5rem); + + @media ${breakpoints.mobile} { + width: 100%; + position: fixed; + } `; -const StyledToast = styled(Toast)<{ layer: number }>` +const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>` // Stacked toast - position: absolute; - top: 0; - left: 0; - right: 0; - ${({ layer }) => css` - right: calc(${layer} * -2px); - top: calc(${layer} * 2px); - `} + ${({ isStacked, layer }) => + isStacked + ? css` + position: absolute; + left: 0; + top: calc(${layer} * 2px); + right: calc(${layer} * -2px); + ` + : css` + margin-bottom: 0.75rem; + `} +`; + +const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>` + z-index: 2; + pointer-events: auto; + display: none; + position: absolute; + top: 4px; + left: calc(50% - 0.75rem); + --button-width: 2rem; + --button-height: 1rem; + + ${StyledToastArea}:hover & { + display: flex; + } + + > svg { + width: 4px; + margin-top: 1px; + transform: rotate(-90deg); + + ${({ isPressed }) => + !isPressed && + css` + transform: rotate(90deg); + `} + } + + @media ${breakpoints.mobile} { + display: none; + } `;