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.
 
 
 

229 rivejä
6.4 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 EmailInputDerivedElementComponent = 'input' as const;
  6. /**
  7. * Derived HTML element of the {@link EmailInput} component.
  8. */
  9. export type EmailInputDerivedElement = HTMLElementTagNameMap[
  10. typeof EmailInputDerivedElementComponent
  11. ];
  12. /**
  13. * Props of the {@link EmailInput} component.
  14. */
  15. export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode' | 'pattern' | 'autoComplete'> {
  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. * Should the component display a border?
  30. */
  31. border?: boolean,
  32. /**
  33. * Should the component occupy the whole width of its parent?
  34. */
  35. block?: boolean,
  36. /**
  37. * Style of the component.
  38. */
  39. variant?: TextControl.Variant,
  40. /**
  41. * Is the label hidden?
  42. */
  43. hiddenLabel?: boolean,
  44. /**
  45. * Allowed domains for emails.
  46. */
  47. domains?: string[],
  48. /**
  49. * Visual length of the input.
  50. */
  51. length?: number,
  52. /**
  53. * Should the component display a list of suggested values?
  54. */
  55. autoComplete?: boolean,
  56. }
  57. /**
  58. * Component for inputting email addresses.
  59. */
  60. export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputProps>(
  61. (
  62. {
  63. label,
  64. hint,
  65. size = 'medium' as const,
  66. border = false as const,
  67. block = false as const,
  68. variant = 'default' as const,
  69. hiddenLabel = false as const,
  70. className,
  71. id: idProp,
  72. style,
  73. domains = [],
  74. length,
  75. autoComplete = false as const,
  76. ...etcProps
  77. }: EmailInputProps,
  78. forwardedRef,
  79. ) => {
  80. const labelId = React.useId();
  81. const id = useFallbackId(idProp);
  82. const pattern = (
  83. Array.isArray(domains) && domains.length > 0
  84. ? `.+?@(${domains.join('|').replace(/\./g, '\\.')})$`
  85. : undefined
  86. );
  87. return (
  88. <div
  89. className={tw(
  90. 'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
  91. 'focus-within:ring-4',
  92. {
  93. 'block': block,
  94. 'inline-block align-middle': !block,
  95. },
  96. className,
  97. )}
  98. style={style}
  99. >
  100. {label && (
  101. <>
  102. <label
  103. data-testid="label"
  104. id={labelId}
  105. htmlFor={id}
  106. className={tw(
  107. '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',
  108. {
  109. 'sr-only': hiddenLabel,
  110. },
  111. {
  112. 'pr-10': size === 'small',
  113. 'pr-12': size === 'medium',
  114. 'pr-16': size === 'large',
  115. },
  116. )}
  117. >
  118. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  119. {label}
  120. </span>
  121. </label>
  122. {' '}
  123. </>
  124. )}
  125. <EmailInputDerivedElementComponent
  126. {...etcProps}
  127. size={length}
  128. ref={forwardedRef}
  129. id={id}
  130. aria-labelledby={labelId}
  131. type="email"
  132. data-testid="input"
  133. autoComplete={autoComplete ? 'email' : undefined}
  134. pattern={pattern}
  135. className={tw(
  136. 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
  137. 'focus:outline-0',
  138. 'disabled:cursor-not-allowed',
  139. {
  140. 'pl-4': variant === 'default',
  141. 'pl-1.5 pt-4': variant === 'alternate',
  142. },
  143. {
  144. 'pr-10 h-10 text-xxs': size === 'small',
  145. 'pr-12 h-12 text-xs': size === 'medium',
  146. 'pr-16 h-16': size === 'large',
  147. },
  148. )}
  149. />
  150. {hint && (
  151. <div
  152. data-testid="hint"
  153. className={tw(
  154. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
  155. {
  156. 'bottom-0 pl-4 pb-1': variant === 'default',
  157. 'top-0.5': variant === 'alternate',
  158. },
  159. {
  160. 'pt-2': variant === 'alternate' && size === 'small',
  161. 'pt-3': variant === 'alternate' && size !== 'small',
  162. },
  163. {
  164. 'pr-10': size === 'small',
  165. 'pr-12': size === 'medium',
  166. 'pr-16': size === 'large',
  167. },
  168. )}
  169. >
  170. <div
  171. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  172. >
  173. {hint}
  174. </div>
  175. </div>
  176. )}
  177. <div
  178. data-testid="indicator"
  179. className={tw(
  180. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none text-primary group-focus-within:text-secondary',
  181. {
  182. 'w-10': size === 'small',
  183. 'w-12': size === 'medium',
  184. 'w-16': size === 'large',
  185. },
  186. )}
  187. >
  188. <svg
  189. className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
  190. viewBox="0 0 24 24"
  191. role="presentation"
  192. >
  193. <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
  194. <polyline points="22,6 12,13 2,6" />
  195. </svg>
  196. </div>
  197. {border && (
  198. <span
  199. data-testid="border"
  200. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  201. />
  202. )}
  203. </div>
  204. );
  205. },
  206. );
  207. EmailInput.displayName = 'EmailInput';
  208. EmailInput.defaultProps = {
  209. label: undefined,
  210. hint: undefined,
  211. size: 'medium' as const,
  212. border: false as const,
  213. block: false as const,
  214. variant: 'default' as const,
  215. hiddenLabel: false as const,
  216. domains: [],
  217. length: undefined,
  218. autoComplete: false as const,
  219. };