@spaced-out/ui-design-system
Version:
Sense UI components library
287 lines (269 loc) • 8.68 kB
Flow
// @flow strict
import * as React from 'react';
import {classify} from '../../utils/classify';
import {CircularLoader} from '../CircularLoader';
import type {IconType} from '../Icon';
import {Icon} from '../Icon';
import {Truncate} from '../Truncate';
import css from './Button.module.css';
type ClassNames = $ReadOnly<{wrapper?: string, icon?: string, text?: string}>;
/**
* Note(Nishant): Although Button supports gradient as a type, its not currently customizable really.
* This only supports pre-defined gradient that moves from left to right.
* If someone wants to add more gradients, the expectation is that they would add it through a wrapper className.
*
* We could have taken an extra prop to take in the Gradient colors but that should not be encouraged
* as it would add an additional overhead on the component to figure out exact color values from string tokens
* and since this is rarely used type anyway, it should be avoided.
*/
export const BUTTON_TYPES = Object.freeze({
primary: 'primary',
secondary: 'secondary',
tertiary: 'tertiary',
ghost: 'ghost',
danger: 'danger',
gradient: 'gradient',
});
export const BUTTON_ACTION_TYPE = Object.freeze({
button: 'button',
submit: 'submit',
reset: 'reset',
});
export const BUTTON_SIZE = Object.freeze({
small: 'small',
medium: 'medium',
});
export type ButtonType = $Values<typeof BUTTON_TYPES>;
export type ButtonActionType = $Values<typeof BUTTON_ACTION_TYPE>;
export type ButtonSize = $Keys<typeof BUTTON_SIZE>;
export type BaseButtonProps = {
children?: React.Node,
disabled?: mixed,
actionType?: ButtonActionType,
onClick?: ?(SyntheticEvent<HTMLElement>) => mixed,
ariaLabel?: string,
tabIndex?: number,
isLoading?: boolean,
role?: string,
...
};
export type UnstyledButtonProps = {
...BaseButtonProps,
className?: string,
...
};
export type ButtonProps = {
...BaseButtonProps,
classNames?: ClassNames,
iconLeftName?: string,
iconLeftType?: IconType,
iconRightName?: string,
iconRightType?: IconType,
type?: ButtonType,
isFluid?: boolean,
size?: ButtonSize,
...
};
const ButtonTypeToIconColorMap = {
primary: 'inversePrimary',
secondary: 'clickable',
tertiary: 'primary',
ghost: 'primary',
danger: 'inversePrimary',
gradient: 'inversePrimary',
};
const ButtonTypeToLoaderColorMap = {
primary: 'colorTextInversePrimary',
secondary: 'colorTextClickable',
tertiary: 'colorTextPrimary',
ghost: 'colorTextPrimary',
danger: 'colorTextInversePrimary',
gradient: 'colorTextInversePrimary',
};
export const UnstyledButton: React$AbstractComponent<
UnstyledButtonProps,
HTMLButtonElement,
> = React.forwardRef<UnstyledButtonProps, HTMLButtonElement>(
(
{
disabled,
onClick,
className,
ariaLabel,
actionType,
tabIndex = 0,
isLoading,
role = 'button',
...props
}: UnstyledButtonProps,
ref,
) => (
<button
{...props}
{...(ariaLabel ? {'aria-label': ariaLabel} : {})}
className={className}
ref={ref}
role={role}
disabled={disabled}
tabIndex={disabled ? -1 : tabIndex}
type={actionType}
onClick={(event) => {
if (disabled || isLoading) {
event.preventDefault();
} else if (onClick) {
onClick(event);
}
}}
/>
),
);
export const Button: React$AbstractComponent<ButtonProps, HTMLButtonElement> =
React.forwardRef<ButtonProps, HTMLButtonElement>(
(
{
classNames,
children,
iconLeftName = '',
iconLeftType = 'regular',
iconRightName = '',
iconRightType = 'regular',
type = 'primary',
isFluid = false,
disabled = false,
actionType = 'button',
size = 'medium',
isLoading,
...props
}: ButtonProps,
ref,
) => (
<UnstyledButton
{...props}
actionType={actionType}
disabled={disabled}
isLoading={isLoading}
className={classify(
css.button,
{
[css.primary]: type === 'primary',
[css.secondary]: type === 'secondary',
[css.tertiary]: type === 'tertiary',
[css.ghost]: type === 'ghost',
[css.danger]: type === 'danger',
[css.gradient]: type === 'gradient',
[css.disabled]: disabled,
[css.small]: size === 'small',
[css.medium]: size === 'medium',
[css.isFluid]: isFluid === true,
[css.withIconLeft]: !!iconLeftName,
[css.withIconRight]: !!iconRightName,
[css.withBothIcon]: !!(iconLeftName && iconRightName),
[css.onlyIcon]: (iconLeftName || iconRightName) && !children,
},
classNames?.wrapper,
)}
ref={ref}
>
<div className={css.buttonRow}>
{/* Has no icon, only child */}
{!(iconLeftName || iconRightName) ? (
<div className={classify(css.textContainer, classNames?.text)}>
{isLoading && (
<div className={css.loader}>
<CircularLoader
size={size}
colorToken={
disabled
? 'colorTextDisabled'
: ButtonTypeToLoaderColorMap[type]
}
/>
</div>
)}
<Truncate className={classify({[css.hidden]: isLoading})}>
{children}
</Truncate>
</div>
) : // has icon, but no child
children == null ? (
<>
{isLoading && (
<div className={css.loader}>
<CircularLoader
size={size}
colorToken={
disabled
? 'colorTextDisabled'
: ButtonTypeToLoaderColorMap[type]
}
/>
</div>
)}
<Icon
name={iconLeftName || iconRightName}
color={disabled ? 'disabled' : ButtonTypeToIconColorMap[type]}
size={size === 'medium' ? 'medium' : 'small'}
type={iconLeftName ? iconLeftType : iconRightType}
className={classify(
{[css.hidden]: isLoading},
classNames?.icon,
)}
/>
</>
) : (
// has icon _and_ child
(iconLeftName || iconRightName) && (
<>
{iconLeftName && (
<Icon
name={iconLeftName}
color={
disabled ? 'disabled' : ButtonTypeToIconColorMap[type]
}
size={size === 'medium' ? 'medium' : 'small'}
type={iconLeftType}
className={classify(
{[css.hidden]: isLoading},
classNames?.icon,
)}
/>
)}
<div className={classify(css.textContainer, classNames?.text)}>
{isLoading && (
<div className={css.loader}>
<CircularLoader
size={size}
colorToken={
disabled
? 'colorTextDisabled'
: ButtonTypeToLoaderColorMap[type]
}
/>
</div>
)}
<Truncate className={classify({[css.hidden]: isLoading})}>
{children}
</Truncate>
</div>
{iconRightName && (
<Icon
name={iconRightName}
color={
disabled ? 'disabled' : ButtonTypeToIconColorMap[type]
}
size={size === 'medium' ? 'medium' : 'small'}
type={iconRightType}
className={classify(
{[css.hidden]: isLoading},
classNames?.icon,
)}
/>
)}
</>
)
)}
</div>
</UnstyledButton>
),
);
Button.name = Button.displayName = 'Button';