import {forwardRef as reactForwardRef, useCallback} from 'react'; import isPropValid from '@emotion/is-prop-valid'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import Tooltip from 'sentry/components/tooltip'; import space from 'sentry/styles/space'; import mergeRefs from 'sentry/utils/mergeRefs'; import {Theme} from 'sentry/utils/theme'; /** * The button can actually also be an anchor or React router Link (which seems * to be poorly typed as `any`). So this is a bit of a workaround to receive * the proper html attributes. */ type ButtonElement = HTMLButtonElement & HTMLAnchorElement & any; type TooltipProps = React.ComponentProps; type ButtonSize = 'zero' | 'xs' | 'sm' | 'md'; interface BaseButtonProps extends Omit< React.ButtonHTMLAttributes, 'ref' | 'label' | 'size' | 'title' > { /** * Positions the text within the button. */ align?: 'center' | 'left' | 'right'; /** * Used by ButtonBar to determine active status. */ barId?: string; /** * Removes borders from the button. */ borderless?: boolean; /** * Indicates that the button is "doing" something. */ busy?: boolean; /** * Test ID for the button. */ 'data-test-id'?: string; /** * Disables the button, assigning appropriate aria attributes and disallows * interactions with the button. */ disabled?: boolean; /** * For use with `href` and `data:` or `blob:` schemes. Tells the browser to * download the contents. * * See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-download */ download?: HTMLAnchorElement['download']; /** * The button is an external link. Similar to the `Link` `external` property. */ external?: boolean; /** * @internal Used in the Button forwardRef */ forwardRef?: React.Ref; /** * When set the button acts as an anchor link. Use with `external` to have * the link open in a new tab. */ href?: string; /** * The icon to render inside of the button. The size will be set * appropriately based on the size of the button. */ icon?: React.ReactNode; /** * Used when the button is part of a form. */ name?: string; /** * Callback for when the button is clicked. */ onClick?: (e: React.MouseEvent) => void; /** * The semantic "priority" of the button. Use `primary` when the action is * contextually the primary action, `danger` if the button will do something * destructive, `link` for visual similarity to a link. */ priority?: 'default' | 'primary' | 'danger' | 'link' | 'form'; /** * @deprecated Use `external` */ rel?: HTMLAnchorElement['rel']; /** * The size of the button */ size?: ButtonSize; /** * @deprecated Use `external` */ target?: HTMLAnchorElement['target']; /** * Display a tooltip for the button. */ title?: TooltipProps['title']; /** * Similar to `href`, but for internal links within the app. */ to?: string | object; /** * Additional properites for the Tooltip when `title` is set. */ tooltipProps?: Omit; /** * Userful in scenarios where the border of the button should blend with the * background behind the button. */ translucentBorder?: boolean; } export interface ButtonPropsWithoutAriaLabel extends BaseButtonProps { children: React.ReactNode; } export interface ButtonPropsWithAriaLabel extends BaseButtonProps { 'aria-label': string; children?: never; } export type ButtonProps = ButtonPropsWithoutAriaLabel | ButtonPropsWithAriaLabel; type Url = ButtonProps['to'] | ButtonProps['href']; function BaseButton({ size = 'md', to, busy, href, title, icon, children, 'aria-label': ariaLabel, borderless, translucentBorder, align = 'center', priority, disabled = false, tooltipProps, onClick, ...buttonProps }: ButtonProps) { // Intercept onClick and propagate const handleClick = useCallback( (e: React.MouseEvent) => { // Don't allow clicks when disabled or busy if (disabled || busy) { e.preventDefault(); e.stopPropagation(); return; } if (typeof onClick !== 'function') { return; } onClick(e); }, [onClick, busy, disabled] ); function getUrl(prop: T): T | undefined { return disabled ? undefined : prop; } // Fallbacking aria-label to string children is not necessary as screen readers natively understand that scenario. // Leaving it here for a bunch of our tests that query by aria-label. const screenReaderLabel = ariaLabel || (typeof children === 'string' ? children : undefined); const hasChildren = Array.isArray(children) ? children.some(child => !!child) : !!children; // Buttons come in 4 flavors: , , , and