UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

271 lines (270 loc) 6.95 kB
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; };