import {forwardRef as reactForwardRef, useCallback} from 'react'; import isPropValid from '@emotion/is-prop-valid'; import {css, Theme} from '@emotion/react'; import styled from '@emotion/styled'; import InteractionStateLayer from 'sentry/components/interactionStateLayer'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import {Tooltip} from 'sentry/components/tooltip'; import HookStore from 'sentry/stores/hookStore'; import {space} from 'sentry/styles/space'; import mergeRefs from 'sentry/utils/mergeRefs'; /** * 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, 'label' | 'size' | 'title'> { /** * Used when you want to overwrite the default Reload event key for analytics */ analyticsEventKey?: string; /** * Used when you want to send an Amplitude Event. By default, Amplitude events are not sent so * you must pass in a eventName to send an Amplitude event. */ analyticsEventName?: string; /** * Adds extra parameters to the analytics tracking */ analyticsParams?: Record; /** * 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; /** * 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'; /** * The size of the button */ size?: ButtonSize; /** * 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; } interface ButtonPropsWithoutAriaLabel extends BaseButtonProps { children: React.ReactNode; } interface ButtonPropsWithAriaLabel extends BaseButtonProps { 'aria-label': string; children?: never; } export type ButtonProps = ButtonPropsWithoutAriaLabel | ButtonPropsWithAriaLabel; function BaseButton({ size = 'md', to, busy, href, title, icon, children, 'aria-label': ariaLabel, borderless, translucentBorder, priority, disabled = false, tooltipProps, onClick, analyticsEventName, analyticsEventKey, analyticsParams, ...buttonProps }: ButtonProps) { // 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 accessibleLabel = ariaLabel ?? (typeof children === 'string' ? children : undefined); const useButtonTracking = HookStore.get('react-hook:use-button-tracking')[0]; const buttonTracking = useButtonTracking?.({ analyticsEventName, analyticsEventKey, analyticsParams: { priority, href, ...analyticsParams, }, 'aria-label': accessibleLabel || '', }); const handleClick = useCallback( (e: React.MouseEvent) => { // Don't allow clicks when disabled or busy if (disabled || busy) { e.preventDefault(); e.stopPropagation(); return; } buttonTracking?.(); onClick?.(e); }, [disabled, busy, onClick, buttonTracking] ); const hasChildren = Array.isArray(children) ? children.some(child => !!child) : !!children; // Buttons come in 4 flavors: , , , and