Add button to stack/unstack Toast notifications (#149)

* Add button to stack/unstack

* Button -> Toggle button, add offset for notifs
This commit is contained in:
Jared Vu 2023-11-20 18:12:38 -05:00 committed by GitHub
parent 532df49a06
commit 20521b5b3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 19 deletions

View File

@ -76,7 +76,7 @@ export const Toast = ({
<Close asChild>
<$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;

View File

@ -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}>
<Provider swipeDirection={swipeDirection}>
{children}

View File

@ -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 (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{hasMultipleToasts && (
<StyledToggleButton
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
isPressed={shouldStackNotifications}
onPressedChange={() => setshouldStackNotifications(!shouldStackNotifications)}
>
<ChevronLeftIcon />
</StyledToggleButton>
)}
{notificationMap.map(({ notification, key, displayData }, idx) => (
<StyledToast
key={key}
layer={notificationMap.length - 1 - idx}
isStacked={!isMobile && shouldStackNotifications}
isOpen={notification.status < NotificationStatus.Unseen}
layer={notificationMap.length - 1 - idx}
notification={notification}
slotIcon={displayData.icon}
slotTitle={displayData.title}
@ -74,7 +95,7 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
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;
}
`;