Design system.
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 

249 líneas
7.0 KiB

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