Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 tasks done
Tracked by #38374
jonas-scytech opened this issue Dec 13, 2018 · 41 comments
Open
2 tasks done
Tracked by #38374
Assignees
Labels
component: text field This is the name of the generic UI component, not the React module! design: material you

Comments

@jonas-scytech
Copy link

  • This is not a v0.x issue.
  • 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
@oliviertassinari oliviertassinari added component: text field This is the name of the generic UI component, not the React module! low priority labels Dec 14, 2018
@oliviertassinari
Copy link
Member

@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 oliviertassinari added the new feature New feature or request label Dec 14, 2018
@jonas-scytech
Copy link
Author

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.

@TidyIQ
Copy link
Contributor

TidyIQ commented Apr 21, 2019

Any update on this?

@eps1lon
Copy link
Member

eps1lon commented Apr 22, 2019

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.

@jonas-scytech
Copy link
Author

jonas-scytech commented Apr 22, 2019

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

@eps1lon
Copy link
Member

eps1lon commented Apr 23, 2019

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.

@jonas-scytech
Copy link
Author

Screen Shot 2019-04-23 at 11 32 59

@oliviertassinari had already shared it here

@eps1lon
Copy link
Member

eps1lon commented Apr 23, 2019

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

@TidyIQ
Copy link
Contributor

TidyIQ commented Apr 23, 2019

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
Copy link
Contributor

TidyIQ commented Apr 23, 2019

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.

@eps1lon
Copy link
Member

eps1lon commented Apr 23, 2019

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.

@TidyIQ
Copy link
Contributor

TidyIQ commented Apr 23, 2019

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.

@jonas-scytech
Copy link
Author

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

@jonas-scytech
Copy link
Author

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.

@PsyGik
Copy link

PsyGik commented May 22, 2019

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 😓

@NoahDavidATL
Copy link

NoahDavidATL commented May 24, 2019

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

@TidyIQ
Copy link
Contributor

TidyIQ commented Jul 18, 2019

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.

@oliviertassinari
Copy link
Member

the label should not start "shrunk"

@TidyIQ For sure 👍

@kelly-tock
Copy link

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.

@Cristy94
Copy link

Cristy94 commented Dec 4, 2019

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.

@chenasraf
Copy link

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

@richardanewman
Copy link

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;

@otaviobonder-deel
Copy link

otaviobonder-deel commented Nov 23, 2021

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

@Dentrax
Copy link

Dentrax commented Mar 7, 2022

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? 🤞

@kbooz
Copy link

kbooz commented Apr 26, 2022

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!

@niklaswallerstedt
Copy link

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

@ytoubal
Copy link

ytoubal commented Jul 9, 2022

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

@norayr93
Copy link

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.

@BananaHotSauce
Copy link

looks like everyone gave up on this one

@BananaHotSauce
Copy link

This gave me a really hard time. but i have hacked around it. feel free to change my code.

`import React,{useState} from "react"
import {TextField,InputAdornment,IconButton} from "@mui/material"
import VisibilityIcon from '@mui/icons-material/Visibility';
import EmailIcon from '@mui/icons-material/Email';
import PasswordIcon from '@mui/icons-material/Password';

function CustomTextfield({
id,
label,
endAdornment,
startAdornment,
isPassword,
hasBottomMargin,
value,
onChange
}){

const [shrink,setShrink] = useState(false)
const [meow,setMeow] = useState(false)

const EndAdornment = endAdornment
const StartAdornment = startAdornment

const end_Adornment = endAdornment ? (
        <InputAdornment position='end'>
            <IconButton
            aria-label='toggle password visibility'>
                <EndAdornment />
            </IconButton>
        </InputAdornment>
    ) : null


const start_Adornment = startAdornment ? (
    <InputAdornment position='end'>
        <IconButton
        aria-label='toggle password visibility'>
            <StartAdornment sx={{pl:"0px"}}/>
        </IconButton>
    </InputAdornment>
) : null

const fffff = !value ? {
    '&.MuiInputLabel-root': {
        transform: `translate(54px, 1rem)`,
    },
    '&.Mui-focused': {
    transform: "translate(14px, -9px) scale(0.75)",
    },
    '&.MuiInputLabel-root:not(.Mui-focused) ~ .MuiInputBase-root .MuiOutlinedInput-notchedOutline legend': {
        maxWidth: 0,
    }
} : {}

const labelHack = (startAdornment || endAdornment) ? {
    sx: {
        ...fffff
    }
} : {}

return(
    <>            
        <TextField
            id={id}
            label={label}
            type={isPassword ? 'password' : 'email'}
            InputProps={{
                endAdornment: end_Adornment,
                startAdornment: start_Adornment,
                style: {
                    padding: "0 14px 0 0"
                },
                className:'we-track-custom-texhfield'
            }}
            InputLabelProps={
            {
                ...labelHack
                
            }}
            fullWidth
            sx={hasBottomMargin ? {mb:2} : {}}
            value={value}
            onChange={e => onChange(e.target.value)}
        />
    </>
)

}

export default CustomTextfield`

@MehbubRashid
Copy link

MehbubRashid commented Sep 15, 2022

Not very hard to implement.

Animation
I faced the issue in the autocomplete component. So i solved it in autocomplete. But the code is same for textfield as well since autocomplete is a wrapper on top of textfield.

Code:
You might have to calibrate the transform: translate(x, y) values to suit your input field size.

const [shrink, setShrink] = useState(false);

return (
    <TextField 
        sx={{
            '& .MuiInputLabel-root:not(.MuiInputLabel-shrink)': {
                transform: "translate(41px, 17px)"
            }
        }}
        onFocus={() => setShrink(true)}
        onBlur={(e) => {
            !e.target.value && setShrink(false);
        }}
        label={label} 
        InputProps={{
            startAdornment: (
                <InputAdornment position="start">
                        <DirectionsBusIcon></DirectionsBusIcon>
                </InputAdornment>
            )
        }}
        InputLabelProps={{
            shrink: shrink,
        }}
    />
)

@dtrenz
Copy link

dtrenz commented Nov 2, 2022

Disappointed to come across this abandoned issue. I'm getting hit by it today. I appreciate that devs have provided some workarounds, but it feels bad to have to add local state and implement event handlers just to get the text field component to behave as it should.

@fakhamatia
Copy link

Same problem
InputLabel not show properly with startAdornment and direction rtl

Demo

        <FormControl sx={{ m: 1, width: '25ch' }} variant="outlined">
          <InputLabel htmlFor="outlined-adornment-password">Password</InputLabel>
          <OutlinedInput
            id="outlined-adornment-password"
            type={values.showPassword ? 'text' : 'password'}
            value={values.password}
            onChange={handleChange('password')}
            style={{ direction: "rtl" }}
            startAdornment={
              <InputAdornment position="start">
                <IconButton
                  aria-label="toggle password visibility"
                  onClick={handleClickShowPassword}
                  onMouseDown={handleMouseDownPassword}
                  edge="start"
                >
                  {values.showPassword ? <VisibilityOff /> : <Visibility />}
                </IconButton>
              </InputAdornment>
            }
            label="Password"
          />
        </FormControl>

@solomon23
Copy link

June 2023 this is still a problem - trying to add a start adornment to a multi select and it the label moves as if it has focus

<FormControl sx={{ m: 1, width: 300 }}>
        <InputLabel id="practice-location-input">Location</InputLabel>
        <Select
          labelId="practice-location-input"
          multiple
          value={locations}
          onChange={handleLocationChange}
          input={<OutlinedInput label="Location" />}
          renderValue={(selected) => selected.join(', ')}
          MenuProps={MenuProps}
          startAdornment={<VaccinesIcon />}
        >
          {data.locations.map((location) => {
....
          })}
        </Select>
      </FormControl>

@mj12albert
Copy link
Member

mj12albert commented Nov 1, 2023

I will see if we can fix this in v6 v7! #38374

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment