import type { ButtonHTMLAttributes } from 'react';
import styled, { css } from 'styled-components';
import { spacing } from '../../spacing';
import { fontSize, fontWeight } from '../../style/theme';
import { getContrastText } from '../../utils';
import { Loader } from '../loader/Loader.component';
import { Tooltip, Props as TooltipProps } from '../tooltip/Tooltip.component';
export const FocusVisibleStyle = css`
outline: dashed ${spacing.r2} ${(props) => props.theme.selectedActive};
outline-offset: ${spacing.r2};
z-index: 1000;
`;
/** Props used by ButtonStyled for styling (no tooltip) */
type ButtonStyledProps = Omit<
ButtonHTMLAttributes,
'size' | 'label'
> & {
variant?: 'primary' | 'secondary' | 'danger' | 'outline';
size?: 'default' | 'inline';
disabled?: boolean;
onClick?: (event: React.MouseEvent) => void;
icon?: React.ReactNode;
label?: React.ReactNode;
isLoading?: boolean;
};
/** Button with a visible label - tooltip is optional */
type ButtonWithLabel = ButtonStyledProps & {
label: React.ReactNode;
tooltip?: Omit;
};
/** Icon-only button - requires either string tooltip OR explicit aria-label */
type IconOnlyButton = ButtonStyledProps & {
label?: never;
} & (
| {
tooltip: Omit & { overlay: string };
'aria-label'?: string;
}
| {
tooltip: Omit;
'aria-label': string;
}
);
export type Props = ButtonWithLabel | IconOnlyButton;
export const ButtonStyled = styled.button`
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
position: relative;
display: inline-flex;
user-select: none;
vertical-align: middle;
align-items: center;
justify-content: center;
box-sizing: border-box;
text-decoration: none;
border: none;
text-decoration: none;
font-family: 'Lato';
font-weight: ${fontWeight.base};
padding: ${spacing.r4} ${spacing.r8};
font-size: ${fontSize.base};
border-radius: ${spacing.r4};
white-space: nowrap;
height: ${(props) => (props.size === 'inline' ? spacing.r24 : spacing.r32)};
${(props) => {
const brand = props.theme;
switch (props.variant) {
case 'primary': {
const primaryTextColor = getContrastText(brand.buttonPrimary, brand.textPrimary, brand.textReverse) ?? brand.textPrimary;
return css`
background: ${brand.buttonPrimary};
background-clip: padding-box, border-box;
border: ${spacing.r1} solid transparent;
border-color: ${brand.buttonPrimary};
color: ${primaryTextColor};
&:hover:enabled {
cursor: pointer;
border: ${spacing.r1} solid ${brand.infoPrimary};
color: ${primaryTextColor};
}
// :focus-visible is the keyboard-only version of :focus
&:focus-visible:enabled {
${FocusVisibleStyle}
color: ${primaryTextColor};
}
&:active:enabled {
cursor: pointer;
color: ${primaryTextColor};
border: ${spacing.r1} solid ${brand.infoSecondary};
}
`;
}
case 'secondary':
return css`
background: ${brand.buttonSecondary};
background-clip: padding-box, border-box;
border: ${spacing.r1} solid transparent;
border-color: ${brand.buttonSecondary};
color: ${brand.textPrimary};
&:hover:enabled {
cursor: pointer;
border: ${spacing.r1} solid ${brand.infoPrimary};
color: ${brand.textPrimary};
}
&:focus-visible:enabled {
${FocusVisibleStyle}
color: ${brand.textPrimary};
}
&:active:enabled {
cursor: pointer;
color: ${brand.textPrimary};
border: ${spacing.r1} solid transparent;
border-color: ${brand.buttonSecondary};
}
`;
case 'danger':
return css`
background-color: ${brand.buttonDelete};
border: ${spacing.r1} solid ${brand.buttonDelete};
color: ${brand.statusCritical};
&:hover:enabled {
cursor: pointer;
border: ${spacing.r1} solid ${brand.infoPrimary};
}
&:focus-visible:enabled {
${FocusVisibleStyle}
}
&:active:enabled {
cursor: pointer;
border: ${spacing.r1} solid ${brand.infoSecondary};
}
`;
case 'outline':
return css`
border: ${spacing.r1} solid transparent;
border-color: ${brand.border}; // fallback for linear-gradient button themes
border-color: ${brand.buttonSecondary};
background-color: transparent;
color: ${brand.textPrimary};
&:hover:enabled {
cursor: pointer;
border-color: ${brand.infoPrimary};
color: ${brand.textPrimary};
&::before {
background-image: ${brand.buttonPrimary};
}
}
&:focus-visible:enabled {
${FocusVisibleStyle}
border-color: ${brand.buttonSecondary};
}
&:active:enabled {
cursor: pointer;
border: ${spacing.r1} solid ${brand.infoSecondary};
color: ${brand.textPrimary};
}
&::before {
content: '';
position: absolute;
inset: 0;
padding: ${spacing.r1};
border-radius: inherit;
mask: linear-gradient(white, white) content-box, linear-gradient(white, white);
mask-composite: exclude;
pointer-events: none;
}
`;
default:
}
}}
${(props) => {
return css`
${props.disabled
? `
cursor: not-allowed !important;
pointer-events: auto !important;
opacity: 0.5;
`
: null}
`;
}}
${(props) => {
const brand = props.theme;
return css`
${props.icon && !props.label && !props.variant
? `
background-color: transparent;
border: none;
color: ${brand.textSecondary};
&:hover{
cursor: pointer;
border: none;
color: ${brand.textPrimary};
}
&:focus-visible:enabled {
outline: dashed ${spacing.r2} ${brand.selectedActive};
}
&:active {
cursor: pointer;
color: ${brand.textPrimary};
}
`
: null}
`;
}}
`;
export const ButtonLabel = styled.span`
display: inline-flex;
justify-content: center;
align-items: center;
`;
export const ButtonIcon = styled.span<{ label: React.ReactNode }>`
${(props) =>
props.label &&
css`
padding-right: ${spacing.r8};
display: inline-flex;
justify-content: center;
align-items: center;
`}
`;
export const ButtonLoader = styled(Loader)<{ label; variant }>`
${(props) => {
return css`
margin-right: ${props.label ? spacing.r8 : '0'};
svg {
fill: ${props.variant === 'danger'
? props.theme.statusCritical
: props.variant === 'outline'
? props.theme.textPrimary
: props.theme.textSecondary};
}
`;
}}
`;
function Button({
variant,
size,
disabled,
label,
icon,
onClick,
tooltip,
isLoading,
...rest
}: Props) {
if (!icon && !label) {
console.warn(
'Please specify either icon or label prop for the button component.',
);
}
// For icon-only buttons, use tooltip.overlay as aria-label (typed as string for IconOnlyButton)
const buttonAriaLabel =
!label && icon && tooltip?.overlay
? (tooltip.overlay as string)
: undefined;
return (
{icon &&
(isLoading ? (
) : (
{icon}
))}
{!icon && isLoading && (
)}
{label}
);
}
export { Button };