checkbox.tsx 5.0 KB

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