@spaced-out/ui-design-system
Version:
Sense UI components library
250 lines (220 loc) • 5.95 kB
Flow
// @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>
);
};