textarea.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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. onCompositionStart: PropTypes.func,
  87. onCompositionEnd: PropTypes.func,
  88. onCompositionUpdate: PropTypes.func,
  89. getValueLength: PropTypes.func,
  90. disabledEnterStartNewLine: PropTypes.bool,
  91. // TODO
  92. // resize: PropTypes.bool,
  93. };
  94. static defaultProps = {
  95. autosize: false,
  96. borderless: false,
  97. rows: 4,
  98. cols: 20,
  99. showCounter: false,
  100. showClear: false,
  101. onEnterPress: noop,
  102. onChange: noop,
  103. onBlur: noop,
  104. onFocus: noop,
  105. onKeyDown: noop,
  106. onResize: noop,
  107. onClear: noop,
  108. onCompositionStart: noop,
  109. onCompositionEnd: noop,
  110. onCompositionUpdate: noop,
  111. // resize: false,
  112. };
  113. focusing: boolean;
  114. libRef: React.RefObject<HTMLInputElement>;
  115. foundation: TextAreaFoundation;
  116. throttledResizeTextarea: DebouncedFunc<typeof this.foundation.resizeTextarea>;
  117. constructor(props: TextAreaProps) {
  118. super(props);
  119. const initValue = 'value' in props ? props.value : props.defaultValue;
  120. this.state = {
  121. value: initValue,
  122. isFocus: false,
  123. isHover: false,
  124. height: 0,
  125. minLength: props.minLength,
  126. cachedValue: props.value,
  127. };
  128. this.focusing = false;
  129. this.foundation = new TextAreaFoundation(this.adapter);
  130. this.libRef = React.createRef<HTMLInputElement>();
  131. this.throttledResizeTextarea = throttle(this.foundation.resizeTextarea, 10);
  132. }
  133. get adapter() {
  134. return {
  135. ...super.adapter,
  136. setValue: (value: string) =>
  137. this.setState({ value }, () => {
  138. if (this.props.autosize) {
  139. this.foundation.resizeTextarea();
  140. }
  141. }),
  142. getRef: () => this.libRef.current,
  143. toggleFocusing: (focusing: boolean) => this.setState({ isFocus: focusing }),
  144. toggleHovering: (hovering: boolean) => this.setState({ isHover: hovering }),
  145. notifyChange: (val: string, e: React.MouseEvent<HTMLTextAreaElement>) => {
  146. this.props.onChange(val, e);
  147. },
  148. notifyClear: (e: React.MouseEvent<HTMLTextAreaElement>) => this.props.onClear(e),
  149. notifyBlur: (val: string, e: React.FocusEvent<HTMLTextAreaElement>) => this.props.onBlur(e),
  150. notifyFocus: (val: string, e: React.FocusEvent<HTMLTextAreaElement>) => this.props.onFocus(e),
  151. notifyKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
  152. this.props.onKeyDown(e);
  153. },
  154. notifyHeightUpdate: (height: number) => {
  155. this.setState({ height });
  156. this.props.onResize({ height });
  157. },
  158. notifyPressEnter: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
  159. this.props.onEnterPress && this.props.onEnterPress(e);
  160. },
  161. notifyCompositionStart: (e: React.CompositionEvent<HTMLTextAreaElement>) => this.props.onCompositionStart(e),
  162. notifyCompositionEnd: (e: React.CompositionEvent<HTMLTextAreaElement>) => this.props.onCompositionEnd(e),
  163. notifyCompositionUpdate: (e: React.CompositionEvent<HTMLTextAreaElement>) => this.props.onCompositionUpdate(e),
  164. setMinLength: (minLength: number) => this.setState({ minLength }),
  165. };
  166. }
  167. static getDerivedStateFromProps(props: TextAreaProps, state: TextAreaState) {
  168. const willUpdateStates: Partial<TextAreaState> = {};
  169. if (props.value !== state.cachedValue) {
  170. willUpdateStates.value = props.value;
  171. willUpdateStates.cachedValue = props.value;
  172. }
  173. return willUpdateStates;
  174. }
  175. componentWillUnmount(): void {
  176. if (this.throttledResizeTextarea) {
  177. this.throttledResizeTextarea?.cancel?.();
  178. this.throttledResizeTextarea = null;
  179. }
  180. }
  181. componentDidUpdate(prevProps: TextAreaProps, prevState: TextAreaState) {
  182. if (
  183. (this.props.value !== prevProps.value || this.props.placeholder !== prevProps.placeholder) &&
  184. this.props.autosize
  185. ) {
  186. this.foundation.resizeTextarea();
  187. }
  188. }
  189. handleClear = (e: React.MouseEvent<HTMLDivElement>) => {
  190. this.foundation.handleClear(e);
  191. };
  192. renderClearBtn() {
  193. const { showClear } = this.props;
  194. const displayClearBtn = this.foundation.isAllowClear();
  195. const clearCls = cls(`${prefixCls}-clearbtn`, {
  196. [`${prefixCls}-clearbtn-hidden`]: !displayClearBtn,
  197. });
  198. if (showClear) {
  199. return (
  200. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  201. <div className={clearCls} onClick={this.handleClear}>
  202. <IconClear />
  203. </div>
  204. );
  205. }
  206. return null;
  207. }
  208. renderCounter() {
  209. let counter: React.ReactNode, current: number, total: number, countCls: string;
  210. const { showCounter, maxCount, getValueLength } = this.props;
  211. if (showCounter || maxCount) {
  212. const { value } = this.state;
  213. // eslint-disable-next-line no-nested-ternary
  214. current = value ? (isFunction(getValueLength) ? getValueLength(value) : value.length) : 0;
  215. total = maxCount || null;
  216. countCls = cls(`${prefixCls}-textarea-counter`, {
  217. [`${prefixCls}-textarea-counter-exceed`]: current > total,
  218. });
  219. counter = (
  220. <div className={countCls}>
  221. {current}
  222. {total ? '/' : null}
  223. {total}
  224. </div>
  225. );
  226. } else {
  227. counter = null;
  228. }
  229. return counter;
  230. }
  231. setRef = (node: HTMLTextAreaElement) => {
  232. (this.libRef as any).current = node;
  233. const { forwardRef } = this.props;
  234. if (typeof forwardRef === 'function') {
  235. forwardRef(node);
  236. } else if (forwardRef && typeof forwardRef === 'object') {
  237. forwardRef.current = node;
  238. }
  239. };
  240. render() {
  241. const {
  242. autosize,
  243. placeholder,
  244. onEnterPress,
  245. onResize,
  246. // resize,
  247. disabled,
  248. readonly,
  249. className,
  250. showCounter,
  251. validateStatus,
  252. maxCount,
  253. defaultValue,
  254. style,
  255. forwardRef,
  256. getValueLength,
  257. maxLength,
  258. minLength,
  259. showClear,
  260. borderless,
  261. autoFocus,
  262. ...rest
  263. } = this.props;
  264. const { isFocus, value, minLength: stateMinLength } = this.state;
  265. const wrapperCls = cls(className, `${prefixCls}-textarea-wrapper`, {
  266. [`${prefixCls}-textarea-borderless`]: borderless,
  267. [`${prefixCls}-textarea-wrapper-disabled`]: disabled,
  268. [`${prefixCls}-textarea-wrapper-readonly`]: readonly,
  269. [`${prefixCls}-textarea-wrapper-${validateStatus}`]: Boolean(validateStatus),
  270. [`${prefixCls}-textarea-wrapper-focus`]: isFocus,
  271. // [`${prefixCls}-textarea-wrapper-resize`]: !autosize && resize,
  272. });
  273. // const ref = this.props.forwardRef || this.textAreaRef;
  274. const itemCls = cls(`${prefixCls}-textarea`, {
  275. [`${prefixCls}-textarea-disabled`]: disabled,
  276. [`${prefixCls}-textarea-readonly`]: readonly,
  277. [`${prefixCls}-textarea-autosize`]: isObject(autosize) ? isUndefined(autosize?.maxRows) : autosize,
  278. [`${prefixCls}-textarea-showClear`]: showClear,
  279. });
  280. const itemProps = {
  281. ...omit(rest, 'insetLabel', 'insetLabelId', 'getValueLength', 'onClear', 'showClear', 'disabledEnterStartNewLine'),
  282. autoFocus: autoFocus || this.props['autofocus'],
  283. className: itemCls,
  284. disabled,
  285. readOnly: readonly,
  286. placeholder: !placeholder ? null : placeholder,
  287. onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => this.foundation.handleChange(e.target.value, e),
  288. onFocus: (e: React.FocusEvent<HTMLTextAreaElement>) => this.foundation.handleFocus(e),
  289. onBlur: (e: React.FocusEvent<HTMLTextAreaElement>) => this.foundation.handleBlur(e.nativeEvent),
  290. onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => this.foundation.handleKeyDown(e),
  291. value: value === null || value === undefined ? '' : value,
  292. onCompositionStart: this.foundation.handleCompositionStart,
  293. onCompositionEnd: this.foundation.handleCompositionEnd,
  294. onCompositionUpdate: this.foundation.handleCompositionUpdate,
  295. };
  296. if (!isFunction(getValueLength)) {
  297. (itemProps as any).maxLength = maxLength;
  298. }
  299. if (stateMinLength) {
  300. (itemProps as any).minLength = stateMinLength;
  301. }
  302. return (
  303. <div
  304. className={wrapperCls}
  305. style={style}
  306. onMouseEnter={e => this.foundation.handleMouseEnter(e)}
  307. onMouseLeave={e => this.foundation.handleMouseLeave(e)}
  308. >
  309. {autosize ? (
  310. <ResizeObserver onResize={this.throttledResizeTextarea}>
  311. <textarea {...itemProps} ref={this.setRef} />
  312. </ResizeObserver>
  313. ) : (
  314. <textarea {...itemProps} ref={this.setRef} />
  315. )}
  316. {this.renderClearBtn()}
  317. {this.renderCounter()}
  318. </div>
  319. );
  320. }
  321. }
  322. const ForwardTextarea = React.forwardRef<HTMLTextAreaElement, Omit<TextAreaProps, 'forwardRef'>>((props, ref) => (
  323. <TextArea {...props} forwardRef={ref} />
  324. ));
  325. export default ForwardTextarea;