Files
2024-02-15 17:01:50 +00:00

198 lines
4.9 KiB
TypeScript

import {useEffect, useRef} from 'react';
import type {Theme} from '@emotion/react';
import {css} from '@emotion/react';
import type {Interpolation} from '@emotion/styled';
import styled from '@emotion/styled';
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
import type {FormSize} from 'sentry/utils/theme';
type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement>;
interface Props extends Omit<CheckboxProps, 'checked' | 'size'> {
/**
* The background color of the filled in checkbox.
*/
checkboxColor?: string;
/**
* Is the checkbox active? Supports 'indeterminate'
*/
checked?: CheckboxProps['checked'] | 'indeterminate';
/**
* Styles to be applied to the hidden <input> element.
*/
inputCss?: Interpolation<Theme>;
/**
* Whether to invert the colors of the checkbox and the checkmark.
*/
invertColors?: boolean;
/**
* The size of the checkbox. Defaults to 'sm'.
*/
size?: FormSize;
}
type CheckboxConfig = {
borderRadius: string;
box: string;
icon: string;
};
const checkboxSizeMap: Record<FormSize, CheckboxConfig> = {
xs: {box: '12px', borderRadius: '2px', icon: '10px'},
sm: {box: '16px', borderRadius: '4px', icon: '12px'},
md: {box: '22px', borderRadius: '6px', icon: '18px'},
};
function Checkbox({
checkboxColor,
className,
inputCss,
checked = false,
invertColors,
size = 'sm',
...props
}: Props) {
const checkboxRef = useRef<HTMLInputElement>(null);
// Support setting the indeterminate value, which is only possible through
// setting this attribute
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = checked === 'indeterminate';
}
}, [checked]);
return (
<Wrapper {...{className, checked, size}}>
<HiddenInput
ref={checkboxRef}
css={inputCss}
checked={checked !== 'indeterminate' && checked}
type="checkbox"
{...props}
/>
<StyledCheckbox
aria-hidden
checked={checked}
size={size}
color={checkboxColor}
invertColors={invertColors}
>
{checked === true && (
<VariableWeightIcon
viewBox="0 0 16 16"
size={checkboxSizeMap[size].icon}
invertColors={props.disabled ? false : invertColors}
>
<path d="M2.86 9.14C4.42 10.7 6.9 13.14 6.86 13.14L12.57 3.43" />
</VariableWeightIcon>
)}
{checked === 'indeterminate' && (
<VariableWeightIcon viewBox="0 0 16 16" size={checkboxSizeMap[size].icon}>
<path d="M3 8H13" />
</VariableWeightIcon>
)}
</StyledCheckbox>
{!props.disabled && (
<InteractionStateLayer
higherOpacity={checked === true || checked === 'indeterminate'}
/>
)}
</Wrapper>
);
}
const Wrapper = styled('div')<{checked: Props['checked']; size: FormSize}>`
position: relative;
cursor: pointer;
display: inline-flex;
justify-content: flex-start;
color: ${p => (p.checked ? p.theme.white : p.theme.textColor)};
border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
`;
const HiddenInput = styled('input')`
position: absolute;
opacity: 0;
top: 0;
left: 0;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
cursor: pointer;
&:focus-visible + * {
${p =>
p.checked
? `
box-shadow: ${p.theme.focus} 0 0 0 3px;
`
: `
border-color: ${p.theme.focusBorder};
box-shadow: ${p.theme.focusBorder} 0 0 0 1px;
`}
}
&:disabled + * {
${p =>
p.checked
? css`
background: ${p.theme.disabled};
`
: css`
background: ${p.theme.backgroundSecondary};
border-color: ${p.theme.disabledBorder};
`}
}
`;
const StyledCheckbox = styled('div')<{
checked: Props['checked'];
size: FormSize;
color?: Props['checkboxColor'];
invertColors?: Props['invertColors'];
}>`
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: inherit;
box-shadow: ${p => p.theme.dropShadowMedium} inset;
width: ${p => checkboxSizeMap[p.size].box};
height: ${p => checkboxSizeMap[p.size].box};
border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
pointer-events: none;
${p =>
p.invertColors
? css`
background: ${p.theme.white};
border: 0;
`
: p.checked
? css`
background: ${p.color ?? p.theme.active};
border: 0;
`
: css`
background: ${p.theme.background};
border: 1px solid ${p.theme.gray200};
`}
`;
const VariableWeightIcon = styled('svg')<{size: string; invertColors?: boolean}>`
width: ${p => p.size};
height: ${p => p.size};
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke: ${p => (p.invertColors ? p.theme.active : p.theme.white)};
stroke-width: calc(1.4px + ${p => p.size} * 0.04);
`;
export default Checkbox;