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.
 
 
 

216 lines
5.7 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. import { PluginCreator } from 'tailwindcss/types/config';
  5. const { tw } = tailwind;
  6. const RadioButtonDerivedElementComponent = 'input' as const;
  7. /**
  8. * Derived HTML element of the {@link RadioButton} component.
  9. */
  10. export type RadioButtonDerivedElement = HTMLElementTagNameMap[
  11. typeof RadioButtonDerivedElementComponent
  12. ];
  13. /**
  14. * Props of the {@link RadioButton} component.
  15. */
  16. export interface RadioButtonProps extends Omit<React.InputHTMLAttributes<RadioButtonDerivedElement>, 'type' | 'size'> {
  17. /**
  18. * Should the component occupy the whole width of its parent?
  19. */
  20. block?: boolean;
  21. /**
  22. * Should the component's content use minimal space?
  23. */
  24. compact?: boolean;
  25. /**
  26. * Size of the component.
  27. */
  28. size?: Button.Size;
  29. /**
  30. * Complementary content of the component.
  31. */
  32. subtext?: React.ReactNode;
  33. /**
  34. * Short complementary content displayed at the edge of the component.
  35. */
  36. badge?: React.ReactNode;
  37. /**
  38. * Variant of the component.
  39. */
  40. variant?: Button.Variant;
  41. }
  42. export const radioButtonPlugin: PluginCreator = ({ addComponents }) => {
  43. addComponents({
  44. '.radio-button': {
  45. '& + label > :first-child > :first-child': {
  46. 'display': 'none',
  47. },
  48. '&:checked + label > :first-child > :first-child': {
  49. 'display': 'block',
  50. },
  51. },
  52. });
  53. };
  54. /**
  55. * Component for selecting a single value from an array of choices grouped by name.
  56. *
  57. * This component is displayed as a regular button.
  58. */
  59. export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButtonProps>((
  60. {
  61. children,
  62. block = false as const,
  63. compact = false as const,
  64. size = 'medium' as const,
  65. id: idProp,
  66. className,
  67. subtext,
  68. badge,
  69. variant = 'bare' as const,
  70. style,
  71. ...etcProps
  72. },
  73. forwardedRef,
  74. ) => {
  75. const id = useFallbackId(idProp);
  76. return (
  77. <span
  78. className="contents"
  79. >
  80. <RadioButtonDerivedElementComponent
  81. {...etcProps}
  82. ref={forwardedRef}
  83. type="radio"
  84. id={id}
  85. className="sr-only peer radio-button"
  86. />
  87. <label
  88. style={style}
  89. htmlFor={id}
  90. data-testid="button"
  91. className={tw(
  92. 'items-center justify-start rounded overflow-hidden ring-secondary/50 leading-none select-none cursor-pointer relative',
  93. 'peer-focus:outline-0 peer-focus:ring-4 peer-focus:ring-secondary/50',
  94. 'active:ring-tertiary/50 active:ring-4',
  95. 'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:ring-0',
  96. 'text-primary focus:text-secondary active:text-tertiary',
  97. {
  98. 'flex w-full': block,
  99. 'inline-flex max-w-full align-middle': !block,
  100. },
  101. {
  102. 'pl-2 gap-2 pr-2': compact,
  103. 'pl-4 gap-4 pr-4': !compact,
  104. },
  105. {
  106. 'bg-current': variant === 'filled',
  107. },
  108. {
  109. 'text-primary peer-disabled:text-primary peer-focus:text-secondary peer-checked:text-tertiary active:text-tertiary': variant !== 'filled',
  110. 'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled',
  111. },
  112. {
  113. 'h-10': size === 'small',
  114. 'h-12': size === 'medium',
  115. 'h-16': size === 'large',
  116. },
  117. className,
  118. )}
  119. >
  120. <span
  121. className={tw(
  122. 'w-6 h-6 block rounded-full border-2 p-0.5 box-border',
  123. {
  124. 'border-current': variant !== 'filled',
  125. 'border-negative': variant === 'filled',
  126. },
  127. )}
  128. >
  129. <span
  130. className={tw(
  131. 'w-full h-full rounded-full bg-current',
  132. {
  133. 'text-current': variant !== 'filled',
  134. 'text-negative': variant === 'filled',
  135. },
  136. )}
  137. />
  138. </span>
  139. <span
  140. className={tw(
  141. 'contents',
  142. {
  143. 'text-current': variant !== 'filled',
  144. 'text-negative': variant === 'filled',
  145. },
  146. )}
  147. >
  148. <span
  149. className={tw(
  150. 'flex-auto min-w-0',
  151. )}
  152. >
  153. <span
  154. className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
  155. data-testid="children"
  156. >
  157. {children}
  158. </span>
  159. {subtext && (
  160. <>
  161. <span className="sr-only">
  162. {' - '}
  163. </span>
  164. <span
  165. className="block h-[1.3em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded font-bold text-xs"
  166. data-testid="subtext"
  167. >
  168. {subtext}
  169. </span>
  170. </>
  171. )}
  172. </span>
  173. {badge && (
  174. <>
  175. <span className="sr-only">
  176. {' - '}
  177. </span>
  178. <span
  179. data-testid="badge"
  180. >
  181. {badge}
  182. </span>
  183. </>
  184. )}
  185. </span>
  186. {
  187. variant !== 'bare'
  188. && (
  189. <span
  190. className="border-current border-2 rounded-inherit absolute w-full h-full top-0 left-0"
  191. />
  192. )
  193. }
  194. </label>
  195. </span>
  196. );
  197. });
  198. RadioButton.displayName = 'RadioButton' as const;
  199. RadioButton.defaultProps = {
  200. badge: undefined,
  201. block: false as const,
  202. compact: false as const,
  203. subtext: undefined,
  204. size: 'medium' as const,
  205. variant: 'bare' as const,
  206. };