textarea.tsx 12 KB

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