textarea.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import TextAreaFoundation from '@douyinfe/semi-foundation/input/textareaFoundation';
  5. import { cssClasses } from '@douyinfe/semi-foundation/input/constants';
  6. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  7. import '@douyinfe/semi-foundation/input/textarea.scss';
  8. import { noop, omit, isFunction, isUndefined, isObject, throttle } from 'lodash';
  9. import type { DebouncedFunc } from 'lodash';
  10. import { IconClear } from '@douyinfe/semi-icons';
  11. import ResizeObserver from '../resizeObserver';
  12. const prefixCls = cssClasses.PREFIX;
  13. type OmitTextareaAttr =
  14. | 'onChange'
  15. | 'onInput'
  16. | 'prefix'
  17. | 'size'
  18. | 'onFocus'
  19. | 'onBlur'
  20. | 'onKeyDown'
  21. | 'onKeyPress'
  22. | 'onKeyUp'
  23. | 'onResize';
  24. export type AutosizeRow = {
  25. minRows?: number;
  26. maxRows?: number
  27. };
  28. export interface TextAreaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, OmitTextareaAttr> {
  29. autosize?: boolean | AutosizeRow;
  30. borderless?: boolean;
  31. placeholder?: string;
  32. value?: string;
  33. rows?: number;
  34. cols?: number;
  35. maxCount?: number;
  36. validateStatus?: ValidateStatus;
  37. defaultValue?: string;
  38. disabled?: boolean;
  39. readonly?: boolean;
  40. autoFocus?: boolean;
  41. showCounter?: boolean;
  42. showClear?: boolean;
  43. onClear?: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
  44. onChange?: (value: string, e: React.MouseEvent<HTMLTextAreaElement>) => void;
  45. onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
  46. onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
  47. onInput?: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
  48. onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
  49. onKeyUp?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
  50. onKeyPress?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
  51. onEnterPress?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
  52. onPressEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
  53. onResize?: (data: { height: number }) => void;
  54. getValueLength?: (value: string) => number;
  55. forwardRef?: ((instance: HTMLTextAreaElement) => void) | React.MutableRefObject<HTMLTextAreaElement> | null;
  56. /* Inner params for TextArea, Chat use it, 。
  57. Used to disable line breaks by pressing the enter key。
  58. Press enter + shift at the same time can start new line.
  59. */
  60. disabledEnterStartNewLine?: boolean
  61. }
  62. export interface TextAreaState {
  63. value: string;
  64. isFocus: boolean;
  65. isHover: boolean;
  66. height: number;
  67. minLength: number;
  68. cachedValue?: string
  69. }
  70. class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
  71. static propTypes = {
  72. autosize: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
  73. borderless: PropTypes.bool,
  74. placeholder: PropTypes.string,
  75. value: PropTypes.string,
  76. rows: PropTypes.number,
  77. cols: PropTypes.number,
  78. maxCount: PropTypes.number,
  79. onEnterPress: PropTypes.func,
  80. validateStatus: PropTypes.string,
  81. className: PropTypes.string,
  82. style: PropTypes.object,
  83. showClear: PropTypes.bool,
  84. onClear: PropTypes.func,
  85. onResize: PropTypes.func,
  86. getValueLength: PropTypes.func,
  87. disabledEnterStartNewLine: PropTypes.bool,
  88. // TODO
  89. // resize: PropTypes.bool,
  90. };
  91. static defaultProps = {
  92. autosize: false,
  93. borderless: false,
  94. rows: 4,
  95. cols: 20,
  96. showCounter: false,
  97. showClear: false,
  98. onEnterPress: noop,
  99. onChange: noop,
  100. onBlur: noop,
  101. onFocus: noop,
  102. onKeyDown: noop,
  103. onResize: noop,
  104. onClear: noop,
  105. // resize: false,
  106. };
  107. focusing: boolean;
  108. libRef: React.RefObject<HTMLInputElement>;
  109. foundation: TextAreaFoundation;
  110. throttledResizeTextarea: DebouncedFunc<typeof this.foundation.resizeTextarea>;
  111. constructor(props: TextAreaProps) {
  112. super(props);
  113. const initValue = 'value' in props ? props.value : props.defaultValue;
  114. this.state = {
  115. value: initValue,
  116. isFocus: false,
  117. isHover: false,
  118. height: 0,
  119. minLength: props.minLength,
  120. cachedValue: props.value,
  121. };
  122. this.focusing = false;
  123. this.foundation = new TextAreaFoundation(this.adapter);
  124. this.libRef = React.createRef<HTMLInputElement>();
  125. this.throttledResizeTextarea = throttle(this.foundation.resizeTextarea, 10);
  126. }
  127. get adapter() {
  128. return {
  129. ...super.adapter,
  130. setValue: (value: string) =>
  131. this.setState({ value }, () => {
  132. if (this.props.autosize) {
  133. this.foundation.resizeTextarea();
  134. }
  135. }),
  136. getRef: () => this.libRef.current,
  137. toggleFocusing: (focusing: boolean) => this.setState({ isFocus: focusing }),
  138. toggleHovering: (hovering: boolean) => this.setState({ isHover: hovering }),
  139. notifyChange: (val: string, e: React.MouseEvent<HTMLTextAreaElement>) => {
  140. this.props.onChange(val, e);
  141. },
  142. notifyClear: (e: React.MouseEvent<HTMLTextAreaElement>) => this.props.onClear(e),
  143. notifyBlur: (val: string, e: React.FocusEvent<HTMLTextAreaElement>) => this.props.onBlur(e),
  144. notifyFocus: (val: string, e: React.FocusEvent<HTMLTextAreaElement>) => this.props.onFocus(e),
  145. notifyKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
  146. this.props.onKeyDown(e);
  147. },
  148. notifyHeightUpdate: (height: number) => {
  149. this.setState({ height });
  150. this.props.onResize({ height });
  151. },
  152. notifyPressEnter: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
  153. this.props.onEnterPress && this.props.onEnterPress(e);
  154. },
  155. setMinLength: (minLength: number) => this.setState({ minLength }),
  156. };
  157. }
  158. static getDerivedStateFromProps(props: TextAreaProps, state: TextAreaState) {
  159. const willUpdateStates: Partial<TextAreaState> = {};
  160. if (props.value !== state.cachedValue) {
  161. willUpdateStates.value = props.value;
  162. willUpdateStates.cachedValue = props.value;
  163. }
  164. return willUpdateStates;
  165. }
  166. componentWillUnmount(): void {
  167. if (this.throttledResizeTextarea) {
  168. this.throttledResizeTextarea?.cancel?.();
  169. this.throttledResizeTextarea = null;
  170. }
  171. }
  172. componentDidUpdate(prevProps: TextAreaProps, prevState: TextAreaState) {
  173. if (
  174. (this.props.value !== prevProps.value || this.props.placeholder !== prevProps.placeholder) &&
  175. this.props.autosize
  176. ) {
  177. this.foundation.resizeTextarea();
  178. }
  179. }
  180. handleClear = (e: React.MouseEvent<HTMLDivElement>) => {
  181. this.foundation.handleClear(e);
  182. };
  183. renderClearBtn() {
  184. const { showClear } = this.props;
  185. const displayClearBtn = this.foundation.isAllowClear();
  186. const clearCls = cls(`${prefixCls}-clearbtn`, {
  187. [`${prefixCls}-clearbtn-hidden`]: !displayClearBtn,
  188. });
  189. if (showClear) {
  190. return (
  191. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  192. <div className={clearCls} onClick={this.handleClear}>
  193. <IconClear />
  194. </div>
  195. );
  196. }
  197. return null;
  198. }
  199. renderCounter() {
  200. let counter: React.ReactNode, current: number, total: number, countCls: string;
  201. const { showCounter, maxCount, getValueLength } = this.props;
  202. if (showCounter || maxCount) {
  203. const { value } = this.state;
  204. // eslint-disable-next-line no-nested-ternary
  205. current = value ? (isFunction(getValueLength) ? getValueLength(value) : value.length) : 0;
  206. total = maxCount || null;
  207. countCls = cls(`${prefixCls}-textarea-counter`, {
  208. [`${prefixCls}-textarea-counter-exceed`]: current > total,
  209. });
  210. counter = (
  211. <div className={countCls}>
  212. {current}
  213. {total ? '/' : null}
  214. {total}
  215. </div>
  216. );
  217. } else {
  218. counter = null;
  219. }
  220. return counter;
  221. }
  222. setRef = (node: HTMLTextAreaElement) => {
  223. (this.libRef as any).current = node;
  224. const { forwardRef } = this.props;
  225. if (typeof forwardRef === 'function') {
  226. forwardRef(node);
  227. } else if (forwardRef && typeof forwardRef === 'object') {
  228. forwardRef.current = node;
  229. }
  230. };
  231. render() {
  232. const {
  233. autosize,
  234. placeholder,
  235. onEnterPress,
  236. onResize,
  237. // resize,
  238. disabled,
  239. readonly,
  240. className,
  241. showCounter,
  242. validateStatus,
  243. maxCount,
  244. defaultValue,
  245. style,
  246. forwardRef,
  247. getValueLength,
  248. maxLength,
  249. minLength,
  250. showClear,
  251. borderless,
  252. autoFocus,
  253. ...rest
  254. } = this.props;
  255. const { isFocus, value, minLength: stateMinLength } = this.state;
  256. const wrapperCls = cls(className, `${prefixCls}-textarea-wrapper`, {
  257. [`${prefixCls}-textarea-borderless`]: borderless,
  258. [`${prefixCls}-textarea-wrapper-disabled`]: disabled,
  259. [`${prefixCls}-textarea-wrapper-readonly`]: readonly,
  260. [`${prefixCls}-textarea-wrapper-${validateStatus}`]: Boolean(validateStatus),
  261. [`${prefixCls}-textarea-wrapper-focus`]: isFocus,
  262. // [`${prefixCls}-textarea-wrapper-resize`]: !autosize && resize,
  263. });
  264. // const ref = this.props.forwardRef || this.textAreaRef;
  265. const itemCls = cls(`${prefixCls}-textarea`, {
  266. [`${prefixCls}-textarea-disabled`]: disabled,
  267. [`${prefixCls}-textarea-readonly`]: readonly,
  268. [`${prefixCls}-textarea-autosize`]: isObject(autosize) ? isUndefined(autosize?.maxRows) : autosize,
  269. [`${prefixCls}-textarea-showClear`]: showClear,
  270. });
  271. const itemProps = {
  272. ...omit(rest, 'insetLabel', 'insetLabelId', 'getValueLength', 'onClear', 'showClear', 'disabledEnterStartNewLine'),
  273. autoFocus: autoFocus || this.props['autofocus'],
  274. className: itemCls,
  275. disabled,
  276. readOnly: readonly,
  277. placeholder: !placeholder ? null : placeholder,
  278. onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => this.foundation.handleChange(e.target.value, e),
  279. onFocus: (e: React.FocusEvent<HTMLTextAreaElement>) => this.foundation.handleFocus(e),
  280. onBlur: (e: React.FocusEvent<HTMLTextAreaElement>) => this.foundation.handleBlur(e.nativeEvent),
  281. onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => this.foundation.handleKeyDown(e),
  282. value: value === null || value === undefined ? '' : value,
  283. onCompositionStart: this.foundation.handleCompositionStart,
  284. onCompositionEnd: this.foundation.handleCompositionEnd,
  285. };
  286. if (!isFunction(getValueLength)) {
  287. (itemProps as any).maxLength = maxLength;
  288. }
  289. if (stateMinLength) {
  290. (itemProps as any).minLength = stateMinLength;
  291. }
  292. return (
  293. <div
  294. className={wrapperCls}
  295. style={style}
  296. onMouseEnter={e => this.foundation.handleMouseEnter(e)}
  297. onMouseLeave={e => this.foundation.handleMouseLeave(e)}
  298. >
  299. {autosize ? (
  300. <ResizeObserver onResize={this.throttledResizeTextarea}>
  301. <textarea {...itemProps} ref={this.setRef} />
  302. </ResizeObserver>
  303. ) : (
  304. <textarea {...itemProps} ref={this.setRef} />
  305. )}
  306. {this.renderClearBtn()}
  307. {this.renderCounter()}
  308. </div>
  309. );
  310. }
  311. }
  312. const ForwardTextarea = React.forwardRef<HTMLTextAreaElement, Omit<TextAreaProps, 'forwardRef'>>((props, ref) => (
  313. <TextArea {...props} forwardRef={ref} />
  314. ));
  315. export default ForwardTextarea;