Design system.
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

264 linhas
7.3 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 ComboBoxDerivedElementComponent = 'input' as const;
  6. /**
  7. * Derived HTML element of the {@link ComboBox} component.
  8. */
  9. export type ComboBoxDerivedElement = HTMLElementTagNameMap[
  10. typeof ComboBoxDerivedElementComponent
  11. ];
  12. /**
  13. * Props of the {@link ComboBox} component.
  14. */
  15. export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> {
  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. * Type of the component value.
  42. */
  43. type?: TextControl.InputType,
  44. /**
  45. * Style of the component.
  46. */
  47. variant?: TextControl.Variant,
  48. /**
  49. * Is the label hidden?
  50. */
  51. hiddenLabel?: boolean,
  52. /**
  53. * Input mode of the component.
  54. */
  55. inputMode?: TextControl.InputMode,
  56. /**
  57. * Visual length of the input.
  58. */
  59. length?: number,
  60. }
  61. /**
  62. * Component for inputting textual values.
  63. *
  64. * This component supports multiline input and adjusts its layout accordingly.
  65. */
  66. export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>((
  67. {
  68. label,
  69. hint,
  70. indicator,
  71. size = 'medium' as const,
  72. border = false as const,
  73. block = false as const,
  74. type = 'text' as const,
  75. variant = 'default' as const,
  76. hiddenLabel = false as const,
  77. className,
  78. children,
  79. inputMode = 'text' as const,
  80. id: idProp,
  81. style,
  82. length,
  83. ...etcProps
  84. }: ComboBoxProps,
  85. forwardedRef,
  86. ) => {
  87. const labelId = React.useId();
  88. const datalistId = React.useId();
  89. const id = useFallbackId(idProp);
  90. let resultInputMode = inputMode as React.HTMLProps<ComboBoxDerivedElement>['inputMode'];
  91. if (type === 'text' && resultInputMode === 'search') {
  92. resultInputMode = 'text';
  93. } else if (type === 'search' && resultInputMode === 'text') {
  94. resultInputMode = 'search';
  95. }
  96. return (
  97. <>
  98. <datalist
  99. id={datalistId}
  100. >
  101. {children}
  102. </datalist>
  103. <div
  104. className={tw(
  105. 'relative rounded ring-secondary/50 overflow-hidden group has-[:disabled]:opacity-50',
  106. 'focus-within:ring-4',
  107. {
  108. 'block': block,
  109. 'inline-block align-middle': !block,
  110. },
  111. className,
  112. )}
  113. style={style}
  114. data-testid="base"
  115. >
  116. {label && (
  117. <>
  118. <label
  119. data-testid="label"
  120. id={labelId}
  121. htmlFor={id}
  122. className={tw(
  123. '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',
  124. {
  125. 'sr-only': hiddenLabel,
  126. },
  127. {
  128. 'pr-1': !indicator,
  129. },
  130. {
  131. 'pr-10': indicator && size === 'small',
  132. 'pr-12': indicator && size === 'medium',
  133. 'pr-16': indicator && size === 'large',
  134. },
  135. )}
  136. >
  137. <span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  138. {label}
  139. </span>
  140. </label>
  141. {' '}
  142. </>
  143. )}
  144. <ComboBoxDerivedElementComponent
  145. {...etcProps}
  146. size={length}
  147. ref={forwardedRef}
  148. id={id}
  149. aria-labelledby={labelId}
  150. type={type}
  151. inputMode={resultInputMode}
  152. list={datalistId}
  153. data-testid="input"
  154. className={tw(
  155. 'bg-negative rounded-inherit w-full peer block font-inherit',
  156. 'focus:outline-0',
  157. 'disabled:opacity-50 disabled:cursor-not-allowed',
  158. {
  159. 'text-xxs': size === 'small',
  160. 'text-xs': size === 'medium',
  161. },
  162. {
  163. 'pl-4': variant === 'default',
  164. 'pl-1.5': variant === 'alternate',
  165. },
  166. {
  167. 'pt-4': variant === 'alternate',
  168. },
  169. {
  170. 'pr-4': variant === 'default' && !indicator,
  171. 'pr-1.5': variant === 'alternate' && !indicator,
  172. },
  173. {
  174. 'pr-10': indicator && size === 'small',
  175. 'pr-12': indicator && size === 'medium',
  176. 'pr-16': indicator && size === 'large',
  177. },
  178. {
  179. 'h-10': size === 'small',
  180. 'h-12': size === 'medium',
  181. 'h-16': size === 'large',
  182. },
  183. )}
  184. />
  185. {hint && (
  186. <div
  187. data-testid="hint"
  188. className={tw(
  189. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
  190. {
  191. 'bottom-0 pl-4 pb-1': variant === 'default',
  192. 'top-0.5': variant === 'alternate',
  193. },
  194. {
  195. 'pt-2': variant === 'alternate' && size === 'small',
  196. 'pt-3': variant === 'alternate' && size !== 'small',
  197. },
  198. {
  199. 'pr-4': !indicator && variant === 'default',
  200. 'pr-1': !indicator && variant === 'alternate',
  201. },
  202. {
  203. 'pr-10': indicator && size === 'small',
  204. 'pr-12': indicator && size === 'medium',
  205. 'pr-16': indicator && size === 'large',
  206. },
  207. )}
  208. >
  209. <div
  210. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  211. >
  212. {hint}
  213. </div>
  214. </div>
  215. )}
  216. {indicator && (
  217. <div
  218. data-testid="indicator"
  219. className={tw(
  220. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
  221. {
  222. 'w-10': size === 'small',
  223. 'w-12': size === 'medium',
  224. 'w-16': size === 'large',
  225. },
  226. )}
  227. >
  228. {indicator}
  229. </div>
  230. )}
  231. {border && (
  232. <span
  233. data-testid="border"
  234. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  235. />
  236. )}
  237. </div>
  238. </>
  239. );
  240. });
  241. ComboBox.displayName = 'ComboBox' as const;
  242. ComboBox.defaultProps = {
  243. label: undefined,
  244. hint: undefined,
  245. indicator: undefined,
  246. size: 'medium' as const,
  247. border: false as const,
  248. block: false as const,
  249. type: 'text' as const,
  250. variant: 'default' as const,
  251. hiddenLabel: false as const,
  252. inputMode: 'text' as const,
  253. length: undefined,
  254. };