checkbox.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import {useEffect, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  4. import {IconCheckmark, IconSubtract} from 'sentry/icons';
  5. import {FormSize} from 'sentry/utils/theme';
  6. type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement>;
  7. interface Props extends Omit<CheckboxProps, 'checked' | 'size'> {
  8. /**
  9. * Is the checkbox active? Supports 'indeterminate'
  10. */
  11. checked?: CheckboxProps['checked'] | 'indeterminate';
  12. /**
  13. *
  14. */
  15. size?: FormSize;
  16. }
  17. type CheckboxConfig = {borderRadius: string; box: string; icon: string};
  18. const checkboxSizeMap: Record<FormSize, CheckboxConfig> = {
  19. xs: {box: '12px', icon: '10px', borderRadius: '2px'},
  20. sm: {box: '16px', icon: '12px', borderRadius: '4px'},
  21. md: {box: '22px', icon: '16px', borderRadius: '6px'},
  22. };
  23. const Checkbox = ({checked = false, size = 'sm', ...props}: Props) => {
  24. const checkboxRef = useRef<HTMLInputElement>(null);
  25. // Support setting the indeterminate value, which is only possible through
  26. // setting this attribute
  27. useEffect(() => {
  28. if (checkboxRef.current) {
  29. checkboxRef.current.indeterminate = checked === 'indeterminate';
  30. }
  31. }, [checked]);
  32. return (
  33. <Wrapper {...{checked, size}}>
  34. <HiddenInput
  35. checked={checked !== 'indeterminate' && checked}
  36. type="checkbox"
  37. {...props}
  38. />
  39. <StyledCheckbox aria-hidden checked={checked} size={size}>
  40. {checked === true && <IconCheckmark size={checkboxSizeMap[size].icon} />}
  41. {checked === 'indeterminate' && (
  42. <IconSubtract size={checkboxSizeMap[size].icon} />
  43. )}
  44. </StyledCheckbox>
  45. <InteractionStateLayer
  46. higherOpacity={checked === true || checked === 'indeterminate'}
  47. />
  48. </Wrapper>
  49. );
  50. };
  51. const Wrapper = styled('div')<{checked: Props['checked']; size: FormSize}>`
  52. position: relative;
  53. cursor: pointer;
  54. display: inline-flex;
  55. justify-content: flex-start;
  56. color: ${p => (p.checked ? p.theme.white : p.theme.textColor)};
  57. border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
  58. `;
  59. const HiddenInput = styled('input')`
  60. position: absolute;
  61. opacity: 0;
  62. height: 100%;
  63. width: 100%;
  64. top: 0;
  65. left: 0;
  66. margin: 0;
  67. cursor: pointer;
  68. &.focus-visible + * {
  69. box-shadow: ${p => p.theme.focusBorder} 0 0 0 2px;
  70. }
  71. &:disabled + * {
  72. background: ${p => (p.checked ? p.theme.disabled : p.theme.backgroundSecondary)};
  73. border-color: ${p => (p.checked ? p.theme.disabled : p.theme.disabledBorder)};
  74. }
  75. `;
  76. const StyledCheckbox = styled('div')<{
  77. checked: Props['checked'];
  78. size: FormSize;
  79. }>`
  80. position: relative;
  81. display: flex;
  82. align-items: center;
  83. justify-content: center;
  84. color: inherit;
  85. box-shadow: ${p => p.theme.dropShadowLight} inset;
  86. width: ${p => checkboxSizeMap[p.size].box};
  87. height: ${p => checkboxSizeMap[p.size].box};
  88. border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
  89. background: ${p => (p.checked ? p.theme.active : p.theme.background)};
  90. border: 1px solid ${p => (p.checked ? p.theme.active : p.theme.gray200)};
  91. pointer-events: none;
  92. `;
  93. export default Checkbox;