Repository: mui/material-ui (TypeScript)
Date: 2019-04-21 20:47:37+00:00
Task type: Bug Fix

Issue:
[TextField] multiLine TextField loses cursor position
<!--- Provide a general summary of the issue in the Title above -->
Cursor jumps to the end in a multiLine TextField
<!--
    Thank you very much for contributing to Material-UI by creating an issue! ❤️
    To avoid duplicate issues we ask you to check off the following list.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
<!---
    Describe what should happen.
-->
The cursor should not jump to the end just like a regular TextField (multiline = false)

## Current Behavior 😯
<!---
    Describe what happens instead of the expected behavior.
-->
Cursor jumps to the end when typing in a multiline TextField

## Steps to Reproduce 🕹
<!---
    Provide a link to a live example (you can use codesandbox.io) and an unambiguous set of steps to reproduce this bug.
    Include code to reproduce, if relevant (which it most likely is).

    This codesandbox.io template _may_ be a good starting point:
    https://codesandbox.io/s/github/mui-org/material-ui/tree/next/examples/create-react-app

    If you're using typescript a better starting point would be
    https://codesandbox.io/s/github/mui-org/material-ui/tree/next/examples/create-react-app-with-typescript

    If YOU DO NOT take time to provide a codesandbox.io reproduction, should the COMMUNITY take time to help you?

-->
Link: https://codesandbox.io/s/62jzjowq4k

## Your Environment 🌎
<!---
    Include as many relevant details about the environment with which you experienced the bug.
    If you encounter issues with typescript please include version and tsconfig.
-->

| Tech         | Version |
|--------------|---------|
| Material-UI  | v4.0.0-alpha.8  |
| React        |   16.8.6      |
| Browser      |   Chrome      |



Issue discussion:

Solution patch:
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
@@ -9,6 +9,19 @@ function getStyleValue(computedStyle, property) {
 
 const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
 
+const styles = {
+  /* Styles applied to the shadow textarea element. */
+  shadow: {
+    // Visibility needed to hide the extra text area on iPads
+    visibility: 'hidden',
+    // Remove from the content flow
+    position: 'absolute',
+    // Ignore the scrollbar width
+    overflow: 'hidden',
+    height: '0',
+  },
+};
+
 /**
  * @ignore - internal component.
  *
@@ -20,27 +33,24 @@ const Textarea = React.forwardRef(function Textarea(props, ref) {
   const { current: isControlled } = React.useRef(value != null);
   const inputRef = React.useRef();
   const [state, setState] = React.useState({});
+  const shadowRef = React.useRef();
   const handleRef = useForkRef(ref, inputRef);
 
   const syncHeight = React.useCallback(() => {
     const input = inputRef.current;
-    const savedValue = input.value;
-    const savedHeight = input.style.height;
-    const savedOverflow = input.style.overflow;
+    const inputShallow = shadowRef.current;
 
-    input.style.overflow = 'hidden';
-    input.style.height = '0';
+    const computedStyle = window.getComputedStyle(input);
+    inputShallow.style.width = computedStyle.width;
+    inputShallow.value = input.value || props.placeholder || 'x';
 
     // The height of the inner content
-    input.value = savedValue || props.placeholder || 'x';
-    const innerHeight = input.scrollHeight;
-
-    const computedStyle = window.getComputedStyle(input);
+    const innerHeight = inputShallow.scrollHeight;
     const boxSizing = computedStyle['box-sizing'];
 
     // Measure height of a textarea with a single row
-    input.value = 'x';
-    const singleRowHeight = input.scrollHeight;
+    inputShallow.value = 'x';
+    const singleRowHeight = inputShallow.scrollHeight;
 
     // The height of the outer content
     let outerHeight = innerHeight;
@@ -63,10 +73,6 @@ const Textarea = React.forwardRef(function Textarea(props, ref) {
         getStyleValue(computedStyle, 'border-top-width');
     }
 
-    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.
@@ -108,17 +114,27 @@ const Textarea = React.forwardRef(function Textarea(props, ref) {
   };
 
   return (
-    <textarea
-      value={value}
-      onChange={handleChange}
-      ref={handleRef}
-      style={{
-        height: state.outerHeight,
-        overflow: state.outerHeight === state.innerHeight ? 'hidden' : null,
-        ...style,
-      }}
-      {...other}
-    />
+    <React.Fragment>
+      <textarea
+        value={value}
+        onChange={handleChange}
+        ref={handleRef}
+        style={{
+          height: state.outerHeight,
+          overflow: state.outerHeight === state.innerHeight ? 'hidden' : null,
+          ...style,
+        }}
+        {...other}
+      />
+      <textarea
+        aria-hidden="true"
+        className={props.className}
+        readOnly
+        ref={shadowRef}
+        tabIndex={-1}
+        style={{ ...styles.shadow, ...style }}
+      />
+    </React.Fragment>
   );
 });
 


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
@@ -38,11 +38,19 @@ describe('<Textarea />', () => {
     const getComputedStyleStub = {};
 
     function setLayout(wrapper, { getComputedStyle, scrollHeight, lineHeight }) {
-      const input = wrapper.find('textarea').instance();
+      const input = wrapper
+        .find('textarea')
+        .at(0)
+        .instance();
+      const shadow = wrapper
+        .find('textarea')
+        .at(1)
+        .instance();
+
       getComputedStyleStub[input] = getComputedStyle;
 
       let index = 0;
-      stub(input, 'scrollHeight').get(() => {
+      stub(shadow, 'scrollHeight').get(() => {
         index += 1;
         return index % 2 === 1 ? scrollHeight : lineHeight;
       });