Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
felixhabib committed Nov 13, 2024
1 parent aff4993 commit 84cefd5
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 76 deletions.
103 changes: 55 additions & 48 deletions packages/braid-design-system/src/lib/components/useToast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import type { InternalToast, ToastAction } from './ToastTypes';
import { lineHeightContainer } from '../../css/lineHeightContainer.css';
import buildDataAttributes from '../private/buildDataAttributes';
import * as styles from './Toast.css';
import { toastGap } from './consts';

const toneToIcon = {
critical: IconCritical,
positive: IconPositive,
};

export const toastDuration = 10000;
// todo - revert change
export const toastDuration = 1000 * 4;

const borderRadius = 'large';

Expand Down Expand Up @@ -61,6 +63,7 @@ const ToastIcon = ({ tone, icon }: Pick<InternalToast, 'tone' | 'icon'>) => {

interface ToastProps extends InternalToast {
onClose: (dedupeKey: string, id: string) => void;
expanded: boolean;
}
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
(
Expand All @@ -77,6 +80,7 @@ const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
action,
shouldRemove,
data,
expanded,
...restProps
},
ref,
Expand All @@ -97,6 +101,11 @@ const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
}
}, [shouldRemove, remove, stopTimeout]);

useEffect(
() => (expanded ? stopTimeout() : startTimeout()),
[expanded, startTimeout, stopTimeout],
);

assert(
!icon || (icon.props.size === undefined && icon.props.tone === undefined),
"Icons cannot set the 'size' or 'tone' prop when passed to a Toast component",
Expand Down Expand Up @@ -141,58 +150,56 @@ const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
textAlign="left"
role="alert"
ref={ref}
onMouseEnter={stopTimeout}
onMouseLeave={startTimeout}
className={vanillaTheme}
className={[vanillaTheme, styles.toast]}
overflow="hidden"
boxShadow="large"
borderRadius={borderRadius}
{...buildDataAttributes({ data, validateRestProps: restProps })}
>
<Box boxShadow="large" borderRadius={borderRadius}>
<ContentBlock width="xsmall">
<Box
background={{ lightMode: 'surfaceDark', darkMode: 'surface' }}
position="relative"
borderRadius={borderRadius}
paddingY="medium"
paddingX="gutter"
overflow="hidden"
className={styles.toast}
>
<Columns space="none">
{tone !== 'neutral' || (tone === 'neutral' && icon) ? (
<Column width="content">
<Box paddingRight="small">
<ToastIcon tone={tone} icon={icon} />
</Box>
</Column>
) : null}
<Column>{content}</Column>
<ContentBlock width="xsmall">
<Box
background={{ lightMode: 'surfaceDark', darkMode: 'surface' }}
position="relative"
borderRadius={borderRadius}
paddingY="medium"
paddingX="gutter"
marginTop={toastGap}
>
<Columns space="none">
{tone !== 'neutral' || (tone === 'neutral' && icon) ? (
<Column width="content">
<Box
width="touchable"
display="flex"
justifyContent="flexEnd"
alignItems="center"
className={lineHeightContainer.standard}
aria-hidden
>
<ButtonIcon
id={`${dedupeKey}-clear`}
icon={<IconClear tone="secondary" />}
variant="transparent"
onClick={remove}
label={closeLabel}
data={
process.env.NODE_ENV !== 'production'
? { testid: 'clearToast' }
: {}
}
/>
<Box paddingRight="small">
<ToastIcon tone={tone} icon={icon} />
</Box>
</Column>
</Columns>
</Box>
</ContentBlock>
</Box>
) : null}
<Column>{content}</Column>
<Column width="content">
<Box
width="touchable"
display="flex"
justifyContent="flexEnd"
alignItems="center"
className={lineHeightContainer.standard}
aria-hidden
>
<ButtonIcon
id={`${dedupeKey}-clear`}
icon={<IconClear tone="secondary" />}
variant="transparent"
onClick={remove}
label={closeLabel}
data={
process.env.NODE_ENV !== 'production'
? { testid: 'clearToast' }
: {}
}
/>
</Box>
</Column>
</Columns>
</Box>
</ContentBlock>
</Box>
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';

export const toaster = style({
bottom: '0',
left: '50%',
transform: 'translateX(-50%)',
width: '420px',
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';

import { Box } from '../Box/Box';
import ToastComponent from './Toast';
import { useFlipList } from './useFlipList';
import type { InternalToast } from './ToastTypes';

import * as styles from './Toaster.css';

interface ToasterProps {
toasts: InternalToast[];
removeToast: (key: string) => void;
}

export const Toaster = ({ toasts, removeToast }: ToasterProps) => {
const { itemRef, remove } = useFlipList();
const [expanded, setExpanded] = useState(false);
const { itemRef, remove } = useFlipList(expanded);

const onClose = useCallback(
(dedupeKey: string, id: string) => {
Expand All @@ -25,20 +29,23 @@ export const Toaster = ({ toasts, removeToast }: ToasterProps) => {
<Box
position="fixed"
zIndex="notification"
width="full"
pointerEvents="none"
paddingX="gutter"
bottom={0}
pointerEvents={toasts.length === 0 ? 'none' : undefined}
padding={toasts.length === 0 ? 'none' : 'xsmall'}
onMouseEnter={() => setExpanded(true)}
onMouseLeave={() => setExpanded(false)}
onFocus={() => setExpanded(true)}
onBlur={() => setExpanded(false)}
className={styles.toaster}
>
{toasts.map(({ id, ...rest }) => (
<Box key={id} paddingBottom="small">
<ToastComponent
ref={itemRef(id)}
id={id}
onClose={onClose}
{...rest}
/>
</Box>
<ToastComponent
key={id}
ref={itemRef(id)}
id={id}
onClose={onClose}
expanded={expanded}
{...rest}
/>
))}
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const toastGap = 'xxsmall';
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { useMemo, useCallback } from 'react';
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
import { calc } from '@vanilla-extract/css-utils';
import { vars } from '../../../entries/css';
import { toastGap } from './consts';

const px = (v: string | number) => `${v}px`;

const animationTimeout = 300;

const entranceTransition = 'transform 0.2s ease, opacity 0.2s ease';
const exitTransition = 'opacity 0.1s ease';
const entranceTransition = 'all 0.2s ease';
const exitTransition = 'opacity 0.2s ease, height 0.2s ease';

const visibleStackedToasts = 3;

interface Transform {
property: 'opacity' | 'transform' | 'scale';
property: 'opacity' | 'transform' | 'scale' | 'height';
from?: string;
to?: string;
}
Expand Down Expand Up @@ -58,7 +65,7 @@ const animate = (
});
};

export const useFlipList = () => {
export const useFlipList = (expanded: boolean) => {
const refs = useMemo(() => new Map<string, HTMLElement | null>(), []);
const positions = useMemo(() => new Map<string, number>(), []);

Expand All @@ -71,35 +78,85 @@ export const useFlipList = () => {

Array.from(refs.entries()).forEach(([id, element]) => {
if (element) {
const index = Array.from(refs.keys()).indexOf(id);
const toastsLength = refs.size;
const position = toastsLength - index - 1;

const { scale, opacity } = element.style;
const height = element.getBoundingClientRect().height;

element.style.scale = '1';
element.style.height = 'auto';
const fullHeight = element.getBoundingClientRect().height;

element.style.height = px(height);
element.style.scale = scale;

const collapsedHeight = '8px';

const prevTop = positions.get(id);
const { top, height } = element.getBoundingClientRect();
const isNew = typeof prevTop !== 'number';

const collapsedScale = position === 1 ? 0.9 : 0.8;

if (typeof prevTop === 'number' && prevTop !== top) {
// Move animation
if (position > 0) {
// Move animation for toasts that are not the first
animations.push({
element,
transition: entranceTransition,
transforms: [
{
property: 'transform',
from: `translateY(${prevTop - top}px)`,
property: 'height',
from: px(height),
to: expanded
? px(fullHeight)
: `${calc(collapsedHeight).add(vars.space[toastGap])}`,
},
{
property: 'scale',
from: scale,
to: expanded ? '1' : `${collapsedScale}`,
},
{
property: 'opacity',
from: opacity,
to: position < visibleStackedToasts || expanded ? '1' : '0',
},
],
});
} else if (typeof prevTop !== 'number') {
} else if (isNew) {
// Enter animation
animations.push({
element,
transition: entranceTransition,
transforms: [
{
property: 'transform',
from: `translateY(${height}px)`,
},
{
property: 'opacity',
from: '0',
},
{
property: 'height',
from: '0px',
to: px(fullHeight),
},
],
});
} else {
// Move animation for the first toast
animations.push({
element,
transition: entranceTransition,
transforms: [
{
property: 'height',
from: px(height),
to: px(fullHeight),
},
{
property: 'scale',
from: scale,
to: '1',
},
],
});
}
Expand Down Expand Up @@ -128,6 +185,11 @@ export const useFlipList = () => {
property: 'opacity',
to: '0',
},
{
property: 'height',
from: element.style.height,
to: '0px',
},
],
exitTransition,
cb,
Expand Down

0 comments on commit 84cefd5

Please sign in to comment.