Material-ui: [TextField][InputAdornment] InputLabel should not start shrunken if TextField has an InputAdornment

Created on 13 Dec 2018  路  26Comments  路  Source: mui-org/material-ui

  • [x] This is not a v0.x issue.
  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior


Input label should start on its normal position, as seen here:
https://material-components.github.io/material-components-web-catalog/#/component/text-field

Current Behavior


Input label starts shrunken

Steps to Reproduce

https://material-ui.com/demos/text-fields/#outlined-input-adornments

Your Environment

| Tech | Version |
|--------------|---------|
| Material-UI | 3.6.1 |
| Material-UI styles | 3.0.0-alpha.2 |
| React | 16.7.0-alpha.2 |
| Browser | Chrome 71.0.3578.98 |
| TypeScript | 3.2.1 |

TextField enhancement

Most helpful comment

Any updates on this? This is a pretty common use case, most header search inputs for example have a search icon, and it should not be in minimzed state.

All 26 comments

@jonas-scytech Right now, we don't support this case to simplify the text field implementation. It can potentially bloat everybody bundle, for a limited value. To investigate.

Ok, I understand, thank you. I don't have time now, but I will look at this later and see if I find a solution with a small footprint.

Any update on this?

We discussed this before and I agree that the label shouldn't start shrunk with an input adornment. There was some discussion in #14126 with a lot of confusion around how it should look. IMO I don't see any issue with the MWC implementation. There were some points raised that the label "clashes" with the adornment but that happens during transition. I don't think anybody cares that the label is in front of the adornment for ~10 frames.

I'm still missing a spec example that confirms our implementation. As far as I can tell it should never start shrunk regardless of start adornment or not.

https://material.io/design/components/text-fields.html#anatomy

They have a Icons section showing a text field with a start adornment and shrunk label, I assume that if this was not the behaviour for Outlined text field they would say something there or in the dedicated section for the Outlined text field.

Edit: I should have read #14126 first, this was already mentioned there

They have a Icons section showing a text field with a start adornment and shrunk label

Could you include a screenshot? I can't find text fields in the linked document that have a start adornment, no input and a shrunk label.

Screen Shot 2019-04-23 at 11 32 59

@oliviertassinari had already shared it here

Do you mean the third example? The label is shrunk because the text field has an input value not because of the adornment (as is shown in the first example).

No, the first example. That's how it should look when there is no input value, but currently in MUI it starts off shrunk (like examples 2 and 3 except without any input value).

I really think this needs to be a big focus. It's the only component I've encountered in all of Material-UI that doesn't match the Material Design specs and looks substantially worse because of it.

No, the first example. That's how it should look when there is no input value, but currently in MUI it starts off shrunk (like examples 2 and 3 except without any input value).

So we agree. It sounded like @jonas-scytech was arguing that current implementation matches the specification.

Yeah sorry, I misread your comment.

You can almost get it to work properly by making the following changes:

const useStyles= makeStyles(theme => ({
  focused: {
    transform: "translate(12px, 7px) scale(0.75)"
  }
}))

...
<InputLabel
  classes={{ focused: classes.focused }}
  shrink={false}
>
Text
</InputLabel>

This results in the label starting in the non-shrink state (as per MD specs), then shrinks appropriately when focused. The only issue with it is that it doesn't stay in the shrink-state after the user clicks out. It expands back to the full size which causes the label to overlap the input value. If anyone knows how to keep it in the shrink-state when 1) not in focus, AND 2) has user input, then that's at least a workaround for now.

edit: Actually I should probably be able to solve this using state. I'll give it a go and will let you know if it works.

edit 2: Yep, got it working properly using state! The shrink prop on the label component is equal to a boolean state value, which gets changed using the onChange prop in the input component (based on event.target.value.length. If > 0 then set to true, if === 0 then set to false).

You still need to use a class override for 'focused' for the initial focus before the user inputs any text, and I also had to create another class override for 'marginDense' as I've set margins='dense' on my formcontrol component.

Finally! I wish I thought of this sooner. It's been bugging me for the longest time.

Sorry about the confusion, I meant "text field with a start adornment and NOT shrunk label" :/

Looks like Material Design have a different behaviour for Text fields with icons and text fields with affixes as seem here:
1
and here:
Screen Shot 2019-04-24 at 12 37 07
But Material-UI treat both as InputAdornment and I think there no easy way to tell each other apart.
I will try to split InputAdornment into InputIcon and InputAffix and see if it makes fixing this issue easier.

edit 2: Yep, got it working properly using state! The shrink prop on the label component is equal to a boolean state value, which gets changed using the onChange prop in the input component (based on event.target.value.length. If > 0 then set to true, if === 0 then set to false).

You still need to use a class override for 'focused' for the initial focus before the user inputs any text, and I also had to create another class override for 'marginDense' as I've set margins='dense' on my formcontrol component.

Finally! I wish I thought of this sooner. It's been bugging me for the longest time.

My initial approach to solve this was to extend the bottom-border (or underline if you may) to cover the icon as well. As I progressed I saw that I wrote a lot of code maintaining the hover, focused, disabled, error states. Scraped the whole thing.

Based on the inputs from @TidyIQ (You're a champion!!! 馃檶 ) this is what I was able to come up with for my use case. I used onFocus and onBlur instead of onChange because it made more sense to me.

````
import React from "react";

import TextField from "@material-ui/core/TextField";
import { withStyles } from "@material-ui/core/styles";
import InputAdornment from '@material-ui/core/InputAdornment';

const styles = theme => ({
formControl: {
left: 30, // this moves our label to the left, so it doesn't overlap when shrunk.
top: 0,
},
disabled: {},
});

class TextFieldIcon extends React.Component {

constructor(props) {
    super(props);
    this.state = {
        shrink: false // this is used to shrink/unshrink ( is this a correct word? ) the label
    }
}

shrinkLabel = (event) => {
    const { onFocus } = this.props;
    this.setState({shrink: true});
    onFocus && onFocus(event); // let the child do it's thing
};

unShrinkLabel = (event) => {
    const { onBlur } = this.props;
    if(event.target.value.length === 0) {
        this.setState({shrink: false}) //gotta make sure the input is empty before shrinking the label
    }
    onBlur && onBlur(event); // let the child do it's thing
};

render() {
   // make sure to check endIcon and startIcon, we don't need errors in our console
    const { classes, endIcon, autoComplete, startIcon, ...other } = this.props;
    return <TextField {...other}
                      onFocus={this.shrinkLabel}
                      onBlur={this.unShrinkLabel}
                      InputLabelProps={{shrink: this.state.shrink, classes: classes }}
                      InputProps={{
                          autoComplete,
                          endAdornment: endIcon && (
                              <InputAdornment position={"end"}>
                                  {endIcon}
                              </InputAdornment>
                          ),
                          startAdornment: startIcon && (
                              <InputAdornment position={"start"}>
                                 {startIcon}
                              </InputAdornment>
                          )}}
    />;
}

}

export default withStyles(styles)(TextFieldIcon);
````

I honestly believe that this should be baked in the library. I mean, the endAdornment works as in the specs. I'm not sure why the startAdornment doesn't follow the specs. Since I have a workaround for now, I won't complain. 馃槄Next challenge, get this working with rtl 馃槗

The InputAdornment API seemed to have been updated with V4 release, but it still doesn't work: https://codesandbox.io/s/pznrz -- this has been the biggest thorn in my side. Why can't it work like a normal text box, with a little adornment added to the front.

Also, the Github link appears to be broken: https://github.com/mui-org/material-ui/blob/master/docs/src/pages/demos/text-fields/ShrinkAuto.js

Just a quick FYI to further prove that the label should not start "shrunk". The official Material Design docs now has an interactive demo at https://material.io/design/components/text-fields.html#text-fields-single-line-text-field

In the configuration options, click "Leading icon". You can see that the label starts "unshrunken" and only shrinks when text is entered.

the label should not start "shrunk"

@TidyIQ For sure 馃憤

Just encountered this as well, its a very strange inconsistency to require the adornments to be an endAdornment to just get it to look and behave like other text fields in the same form.

https://material-components.github.io/material-components-web-catalog/#/component/text-field

in the demos section all variants are behaving the same way regardless or adornment start or end.

Any updates on this? This is a pretty common use case, most header search inputs for example have a search icon, and it should not be in minimzed state.

I was so happy to finally refactor our website to use MUI, and then the first thing I tried to change - the text inputs - I immediately ran into this problem, our designs are full of inputs that slide the label up on focus, regardless whether it has an icon/adornment or not. The offset needs to be modified still.

Will this be worked on soon? 馃檶 Or maybe a good workaround?... @PsyGik and @TidyIQ's solutions didn't work for me :/

Had the same issue, so want to share my solution. Big thanks to @PsyGik for sharing his solution, which I borrowed to come up with this one. Please let me know if you see any room for improvement. I just started working with React, so I could definitely be missing something. But so far, so good. It's working. Apologies about the formatting. Github isn't liking tabs right now.

import React, { useState } from 'react';
import { TextField, InputAdornment, withStyles } from '@material-ui/core';

const PriceField = withStyles({
        //Pushes label to right to clear start adornment
    root: {
        '& label': {
            marginLeft: '3.75rem'
        }
    }
})(TextField);

const StyledInputAdornment = withStyles({
    root: {
                //MUI puts .75rem padding-left by default. Could not override
        //so padding-right is .75 short to offset the difference
        padding: '1.125rem 1.75rem 1.125rem 1rem',
        borderRight: '1px solid #BBC8D8',
        height: 'inherit'
    }
})(InputAdornment);


const ExampleComponent = () => {

const [shrink, setShrink] = useState(false);
    const shrinkLabel = () => {
        setShrink(true);
    };
    const unShrinkLabel = e => {
        if (e.target.value.length === 0) {
            setShrink(false);
        }
    };

return (
        <PriceField
            type="number"
            label="Product Price"
            fullWidth
            onFocus={shrinkLabel}
            onBlur={unShrinkLabel}
            InputLabelProps={{ shrink: shrink }}
            InputProps={{
                startAdornment: currencyForIcon && currencyForIcon.symbol && (
                    <StyledInputAdornment variant="outlined" position="start">
                            {currencyForIcon.symbol}
                    </StyledInputAdornment>
                    )
                }}
            />
    );
};

export default ExampleComponent;

Came across this issue today and @richardanewman got me going in the right direction, and I found a solution for the outline variant with out-of-the-box MUI style. If you override the transform style in the .MuiInputLabel-outlined class. You can have the label offset with the adornment and it will still shrink to the default location with gap. Here is a code snippet:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '@material-ui/core/TextField';
import SearchIcon from '@material-ui/icons/Search';

import { withStyles, createStyles } from '@material-ui/core/styles';


const styles = (theme) => createStyles({
  labelOffset: {
    transform: "translate(44px, 20px) scale(1)",
  }
});



class TextBox extends Component {
  constructor(props) {
    super(props);

    this.state = {
      shrink: false,
    }

    this.onFocus = this.onFocus.bind(this);
    this.onBlur = this.onBlur.bind(this);
  }

  onFocus(event) {
    this.setState({ shrink: true });
  }

  onBlur(event) {
    if (event.target.value.length === 0)
      this.setState({ shrink: false });
  }

  render() {
    const { classes } = this.props;
    const { shrink } = this.state;

    return(
        <div>
        <TextField
           id="outlined-textarea"
           label="Place Label Here"
           placeholder="Placeholder"
           variant="outlined"
           onFocus={ this.onFocus }
           onBlur={ this.onBlur }
           InputLabelProps={{ shrink: shrink, classes:{ root: classes.labelOffset } }}
           InputProps={{
             startAdornment: (
               <InputAdornment variant="outlined" position="start">
                 <SearchIcon/>
               </InputAdornment>
             )
           }}
         />

        </div>
    );
  }

}

TextBox.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(TextBox);

result:
image

This works for me so I can keep going, but I plan to come back an investigate why the shrink gets automatically disabled when a startAdornment is added.

yes, this should just be built in IMO.

Here is my solution which works with both outlined and standard text fields (padding for filled is wonky):

        <StartAdornmentTextField
          label="Twitter Handle"
          fullWidth={true}
          startAndornmentText="@"
        />

import React, { useState, useCallback, useRef, useEffect } from "react";
import {
  makeStyles,
  TextField,
  TextFieldProps,
  InputAdornment,
} from "@material-ui/core";
import clsx from "clsx";

type StyleProps = {
  labelOffset: number | undefined;
};

const useStyles = makeStyles((theme) => ({
  inputLabelRoot: {
    display: ({ labelOffset }: StyleProps) =>
      labelOffset !== undefined ? "block" : "none",
    transition: ".3s cubic-bezier(.25,.8,.5,1)",
    marginLeft: ({ labelOffset }: StyleProps) => (labelOffset || 0) + 8,
  },
  inputLabelShrink: {
    marginLeft: () => 0,
  },
}));

export const StartAdornmentTextField: React.FC<
  TextFieldProps & { startAndornmentText: string | number }
> = ({ startAndornmentText, ...props }) => {
  const startAdornmentRef = useRef<HTMLDivElement>(null);

  const [labelOffset, setLabelOffset] = useState<number>();

  useEffect(() => {
    setLabelOffset(startAdornmentRef.current?.offsetWidth);
  }, [startAndornmentText]);

  const classes = useStyles({
    labelOffset,
  });

 const [shrink, setShrink] = useState<boolean>(
    (typeof props.value === "string" && props.value.length !== 0) ||
      (typeof props.value === "number" && String(props.value).length !== 0) ||
      false
  );

  const onFocus = useCallback(
    (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      setShrink(true);
      if (props.onFocus) {
        props.onFocus(event);
      }
    },
    [props]
  );

  const onBlur = useCallback(
    (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      if (event.target.value.length === 0) {
        setShrink(false);
      }
      if (props.onBlur) {
        props.onBlur(event);
      }
    },
    [props]
  );

  return (
    <TextField
      {...props}
      onFocus={onFocus}
      onBlur={onBlur}
      InputLabelProps={{
        shrink: shrink,
        classes: {
          shrink: clsx(
            classes.inputLabelShrink,
            props.InputLabelProps?.classes?.shrink
          ),
          root: clsx(
            classes.inputLabelRoot,
            props.InputLabelProps?.classes?.root
          ),
          ...props.InputLabelProps?.classes,
        },
        ...props.InputLabelProps,
      }}
      InputProps={{
        startAdornment: (
          <InputAdornment
            ref={startAdornmentRef}
            variant="outlined"
            position="start"
          >
            {startAndornmentText}
          </InputAdornment>
        ),
      }}
    />
  );
};

I was able to handle this with the following in the theme file. You just need to update the aria-label to match your needs. The spacing is not perfect, but its pretty close. Additionally, it does not mess with the label and has minimal impact on the input spacing.

    MuiIconButton: {
      root: {
        '&[aria-label="toggle password visibility"]': {
          padding: '0 23px 4px 0',
        },
      },
    },

Tweak the padding to match your needs

Was this page helpful?
0 / 5 - 0 ratings