import * as React from 'react' import { useBoxSize, useThemeValue } from '../system' import Icon from '../Icon' import { BoxSizes, MediaQueries } from '../index' import Link from '../Link' import Spinner from '../Spinner' import { StackView, StackViewProps } from '../StackView' import Tooltip from '../Tooltip' import { mergeProps } from '../utils' import { useAccessibilityViolation } from '../hooks/use-accessibility-violation' import Input from './Input' type ButtonProps = { /** * Disables button by removing click handlers and making the button transparent. */ disabled?: boolean /** * Opens the linked destination in a new window or tab. */ external?: boolean /** * Renders an [Icon](/icon) in the center of the button. Pass a string or any valid `Icon` props. * Must pass a [`title`](/button#title) or [`tooltip.title`](/tooltip#title) when using icon-only buttons. */ icon?: string | Object /** * Same as above, but renders an icon in the left portion of the button. */ iconLeft?: string | Object /** * Same as above, but renders an icon in the right portion of the button. */ iconRight?: string | Object /** * Sizes the button using values defined in [boxSizes](/theming) */ size?: keyof BoxSizes /** * Renders a [``](/spinner) in the center of the button. Accepts any valid Spinner props. */ spinner?: any /** * Uses a 1:1 ratio for sizing the button. */ square?: boolean /** * Uses theme defined in [`buttons.theme`](/theming). Pass `false` to not use theme at all. */ theme?: string | boolean /** * Renders a text string in the center of the button. */ title?: string | number /** * Where the browser should navigate to when pressed. */ to?: string /** * Wraps button in a [``](./tooltip). Accepts any valid Tooltip props. */ tooltip?: string | Object /** * Determines the modified style of the button. */ variant?: 'fill' | 'outline' | 'naked' } & StackViewProps type Props = React.RefAttributes & React.HTMLAttributes & ButtonProps & MediaQueries export function Button({ children, disabled, external, icon, iconLeft, iconRight, onClick, onKeyDown, size, spinner, square, theme = 'default', title, to, tooltip, variant, ...restProps }: Props) { const buttonThemes = useThemeValue('button.themes') let { boxSize, fontSize, lineHeight, paddingHorizontal, paddingHorizontalDense, paddingVertical, } = useBoxSize(size) let buttonProps: any = { alignment: 'center', axis: 'horizontal', distribution: 'center', fontSize, fontWeight: 700, inline: true, lineHeight, position: 'relative', radius: 3, role: 'button', strokeAlign: 'inside', strokeWeight: 1, as: (restProps as any).href || to ? 'a' : 'div', userSelect: 'none', zIndex: 1, } let spinnerColor if (theme) { const buttonTheme = buttonThemes[theme] if (buttonTheme) { const buttonThemeVariant = buttonTheme[variant || 'fill'] if (buttonThemeVariant) { buttonProps = { ...buttonProps, ...buttonThemeVariant, } } else { console.warn( `Could not find "${variant}" in "${theme}" theme. Check your ThemeProvider and make sure "${variant}" variant exists in "${theme}".` ) } } else if (theme === 'false') { console.warn( `The string "false" is not a valid theme. Please pass a boolean %cfalse%c instead.`, 'color:red', 'color:inherit' ) } else { console.warn( `Could not find "${theme}" in button.themes. Check your ThemeProvider and make sure "${theme}" exists.` ) } } // normalize strings that should end up as objects if (typeof icon === 'string') { icon = { name: icon } } if (typeof iconLeft === 'string') { iconLeft = { name: iconLeft } } if (typeof iconRight === 'string') { iconRight = { name: iconRight } } if (typeof tooltip === 'string') { tooltip = { title: tooltip } } // if using an icon only button, apply dense padding and possibly an aria label if (icon) { paddingHorizontal = paddingHorizontalDense buttonProps['aria-label'] = tooltip ? tooltip['title'] : title } // square width and height if applicable props present if (square) { buttonProps.width = boxSize buttonProps.height = boxSize } else if (icon) { buttonProps.minWidth = boxSize buttonProps.minHeight = boxSize } // handle padding based on the incoming radius or whether icons are present or not if (restProps['radius'] === 'pill') { buttonProps.paddingHorizontal = paddingHorizontal * 1.25 } else if (restProps['radius'] === 'pill-left') { buttonProps.paddingLeft = paddingHorizontal * 1.25 buttonProps.paddingRight = paddingHorizontal } else if (restProps['radius'] === 'pill-right') { buttonProps.paddingLeft = paddingHorizontal buttonProps.paddingRight = paddingHorizontal * 1.25 } else if (!(icon && !iconLeft && !iconRight)) { buttonProps.paddingHorizontal = paddingHorizontal buttonProps.paddingVertical = paddingVertical } // clear text and/or icons when spinner is present if (spinner) { spinnerColor = buttonProps.color buttonProps.color = 'transparent' if (icon) { icon['color'] = 'transparent' } if (iconLeft) { iconLeft['color'] = 'transparent' } if (iconRight) { iconRight['color'] = 'transparent' } } // don't allow interacting with button when disabled if (disabled) { buttonProps['aria-disabled'] = true buttonProps.cursor = 'not-allowed' buttonProps.opacity = 0.65 } // don't apply hover/active styles or events when disabled or spinner is present if (disabled || spinner) { buttonProps.hover = undefined buttonProps.active = undefined } else { buttonProps.focus = { zIndex: 5 } buttonProps.cursor = 'pointer' buttonProps.touchAction = 'manipulation' buttonProps.tabIndex = 0 buttonProps.onClick = onClick buttonProps.onKeyDown = (event) => { if (onClick && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault() onClick(event) } if (onKeyDown) { onKeyDown(event) } } } // apply rel and target for external links if (external) { buttonProps.rel = 'noopener noreferrer' buttonProps.target = '_blank' } // apply href value when specified if (to) { restProps['href'] = to } // apply stroke defaults and higher z-index when hovering to show outline in group properly if (variant === 'outline') { buttonProps = { ...buttonProps, hover: { strokeAlign: 'inside', strokeWeight: 1, zIndex: 2, ...buttonProps.hover, }, } } if (process.env.NODE_ENV !== 'production') { if (icon && !(title || (tooltip && (tooltip as any).title))) { const { tooltipProps, wrapperProps } = useAccessibilityViolation( <> Icon only buttons must define either a{' '} tooltip {' '} or{' '} title {' '} prop. , `UIKit: