material-ui icon indicating copy to clipboard operation
material-ui copied to clipboard

[TextField][InputAdornment] InputLabel should not start shrunken if TextField has an InputAdornment

Open jonas-scytech opened this issue 6 years ago β€’ 34 comments

  • [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

jonas-scytech avatar Dec 13 '18 17:12 jonas-scytech

@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.

oliviertassinari avatar Dec 14 '18 09:12 oliviertassinari

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.

jonas-scytech avatar Dec 14 '18 13:12 jonas-scytech

Any update on this?

TidyIQ avatar Apr 21 '19 10:04 TidyIQ

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.

eps1lon avatar Apr 22 '19 06:04 eps1lon

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

jonas-scytech avatar Apr 22 '19 16:04 jonas-scytech

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.

eps1lon avatar Apr 23 '19 13:04 eps1lon

Screen Shot 2019-04-23 at 11 32 59

@oliviertassinari had already shared it here

jonas-scytech avatar Apr 23 '19 14:04 jonas-scytech

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).

eps1lon avatar Apr 23 '19 14:04 eps1lon

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).

TidyIQ avatar Apr 23 '19 14:04 TidyIQ

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.

TidyIQ avatar Apr 23 '19 14:04 TidyIQ

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.

eps1lon avatar Apr 23 '19 15:04 eps1lon

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.

TidyIQ avatar Apr 23 '19 15:04 TidyIQ

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

jonas-scytech avatar Apr 23 '19 15:04 jonas-scytech

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.

jonas-scytech avatar Apr 24 '19 15:04 jonas-scytech

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 πŸ˜“

PsyGik avatar May 22 '19 04:05 PsyGik

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

NoahDavidATL avatar May 24 '19 15:05 NoahDavidATL

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.

TidyIQ avatar Jul 18 '19 02:07 TidyIQ

the label should not start "shrunk"

@TidyIQ For sure πŸ‘

oliviertassinari avatar Jul 18 '19 17:07 oliviertassinari

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.

kelly-tock avatar Dec 03 '19 15:12 kelly-tock

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.

Cristy94 avatar Dec 04 '19 14:12 Cristy94

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 :/

chenasraf avatar Dec 26 '19 10:12 chenasraf

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;

richardanewman avatar Apr 30 '20 06:04 richardanewman

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.

sappster avatar May 26 '20 01:05 sappster

yes, this should just be built in IMO.

kelly-tock avatar May 26 '20 13:05 kelly-tock

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>
        ),
      }}
    />
  );
};

TheAschr avatar Aug 22 '20 14:08 TheAschr

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

peteplays avatar Oct 22 '20 13:10 peteplays

IMHO this is a bug, not an enhancement.

softwareplumber avatar Jan 27 '21 15:01 softwareplumber

Could this be addressed in v5?

kelly-tock avatar Jan 27 '21 18:01 kelly-tock

I extended the very nice example from @TheAschr to add the possibility to add a custom icon prop to the existing TextField:

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { InputAdornment, TextField as MuiTextField } from '@mui/material';
import {
  StandardTextFieldProps as STFP,
  FilledTextFieldProps as FTFP,
  OutlinedTextFieldProps as OTFP,
} from '@mui/material';

interface CommonTextFieldProps {
  startIcon?: React.ReactNode;
}

type TextFieldProps =
  | (CommonTextFieldProps & STFP)
  | (CommonTextFieldProps & FTFP)
  | (CommonTextFieldProps & OTFP);

interface StyleProps {
  labelOffset?: number;
}

const textFieldStyles = ({ labelOffset }: StyleProps) => {
  return {
    inputLabelRoot: {
      transition: '300ms cubic-bezier(.25, .8, .5, 1)',
      marginLeft: labelOffset ? `${(labelOffset || 0) + 20}px` : '1px',
    },
    inputAdornment: {
      marginTop: '5px!important',
    },
  };
};

const TextField = (props: TextFieldProps) => {
  const { startIcon, ...rest } = props;

  const startAdornmentRef = useRef<HTMLDivElement>(null);

  const [labelOffset, setLabelOffset] = useState<number>();
  const [shrink, setShrink] = useState<boolean>(
    (typeof props.value === 'string' && props.value.length !== 0) ||
      (typeof props.value === 'number' && String(props.value).length !== 0) ||
      !!props.InputProps?.startAdornment ||
      false
  );

  const styles = useMemo(() => textFieldStyles({ labelOffset }), [labelOffset]);

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

  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 && !props.InputProps?.startAdornment) {
        setShrink(false);
      }
      if (props.onBlur) {
        props.onBlur(event);
      }
    },
    [props]
  );

  const StartAdornment = useMemo(() => {
    if (startIcon) {
      return (
        <InputAdornment sx={styles.inputAdornment} position="start" ref={startAdornmentRef}>
          {startIcon}
        </InputAdornment>
      );
    }
  }, [startIcon, styles.inputAdornment]);

  return (
    <MuiTextField
      {...rest}
      onFocus={onFocus}
      onBlur={onBlur}
      sx={{
        '& .MuiFilledInput-input': {
          marginLeft: labelOffset ? `${(labelOffset || 0) - 13}px` : 0,
        },
        ...props.sx,
      }}
      InputLabelProps={{
        shrink,
        sx: {
          ...styles.inputLabelRoot,
        },
        ...props.InputLabelProps,
      }}
      InputProps={{
        startAdornment: StartAdornment,
        ...props.InputProps,
      }}
    />
  );
};

So you can use it like:

<TextField label="I am an input" startIcon={<Icon.Magnifier />} />

image image

Suggestions are appreciated

otaviobonder-deel avatar Nov 23 '21 20:11 otaviobonder-deel

Almost 4 years passed since the issue, I'm newbie at material-ui, and eventually I had this problem and I'm here:

<TextField
  margin="normal"
  required 
  fullWidth
  id="outlined-required" 
  label="NAME"
  InputProps={{
    startAdornment: (
      <InputAdornment position="start">
        <ReceiptIcon />
      </InputAdornment>
    ),
  }}
/>

Label animation does not work if i pass startAdornment, but works if I set endAdornment. What's the workaround? How can I fix this?

Any updates on this? 🀞

Dentrax avatar Mar 07 '22 09:03 Dentrax

This is how I fixed using the sx prop only:

search bar

<TextField 
  InputLabelProps={{
    sx: {
      '&.MuiInputLabel-root': {
        transform: `translate(36px, 0.5rem)`,
      },
      '&.Mui-focused': {
        transform: "translate(14px, -9px) scale(0.75)",
      },
      '&.MuiInputLabel-root:not(.Mui-focused) ~ .MuiInputBase-root .MuiOutlinedInput-notchedOutline legend': {
        maxWidth: 0,
      }
    }
  }}
  InputProps={{
    startAdornment:(<InputAdornment position="start" ><SearchIcon /></InputAdornment>),
    size: "small"
  }}
/>

The translate values are currently totally arbitrary and customized for the "small" size variant, so feel free to change it!

kbooz avatar Apr 26 '22 07:04 kbooz

The way I went about it was inspired from previous suggestions in the thread. Something like this, you will need to adapt the initial value depending on if you have any data at first in the text field.

const [shrink, setShrink] = React.useState(false);
<TextField
onFocus={() => setShrink(true)}
onBlur={(e) => {
  !e.target.value && setShrink(false);
}}
InputLabelProps={{
  shrink: shrink,
}}
startAdornment: ...
...

niklaswallerstedt avatar Apr 26 '22 08:04 niklaswallerstedt

Any updates on this? I just started using MUI and I stumbled upon this issue

ytoubal avatar Jul 09 '22 01:07 ytoubal

Would be great if you could provide some easy workaround to toggle that behavior.I believe there are some edge cases that you haven't included that yet, but maybe some scenarios don't face that edge cases and the important aspect of the field is not working properly.

norayr93 avatar Aug 10 '22 02:08 norayr93