@itwin/itwinui-react
Version:
A react component library for iTwinUI
271 lines (270 loc) • 6.95 kB
JavaScript
import * as React from 'react';
import cx from 'classnames';
import {
getWindow,
StatusIconMap,
SvgCloseSmall,
Box,
useSafeContext,
ButtonBase,
useMediaQuery,
useLatestRef,
} from '../../utils/index.js';
import { IconButton } from '../Buttons/IconButton.js';
import { ToasterStateContext } from './Toaster.js';
export const Toast = (props) => {
let {
content,
category,
type = 'temporary',
isVisible: isVisibleProp,
link,
duration = 7000,
hasCloseButton,
onRemove,
animateOutTo,
domProps,
} = props;
let closeTimeout = React.useRef(0);
let { placement } = useSafeContext(ToasterStateContext).settings;
let placementPosition = placement.startsWith('top') ? 'top' : 'bottom';
let [visible, setVisible] = React.useState(isVisibleProp ?? true);
let isVisible = isVisibleProp ?? visible;
let [height, setHeight] = React.useState(0);
let thisElement = React.useRef(null);
let [margin, setMargin] = React.useState(0);
let marginStyle = () => {
if ('top' === placementPosition)
return {
marginBlockEnd: margin,
};
return {
marginBlockStart: margin,
};
};
React.useEffect(() => {
if ('temporary' === type) setCloseTimeout(duration);
return () => {
clearCloseTimeout();
};
}, [duration, type]);
React.useEffect(() => {
if (!isVisible && !animateOutTo) setMargin(-height);
}, [isVisible, animateOutTo, setMargin, height]);
let close = () => {
clearCloseTimeout();
setMargin(-height);
setVisible(false);
};
let setCloseTimeout = (timeout) => {
let definedWindow = getWindow();
if (!definedWindow) return;
closeTimeout.current = definedWindow.setTimeout(() => {
close();
}, timeout);
};
let clearCloseTimeout = () => {
getWindow()?.clearTimeout(closeTimeout.current);
};
let onRef = (ref) => {
if (ref) {
let { height } = ref.getBoundingClientRect();
setHeight(height);
}
};
let shouldBeMounted = useAnimateToastBasedOnVisibility(isVisible, {
thisElement,
animateOutTo,
onRemove,
});
return shouldBeMounted
? React.createElement(
Box,
{
ref: thisElement,
className: 'iui-toast-all',
style: {
height,
...marginStyle(),
},
},
React.createElement(
'div',
{
ref: onRef,
},
React.createElement(ToastPresentation, {
as: 'div',
category: category,
content: content,
link: link,
type: type,
hasCloseButton: hasCloseButton,
onClose: close,
...domProps?.toastProps,
contentProps: domProps?.contentProps,
}),
),
)
: null;
};
export const ToastPresentation = React.forwardRef((props, forwardedRef) => {
let {
content,
category,
type = 'temporary',
link,
hasCloseButton,
onClose,
className,
contentProps,
...rest
} = props;
let StatusIcon = StatusIconMap[category];
return React.createElement(
Box,
{
className: cx(`iui-toast iui-${category}`, className),
ref: forwardedRef,
...rest,
},
React.createElement(
Box,
{
className: 'iui-status-area',
},
React.createElement(StatusIcon, {
className: 'iui-icon',
}),
),
React.createElement(
Box,
{
as: 'div',
...contentProps,
className: cx('iui-message', contentProps?.className),
},
content,
),
link &&
React.createElement(
ButtonBase,
{
...link,
className: cx('iui-anchor', 'iui-toast-anchor', link.className),
title: void 0,
'data-iui-status': category,
'data-iui-underline': true,
},
link.title,
),
('persisting' === type || hasCloseButton) &&
React.createElement(
IconButton,
{
size: 'small',
styleType: 'borderless',
onClick: onClose,
'aria-label': 'Close',
},
React.createElement(SvgCloseSmall, null),
),
);
});
let useAnimateToastBasedOnVisibility = (isVisible, args) => {
let { thisElement, animateOutTo, onRemove } = args;
let [shouldBeMounted, setShouldBeMounted] = React.useState(isVisible);
let motionOk = useMediaQuery('(prefers-reduced-motion: no-preference)');
let onRemoveRef = useLatestRef(onRemove);
let [prevIsVisible, setPrevIsVisible] = React.useState(void 0);
React.useEffect(() => {
if (prevIsVisible !== isVisible) {
setPrevIsVisible(isVisible);
if (isVisible) safeAnimateIn();
else safeAnimateOut();
}
function calculateOutAnimation(node) {
let translateX = 0;
let translateY = 0;
if (animateOutTo && node) {
let { x: startX, y: startY } = node.getBoundingClientRect();
let { x: endX, y: endY } = animateOutTo.getBoundingClientRect();
translateX = endX - startX;
translateY = endY - startY;
}
return {
translateX,
translateY,
};
}
function safeAnimateIn() {
setShouldBeMounted(true);
queueMicrotask(() => {
animateIn();
});
}
function safeAnimateOut() {
if (motionOk) {
let animation = animateOut();
animation?.addEventListener('finish', () => {
setShouldBeMounted(false);
onRemoveRef.current?.();
});
} else {
setShouldBeMounted(false);
onRemoveRef.current?.();
}
}
function animateIn() {
if (!motionOk) return;
thisElement.current?.animate?.(
[
{
transform: 'translateY(15%)',
},
{
transform: 'translateY(0)',
},
],
{
duration: 240,
fill: 'forwards',
},
);
}
function animateOut() {
if (null == thisElement.current || !motionOk) return;
let { translateX, translateY } = calculateOutAnimation(
thisElement.current,
);
let animationDuration = animateOutTo ? 400 : 120;
let animation = thisElement.current?.animate?.(
[
{
transform: animateOutTo
? `scale(0.9) translate(${translateX}px,${translateY}px)`
: 'scale(0.9)',
opacity: 0,
transitionDuration: `${animationDuration}ms`,
transitionTimingFunction: 'cubic-bezier(0.4, 0, 1, 1)',
},
],
{
duration: animationDuration,
iterations: 1,
fill: 'forwards',
},
);
return animation;
}
}, [
isVisible,
prevIsVisible,
animateOutTo,
motionOk,
thisElement,
setShouldBeMounted,
onRemoveRef,
]);
return shouldBeMounted;
};