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: icon only buttons must define either a "tooltip" or "title" prop for proper accessibility.`
)
tooltip = tooltipProps
restProps = mergeProps(restProps, wrapperProps)
}
if (restProps['type'] === 'submit') {
throw Error(
`UIKit: does not support type="submit" please add an explicit onClick handler.`
)
}
}
return (
{iconLeft && (
)}
{icon ? (
) : (
title || children
)}
{iconRight && (
)}
{spinner && (
)}
)
}
Button.Input = Input