UNPKG

@spaced-out/ui-design-system

Version:
250 lines (220 loc) 5.95 kB
// @flow strict import * as React from 'react'; import { // $FlowFixMe[untyped-import] FloatingFocusManager, // $FlowFixMe[untyped-import] useFloating, } from '@floating-ui/react'; import classify from '../../utils/classify'; import {CloseIcon, Icon} from '../Icon'; import {SubTitleExtraSmall, TEXT_COLORS} from '../Text'; import typography from '../../styles/typography.module.css'; import css from './Toast.module.css'; type ClassNames = $ReadOnly<{wrapper?: string, time?: string}>; export const TOAST_SEMANTIC = Object.freeze({ success: 'success', information: 'information', warning: 'warning', danger: 'danger', primary: 'primary', }); export type ToastSemanticType = $Values<typeof TOAST_SEMANTIC>; export type ToastProps = { classNames?: ClassNames, children?: React.Node, time?: string, semantic?: ToastSemanticType, onClose?: () => void, initialFocus?: number, customIcon?: React.Node, hideCloseIcon?: boolean, }; const ToastIcon = ({semantic}: {semantic: ToastSemanticType}) => { switch (semantic) { case 'success': return ( <Icon name="circle-check" size="medium" color={TEXT_COLORS.success} type="solid" /> ); case TOAST_SEMANTIC.information: return ( <Icon name="circle-info" size="medium" color={TEXT_COLORS.information} type="solid" /> ); case TOAST_SEMANTIC.warning: return ( <Icon name="circle-exclamation" size="medium" color={TEXT_COLORS.warning} type="solid" /> ); case TOAST_SEMANTIC.danger: return ( <Icon name="shield-exclamation" size="medium" color={TEXT_COLORS.danger} type="solid" /> ); default: return ( <Icon color={TEXT_COLORS.neutral} name="face-smile" size="medium" type="solid" /> ); } }; export type ToastTitleProps = { children?: React.Node, className?: string, semantic?: ToastSemanticType, }; export const ToastTitle: React$AbstractComponent< ToastTitleProps, HTMLDivElement, > = React.forwardRef<ToastTitleProps, HTMLDivElement>( ( {children, semantic = '', className, ...props}: ToastTitleProps, ref, ): React.Node => ( <div className={classify(css.toastTitle, typography[semantic], className)} {...props} ref={ref} > {children} </div> ), ); ToastTitle.displayName = 'ToastTitle'; export type ToastBodyProps = { children?: React.Node, className?: string, }; export const ToastBody: React$AbstractComponent< ToastBodyProps, HTMLDivElement, > = React.forwardRef<ToastBodyProps, HTMLDivElement>( ({children, className, ...props}: ToastBodyProps, ref): React.Node => ( <div className={classify(css.toastBody, className)} {...props} ref={ref}> {children} </div> ), ); ToastBody.displayName = 'ToastBody'; export type ToastFooterProps = { children?: React.Node, onClose?: () => void, }; export const ToastFooter = ({ children, onClose, }: ToastFooterProps): React.Node => { const arrayChildren = React.Children.toArray(children); const footerActions = React.Children.map(children, (child, index) => { const isLast = index === arrayChildren.length - 1; if (React.isValidElement(child)) { const {onClick} = child.props; const buttonClickHandler = (e) => { onClose && onClose(); onClick && onClick(e); }; if (child?.type?.displayName === 'Button' && isLast) { return React.cloneElement(child, { size: 'small', type: 'primary', ...child.props, onClick: buttonClickHandler, }); } else if (child?.type?.displayName === 'Button') { return React.cloneElement(child, { size: 'small', type: 'tertiary', ...child.props, onClick: buttonClickHandler, }); } } return child; }); return React.Children.count(children) > 0 ? ( <div className={css.toastFooterActions}>{footerActions}</div> ) : null; }; ToastFooter.displayName = 'ToastFooter'; export const Toast = ({ classNames, children, time, semantic = TOAST_SEMANTIC.success, onClose, initialFocus = -1, customIcon, hideCloseIcon, }: ToastProps): React.Node => { const {refs, context} = useFloating({ open: true, }); const getComp = (comp: string) => { const childrenArray = React.Children.toArray(children); if (childrenArray.length) { const nodes: React.Node[] = []; for (const child of childrenArray) { if (child?.type?.displayName === comp) { nodes.push(React.cloneElement(child, {semantic})); } } return nodes.length > 1 ? nodes : nodes[0]; } return null; }; const footer = getComp('ToastFooter'); // $FlowFixMe const footerWithClose = footer ? React.cloneElement(footer, {onClose}) : null; return ( <FloatingFocusManager context={context} initialFocus={initialFocus}> <div className={classify(css.toastContainer, classNames?.wrapper)} ref={refs.setFloating} > {customIcon || <ToastIcon semantic={semantic} />} <div className={css.toastMidSection}> <div className={css.contentWrap}> <div className={css.titleBodyWrap}> {getComp('ToastTitle')} {getComp('ToastBody')} </div> {time && ( <SubTitleExtraSmall className={classNames?.time}> {time} </SubTitleExtraSmall> )} </div> {footerWithClose} </div> {!hideCloseIcon && ( <CloseIcon classNames={{button: css.closeIcon}} onClick={onClose} ariaLabel="Close Button" /> )} </div> </FloatingFocusManager> ); };