/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ import React from 'react' import slugify from 'slugify' import Link from 'next/link' import fragment from './fragment.graphql' import classNames from 'classnames' import useProductMeta from '@hashicorp/platform-product-meta' import InlineSvg from '@hashicorp/react-inline-svg' import svgArrowRight from './icons/arrow-right.svg?include' import svgExternalLink from './icons/external-link.svg?include' import svgCornerRightDown from './icons/corner-right-down.svg?include' import svgDownload from './icons/download.svg?include' import s from './style.module.css' import sTheme from './theme.module.css' import useHover from './hooks/use-hover' import normalizeButtonTheme from './helpers/normalizeButtonTheme.js' import { Size, LinkType, IconObject, Theme, IconProps } from './types' const linkTypeToIcon = { inbound: svgArrowRight, outbound: svgExternalLink, anchor: svgCornerRightDown, download: svgDownload, } interface ButtonProps { title: string url?: string label?: string external?: boolean theme?: Theme ga_prefix?: string onClick?: React.MouseEventHandler<HTMLAnchorElement> & React.MouseEventHandler<HTMLButtonElement> disabled?: boolean className?: string linkType?: LinkType icon?: IconObject size?: Size /** * Note: Removing this TS "any" seems like it'll be quite a task. * One path forward might be to fully separate our * "ButtonButton" and "AnchorButton", and ask the consumer * to choose the correct component based on whether they need * a <a> or <button>. * Task ref: https://app.asana.com/0/1100423001970639/1200880473915564/f */ [attr: string]: $TSFixMe } function Button({ title, url, label, external, theme = { variant: 'primary', brand: 'hashicorp', background: 'light', }, ga_prefix, onClick, disabled, className, linkType, icon, size = 'medium', ...attrs }: ButtonProps): React.ReactElement { const [hoverRef, isHovered] = useHover< HTMLAnchorElement & HTMLButtonElement >() const themeObj = normalizeButtonTheme(theme) const { themeClass } = useProductMeta(themeObj.brand) const gaSlug = slugify(title, { lower: true }) const isExternal = url && (linkType === 'outbound' || external) const Elem = url ? 'a' : 'button' const iconProps = linkType && linkTypeToIcon[linkType] ? ({ svg: linkTypeToIcon[linkType], position: icon?.position || 'right', animationId: linkType, isAnimated: icon?.isAnimated || true, isHovered, size, } as IconProps) : { ...icon, position: icon?.position || 'right', size, isHovered } const hasIcon = iconProps && iconProps.svg const hasRightIcon = hasIcon && iconProps.position !== 'left' const hasLeftIcon = hasIcon && iconProps.position === 'left' const content = ( <Elem className={classNames( s.root, themeClass, s[`size-${size}`], sTheme[`variant-${themeObj.variant}`], { [sTheme['brand-neutral']]: themeObj.brand === 'neutral' }, sTheme[`background-${themeObj.background}`], className )} data-ga-button={`${ga_prefix ? ga_prefix + ' | ' : ''}${gaSlug}`} ref={hoverRef} rel={isExternal ? 'noopener' : undefined} target={isExternal ? '_blank' : undefined} onClick={onClick} disabled={disabled} aria-label={label} {...attrs} > {hasLeftIcon && <Icon {...iconProps} svg={iconProps.svg!} />} <span className={s.text}>{title}</span> {hasRightIcon && <Icon {...iconProps} svg={iconProps.svg!} />} </Elem> ) return url ? ( <Link href={url} legacyBehavior> {content} </Link> ) : ( content ) } function Icon({ svg, position, animationId, isAnimated, isHovered, size, }: IconProps) { return ( <InlineSvg className={classNames( s.icon, s[`size-${size}`], s[`at-${position}`], { [s.isHovered]: isHovered }, { [s[`animation-${animationId}`]]: isAnimated } )} src={svg} /> ) } Button.fragmentSpec = { fragment } export default Button