Design system.
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 

209 行
5.5 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 UrlInputDerivedElementComponent = 'input' as const;
  6. /**
  7. * Derived HTML element of the {@link UrlInput} component.
  8. */
  9. export type UrlInputDerivedElement = HTMLElementTagNameMap[
  10. typeof UrlInputDerivedElementComponent
  11. ];
  12. /**
  13. * Props of the {@link UrlInput} component.
  14. */
  15. export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label' | '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. * 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. * Visual length of the input.
  46. */
  47. length?: number,
  48. }
  49. /**
  50. * Component for inputting URLs.
  51. */
  52. export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>((
  53. {
  54. label,
  55. hint,
  56. size = 'medium' as const,
  57. border = false,
  58. block = false,
  59. variant = 'default' as const,
  60. hiddenLabel = false,
  61. className,
  62. style,
  63. length,
  64. id: idProp,
  65. ...etcProps
  66. }: UrlInputProps,
  67. forwardedRef,
  68. ) => {
  69. const labelId = React.useId();
  70. const id = useFallbackId(idProp);
  71. return (
  72. <div
  73. className={tw(
  74. 'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
  75. 'focus-within:ring-4',
  76. {
  77. 'block': block,
  78. 'inline-block align-middle': !block,
  79. },
  80. className,
  81. )}
  82. style={style}
  83. data-testid="base"
  84. >
  85. {label && (
  86. <>
  87. <label
  88. data-testid="label"
  89. htmlFor={id}
  90. id={labelId}
  91. className={tw(
  92. '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',
  93. {
  94. 'sr-only': hiddenLabel,
  95. },
  96. {
  97. 'pr-10': size === 'small',
  98. 'pr-12': size === 'medium',
  99. 'pr-16': size === 'large',
  100. },
  101. )}
  102. >
  103. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  104. {label}
  105. </span>
  106. </label>
  107. {' '}
  108. </>
  109. )}
  110. <UrlInputDerivedElementComponent
  111. {...etcProps}
  112. size={length}
  113. ref={forwardedRef}
  114. id={id}
  115. aria-labelledby={labelId}
  116. type="url"
  117. data-testid="input"
  118. className={tw(
  119. 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
  120. 'focus:outline-0',
  121. 'disabled:cursor-not-allowed',
  122. {
  123. 'pl-4': variant === 'default',
  124. 'pl-1.5 pt-4': variant === 'alternate',
  125. },
  126. {
  127. 'pr-10 h-10 text-xxs': size === 'small',
  128. 'pr-12 h-12 text-xs': size === 'medium',
  129. 'pr-16 h-16': size === 'large',
  130. },
  131. )}
  132. />
  133. {hint && (
  134. <div
  135. data-testid="hint"
  136. className={tw(
  137. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
  138. {
  139. 'bottom-0 pl-4 pb-1': variant === 'default',
  140. 'top-0.5': variant === 'alternate',
  141. },
  142. {
  143. 'pt-2': variant === 'alternate' && size === 'small',
  144. 'pt-3': variant === 'alternate' && size !== 'small',
  145. },
  146. {
  147. 'pr-10': size === 'small',
  148. 'pr-12': size === 'medium',
  149. 'pr-16': size === 'large',
  150. },
  151. )}
  152. >
  153. <div
  154. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  155. >
  156. {hint}
  157. </div>
  158. </div>
  159. )}
  160. <div
  161. data-testid="indicator"
  162. className={tw(
  163. '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',
  164. {
  165. 'w-10': size === 'small',
  166. 'w-12': size === 'medium',
  167. 'w-16': size === 'large',
  168. },
  169. )}
  170. >
  171. <svg
  172. className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
  173. viewBox="0 0 24 24"
  174. role="presentation"
  175. >
  176. <circle cx="12" cy="12" r="10" />
  177. <line x1="2" y1="12" x2="22" y2="12" />
  178. <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
  179. </svg>
  180. </div>
  181. {border && (
  182. <span
  183. data-testid="border"
  184. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  185. />
  186. )}
  187. </div>
  188. );
  189. });
  190. UrlInput.displayName = 'UrlInput';
  191. UrlInput.defaultProps = {
  192. label: undefined,
  193. hint: undefined,
  194. size: 'medium',
  195. border: false,
  196. block: false,
  197. variant: 'default',
  198. hiddenLabel: false,
  199. length: undefined,
  200. };