TryGhost/Ghost

View on GitHub
apps/admin-x-design-system/src/global/Button.tsx

Summary

Maintainability
D
2 days
Test Coverage
import Icon, {IconSize} from './Icon';
import React, {HTMLProps} from 'react';
import clsx from 'clsx';
import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator';

export type ButtonColor = 'clear' | 'light-grey' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
export type ButtonSize = 'sm' | 'md';

export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label' | 'size' | 'children'> {
    size?: ButtonSize;
    label?: React.ReactNode;
    hideLabel?: boolean;
    icon?: string;
    iconSize?: IconSize;
    iconColorClass?: string;
    key?: string;
    color?: ButtonColor;
    fullWidth?: boolean;
    link?: boolean;
    linkWithPadding?: boolean;
    disabled?: boolean;
    unstyled?: boolean;
    className?: string;
    tag?: string;
    loading?: boolean;
    loadingIndicatorSize?: LoadingIndicatorSize;
    loadingIndicatorColor?: LoadingIndicatorColor;
    outlineOnMobile?: boolean;
    onClick?: (e?:React.MouseEvent<HTMLElement>) => void;
    testId?: string;
}

const Button: React.FC<ButtonProps> = ({
    testId,
    size = 'md',
    label = '',
    hideLabel = false,
    icon = '',
    iconSize,
    iconColorClass,
    color = 'clear',
    fullWidth,
    link,
    linkWithPadding = false,
    disabled,
    unstyled = false,
    className = '',
    tag = 'button',
    loading = false,
    loadingIndicatorColor,
    outlineOnMobile = false,
    onClick,
    ...props
}) => {
    if (!color) {
        color = 'clear';
    }

    if (!unstyled) {
        className = clsx(
            'inline-flex items-center justify-center whitespace-nowrap rounded text-sm transition',
            ((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? 'font-bold' : 'font-semibold',
            !link ? `${size === 'sm' ? 'h-7' : 'h-[34px]'}` : '',
            !link ? `${size === 'sm' || label && icon ? 'px-3' : 'px-4'}` : '',
            (link && linkWithPadding) && '-m-1 p-1',
            className
        );

        switch (color) {
        case 'black':
            className = clsx(
                link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`,
                className
            );
            loadingIndicatorColor = 'light';
            iconColorClass = iconColorClass || 'text-white';
            break;
        case 'light-grey':
            className = clsx(
                link ? 'text-grey-800 hover:text-green-400 dark:text-white' : `bg-grey-200 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`,
                className
            );
            loadingIndicatorColor = 'dark';
            break;
        case 'grey':
            className = clsx(
                link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`,
                className
            );
            loadingIndicatorColor = 'dark';
            break;
        case 'green':
            className = clsx(
                link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`,
                className
            );
            loadingIndicatorColor = 'light';
            iconColorClass = iconColorClass || 'text-white';
            break;
        case 'red':
            className = clsx(
                link ? 'text-red hover:text-red-400' : `bg-red text-white ${!disabled && 'hover:bg-red-400'}`,
                className
            );
            loadingIndicatorColor = 'light';
            iconColorClass = iconColorClass || 'text-white';
            break;
        case 'white':
            className = clsx(
                link ? 'text-white hover:text-white dark:text-black dark:hover:text-grey-800' : `bg-white text-black dark:bg-black dark:text-white`,
                className
            );
            loadingIndicatorColor = 'dark';
            break;
        case 'outline':
            className = clsx(
                link ? 'text-black hover:text-grey-800 dark:text-white' : `border border-grey-300 bg-transparent text-black dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`,
                className
            );
            loadingIndicatorColor = 'dark';
            break;
        default:
            className = clsx(
                link ? ' text-black hover:text-grey-800 dark:text-white' : `text-grey-900 dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200 hover:text-black'}`,
                (outlineOnMobile && !link) && 'border border-grey-300 hover:border-transparent md:border-transparent',
                className
            );
            loadingIndicatorColor = 'dark';
            break;
        }

        className = clsx(
            (fullWidth && !link) && ' w-full',
            disabled ? 'opacity-40' : 'cursor-pointer',
            className
        );
    }

    const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';

    let labelClasses = '';
    labelClasses += (label && hideLabel) ? 'sr-only' : '';
    labelClasses += loading ? 'invisible' : '';

    iconSize = iconSize || ((size === 'sm') || (label && icon) ? 'sm' : 'md');

    const buttonChildren = <>
        {icon && <Icon className={iconClasses} colorClass={iconColorClass} name={icon} size={iconSize} />}
        <span className={labelClasses}>{label}</span>
        {loading && <div className='absolute flex'><LoadingIndicator color={loadingIndicatorColor} size={size}/><span className='sr-only'>Loading...</span></div>}
    </>;

    const buttonElement = React.createElement(tag, {className: className,
        'data-testid': testId,
        disabled: disabled,
        type: 'button',
        onClick: onClick,
        ...props}, buttonChildren);

    return buttonElement;
};

export default Button;