checkbox.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {useEffect, useRef} from 'react';
  2. import {css, Theme} from '@emotion/react';
  3. import styled, {Interpolation} from '@emotion/styled';
  4. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  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. * Styles to be applied to the hidden <input> element.
  14. */
  15. inputCss?: Interpolation<Theme>;
  16. /**
  17. * The size of the checkbox. Defaults to 'sm'.
  18. */
  19. size?: FormSize;
  20. }
  21. type CheckboxConfig = {
  22. borderRadius: string;
  23. box: string;
  24. icon: string;
  25. };
  26. const checkboxSizeMap: Record<FormSize, CheckboxConfig> = {
  27. xs: {box: '12px', borderRadius: '2px', icon: '10px'},
  28. sm: {box: '16px', borderRadius: '4px', icon: '12px'},
  29. md: {box: '22px', borderRadius: '6px', icon: '18px'},
  30. };
  31. const Checkbox = ({
  32. className,
  33. inputCss,
  34. checked = false,
  35. size = 'sm',
  36. ...props
  37. }: Props) => {
  38. const checkboxRef = useRef<HTMLInputElement>(null);
  39. // Support setting the indeterminate value, which is only possible through
  40. // setting this attribute
  41. useEffect(() => {
  42. if (checkboxRef.current) {
  43. checkboxRef.current.indeterminate = checked === 'indeterminate';
  44. }
  45. }, [checked]);
  46. return (
  47. <Wrapper {...{className, checked, size}}>
  48. <HiddenInput
  49. ref={checkboxRef}
  50. css={inputCss}
  51. checked={checked !== 'indeterminate' && checked}
  52. type="checkbox"
  53. {...props}
  54. />
  55. <StyledCheckbox aria-hidden checked={checked} size={size}>
  56. {checked === true && (
  57. <VariableWeightIcon viewBox="0 0 16 16" size={checkboxSizeMap[size].icon}>
  58. <path d="M2.86 9.14C4.42 10.7 6.9 13.14 6.86 13.14L12.57 3.43" />
  59. </VariableWeightIcon>
  60. )}
  61. {checked === 'indeterminate' && (
  62. <VariableWeightIcon viewBox="0 0 16 16" size={checkboxSizeMap[size].icon}>
  63. <path d="M3 8H13" />
  64. </VariableWeightIcon>
  65. )}
  66. </StyledCheckbox>
  67. {!props.disabled && (
  68. <InteractionStateLayer
  69. higherOpacity={checked === true || checked === 'indeterminate'}
  70. />
  71. )}
  72. </Wrapper>
  73. );
  74. };
  75. const Wrapper = styled('div')<{checked: Props['checked']; size: FormSize}>`
  76. position: relative;
  77. cursor: pointer;
  78. display: inline-flex;
  79. justify-content: flex-start;
  80. color: ${p => (p.checked ? p.theme.white : p.theme.textColor)};
  81. border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
  82. `;
  83. const HiddenInput = styled('input')`
  84. position: absolute;
  85. opacity: 0;
  86. top: 0;
  87. left: 0;
  88. height: 100%;
  89. width: 100%;
  90. margin: 0;
  91. padding: 0;
  92. cursor: pointer;
  93. &.focus-visible + * {
  94. ${p =>
  95. p.checked
  96. ? `
  97. box-shadow: ${p.theme.focus} 0 0 0 3px;
  98. `
  99. : `
  100. border-color: ${p.theme.focusBorder};
  101. box-shadow: ${p.theme.focusBorder} 0 0 0 1px;
  102. `}
  103. }
  104. &:disabled + * {
  105. ${p =>
  106. p.checked
  107. ? css`
  108. background: ${p.theme.disabled};
  109. `
  110. : css`
  111. background: ${p.theme.backgroundSecondary};
  112. border-color: ${p.theme.disabledBorder};
  113. `}
  114. }
  115. `;
  116. const StyledCheckbox = styled('div')<{
  117. checked: Props['checked'];
  118. size: FormSize;
  119. }>`
  120. position: relative;
  121. display: flex;
  122. align-items: center;
  123. justify-content: center;
  124. color: inherit;
  125. box-shadow: ${p => p.theme.dropShadowMedium} inset;
  126. width: ${p => checkboxSizeMap[p.size].box};
  127. height: ${p => checkboxSizeMap[p.size].box};
  128. border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
  129. pointer-events: none;
  130. ${p =>
  131. p.checked
  132. ? css`
  133. background: ${p.theme.active};
  134. border: 0;
  135. `
  136. : css`
  137. background: ${p.theme.background};
  138. border: 1px solid ${p.theme.gray200};
  139. `}
  140. `;
  141. const VariableWeightIcon = styled('svg')<{size: string}>`
  142. width: ${p => p.size};
  143. height: ${p => p.size};
  144. fill: none;
  145. stroke-linecap: round;
  146. stroke-linejoin: round;
  147. stroke: ${p => p.theme.white};
  148. stroke-width: calc(1.4px + ${p => p.size} * 0.04);
  149. `;
  150. export default Checkbox;