Design system.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

245 lines
6.9 KiB

  1. import * as React from 'react';
  2. import clsx from 'clsx';
  3. import { Button } from '@tesseract-design/web-base';
  4. import plugin from 'tailwindcss/plugin';
  5. import { useFallbackId } from '@modal-sh/react-utils';
  6. /**
  7. * Derived HTML element of the {@link ToggleButton} component.
  8. */
  9. export type ToggleButtonDerivedElement = HTMLInputElement;
  10. /**
  11. * Props of the {@link ToggleButton} component.
  12. */
  13. export interface ToggleButtonProps extends Omit<React.InputHTMLAttributes<ToggleButtonDerivedElement>, 'type' | 'size'> {
  14. /**
  15. * Should the component occupy the whole width of its parent?
  16. */
  17. block?: boolean;
  18. /**
  19. * Should the component's content use minimal space?
  20. */
  21. compact?: boolean;
  22. /**
  23. * Size of the component.
  24. */
  25. size?: Button.Size;
  26. /**
  27. * Complementary content of the component.
  28. */
  29. subtext?: React.ReactNode;
  30. /**
  31. * Short complementary content displayed at the edge of the component.
  32. */
  33. badge?: React.ReactNode;
  34. /**
  35. * Variant of the component.
  36. */
  37. variant?: Button.Variant;
  38. /**
  39. * Is the component in an indeterminate state?
  40. */
  41. indeterminate?: boolean;
  42. }
  43. export const toggleButtonPlugin = plugin(({ addComponents, }) => {
  44. addComponents({
  45. '.toggle-button': {
  46. '& + label > :first-child > :first-child': {
  47. 'display': 'none',
  48. },
  49. '&:checked + label > :first-child > :first-child': {
  50. 'display': 'block',
  51. },
  52. '& + label > :first-child > :first-child + *': {
  53. 'display': 'none',
  54. },
  55. '&:indeterminate + label > :first-child > :first-child + *': {
  56. 'display': 'block',
  57. },
  58. },
  59. });
  60. });
  61. /**
  62. * Component for toggling a Boolean value.
  63. */
  64. export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>((
  65. {
  66. children,
  67. block = false,
  68. compact = false,
  69. size = 'medium' as const,
  70. id: idProp,
  71. className,
  72. subtext,
  73. badge,
  74. variant = 'bare' as const,
  75. indeterminate = false,
  76. style,
  77. ...etcProps
  78. },
  79. forwardedRef,
  80. ) => {
  81. const defaultRef = React.useRef<ToggleButtonDerivedElement>(null);
  82. const ref = forwardedRef ?? defaultRef;
  83. const id = useFallbackId(idProp);
  84. React.useEffect(() => {
  85. if (typeof ref === 'function') {
  86. const defaultElement = defaultRef.current as ToggleButtonDerivedElement;
  87. defaultElement.indeterminate = indeterminate;
  88. ref(defaultElement);
  89. return;
  90. }
  91. const element = ref.current as ToggleButtonDerivedElement;
  92. element.indeterminate = indeterminate;
  93. }, [indeterminate, defaultRef, ref]);
  94. return (
  95. <span
  96. className="contents"
  97. >
  98. <input
  99. {...etcProps}
  100. ref={typeof ref === 'function' ? defaultRef : ref}
  101. type="checkbox"
  102. id={id}
  103. className="sr-only peer toggle-button"
  104. />
  105. <label
  106. data-testid="button"
  107. htmlFor={id}
  108. className={clsx(
  109. 'items-center justify-start rounded overflow-hidden ring-secondary/50 leading-none select-none cursor-pointer',
  110. 'peer-focus:outline-0 peer-focus:ring-4 peer-focus:ring-secondary/50',
  111. 'active:ring-tertiary/50 active:ring-4',
  112. 'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:ring-0',
  113. 'text-primary peer-disabled:text-primary peer-focus:text-secondary peer-checked:text-tertiary active:text-tertiary',
  114. {
  115. 'flex w-full': block,
  116. 'inline-flex max-w-full align-middle': !block,
  117. },
  118. {
  119. 'pl-2 gap-2 pr-2': compact,
  120. 'pl-4 gap-4 pr-4': !compact,
  121. },
  122. {
  123. 'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare',
  124. 'bg-negative text-primary peer-disabled:text-primary peer-focus:text-secondary peer-checked:text-tertiary active:text-tertiary': variant !== 'filled',
  125. 'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled',
  126. },
  127. {
  128. 'h-10': size === 'small',
  129. 'h-12': size === 'medium',
  130. 'h-16': size === 'large',
  131. },
  132. className,
  133. )}
  134. style={style}
  135. >
  136. <span
  137. className={clsx(
  138. 'w-6 h-6 block rounded border-2 p-0.5 box-border',
  139. {
  140. 'border-current': variant !== 'filled',
  141. 'border-negative': variant === 'filled',
  142. },
  143. )}
  144. >
  145. <svg
  146. className={clsx(
  147. 'w-full h-full fill-none stroke-3 linejoin-round linecap-round',
  148. {
  149. 'stroke-negative': variant === 'filled',
  150. 'stroke-current': variant !== 'filled',
  151. },
  152. )}
  153. viewBox="0 0 24 24"
  154. role="presentation"
  155. >
  156. <polyline
  157. points="20 6 9 17 4 12"
  158. />
  159. </svg>
  160. <svg
  161. className={clsx(
  162. 'w-full h-full fill-none stroke-3 linejoin-round linecap-round',
  163. {
  164. 'stroke-negative': variant === 'filled',
  165. 'stroke-current': variant !== 'filled',
  166. },
  167. )}
  168. viewBox="0 0 24 24"
  169. role="presentation"
  170. >
  171. <polyline
  172. points="20 12 4 12"
  173. />
  174. </svg>
  175. </span>
  176. <span
  177. className={clsx(
  178. 'contents',
  179. {
  180. 'text-current': variant !== 'filled',
  181. 'text-negative': variant === 'filled',
  182. },
  183. )}
  184. >
  185. <span
  186. className={clsx(
  187. 'flex-auto min-w-0',
  188. )}
  189. >
  190. <span
  191. className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
  192. data-testid="children"
  193. >
  194. {children}
  195. </span>
  196. {subtext && (
  197. <>
  198. <span className="sr-only">
  199. {' - '}
  200. </span>
  201. <span
  202. className="block h-[1.3em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded font-bold text-xs"
  203. data-testid="subtext"
  204. >
  205. {subtext}
  206. </span>
  207. </>
  208. )}
  209. </span>
  210. {badge && (
  211. <>
  212. <span className="sr-only">
  213. {' - '}
  214. </span>
  215. <span
  216. data-testid="badge"
  217. >
  218. {badge}
  219. </span>
  220. </>
  221. )}
  222. </span>
  223. </label>
  224. </span>
  225. );
  226. });
  227. ToggleButton.displayName = 'ToggleButton';
  228. ToggleButton.defaultProps = {
  229. block: false,
  230. compact: false,
  231. size: 'medium',
  232. subtext: undefined,
  233. badge: undefined,
  234. indeterminate: false,
  235. variant: 'bare',
  236. };