import * as React from 'react'; import ResizeObserver from 'rc-resize-observer'; import omit from 'omit.js'; import classNames from 'classnames'; import calculateNodeHeight from './calculateNodeHeight'; import raf from '../_util/raf'; import { TextAreaProps } from './TextArea'; const RESIZE_STATUS_NONE = 0; const RESIZE_STATUS_RESIZING = 1; const RESIZE_STATUS_RESIZED = 2; export interface AutoSizeType { minRows?: number; maxRows?: number; } export interface TextAreaState { textareaStyles?: React.CSSProperties; /** We need add process style to disable scroll first and then add back to avoid unexpected scrollbar */ resizeStatus?: | typeof RESIZE_STATUS_NONE | typeof RESIZE_STATUS_RESIZING | typeof RESIZE_STATUS_RESIZED; } class ResizableTextArea extends React.Component { nextFrameActionId: number; resizeFrameId: number; constructor(props: TextAreaProps) { super(props); this.state = { textareaStyles: {}, resizeStatus: RESIZE_STATUS_NONE, }; } textArea: HTMLTextAreaElement; saveTextArea = (textArea: HTMLTextAreaElement) => { this.textArea = textArea; }; componentDidMount() { this.resizeTextarea(); } componentDidUpdate(prevProps: TextAreaProps) { // Re-render with the new content then recalculate the height as required. if (prevProps.value !== this.props.value) { this.resizeTextarea(); } } handleResize = (size: { width: number; height: number }) => { const { resizeStatus } = this.state; const { autoSize, onResize } = this.props; if (resizeStatus !== RESIZE_STATUS_NONE) { return; } if (typeof onResize === 'function') { onResize(size); } if (autoSize) { this.resizeOnNextFrame(); } }; resizeOnNextFrame = () => { raf.cancel(this.nextFrameActionId); this.nextFrameActionId = raf(this.resizeTextarea); }; resizeTextarea = () => { const { autoSize } = this.props; if (!autoSize || !this.textArea) { return; } const { minRows, maxRows } = autoSize as AutoSizeType; const textareaStyles = calculateNodeHeight(this.textArea, false, minRows, maxRows); this.setState({ textareaStyles, resizeStatus: RESIZE_STATUS_RESIZING }, () => { raf.cancel(this.resizeFrameId); this.resizeFrameId = raf(() => { this.setState({ resizeStatus: RESIZE_STATUS_RESIZED }, () => { this.resizeFrameId = raf(() => { this.setState({ resizeStatus: RESIZE_STATUS_NONE }); this.fixFirefoxAutoScroll(); }); }); }); }); }; componentWillUnmount() { raf.cancel(this.nextFrameActionId); raf.cancel(this.resizeFrameId); } // https://github.com/ant-design/ant-design/issues/21870 fixFirefoxAutoScroll() { try { if (document.activeElement === this.textArea) { const currentStart = this.textArea.selectionStart; const currentEnd = this.textArea.selectionEnd; this.textArea.setSelectionRange(currentStart, currentEnd); } } catch (e) { // Fix error in Chrome: // Failed to read the 'selectionStart' property from 'HTMLInputElement' // http://stackoverflow.com/q/21177489/3040605 } } renderTextArea = () => { const { prefixCls, autoSize, onResize, className, disabled } = this.props; const { textareaStyles, resizeStatus } = this.state; const otherProps = omit(this.props, [ 'prefixCls', 'onPressEnter', 'autoSize', 'defaultValue', 'allowClear', 'onResize', ]); const cls = classNames(prefixCls, className, { [`${prefixCls}-disabled`]: disabled, }); // Fix https://github.com/ant-design/ant-design/issues/6776 // Make sure it could be reset when using form.getFieldDecorator if ('value' in otherProps) { otherProps.value = otherProps.value || ''; } const style = { ...this.props.style, ...textareaStyles, ...(resizeStatus === RESIZE_STATUS_RESIZING ? // React will warning when mix `overflow` & `overflowY`. // We need to define this separately. { overflowX: 'hidden', overflowY: 'hidden' } : null), }; return (