import classNames from "classnames"; import React, { ReactNode, forwardRef, ButtonHTMLAttributes, ForwardRefExoticComponent, useContext, } from "react"; import { Icon, IconProps, ICON_TYPE, ICON_STYLE_PREFIX } from "../Icon"; import { Link } from "../Link"; import { bem } from "../../utilities/bem"; import { ButtonGroup, ButtonGroupProps } from "./ButtonGroup"; import { THEME } from "../../types"; import { ThemeContext } from "../../contexts"; const cn = "Button"; export enum BUTTON_VARIANT { PRIMARY = "primary", SECONDARY = "secondary", UTILITY = "utility", DANGER = "danger", MINIMAL = "minimal", } export enum BUTTON_SIZE { MEDIUM = "medium", LARGE = "large", } export enum BUTTON_ICON_POSITION { LEFT = "left", RIGHT = "right", } export interface ButtonProps extends ButtonHTMLAttributes { /** * The component or HTML element that should be rendered */ as?: any; /** * If true, the button will expand to the full width available to it */ block?: boolean; /** * If true, the button will appear in an active state */ depressed?: boolean; /** * If true, disables the button and shows a loading spinner in place of the content */ isLoading?: boolean; /** * The FontAwesome icon to show. Can be used alongside button content or with no content for an "icon only" appearance */ icon?: ICON_TYPE; /** * A tailwind color that will apply to the icon provided */ iconColor?: string; /** * To which side of the button content the provided icon will be shown */ iconPosition?: BUTTON_ICON_POSITION; /** * If true, the FontAwesome Icon provided will spin */ iconSpin?: boolean; /** * Font Awesome Icon prefix to apply to icon */ iconPrefix?: ICON_STYLE_PREFIX; /** * Sets the size of the button */ size?: BUTTON_SIZE; /** * Sets the style of the button */ variant?: BUTTON_VARIANT; /** * Sets the theme of the button */ theme?: THEME; [key: string]: any; } function getButtonSizeClasses( size: BUTTON_SIZE, icon?: ICON_TYPE, children?: ReactNode, ): string { const buttonSizeClasses = { [BUTTON_SIZE.MEDIUM]: bem(cn, { m: "medium" }), [BUTTON_SIZE.LARGE]: bem(cn, { m: "large" }), }; const buttonIconOnlySizeClasses = { [BUTTON_SIZE.MEDIUM]: bem(cn, { m: "iconOnly--medium" }), [BUTTON_SIZE.LARGE]: bem(cn, { m: "iconOnly--large" }), }; return icon && !children ? buttonIconOnlySizeClasses[size] : buttonSizeClasses[size]; } // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34757 export type ButtonWithButtonGroup = ForwardRefExoticComponent & { Group: (props: ButtonGroupProps) => JSX.Element; }; export const Button = forwardRef( ( { as: Component = "button", children, // Display props className, block = false, size = BUTTON_SIZE.MEDIUM, variant = BUTTON_VARIANT.PRIMARY, depressed = false, isLoading = false, theme: themeProp, // Icon props icon, iconColor, iconSpin = false, iconPosition = BUTTON_ICON_POSITION.LEFT, iconPrefix, // Dom props disabled, type: typeProp, // Other ...rest }: ButtonProps, ref, ) => { const isDisabled = disabled || isLoading; const themeContext = useContext(ThemeContext); const theme = themeProp || themeContext; const buttonClassNames = classNames( bem(cn), bem(cn, { m: variant }), bem(cn, { m: theme }), block ? ["flex", "w-full"] : "inline-flex", getButtonSizeClasses(size, icon, children), depressed && bem(cn, { m: "depressed" }), isDisabled ? ["opacity-50", "pointer-events-none"] : "", className, ); const iconClassNames = classNames( bem(cn, { e: "icon" }), children && bem(cn, { e: "icon", m: iconPosition }), isLoading && bem(cn, { e: "icon", m: "invisible" }), ); const iconProps: IconProps | undefined = icon ? { icon, color: iconColor, spin: iconSpin, prefix: iconPrefix, className: iconClassNames, } : undefined; let type = typeProp; if (!typeProp && Component === "button") { type = "button"; } const propsIfLink = Component === Link ? { noStyles: true } : {}; return ( {icon && iconProps && iconPosition === BUTTON_ICON_POSITION.LEFT && ( )} {children} {icon && iconProps && iconPosition === BUTTON_ICON_POSITION.RIGHT && ( )} {isLoading && ( )} ); }, ) as ButtonWithButtonGroup; Button.Group = ButtonGroup;