RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/components/GenericModal/GenericModal.tsx

Summary

Maintainability
A
55 mins
Test Coverage
import { Button, Modal } from '@rocket.chat/fuselage';
import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks';
import type { Keys as IconName } from '@rocket.chat/icons';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ComponentProps, ReactElement, ReactNode, ComponentPropsWithoutRef } from 'react';
import React, { useEffect, useRef } from 'react';

import type { RequiredModalProps } from './withDoNotAskAgain';
import { withDoNotAskAgain } from './withDoNotAskAgain';

type VariantType = 'danger' | 'warning' | 'info' | 'success';

type GenericModalProps = RequiredModalProps & {
    variant?: VariantType;
    children?: ReactNode;
    cancelText?: ReactNode;
    confirmText?: ReactNode;
    title?: string | ReactElement;
    icon?: IconName | ReactElement | null;
    confirmDisabled?: boolean;
    tagline?: ReactNode;
    onCancel?: () => Promise<void> | void;
    onClose?: () => Promise<void> | void;
    annotation?: ReactNode;
} & Omit<ComponentPropsWithoutRef<typeof Modal>, 'title'>;

const iconMap: Record<string, IconName> = {
    danger: 'modal-warning',
    warning: 'modal-warning',
    info: 'info',
    success: 'check',
};

const getButtonProps = (variant: VariantType): ComponentProps<typeof Button> => {
    switch (variant) {
        case 'danger':
            return { danger: true };
        case 'warning':
            return { primary: true };
        default:
            return {};
    }
};

const renderIcon = (icon: GenericModalProps['icon'], variant: VariantType): ReactNode => {
    if (icon === null) {
        return null;
    }

    if (icon === undefined) {
        return <Modal.Icon color={variant} name={iconMap[variant]} />;
    }

    if (typeof icon === 'string') {
        return <Modal.Icon name={icon} />;
    }

    return icon;
};

const GenericModal = ({
    variant = 'info',
    children,
    cancelText,
    confirmText,
    title,
    icon,
    onCancel,
    onClose = onCancel,
    onConfirm,
    dontAskAgain,
    confirmDisabled,
    tagline,
    wrapperFunction,
    annotation,
    ...props
}: GenericModalProps) => {
    const t = useTranslation();
    const genericModalId = useUniqueId();

    const dismissedRef = useRef(true);

    const handleConfirm = useEffectEvent(() => {
        dismissedRef.current = false;
        onConfirm?.();
    });

    const handleCancel = useEffectEvent(() => {
        dismissedRef.current = false;
        onCancel?.();
    });

    const handleCloseButtonClick = useEffectEvent(() => {
        dismissedRef.current = true;
        onClose?.();
    });

    useEffect(
        () => () => {
            if (!dismissedRef.current) return;
            onClose?.();
        },
        [onClose],
    );

    return (
        <Modal aria-labelledby={`${genericModalId}-title`} wrapperFunction={wrapperFunction} {...props}>
            <Modal.Header>
                {renderIcon(icon, variant)}
                <Modal.HeaderText>
                    {tagline && <Modal.Tagline>{tagline}</Modal.Tagline>}
                    <Modal.Title id={`${genericModalId}-title`}>{title ?? t('Are_you_sure')}</Modal.Title>
                </Modal.HeaderText>
                {onClose && <Modal.Close aria-label={t('Close')} onClick={handleCloseButtonClick} />}
            </Modal.Header>
            <Modal.Content fontScale='p2'>{children}</Modal.Content>
            <Modal.Footer justifyContent={dontAskAgain ? 'space-between' : 'end'}>
                {dontAskAgain}
                {annotation && !dontAskAgain && <Modal.FooterAnnotation>{annotation}</Modal.FooterAnnotation>}
                <Modal.FooterControllers>
                    {onCancel && (
                        <Button secondary onClick={handleCancel}>
                            {cancelText ?? t('Cancel')}
                        </Button>
                    )}
                    {wrapperFunction && (
                        <Button {...getButtonProps(variant)} type='submit' disabled={confirmDisabled}>
                            {confirmText ?? t('Ok')}
                        </Button>
                    )}
                    {!wrapperFunction && onConfirm && (
                        <Button {...getButtonProps(variant)} onClick={handleConfirm} disabled={confirmDisabled}>
                            {confirmText ?? t('Ok')}
                        </Button>
                    )}
                </Modal.FooterControllers>
            </Modal.Footer>
        </Modal>
    );
};

export const GenericModalDoNotAskAgain = withDoNotAskAgain<GenericModalProps>(GenericModal);

export default GenericModal;