Design system.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

243 rader
6.7 KiB

  1. import * as React from 'react';
  2. import { TextControl, tailwind } from '@tesseract-design/web-base';
  3. import { useFallbackId } from '@modal-sh/react-utils';
  4. const { tw } = tailwind;
  5. const MultilineTextInputDerivedElementComponent = 'textarea' as const;
  6. /**
  7. * Derived HTML element of the {@link MultilineTextInput} component.
  8. */
  9. export type MultilineTextInputDerivedElement = HTMLElementTagNameMap[
  10. typeof MultilineTextInputDerivedElementComponent
  11. ];
  12. /**
  13. * Props of the {@link MultilineTextInput} component.
  14. */
  15. export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode' | 'pattern'> {
  16. /**
  17. * Short textual description indicating the nature of the component's value.
  18. */
  19. label?: React.ReactNode,
  20. /**
  21. * Short textual description as guidelines for valid input values.
  22. */
  23. hint?: React.ReactNode,
  24. /**
  25. * Size of the component.
  26. */
  27. size?: TextControl.Size,
  28. /**
  29. * Additional description, usually graphical, indicating the nature of the component's value.
  30. */
  31. indicator?: React.ReactNode,
  32. /**
  33. * Should the component display a border?
  34. */
  35. border?: boolean,
  36. /**
  37. * Should the component occupy the whole width of its parent?
  38. */
  39. block?: boolean,
  40. /**
  41. * Style of the component.
  42. */
  43. variant?: TextControl.Variant,
  44. /**
  45. * Is the label hidden?
  46. */
  47. hiddenLabel?: boolean,
  48. }
  49. /**
  50. * Component for inputting textual values.
  51. *
  52. * This component supports multiline input and adjusts its layout accordingly.
  53. */
  54. export const MultilineTextInput = React.forwardRef<
  55. MultilineTextInputDerivedElement,
  56. MultilineTextInputProps
  57. >((
  58. {
  59. label,
  60. hint,
  61. indicator,
  62. size = 'medium' as const,
  63. border = false,
  64. block = false,
  65. variant = 'default' as const,
  66. hiddenLabel = false,
  67. className,
  68. style,
  69. id: idProp,
  70. ...etcProps
  71. }: MultilineTextInputProps,
  72. forwardedRef,
  73. ) => {
  74. const labelId = React.useId();
  75. const id = useFallbackId(idProp);
  76. return (
  77. <div
  78. className={tw(
  79. 'relative rounded ring-secondary/50 overflow-hidden group has-[:disabled]:opacity-50',
  80. 'focus-within:ring-4',
  81. {
  82. 'block': block,
  83. 'inline-block align-middle': !block,
  84. },
  85. className,
  86. )}
  87. style={style}
  88. data-testid="base"
  89. >
  90. {label && (
  91. <>
  92. <label
  93. data-testid="label"
  94. id={labelId}
  95. htmlFor={id}
  96. className={tw(
  97. 'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold group-focus-within:text-secondary text-primary leading-none bg-negative select-none',
  98. {
  99. 'sr-only': hiddenLabel,
  100. },
  101. {
  102. 'pr-1': !indicator,
  103. },
  104. {
  105. 'pr-10': indicator && size === 'small',
  106. 'pr-12': indicator && size === 'medium',
  107. 'pr-16': indicator && size === 'large',
  108. },
  109. )}
  110. >
  111. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  112. {label}
  113. </span>
  114. </label>
  115. {' '}
  116. </>
  117. )}
  118. <MultilineTextInputDerivedElementComponent
  119. {...etcProps}
  120. ref={forwardedRef}
  121. aria-labelledby={labelId}
  122. id={id}
  123. data-testid="input"
  124. style={{
  125. height: 0,
  126. }}
  127. className={tw(
  128. 'bg-negative rounded-inherit w-full peer block font-inherit',
  129. 'focus:outline-0',
  130. 'disabled:opacity-50 disabled:cursor-not-allowed',
  131. {
  132. 'resize': !block,
  133. 'resize-y': block,
  134. },
  135. {
  136. 'text-xxs': size === 'small',
  137. 'text-xs': size === 'medium',
  138. },
  139. {
  140. 'pl-4': variant === 'default',
  141. 'pl-1.5': variant === 'alternate',
  142. },
  143. {
  144. 'pt-4': variant === 'alternate' && size === 'small',
  145. 'pt-5': variant === 'alternate' && size === 'medium',
  146. 'pt-8': variant === 'alternate' && size === 'large',
  147. },
  148. {
  149. 'py-2.5': variant === 'default' && size === 'small',
  150. 'py-3': variant === 'default' && size === 'medium',
  151. 'py-5': variant === 'default' && size === 'large',
  152. },
  153. {
  154. 'pr-4': variant === 'default' && !indicator,
  155. 'pr-1.5': variant === 'alternate' && !indicator,
  156. },
  157. {
  158. 'pr-10': indicator && size === 'small',
  159. 'pr-12': indicator && size === 'medium',
  160. 'pr-16': indicator && size === 'large',
  161. },
  162. {
  163. 'min-h-10': size === 'small',
  164. 'min-h-12': size === 'medium',
  165. 'min-h-16': size === 'large',
  166. },
  167. )}
  168. />
  169. {hint && (
  170. <div
  171. data-testid="hint"
  172. className={tw(
  173. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
  174. {
  175. 'bottom-0 pl-4 pb-1': variant === 'default',
  176. 'top-0.5': variant === 'alternate',
  177. },
  178. {
  179. 'pt-2': variant === 'alternate' && size === 'small',
  180. 'pt-3': variant === 'alternate' && size !== 'small',
  181. },
  182. {
  183. 'pr-4': !indicator && variant === 'default',
  184. 'pr-1': !indicator && variant === 'alternate',
  185. },
  186. {
  187. 'pr-10': indicator && size === 'small',
  188. 'pr-12': indicator && size === 'medium',
  189. 'pr-16': indicator && size === 'large',
  190. },
  191. )}
  192. >
  193. <div
  194. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  195. >
  196. {hint}
  197. </div>
  198. </div>
  199. )}
  200. {indicator && (
  201. <div
  202. data-testid="indicator"
  203. className={tw(
  204. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
  205. {
  206. 'w-10': size === 'small',
  207. 'w-12': size === 'medium',
  208. 'w-16': size === 'large',
  209. },
  210. )}
  211. >
  212. {indicator}
  213. </div>
  214. )}
  215. {border && (
  216. <span
  217. data-testid="border"
  218. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  219. />
  220. )}
  221. </div>
  222. );
  223. });
  224. MultilineTextInput.displayName = 'MultilineTextInput';
  225. MultilineTextInput.defaultProps = {
  226. label: undefined,
  227. hint: undefined,
  228. size: 'medium',
  229. indicator: undefined,
  230. border: false,
  231. block: false,
  232. variant: 'default',
  233. hiddenLabel: false,
  234. };