Search code examples
reactjsmaterial-uiformikautofillmutation-observers

Autofilled input fields show both value and label until clicked in React Material-UI TextField


I'm using Material-UI TextField components in my React application for a login form. The issue I'm facing happens when the browser (e.g., Chrome) autofills the saved credentials into the input fields. The values are filled correctly, but the floating labels remain in the input field, making both the label and the value overlap and unreadable.

The labels should float to the top when the inputs are autofilled, but this only happens if I manually click on the input fields. Until then, the labels stay inside the field, overlapping with the value.

My Questions: How can I automatically float the labels to the top when the browser autofills the input fields, without requiring user interaction?

I've tried using inputRef and manually triggering focus and blur events with no success. I also tried using a MutationObserver to detect autofill by monitoring the value changes, but it still didn’t solve the problem of moving the label up automatically.


Solution

  • I use this component in my app:

    export const AutoFillAwareTextField = ({ onChange, inputProps, InputLabelProps, ...rest }: TextFieldProps) => {
      const [fieldHasValue, setFieldHasValue] = useState(false);
      const makeAnimationStartHandler =
        (stateSetter: React.Dispatch<React.SetStateAction<boolean>>) =>
        (e: React.AnimationEvent<HTMLInputElement | HTMLTextAreaElement>) => {
          const autofilledBrowser = Boolean((e.target as any)?.matches('*:-webkit-autofill'));
          const autofilled = autofilledBrowser || (exists(rest.value) && !isEmptyString(rest.value as any));
          if (e.animationName === 'mui-auto-fill' || e.animationName === 'mui-auto-fill-cancel') {
            stateSetter(autofilled);
          }
        };
    
      const _onChange = useCallback(
        (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
          onChange?.(e.target.value as any);
          setFieldHasValue(e.target.value !== '');
        },
        [onChange],
      );
    
      return (
        <TextField
          inputProps={{
            onAnimationStart: makeAnimationStartHandler(setFieldHasValue),
            ...inputProps,
          }}
          InputLabelProps={{
            shrink: fieldHasValue,
            ...InputLabelProps,
          }}
          onChange={_onChange}
          {...rest}
        />
      );
    };

    It basically has an animation listener and wraps the onChange function to store state indicating whether the input has something or not, and shrink the InputLabel based on that.