Design system.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 

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