Repository: mui/material-ui (TypeScript) — Bug Fix

Issue:
[Autocomplete] Adding startAdornment results in extra top padding on IE11 only
- [x] The issue is present in the latest release.
- [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.

## Current Behavior 😯

Using a `startAdornment` on AutoComplete's `renderInput` results in an extra top padding on IE11.

We've seen it with the `outlined` variant, but only when we set a `placeholder`.(i.e. works with `label`).

Manually setting the `TextField`'s `notched` property to `false` appears to "fix" it.

Here's the issue in action (the table structure mimicks the components' placement on the screenshots):
<table class="tg">
  <tr>
    <th class="tg-0lax" rowspan="2">Chrome</th>
    <td class="tg-0lax">Label</th>
    <td class="tg-0lax">Placeholder<br></th>
    <td class="tg-0lax" rowspan="2"><img width="301" alt="Regular" src="https://user-images.githubusercontent.com/431708/72258160-1f2fc600-35ec-11ea-8bee-d326568f12a9.png">
</th>
  </tr>
  <tr>
    <td class="tg-0lax">Label + startAdornment</td>
    <td class="tg-0lax">Placeholder + startAdornment<br></td>
  </tr>
  <tr>
    <th class="tg-0lax" rowspan="2">IE11</td>
    <td class="tg-0lax">Label</td>
    <td class="tg-0lax">Placeholder</td>
    <td class="tg-0lax" rowspan="2"><img width="295" alt="Padded" src="https://user-images.githubusercontent.com/431708/72258163-21922000-35ec-11ea-9c04-d3c2ef3f1e2c.png"></td>
  </tr>
  <tr>
    <td class="tg-0lax">Label + startAdornment</td>
    <td class="tg-0lax">Placeholder + startAdornment</td>
  </tr>
</table>


Inspecting the generated DOM element shows that the `<fieldset>` tag has a `top: -5px` property which, when cleared, fixes the issue: 

<img width="398" alt="Inspected" src="https://user-images.githubusercontent.com/431708/72258168-235be380-35ec-11ea-840d-fe4679175adc.png">

```css
.PrivateNotchedOutline-root-85 {
  top: -5px;    <-----
  left: 0;
  right: 0;
  ...
}
```


## Expected Behavior 🤔

Should be identical to the other browsers (tested on 5 others, listed below)

## Steps to Reproduce 🕹

*  https://codesandbox.io/s/material-demo-o3jlt
 
The modified Material UI autocomplete demo above should show the issue, but unfortunately _codesandbox.io_ is currently broken on IE11 due to transpiling issues.

The gist of the demo is to create an `AutoComplete` with the `placeholder` and `startAdornment` params set on `renderInput`'s `TextField` and visualize it on IE11:

```js
renderInput={params => (
  <TextField
    {...params}
    placeholder="Placeholder text"
    variant="outlined"
    InputProps={{
      ...params.InputProps,
      startAdornment: <span>span</span>
    }}
    fullWidth
  />
)}
```





## Context 🔦

We're happy MaterialUI users with IE11 users 😄 

## 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/core | 4.7.2  |
| @material-ui/lab | 4.0.0-alpha.38
| React       |   16.12.0      |
| Browser     |  IE11 (❌ ), Chrome 79 (✅) , Firefox 72(✅), Edge 18(✅ , Edge 79(✅ ), Safari 13 (✅ )|

Thanks for this **great** project! 


Discussion:

Resolving patch:
diff --git a/docs/pages/api/outlined-input.md b/docs/pages/api/outlined-input.md
--- a/docs/pages/api/outlined-input.md
+++ b/docs/pages/api/outlined-input.md
@@ -38,7 +38,8 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">inputComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'input'</span> | The component used for the native input. Either a string to use a DOM element or a component. |
 | <span class="prop-name">inputProps</span> | <span class="prop-type">object</span> |  | [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element. |
 | <span class="prop-name">inputRef</span> | <span class="prop-type">ref</span> |  | Pass a ref to the `input` element. |
-| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The width of the label. |
+| <span class="prop-name">label</span> | <span class="prop-type">node</span> |  | The label of the input. It is only used for layout. The actual labelling is handled by `InputLabel`. If specified `labelWidth` is ignored. |
+| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The width of the label. Is ignored if `label` is provided. Prefer `label` if the input label appears with a strike through. |
 | <span class="prop-name">margin</span> | <span class="prop-type">'dense'<br>&#124;&nbsp;'none'</span> |  | If `dense`, will adjust vertical spacing. This is normally obtained via context from FormControl. |
 | <span class="prop-name">multiline</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, a textarea element will be rendered. |
 | <span class="prop-name">name</span> | <span class="prop-type">string</span> |  | Name attribute of the `input` element. |
diff --git a/docs/pages/api/select.md b/docs/pages/api/select.md
--- a/docs/pages/api/select.md
+++ b/docs/pages/api/select.md
@@ -32,8 +32,9 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">IconComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">ArrowDropDownIcon</span> | The icon that displays the arrow. |
 | <span class="prop-name">input</span> | <span class="prop-type">element</span> |  | An `Input` element; does not have to be a material-ui specific `Input`. |
 | <span class="prop-name">inputProps</span> | <span class="prop-type">object</span> |  | [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element. When `native` is `true`, the attributes are applied on the `select` element. |
+| <span class="prop-name">label</span> | <span class="prop-type">node</span> |  | See [OutlinedLabel#label](/api/outlined-input/#props) |
 | <span class="prop-name">labelId</span> | <span class="prop-type">string</span> |  | The idea of an element that acts as an additional label. The Select will be labelled by the additional label and the selected value. |
-| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The label width to be used on OutlinedInput. This prop is required when the `variant` prop is `outlined`. |
+| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | See OutlinedLabel#label |
 | <span class="prop-name">MenuProps</span> | <span class="prop-type">object</span> |  | Props applied to the [`Menu`](/api/menu/) element. |
 | <span class="prop-name">multiple</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, `value` must be an array and the menu will support multiple selections. |
 | <span class="prop-name">native</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the component will be using a native `select` element. |
diff --git a/docs/src/pages/components/text-fields/ComposedTextField.js b/docs/src/pages/components/text-fields/ComposedTextField.js
--- a/docs/src/pages/components/text-fields/ComposedTextField.js
+++ b/docs/src/pages/components/text-fields/ComposedTextField.js
@@ -16,15 +16,9 @@ const useStyles = makeStyles(theme => ({
 }));
 
 export default function ComposedTextField() {
-  const [labelWidth, setLabelWidth] = React.useState(0);
   const [name, setName] = React.useState('Composed TextField');
-  const labelRef = React.useRef(null);
   const classes = useStyles();
 
-  React.useEffect(() => {
-    setLabelWidth(labelRef.current.offsetWidth);
-  }, []);
-
   const handleChange = event => {
     setName(event.target.value);
   };
@@ -61,15 +55,8 @@ export default function ComposedTextField() {
         <FormHelperText id="component-error-text">Error</FormHelperText>
       </FormControl>
       <FormControl variant="outlined">
-        <InputLabel ref={labelRef} htmlFor="component-outlined">
-          Name
-        </InputLabel>
-        <OutlinedInput
-          id="component-outlined"
-          value={name}
-          onChange={handleChange}
-          labelWidth={labelWidth}
-        />
+        <InputLabel htmlFor="component-outlined">Name</InputLabel>
+        <OutlinedInput id="component-outlined" value={name} onChange={handleChange} label="Name" />
       </FormControl>
       <FormControl variant="filled">
         <InputLabel htmlFor="component-filled">Name</InputLabel>
diff --git a/docs/src/pages/components/text-fields/ComposedTextField.tsx b/docs/src/pages/components/text-fields/ComposedTextField.tsx
--- a/docs/src/pages/components/text-fields/ComposedTextField.tsx
+++ b/docs/src/pages/components/text-fields/ComposedTextField.tsx
@@ -18,15 +18,9 @@ const useStyles = makeStyles((theme: Theme) =>
 );
 
 export default function ComposedTextField() {
-  const [labelWidth, setLabelWidth] = React.useState(0);
   const [name, setName] = React.useState('Composed TextField');
-  const labelRef = React.useRef<HTMLLabelElement>(null);
   const classes = useStyles();
 
-  React.useEffect(() => {
-    setLabelWidth(labelRef.current!.offsetWidth);
-  }, []);
-
   const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     setName(event.target.value);
   };
@@ -63,15 +57,8 @@ export default function ComposedTextField() {
         <FormHelperText id="component-error-text">Error</FormHelperText>
       </FormControl>
       <FormControl variant="outlined">
-        <InputLabel ref={labelRef} htmlFor="component-outlined">
-          Name
-        </InputLabel>
-        <OutlinedInput
-          id="component-outlined"
-          value={name}
-          onChange={handleChange}
-          labelWidth={labelWidth}
-        />
+        <InputLabel htmlFor="component-outlined">Name</InputLabel>
+        <OutlinedInput id="component-outlined" value={name} onChange={handleChange} label="Name" />
       </FormControl>
       <FormControl variant="filled">
         <InputLabel htmlFor="component-filled">Name</InputLabel>
diff --git a/packages/material-ui/src/OutlinedInput/NotchedOutline.js b/packages/material-ui/src/OutlinedInput/NotchedOutline.js
--- a/packages/material-ui/src/OutlinedInput/NotchedOutline.js
+++ b/packages/material-ui/src/OutlinedInput/NotchedOutline.js
@@ -18,6 +18,7 @@ export const styles = theme => {
       left: 0,
       margin: 0,
       padding: 0,
+      paddingLeft: 8,
       pointerEvents: 'none',
       borderRadius: 'inherit',
       borderStyle: 'solid',
@@ -28,16 +29,40 @@ export const styles = theme => {
         easing: theme.transitions.easing.easeOut,
       }),
     },
-    /* Styles applied to the legend element. */
+    /* Styles applied to the legend element when `labelWidth` is provided. */
     legend: {
       textAlign: 'left',
       padding: 0,
-      lineHeight: '11px',
+      lineHeight: '11px', // sync with `height` in `legend` styles
       transition: theme.transitions.create('width', {
         duration: theme.transitions.duration.shorter,
         easing: theme.transitions.easing.easeOut,
       }),
     },
+    /* Styles applied to the legend element. */
+    legendLabelled: {
+      textAlign: 'left',
+      padding: 0,
+      height: 11, // sync with `lineHeight` in `legend` styles
+      fontSize: '0.75rem',
+      visibility: 'hidden',
+      maxWidth: 0.01,
+      transition: theme.transitions.create('max-width', {
+        duration: theme.transitions.duration.shorter,
+        easing: theme.transitions.easing.easeOut,
+      }),
+      '& span': {
+        paddingLeft: 4,
+        paddingRight: 6,
+      },
+    },
+    legendNotched: {
+      maxWidth: 1000,
+      transition: theme.transitions.create('max-width', {
+        duration: theme.transitions.duration.shorter,
+        easing: theme.transitions.easing.easeOut,
+      }),
+    },
   };
 };
 
@@ -49,6 +74,7 @@ const NotchedOutline = React.forwardRef(function NotchedOutline(props, ref) {
     children,
     classes,
     className,
+    label,
     labelWidth: labelWidthProp,
     notched,
     style,
@@ -56,7 +82,30 @@ const NotchedOutline = React.forwardRef(function NotchedOutline(props, ref) {
   } = props;
   const theme = useTheme();
   const align = theme.direction === 'rtl' ? 'right' : 'left';
-  const labelWidth = labelWidthProp > 0 ? labelWidthProp * 0.75 + 8 : 0;
+
+  if (label !== undefined) {
+    return (
+      <fieldset
+        aria-hidden
+        className={clsx(classes.root, className)}
+        ref={ref}
+        style={style}
+        {...other}
+      >
+        <legend
+          className={clsx(classes.legendLabelled, {
+            [classes.legendNotched]: notched,
+          })}
+        >
+          {/* Use the nominal use case of the legend, avoid rendering artefacts. */}
+          {/* eslint-disable-next-line react/no-danger */}
+          {label ? <span>{label}</span> : <span dangerouslySetInnerHTML={{ __html: '&#8203;' }} />}
+        </legend>
+      </fieldset>
+    );
+  }
+
+  const labelWidth = labelWidthProp > 0 ? labelWidthProp * 0.75 + 8 : 0.01;
 
   return (
     <fieldset
@@ -100,6 +149,10 @@ NotchedOutline.propTypes = {
    * @ignore
    */
   className: PropTypes.string,
+  /**
+   * The label.
+   */
+  label: PropTypes.node,
   /**
    * The width of the label.
    */
diff --git a/packages/material-ui/src/OutlinedInput/OutlinedInput.d.ts b/packages/material-ui/src/OutlinedInput/OutlinedInput.d.ts
--- a/packages/material-ui/src/OutlinedInput/OutlinedInput.d.ts
+++ b/packages/material-ui/src/OutlinedInput/OutlinedInput.d.ts
@@ -3,8 +3,9 @@ import { StandardProps } from '..';
 import { InputBaseProps } from '../InputBase';
 
 export interface OutlinedInputProps extends StandardProps<InputBaseProps, OutlinedInputClassKey> {
+  label?: React.ReactNode;
   notched?: boolean;
-  labelWidth: number;
+  labelWidth?: number;
 }
 
 export type OutlinedInputClassKey =
diff --git a/packages/material-ui/src/OutlinedInput/OutlinedInput.js b/packages/material-ui/src/OutlinedInput/OutlinedInput.js
--- a/packages/material-ui/src/OutlinedInput/OutlinedInput.js
+++ b/packages/material-ui/src/OutlinedInput/OutlinedInput.js
@@ -103,6 +103,7 @@ const OutlinedInput = React.forwardRef(function OutlinedInput(props, ref) {
     classes,
     fullWidth = false,
     inputComponent = 'input',
+    label,
     labelWidth = 0,
     multiline = false,
     notched,
@@ -115,6 +116,7 @@ const OutlinedInput = React.forwardRef(function OutlinedInput(props, ref) {
       renderSuffix={state => (
         <NotchedOutline
           className={classes.notchedOutline}
+          label={label}
           labelWidth={labelWidth}
           notched={
             typeof notched !== 'undefined'
@@ -201,7 +203,13 @@ OutlinedInput.propTypes = {
    */
   inputRef: refType,
   /**
-   * The width of the label.
+   * The label of the input. It is only used for layout. The actual labelling
+   * is handled by `InputLabel`. If specified `labelWidth` is ignored.
+   */
+  label: PropTypes.node,
+  /**
+   * The width of the label. Is ignored if `label` is provided. Prefer `label`
+   * if the input label appears with a strike through.
    */
   labelWidth: PropTypes.number,
   /**
diff --git a/packages/material-ui/src/Select/Select.d.ts b/packages/material-ui/src/Select/Select.d.ts
--- a/packages/material-ui/src/Select/Select.d.ts
+++ b/packages/material-ui/src/Select/Select.d.ts
@@ -11,6 +11,7 @@ export interface SelectProps
   displayEmpty?: boolean;
   IconComponent?: React.ElementType;
   input?: React.ReactNode;
+  label?: React.ReactNode;
   labelId?: string;
   labelWidth?: number;
   MenuProps?: Partial<MenuProps>;
diff --git a/packages/material-ui/src/Select/Select.js b/packages/material-ui/src/Select/Select.js
--- a/packages/material-ui/src/Select/Select.js
+++ b/packages/material-ui/src/Select/Select.js
@@ -24,6 +24,7 @@ const Select = React.forwardRef(function Select(props, ref) {
     id,
     input,
     inputProps,
+    label,
     labelId,
     labelWidth = 0,
     MenuProps,
@@ -53,7 +54,7 @@ const Select = React.forwardRef(function Select(props, ref) {
     input ||
     {
       standard: <Input />,
-      outlined: <OutlinedInput labelWidth={labelWidth} />,
+      outlined: <OutlinedInput label={label} labelWidth={labelWidth} />,
       filled: <FilledInput />,
     }[variant];
 
@@ -141,14 +142,17 @@ Select.propTypes = {
    * When `native` is `true`, the attributes are applied on the `select` element.
    */
   inputProps: PropTypes.object,
+  /**
+   * See [OutlinedLabel#label](/api/outlined-input/#props)
+   */
+  label: PropTypes.node,
   /**
    * The idea of an element that acts as an additional label. The Select will
    * be labelled by the additional label and the selected value.
    */
   labelId: PropTypes.string,
   /**
-   * The label width to be used on OutlinedInput.
-   * This prop is required when the `variant` prop is `outlined`.
+   * See OutlinedLabel#label
    */
   labelWidth: PropTypes.number,
   /**
diff --git a/packages/material-ui/src/TextField/TextField.js b/packages/material-ui/src/TextField/TextField.js
--- a/packages/material-ui/src/TextField/TextField.js
+++ b/packages/material-ui/src/TextField/TextField.js
@@ -1,5 +1,4 @@
 import React from 'react';
-import ReactDOM from 'react-dom';
 import PropTypes from 'prop-types';
 import clsx from 'clsx';
 import { refType } from '@material-ui/utils';
@@ -93,16 +92,6 @@ const TextField = React.forwardRef(function TextField(props, ref) {
     ...other
   } = props;
 
-  const [labelWidth, setLabelWidth] = React.useState(0);
-  const labelRef = React.useRef(null);
-  React.useEffect(() => {
-    if (variant === 'outlined') {
-      // #StrictMode ready
-      const labelNode = ReactDOM.findDOMNode(labelRef.current);
-      setLabelWidth(labelNode != null ? labelNode.offsetWidth : 0);
-    }
-  }, [variant, required, label]);
-
   if (process.env.NODE_ENV !== 'production') {
     if (select && !children) {
       console.error(
@@ -118,7 +107,14 @@ const TextField = React.forwardRef(function TextField(props, ref) {
       InputMore.notched = InputLabelProps.shrink;
     }
 
-    InputMore.labelWidth = labelWidth;
+    InputMore.label = label ? (
+      <React.Fragment>
+        {label}
+        {required && '\u00a0*'}
+      </React.Fragment>
+    ) : (
+      label
+    );
   }
   if (select) {
     // unset defaults from textbox inputs
@@ -170,7 +166,7 @@ const TextField = React.forwardRef(function TextField(props, ref) {
       {...other}
     >
       {label && (
-        <InputLabel htmlFor={id} ref={labelRef} id={inputLabelId} {...InputLabelProps}>
+        <InputLabel htmlFor={id} id={inputLabelId} {...InputLabelProps}>
           {label}
         </InputLabel>
       )}


Test patch:
diff --git a/packages/material-ui/src/TextField/TextField.test.js b/packages/material-ui/src/TextField/TextField.test.js
--- a/packages/material-ui/src/TextField/TextField.test.js
+++ b/packages/material-ui/src/TextField/TextField.test.js
@@ -92,9 +92,19 @@ describe('<TextField />', () => {
 
   describe('with an outline', () => {
     it('should set outline props', () => {
-      const wrapper = mount(<TextField variant="outlined" />);
+      const { container, getAllByTestId } = render(
+        <TextField
+          InputProps={{ classes: { notchedOutline: 'notch' } }}
+          label={<div data-testid="label">label</div>}
+          required
+          variant="outlined"
+        />,
+      );
 
-      expect(wrapper.find(OutlinedInput).props()).to.have.property('labelWidth', 0);
+      const [, fakeLabel] = getAllByTestId('label');
+      const notch = container.querySelector('.notch legend');
+      expect(notch).to.contain(fakeLabel);
+      expect(notch).to.have.text('label\u00a0*');
     });
 
     it('should set shrink prop on outline from label', () => {