Common front-end components for Web using the Tesseract design system, written for React. https://make.modal.sh/tesseract/web/react/common
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

TextInput.tsx 8.7 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import styled from 'styled-components'
  4. import stringify from '../../services/stringify'
  5. import { Size, SizeMap } from '../../services/utilities'
  6. const MIN_HEIGHTS: SizeMap<string | number> = {
  7. small: '2.5rem',
  8. medium: '3rem',
  9. large: '4rem',
  10. }
  11. const LABEL_VERTICAL_PADDING_SIZES: SizeMap<string | number> = {
  12. small: '0.125rem',
  13. medium: '0.25rem',
  14. large: '0.5rem',
  15. }
  16. const VERTICAL_PADDING_SIZES: SizeMap<string | number> = {
  17. small: '0.6rem',
  18. medium: '0.85rem',
  19. large: '1.25rem',
  20. }
  21. const INPUT_FONT_SIZES: SizeMap<string | number> = {
  22. small: '0.85em',
  23. medium: '0.85em',
  24. large: '1em',
  25. }
  26. const SECONDARY_TEXT_SIZES: SizeMap<string | number> = {
  27. small: '0.65em',
  28. medium: '0.75em',
  29. large: '0.85em',
  30. }
  31. const ComponentBase = styled('div')({
  32. position: 'relative',
  33. borderRadius: '0.25rem',
  34. fontFamily: 'var(--font-family-base, sans-serif)',
  35. maxWidth: '100%',
  36. ':focus-within': {
  37. '--color-accent': 'var(--color-active, Highlight)',
  38. },
  39. })
  40. ComponentBase.displayName = 'div'
  41. const CaptureArea = styled('label')({
  42. display: 'block',
  43. borderRadius: 'inherit',
  44. overflow: 'hidden',
  45. })
  46. CaptureArea.displayName = 'label'
  47. const LabelWrapper = styled('span')({
  48. color: 'var(--color-accent, blue)',
  49. boxSizing: 'border-box',
  50. position: 'absolute',
  51. top: 0,
  52. left: 0,
  53. paddingLeft: '0.5rem',
  54. fontSize: '0.85em',
  55. maxWidth: '100%',
  56. overflow: 'hidden',
  57. textOverflow: 'ellipsis',
  58. whiteSpace: 'nowrap',
  59. fontWeight: 'bolder',
  60. zIndex: 2,
  61. pointerEvents: 'none',
  62. transitionProperty: 'color',
  63. lineHeight: 1,
  64. userSelect: 'none',
  65. })
  66. LabelWrapper.displayName = 'span'
  67. const Border = styled('span')({
  68. borderColor: 'var(--color-accent, blue)',
  69. boxSizing: 'border-box',
  70. display: 'inline-block',
  71. borderWidth: '0.125rem',
  72. borderStyle: 'solid',
  73. position: 'absolute',
  74. top: 0,
  75. left: 0,
  76. width: '100%',
  77. height: '100%',
  78. borderRadius: 'inherit',
  79. zIndex: 2,
  80. pointerEvents: 'none',
  81. transitionProperty: 'border-color',
  82. '::before': {
  83. position: 'absolute',
  84. top: 0,
  85. left: 0,
  86. width: '100%',
  87. height: '100%',
  88. content: "''",
  89. borderRadius: '0.125rem',
  90. opacity: 0.5,
  91. pointerEvents: 'none',
  92. },
  93. [`${ComponentBase}:focus-within &::before`]: {
  94. boxShadow: '0 0 0 0.375rem var(--color-accent, blue)',
  95. },
  96. })
  97. const Input = styled('input')({
  98. display: 'block',
  99. backgroundColor: 'var(--color-bg, white)',
  100. color: 'var(--color-fg, black)',
  101. verticalAlign: 'top',
  102. paddingTop: 0,
  103. paddingBottom: 0,
  104. boxSizing: 'border-box',
  105. position: 'relative',
  106. border: 0,
  107. borderRadius: 'inherit',
  108. paddingLeft: '1rem',
  109. margin: 0,
  110. font: 'inherit',
  111. minHeight: '4rem',
  112. minWidth: '8rem',
  113. maxWidth: '100%',
  114. width: '100%',
  115. zIndex: 1,
  116. transitionProperty: 'background-color, color',
  117. ':focus': {
  118. outline: 0,
  119. color: 'var(--color-fg, black)',
  120. },
  121. ':disabled': {
  122. cursor: 'not-allowed',
  123. },
  124. })
  125. Input.displayName = 'input'
  126. const TextArea = styled('textarea')({
  127. display: 'block',
  128. backgroundColor: 'var(--color-bg, white)',
  129. color: 'var(--color-fg, black)',
  130. verticalAlign: 'top',
  131. width: '100%',
  132. boxSizing: 'border-box',
  133. position: 'relative',
  134. border: 0,
  135. borderRadius: 'inherit',
  136. paddingLeft: '1rem',
  137. margin: 0,
  138. font: 'inherit',
  139. minHeight: '4rem',
  140. minWidth: '3rem',
  141. maxWidth: '100%',
  142. zIndex: 1,
  143. transitionProperty: 'background-color, color',
  144. ':focus': {
  145. outline: 0,
  146. },
  147. })
  148. TextArea.displayName = 'textarea'
  149. const HintWrapper = styled('span')({
  150. boxSizing: 'border-box',
  151. position: 'absolute',
  152. bottom: 0,
  153. left: 0,
  154. paddingLeft: '1rem',
  155. fontSize: '0.85em',
  156. opacity: 0.5,
  157. maxWidth: '100%',
  158. overflow: 'hidden',
  159. textOverflow: 'ellipsis',
  160. whiteSpace: 'nowrap',
  161. zIndex: 2,
  162. pointerEvents: 'none',
  163. lineHeight: 1,
  164. userSelect: 'none',
  165. })
  166. const IndicatorWrapper = styled('span')({
  167. color: 'var(--color-accent, blue)',
  168. boxSizing: 'border-box',
  169. position: 'absolute',
  170. bottom: 0,
  171. right: 0,
  172. display: 'grid',
  173. placeContent: 'center',
  174. padding: '0 1rem',
  175. zIndex: 1,
  176. pointerEvents: 'none',
  177. transitionProperty: 'color',
  178. lineHeight: 1,
  179. userSelect: 'none',
  180. })
  181. const propTypes = {
  182. /**
  183. * Short textual description indicating the nature of the component's value.
  184. */
  185. label: PropTypes.any,
  186. /**
  187. * Short textual description as guidelines for valid input values.
  188. */
  189. hint: PropTypes.any,
  190. /**
  191. * Size of the component.
  192. */
  193. size: PropTypes.oneOf<Size>(['small', 'medium', 'large']),
  194. /**
  195. * Additional description, usually graphical, indicating the nature of the component's value.
  196. */
  197. indicator: PropTypes.node,
  198. /**
  199. * Should the component accept multiple lines of input?
  200. */
  201. multiline: PropTypes.bool,
  202. /**
  203. * Is the component active?
  204. */
  205. disabled: PropTypes.bool,
  206. /**
  207. * Should the component resize itself to show all its value?
  208. */
  209. autoResize: PropTypes.bool,
  210. /**
  211. * Placeholder of the component when there is no value.
  212. */
  213. placeholder: PropTypes.string,
  214. /**
  215. * How many rows should the component display if it accepts multiline input?
  216. */
  217. rows: PropTypes.number,
  218. /**
  219. * Does the button display a border?
  220. */
  221. border: PropTypes.bool,
  222. /**
  223. * Event handler triggered when the component changes value.
  224. */
  225. onChange: PropTypes.func,
  226. /**
  227. * Event handler triggered when the component receives focus.
  228. */
  229. onFocus: PropTypes.func,
  230. /**
  231. * Event handler triggered when the component loses focus.
  232. */
  233. onBlur: PropTypes.func,
  234. }
  235. type Props = PropTypes.InferProps<typeof propTypes>
  236. const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props>(
  237. (
  238. {
  239. label = '',
  240. hint = '',
  241. indicator = null,
  242. size = 'medium',
  243. multiline = false,
  244. disabled = false,
  245. autoResize = false,
  246. placeholder = '',
  247. rows = 3,
  248. border = false,
  249. onChange,
  250. onFocus,
  251. onBlur,
  252. ...etcProps
  253. },
  254. ref,
  255. ) => (
  256. <ComponentBase
  257. style={{
  258. opacity: disabled ? 0.5 : undefined,
  259. }}
  260. >
  261. {
  262. border
  263. && (
  264. <Border />
  265. )
  266. }
  267. <CaptureArea>
  268. <LabelWrapper
  269. style={{
  270. paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!],
  271. paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!],
  272. paddingRight: indicator ? MIN_HEIGHTS[size!] : '0.5rem',
  273. fontSize: SECONDARY_TEXT_SIZES[size!],
  274. }}
  275. >
  276. {stringify(label)}
  277. </LabelWrapper>
  278. {stringify(label).length > 0 && ' '}
  279. {multiline && (
  280. <TextArea
  281. {...etcProps}
  282. onChange={onChange as React.ChangeEventHandler}
  283. onFocus={onFocus as React.FocusEventHandler}
  284. onBlur={onBlur as React.FocusEventHandler}
  285. placeholder={placeholder!}
  286. ref={ref as React.Ref<HTMLTextAreaElement>}
  287. disabled={disabled!}
  288. rows={rows!}
  289. style={{
  290. height: `calc(${MIN_HEIGHTS[size!]} * ${rows})`,
  291. fontSize: INPUT_FONT_SIZES[size!],
  292. minHeight: MIN_HEIGHTS[size!],
  293. paddingTop: VERTICAL_PADDING_SIZES[size!],
  294. paddingBottom: VERTICAL_PADDING_SIZES[size!],
  295. paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
  296. }}
  297. />
  298. )}
  299. {!multiline && (
  300. <Input
  301. {...etcProps}
  302. onChange={onChange as React.ChangeEventHandler}
  303. onFocus={onFocus as React.FocusEventHandler}
  304. onBlur={onBlur as React.FocusEventHandler}
  305. placeholder={placeholder!}
  306. ref={ref as React.Ref<HTMLInputElement>}
  307. disabled={disabled!}
  308. style={{
  309. fontSize: INPUT_FONT_SIZES[size!],
  310. minHeight: MIN_HEIGHTS[size!],
  311. paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
  312. }}
  313. />
  314. )}
  315. </CaptureArea>
  316. {stringify(hint).length > 0 && ' '}
  317. {stringify(hint).length > 0 && (
  318. <HintWrapper
  319. style={{
  320. paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!],
  321. paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!],
  322. paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
  323. fontSize: SECONDARY_TEXT_SIZES[size!],
  324. }}
  325. >
  326. ({stringify(hint)})
  327. </HintWrapper>
  328. )}
  329. {(indicator as PropTypes.ReactComponentLike) && (
  330. <IndicatorWrapper
  331. style={{
  332. width: MIN_HEIGHTS[size!],
  333. height: MIN_HEIGHTS[size!],
  334. }}
  335. >
  336. {indicator}
  337. </IndicatorWrapper>
  338. )}
  339. </ComponentBase>
  340. ),
  341. )
  342. TextInput.propTypes = propTypes
  343. TextInput.displayName = 'TextInput'
  344. export default TextInput