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.
 
 
 

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