Repository: mui/material-ui (TypeScript)
Date: 2019-04-12 21:18:54+00:00
Task type: Feature

Issue:
Enhancement on Textarea.js to allow custom ROWS_HEIGHT value
Hello,

Currently in Textarea component the ROWS_HEIGHT is static, and it has 19 as value. 
Can we make it updatable based on a new prop? This will be very useful when having smaller fonts and smaller line heights.

Please see below an example where I've a lot of "empty" space  between the last text line and the underline:

![screen shot 2018-05-14 at 11 17 04](https://user-images.githubusercontent.com/31534838/39991735-62ba0d2c-5768-11e8-8a7d-07525ce64b88.png)

I'll wait for your feedback.

Thanks!



Issue discussion:
This value is only used for the initial rendering, we compute the line height for the following updates.
https://github.com/mui-org/material-ui/blob/9ec2fcf5d8349e8f074fe5e07fc4018f66b90845/packages/material-ui/src/Input/Textarea.js#L8
Do you have a reproduction? I guess we could add a property to change the value.
Ok thanks for your reply. But one thing that I've noticed was that the font-size that I used in my component was 14px but if I use the same used in default component, which is 16px, I do not get that "empty" space. Is there anything that can be done to adjust the height according to the font-size?
Thanks!
@kmestre Let me see a live example. Hard to tell without.
@oliviertassinari  here is the example https://codesandbox.io/s/z2rv8w948p
Thanks!
I was having some issues with this, and solved it by setting line height in the root element and font size in the input element, like this:

```
const styles = createStyles({
    root: {
        lineHeight: "64px"
    },
    input: {
        fontSize: 50,
    }
})

const TextFieldBig = withStyles(styles)((props: TextFieldProps) => {
    const {classes, ...rest} = props
    return (
        <TextField
            InputProps={{
                classes: classes
            }}
            {...rest}
        />
    )
})
```
@CorayThan What's wrong with that?
```jsx
import React from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";

const styles = theme => ({
  input: {
    fontSize: 40
  }
});

class TextFields extends React.Component {
  state = {
    multiline: "Controlled"
  };

  handleChange = name => event => {
    this.setState({
      [name]: event.target.value
    });
  };

  render() {
    const { classes } = this.props;

    return (
      <form className={classes.container} noValidate autoComplete="off">
        <TextField
          id="multiline-flexible"
          multiline
          rowsMax="4"
          value={this.state.multiline}
          onChange={this.handleChange("multiline")}
          InputProps={{
            className: classes.input
          }}
          margin="normal"
        />
      </form>
    );
  }
}

TextFields.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(styles)(TextFields);
```
https://codesandbox.io/s/w7kjjov6kw

Solution patch:
diff --git a/packages/material-ui/src/InputBase/InputBase.js b/packages/material-ui/src/InputBase/InputBase.js
--- a/packages/material-ui/src/InputBase/InputBase.js
+++ b/packages/material-ui/src/InputBase/InputBase.js
@@ -121,7 +121,6 @@ export const styles = theme => {
     /* Styles applied to the `input` element if `multiline={true}`. */
     inputMultiline: {
       height: 'auto',
-      minHeight: '1.1875em', // Reset (19px), match the native input line-height
       resize: 'none',
       padding: 0,
     },
@@ -308,6 +307,7 @@ class InputBase extends React.Component {
       renderPrefix,
       rows,
       rowsMax,
+      rowsMin,
       startAdornment,
       type,
       value,
@@ -370,14 +370,13 @@ class InputBase extends React.Component {
         ref: null,
       };
     } else if (multiline) {
-      if (rows && !rowsMax) {
+      if (rows && !rowsMax && !rowsMin) {
         InputComponent = 'textarea';
       } else {
         inputProps = {
           rowsMax,
-          textareaRef: this.handleRefInput,
+          rowsMin,
           ...inputProps,
-          ref: null,
         };
         InputComponent = Textarea;
       }
@@ -568,6 +567,10 @@ InputBase.propTypes = {
    * Maximum number of rows to display when multiline option is set to true.
    */
   rowsMax: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  /**
+   * Minimum number of rows to display when multiline option is set to true.
+   */
+  rowsMin: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
   /**
    * Start `InputAdornment` for this component.
    */
diff --git a/packages/material-ui/src/InputBase/Textarea.js b/packages/material-ui/src/InputBase/Textarea.js
--- a/packages/material-ui/src/InputBase/Textarea.js
+++ b/packages/material-ui/src/InputBase/Textarea.js
@@ -1,246 +1,156 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import clsx from 'clsx';
+import { useForkRef } from '../utils/reactHelpers';
 import debounce from 'debounce'; // < 1kb payload overhead when lodash/debounce is > 3kb.
-import EventListener from 'react-event-listener';
-import withStyles from '../styles/withStyles';
-import { setRef } from '../utils/reactHelpers';
-
-const ROWS_HEIGHT = 19;
-
-export const styles = {
-  /* Styles applied to the root element. */
-  root: {
-    position: 'relative', // because the shadow has position: 'absolute',
-    width: '100%',
-  },
-  textarea: {
-    width: '100%',
-    height: '100%',
-    resize: 'none',
-    font: 'inherit',
-    padding: 0,
-    cursor: 'inherit',
-    boxSizing: 'border-box',
-    lineHeight: 'inherit',
-    border: 'none',
-    outline: 'none',
-    background: 'transparent',
-  },
-  shadow: {
-    // Overflow also needed to here to remove the extra row
-    // added to textareas in Firefox.
-    overflow: 'hidden',
-    // Visibility needed to hide the extra text area on iPads
-    visibility: 'hidden',
-    position: 'absolute',
-    height: 'auto',
-    whiteSpace: 'pre-wrap',
-  },
-};
+
+function getStyleValue(computedStyle, property) {
+  return parseInt(computedStyle[property], 10) || 0;
+}
+
+const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
 
 /**
  * @ignore - internal component.
+ *
+ * To make public in v4+.
  */
-class Textarea extends React.Component {
-  constructor(props) {
-    super();
-    this.isControlled = props.value != null;
-    // <Input> expects the components it renders to respond to 'value'
-    // so that it can check whether they are filled.
-    this.value = props.value || props.defaultValue || '';
-    this.state = {
-      height: Number(props.rows) * ROWS_HEIGHT,
-    };
+const Textarea = React.forwardRef(function Textarea(props, ref) {
+  const { onChange, rowsMax, rowsMin, style, value, ...other } = props;
 
-    if (typeof window !== 'undefined') {
-      this.handleResize = debounce(() => {
-        this.syncHeightWithShadow();
-      }, 166); // Corresponds to 10 frames at 60 Hz.
-    }
-  }
+  const { current: isControlled } = React.useRef(value != null);
+  const inputRef = React.useRef();
+  const [state, setState] = React.useState({});
+  const handleRef = useForkRef(ref, inputRef);
 
-  componentDidMount() {
-    this.syncHeightWithShadow();
-  }
+  const syncHeight = React.useCallback(() => {
+    const input = inputRef.current;
+    const savedValue = input.value;
+    const savedHeight = input.style.height;
+    const savedOverflow = input.style.overflow;
 
-  componentDidUpdate() {
-    this.syncHeightWithShadow();
-  }
+    input.style.overflow = 'hidden';
+    input.style.height = '0';
 
-  componentWillUnmount() {
-    this.handleResize.clear();
-  }
+    // The height of the inner content
+    input.value = savedValue || props.placeholder || 'x';
+    const innerHeight = input.scrollHeight;
 
-  handleRefInput = ref => {
-    this.inputRef = ref;
+    const computedStyle = window.getComputedStyle(input);
+    const boxSizing = computedStyle['box-sizing'];
 
-    setRef(this.props.textareaRef, ref);
-  };
+    // Measure height of a textarea with a single row
+    input.value = 'x';
+    const singleRowHeight = input.scrollHeight;
 
-  handleRefSinglelineShadow = ref => {
-    this.singlelineShadowRef = ref;
-  };
-
-  handleRefShadow = ref => {
-    this.shadowRef = ref;
-  };
+    // The height of the outer content
+    let outerHeight = innerHeight;
 
-  handleChange = event => {
-    this.value = event.target.value;
-
-    if (!this.isControlled) {
-      // The component is not controlled, we need to update the shallow value.
-      this.shadowRef.value = this.value;
-      this.syncHeightWithShadow();
+    if (rowsMin != null) {
+      outerHeight = Math.max(Number(rowsMin) * singleRowHeight, outerHeight);
     }
-
-    if (this.props.onChange) {
-      this.props.onChange(event);
+    if (rowsMax != null) {
+      outerHeight = Math.min(Number(rowsMax) * singleRowHeight, outerHeight);
     }
-  };
-
-  syncHeightWithShadow() {
-    const props = this.props;
-
-    // Guarding for **broken** shallow rendering method that call componentDidMount
-    // but doesn't handle refs correctly.
-    // To remove once the shallow rendering has been fixed.
-    if (!this.shadowRef) {
-      return;
+    outerHeight = Math.max(outerHeight, singleRowHeight);
+
+    if (boxSizing === 'content-box') {
+      outerHeight -=
+        getStyleValue(computedStyle, 'padding-bottom') +
+        getStyleValue(computedStyle, 'padding-top');
+    } else if (boxSizing === 'border-box') {
+      outerHeight +=
+        getStyleValue(computedStyle, 'border-bottom-width') +
+        getStyleValue(computedStyle, 'border-top-width');
     }
 
-    if (this.isControlled) {
-      // The component is controlled, we need to update the shallow value.
-      this.shadowRef.value = props.value == null ? '' : String(props.value);
-    }
-
-    let lineHeight = this.singlelineShadowRef.scrollHeight;
-    // The Textarea might not be visible (p.ex: display: none).
-    // In this case, the layout values read from the DOM will be 0.
-    lineHeight = lineHeight === 0 ? ROWS_HEIGHT : lineHeight;
+    input.style.overflow = savedOverflow;
+    input.style.height = savedHeight;
+    input.value = savedValue;
+
+    setState(prevState => {
+      // Need a large enough different to update the height.
+      // This prevents infinite rendering loop.
+      if (innerHeight > 0 && Math.abs((prevState.innerHeight || 0) - innerHeight) > 1) {
+        return {
+          innerHeight,
+          outerHeight,
+        };
+      }
+
+      return prevState;
+    });
+  }, [setState, rowsMin, rowsMax, props.placeholder]);
+
+  React.useEffect(() => {
+    const handleResize = debounce(() => {
+      syncHeight();
+    }, 166); // Corresponds to 10 frames at 60 Hz.
+
+    window.addEventListener('resize', handleResize);
+    return () => {
+      handleResize.clear();
+      window.removeEventListener('resize', handleResize);
+    };
+  }, [syncHeight]);
 
-    let newHeight = this.shadowRef.scrollHeight;
+  useEnhancedEffect(() => {
+    syncHeight();
+  });
 
-    // Guarding for jsdom, where scrollHeight isn't present.
-    // See https://github.com/tmpvar/jsdom/issues/1013
-    if (newHeight === undefined) {
-      return;
+  const handleChange = event => {
+    if (!isControlled) {
+      syncHeight();
     }
 
-    if (Number(props.rowsMax) >= Number(props.rows)) {
-      newHeight = Math.min(Number(props.rowsMax) * lineHeight, newHeight);
+    if (onChange) {
+      onChange(event);
     }
+  };
 
-    newHeight = Math.max(newHeight, lineHeight);
-
-    // Need a large enough different to update the height.
-    // This prevents infinite rendering loop.
-    if (Math.abs(this.state.height - newHeight) > 1) {
-      this.setState({
-        height: newHeight,
-      });
-    }
-  }
-
-  render() {
-    const {
-      classes,
-      className,
-      defaultValue,
-      onChange,
-      rows,
-      rowsMax,
-      style,
-      textareaRef,
-      value,
-      ...other
-    } = this.props;
-
-    return (
-      <div className={classes.root}>
-        <EventListener target="window" onResize={this.handleResize} />
-        <textarea
-          aria-hidden="true"
-          className={clsx(classes.textarea, classes.shadow)}
-          readOnly
-          ref={this.handleRefSinglelineShadow}
-          rows="1"
-          tabIndex={-1}
-          value=""
-        />
-        <textarea
-          aria-hidden="true"
-          className={clsx(classes.textarea, classes.shadow)}
-          defaultValue={defaultValue}
-          readOnly
-          ref={this.handleRefShadow}
-          rows={rows}
-          tabIndex={-1}
-          value={value}
-        />
-        <textarea
-          rows={rows}
-          className={clsx(classes.textarea, className)}
-          defaultValue={defaultValue}
-          value={value}
-          onChange={this.handleChange}
-          ref={this.handleRefInput}
-          style={{ height: this.state.height, ...style }}
-          {...other}
-        />
-      </div>
-    );
-  }
-}
+  return (
+    <textarea
+      value={value}
+      onChange={handleChange}
+      ref={handleRef}
+      style={{
+        height: state.outerHeight,
+        overflow: state.outerHeight === state.innerHeight ? 'hidden' : null,
+        ...style,
+      }}
+      {...other}
+    />
+  );
+});
 
 Textarea.propTypes = {
-  /**
-   * Override or extend the styles applied to the component.
-   * See [CSS API](#css) below for more details.
-   */
-  classes: PropTypes.object.isRequired,
   /**
    * @ignore
    */
   className: PropTypes.string,
-  /**
-   * @ignore
-   */
-  defaultValue: PropTypes.any,
-  /**
-   * @ignore
-   */
-  disabled: PropTypes.bool,
   /**
    * @ignore
    */
   onChange: PropTypes.func,
   /**
-   * Number of rows to display when multiline option is set to true.
+   * @ignore
    */
-  rows: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  placeholder: PropTypes.string,
   /**
    * Maximum number of rows to display when multiline option is set to true.
    */
   rowsMax: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
   /**
-   * @ignore
+   * Minimum number of rows to display when multiline option is set to true.
    */
-  style: PropTypes.object,
+  rowsMin: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
   /**
-   * @deprecated
-   * Use that property to pass a ref callback to the native textarea element.
+   * @ignore
    */
-  textareaRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+  style: PropTypes.object,
   /**
    * @ignore
    */
   value: PropTypes.any,
 };
 
-Textarea.defaultProps = {
-  rows: 1,
-};
-
-export default withStyles(styles, { name: 'MuiPrivateTextarea' })(Textarea);
+export default Textarea;
diff --git a/packages/material-ui/src/Modal/ModalManager.js b/packages/material-ui/src/Modal/ModalManager.js
--- a/packages/material-ui/src/Modal/ModalManager.js
+++ b/packages/material-ui/src/Modal/ModalManager.js
@@ -1,4 +1,3 @@
-import ownerWindow from '../utils/ownerWindow';
 import getScrollbarSize from '../utils/getScrollbarSize';
 import ownerDocument from '../utils/ownerDocument';
 import isOverflowing from './isOverflowing';
@@ -17,8 +16,7 @@ function findIndexOf(data, callback) {
 }
 
 function getPaddingRight(node) {
-  const win = ownerWindow(node);
-  return parseInt(win.getComputedStyle(node)['padding-right'], 10) || 0;
+  return parseInt(window.getComputedStyle(node)['padding-right'], 10) || 0;
 }
 
 function setContainerStyle(data) {
diff --git a/packages/material-ui/src/Slide/Slide.js b/packages/material-ui/src/Slide/Slide.js
--- a/packages/material-ui/src/Slide/Slide.js
+++ b/packages/material-ui/src/Slide/Slide.js
@@ -6,7 +6,6 @@ import ReactDOM from 'react-dom';
 import EventListener from 'react-event-listener';
 import debounce from 'debounce'; // < 1kb payload overhead when lodash/debounce is > 3kb.
 import Transition from 'react-transition-group/Transition';
-import ownerWindow from '../utils/ownerWindow';
 import { setRef } from '../utils/reactHelpers';
 import withTheme from '../styles/withTheme';
 import { duration } from '../styles/transitions';
@@ -26,7 +25,7 @@ function getTranslateValue(props, node) {
   if (node.fakeTransform) {
     transform = node.fakeTransform;
   } else {
-    const computedStyle = ownerWindow(node).getComputedStyle(node);
+    const computedStyle = window.getComputedStyle(node);
     transform =
       computedStyle.getPropertyValue('-webkit-transform') ||
       computedStyle.getPropertyValue('transform');
diff --git a/pages/api/input-base.md b/pages/api/input-base.md
--- a/pages/api/input-base.md
+++ b/pages/api/input-base.md
@@ -42,6 +42,7 @@ It contains a load of style reset and some state logic.
 | <span class="prop-name">required</span> | <span class="prop-type">bool</span> |  | If `true`, the `input` element will be required. |
 | <span class="prop-name">rows</span> | <span class="prop-type">union:&nbsp;string&nbsp;&#124;<br>&nbsp;number<br></span> |  | Number of rows to display when multiline option is set to true. |
 | <span class="prop-name">rowsMax</span> | <span class="prop-type">union:&nbsp;string&nbsp;&#124;<br>&nbsp;number<br></span> |  | Maximum number of rows to display when multiline option is set to true. |
+| <span class="prop-name">rowsMin</span> | <span class="prop-type">union:&nbsp;string&nbsp;&#124;<br>&nbsp;number<br></span> |  | Minimum number of rows to display when multiline option is set to true. |
 | <span class="prop-name">startAdornment</span> | <span class="prop-type">node</span> |  | Start `InputAdornment` for this component. |
 | <span class="prop-name">type</span> | <span class="prop-type">string</span> | <span class="prop-default">'text'</span> | Type of the `input` element. It should be [a valid HTML5 input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types). |
 | <span class="prop-name">value</span> | <span class="prop-type">any</span> |  | The value of the `input` element, required for a controlled component. |
diff --git a/scripts/sizeSnapshot/webpack.config.js b/scripts/sizeSnapshot/webpack.config.js
--- a/scripts/sizeSnapshot/webpack.config.js
+++ b/scripts/sizeSnapshot/webpack.config.js
@@ -80,7 +80,14 @@ async function getSizeLimitBundles() {
       // vs https://bundlephobia.com/result?p=react-media
       name: '@material-ui/core/useMediaQuery',
       webpack: true,
-      path: 'packages/material-ui/build/useMediaQuery/index.js',
+      path: 'packages/material-ui/build/esm/useMediaQuery/index.js',
+    },
+    {
+      // vs https://bundlephobia.com/result?p=react-textarea-autosize
+      // vs https://bundlephobia.com/result?p=react-autosize-textarea
+      name: '@material-ui/core/Textarea',
+      webpack: true,
+      path: 'packages/material-ui/build/esm/InputBase/Textarea.js',
     },
     {
       name: 'docs.main',


Test patch:
diff --git a/packages/material-ui/src/InputBase/Textarea.test.js b/packages/material-ui/src/InputBase/Textarea.test.js
--- a/packages/material-ui/src/InputBase/Textarea.test.js
+++ b/packages/material-ui/src/InputBase/Textarea.test.js
@@ -1,34 +1,20 @@
 import React from 'react';
 import { assert } from 'chai';
-import { spy, useFakeTimers } from 'sinon';
-import { createShallow, createMount, unwrap } from '@material-ui/core/test-utils';
+import sinon, { spy, stub, useFakeTimers } from 'sinon';
+import { createMount, describeConformance } from '@material-ui/core/test-utils';
 import Textarea from './Textarea';
 
-function assignRefs(wrapper) {
-  // refs don't work with shallow renders in enzyme so here we directly define
-  // 'this.input', 'this.shadow', etc. for this Textarea via wrapper.instance()
-  const inputRef = wrapper.find('textarea').last();
-  wrapper.instance().inputRef = inputRef;
-  const textareaShadowRef = wrapper.find('textarea').at(2);
-  wrapper.instance().shadowRef = textareaShadowRef;
-  const singlelineShadowRef = wrapper.find('textarea').first();
-  wrapper.instance().singlelineShadowRef = singlelineShadowRef;
-
-  return {
-    inputRef,
-    singlelineShadowRef,
-    textareaShadowRef,
-  };
+function getHeight(wrapper) {
+  return wrapper
+    .find('textarea')
+    .at(0)
+    .props().style.height;
 }
 
-const TextareaNaked = unwrap(Textarea);
-
 describe('<Textarea />', () => {
-  let shallow;
   let mount;
 
   before(() => {
-    shallow = createShallow();
     mount = createMount({ strict: true });
   });
 
@@ -36,131 +22,151 @@ describe('<Textarea />', () => {
     mount.cleanUp();
   });
 
-  it('should render 3 textareas', () => {
-    const wrapper = shallow(<TextareaNaked classes={{}} />);
-    assert.strictEqual(wrapper.find('textarea').length, 3);
-  });
-
-  it('should change its height when the height of its shadows changes', () => {
-    const wrapper = shallow(<TextareaNaked classes={{}} />);
-    assert.strictEqual(wrapper.state().height, 19);
+  describeConformance(<Textarea />, () => ({
+    inheritComponent: 'textarea',
+    mount,
+    refInstanceof: window.HTMLTextAreaElement,
+    skip: ['rootClass', 'componentProp'],
+  }));
 
-    const refs = assignRefs(wrapper);
+  describe('layout', () => {
+    // Only run the test on node.
+    if (!/jsdom/.test(window.navigator.userAgent)) {
+      return;
+    }
 
-    // jsdom doesn't support scroll height so we have to simulate it changing
-    // which makes this not so great of a test :(
-    refs.textareaShadowRef.scrollHeight = 43;
-    refs.singlelineShadowRef.scrollHeight = 43;
-    // this is needed to trigger the resize
-    refs.inputRef.simulate('change', { target: { value: 'x' } });
-    assert.strictEqual(wrapper.state().height, 43);
+    const getComputedStyleStub = {};
 
-    refs.textareaShadowRef.scrollHeight = 19;
-    refs.singlelineShadowRef.scrollHeight = 19;
-    refs.inputRef.simulate('change', { target: { value: '' } });
-    assert.strictEqual(wrapper.state().height, 19);
-  });
+    function setLayout(wrapper, { getComputedStyle, scrollHeight, lineHeight }) {
+      const input = wrapper.find('textarea').instance();
+      getComputedStyleStub[input] = getComputedStyle;
 
-  describe('height behavior', () => {
-    let instanceWrapper;
-    let wrapper;
+      let index = 0;
+      stub(input, 'scrollHeight').get(() => {
+        index += 1;
+        return index % 2 === 1 ? scrollHeight : lineHeight;
+      });
+    }
 
-    beforeEach(() => {
-      wrapper = mount(<Textarea value="f" />);
-      instanceWrapper = wrapper.find('Textarea');
-    });
-
-    afterEach(() => {
-      wrapper.unmount();
-    });
-
-    it('should update the height when the value change', () => {
-      const instance = instanceWrapper.instance();
-      instance.singlelineShadowRef = { scrollHeight: 19 };
-      instance.shadowRef = { scrollHeight: 19 };
-      wrapper.setProps({ value: 'fo' });
-      assert.strictEqual(instanceWrapper.state().height, 19);
-      instance.shadowRef = { scrollHeight: 48 };
-      wrapper.setProps({ value: 'foooooo' });
-      assert.strictEqual(instanceWrapper.state().height, 48);
-    });
-
-    it('should respect the rowsMax property', () => {
-      const instance = instanceWrapper.instance();
-      const rowsMax = 4;
-      const lineHeight = 19;
-      instance.singlelineShadowRef = { scrollHeight: lineHeight };
-      instance.shadowRef = { scrollHeight: lineHeight * 5 };
-      wrapper.setProps({ rowsMax });
-      assert.strictEqual(instanceWrapper.state().height, lineHeight * rowsMax);
+    before(() => {
+      stub(window, 'getComputedStyle').value(node => getComputedStyleStub[node] || {});
     });
-  });
 
-  it('should set filled', () => {
-    const wrapper = shallow(<TextareaNaked classes={{}} />);
-    assert.strictEqual(wrapper.find('textarea').length, 3);
-
-    const refs = assignRefs(wrapper);
-    // this is needed to trigger the resize
-    refs.inputRef.simulate('change', { target: { value: 'x' } });
-    assert.strictEqual(wrapper.instance().value, 'x');
-    // this is needed to trigger the resize
-    refs.inputRef.simulate('change', { target: { value: '' } });
-    assert.strictEqual(wrapper.instance().value, '');
-  });
-
-  describe('prop: textareaRef', () => {
-    it('should be able to access the native textarea', () => {
-      const handleRef = spy();
-      mount(<Textarea textareaRef={handleRef} />);
-      assert.strictEqual(handleRef.callCount, 1);
+    after(() => {
+      sinon.restore();
     });
 
-    it('should be able to return the input node via a ref object', () => {
-      const ref = React.createRef();
-      mount(<Textarea textareaRef={ref} />);
-      assert.strictEqual(ref.current.tagName, 'TEXTAREA');
+    describe('resize', () => {
+      let clock;
+
+      before(() => {
+        clock = useFakeTimers();
+      });
+
+      after(() => {
+        clock.restore();
+      });
+
+      it('should handle the resize event', () => {
+        const wrapper = mount(<Textarea />);
+        assert.strictEqual(getHeight(wrapper), undefined);
+        setLayout(wrapper, {
+          getComputedStyle: {
+            'box-sizing': 'content-box',
+          },
+          scrollHeight: 30,
+          lineHeight: 15,
+        });
+        window.dispatchEvent(new window.Event('resize', {}));
+        clock.tick(166);
+        wrapper.update();
+        assert.strictEqual(getHeight(wrapper), 30);
+      });
     });
-  });
 
-  describe('prop: onChange', () => {
-    it('should be call the callback', () => {
+    it('should update when uncontrolled', () => {
       const handleChange = spy();
-      const wrapper = shallow(<TextareaNaked classes={{}} value="x" onChange={handleChange} />);
-      assert.strictEqual(wrapper.find('textarea').length, 3);
-
-      const refs = assignRefs(wrapper);
-      const event = { target: { value: 'xx' } };
-      refs.inputRef.simulate('change', event);
-      assert.strictEqual(wrapper.instance().value, 'xx');
+      const wrapper = mount(<Textarea onChange={handleChange} />);
+      assert.strictEqual(getHeight(wrapper), undefined);
+      setLayout(wrapper, {
+        getComputedStyle: {
+          'box-sizing': 'content-box',
+        },
+        scrollHeight: 30,
+        lineHeight: 15,
+      });
+      wrapper
+        .find('textarea')
+        .at(0)
+        .simulate('change');
+      wrapper.update();
+      assert.strictEqual(getHeight(wrapper), 30);
       assert.strictEqual(handleChange.callCount, 1);
-      assert.deepEqual(handleChange.args[0], [event]);
     });
-  });
 
-  describe('resize', () => {
-    let clock;
+    it('should take the border into account with border-box', () => {
+      const border = 5;
+      const wrapper = mount(<Textarea />);
+      assert.strictEqual(getHeight(wrapper), undefined);
+      setLayout(wrapper, {
+        getComputedStyle: {
+          'box-sizing': 'border-box',
+          'border-bottom-width': `${border}px`,
+        },
+        scrollHeight: 30,
+        lineHeight: 15,
+      });
+      wrapper.setProps();
+      wrapper.update();
+      assert.strictEqual(getHeight(wrapper), 30 + border);
+    });
 
-    before(() => {
-      clock = useFakeTimers();
+    it('should take the padding into account with content-box', () => {
+      const padding = 5;
+      const wrapper = mount(<Textarea />);
+      setLayout(wrapper, {
+        getComputedStyle: {
+          'box-sizing': 'content-box',
+          'padding-top': `${padding}px`,
+        },
+        scrollHeight: 30,
+        lineHeight: 15,
+      });
+      wrapper.setProps();
+      wrapper.update();
+      assert.strictEqual(getHeight(wrapper), 30 - padding);
     });
 
-    after(() => {
-      clock.restore();
+    it('should have at least "rowsMin" rows', () => {
+      const rowsMin = 3;
+      const lineHeight = 15;
+      const wrapper = mount(<Textarea rowsMin={rowsMin} />);
+      setLayout(wrapper, {
+        getComputedStyle: {
+          'box-sizing': 'content-box',
+        },
+        scrollHeight: 30,
+        lineHeight,
+      });
+      wrapper.setProps();
+      wrapper.update();
+      assert.strictEqual(getHeight(wrapper), lineHeight * rowsMin);
     });
 
-    it('should handle the resize event', () => {
-      const wrapper = shallow(<TextareaNaked classes={{}} />);
-      const refs = assignRefs(wrapper);
-      refs.textareaShadowRef.scrollHeight = 43;
-      refs.singlelineShadowRef.scrollHeight = 43;
-      wrapper
-        .find('EventListener')
-        .at(0)
-        .simulate('resize');
-      assert.strictEqual(wrapper.state().height, 19);
-      clock.tick(166);
-      assert.strictEqual(wrapper.state().height, 43);
+    it('should have at max "rowsMax" rows', () => {
+      const rowsMax = 3;
+      const lineHeight = 15;
+      const wrapper = mount(<Textarea rowsMax={rowsMax} />);
+      setLayout(wrapper, {
+        getComputedStyle: {
+          'box-sizing': 'content-box',
+        },
+        scrollHeight: 100,
+        lineHeight,
+      });
+      wrapper.setProps();
+      wrapper.update();
+      assert.strictEqual(getHeight(wrapper), lineHeight * rowsMax);
     });
   });
 });
diff --git a/test/regressions/tests/Textarea/Textarea.js b/test/regressions/tests/Textarea/Textarea.js
new file mode 100644
--- /dev/null
+++ b/test/regressions/tests/Textarea/Textarea.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Input from '@material-ui/core/Input';
+
+const useStyles = makeStyles({
+  input: {
+    width: 200,
+  },
+  input1: {
+    fontSize: 13,
+    boxSizing: 'border-box',
+    border: '10px solid black',
+  },
+  input2: {
+    fontSize: 13,
+    boxSizing: 'content-box',
+    padding: 10,
+  },
+});
+
+function Textarea() {
+  const [value, setValue] = React.useState(
+    `Hey, sorry for being late to respond. Here is a codesandbox. It actually happens when I reduce the font-size of the input. Try adding some text or paste a long paragraph and you will the bottom margin being increased. It works fine with the default font-size.`,
+  );
+  const classes = useStyles();
+
+  const handleChange = event => {
+    setValue(event.target.value);
+  };
+
+  return (
+    <div>
+      <Input
+        className={classes.input}
+        multiline
+        value={value}
+        onChange={handleChange}
+        classes={{
+          input: classes.input1,
+        }}
+      />
+      <Input
+        className={classes.input}
+        multiline
+        value={value}
+        onChange={handleChange}
+        classes={{
+          input: classes.input2,
+        }}
+      />
+      <Input className={classes.input} multiline placeholder="rowsMin" rowsMin="3" />
+      <Input
+        className={classes.input}
+        multiline
+        value={value}
+        onChange={handleChange}
+        rowsMax="4"
+      />
+      <Input className={classes.input} multiline placeholder="long placeholder long placeholder" />
+      <Input
+        className={classes.input}
+        multiline
+        defaultValue="long default value long default value"
+      />
+    </div>
+  );
+}
+
+export default Textarea;