checkbox.tsx 4.4 KB

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