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

TextArea is wrong size initially (off by a hair), then it adjusts as you type? #337

Open
lancejpollard opened this issue Nov 4, 2021 · 16 comments

Comments

@lancejpollard
Copy link

I have a <TextareaAutosize> used with react-hook-form, and when it starts out, the textarea is slightly smaller than it should be. When I type, it fixes. What should I do to fix this? Is it the font that is throwing it off, or my CSS or something?

Here is a gif.

2021-11-04 01 48 58

Here is a snippet of my code from a larger project:

import TextareaAutosize from 'react-textarea-autosize'
import styles from './index.module.css'
import { Controller } from 'react-hook-form'

export default function TextField({
  label,
  placeholder,
  type,
  value,
  inputId,
  control,
  prefix,
  name,
  minHeight,
  ...props
}) {
  return (
    <div className={styles.field}>
      <label className={styles.label}>{label}</label>
      <div className={styles.inputContainer}>
        <Controller
          name={name}
          control={control}
          render={({ field }) => (
            <TextareaAutosize {...field} minRows={minHeight} className={styles.input} placeholder={placeholder} />
          )}
        />
      </div>
    </div>
  )
}

And usage like this:

function Details() {
  const { handleSubmit, control } = useForm()

  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
      <legend className={styles.legend}>Details</legend>
      <TextField minHeight={4} control={control} name="blurb" label="Blurb" placeholder="Love to learn and make!" />
      <div className={styles.actions}><button className={styles.button} type="submit">Save</button></div>
    </form>
  )
}

The CSS for the file is here:

.field {
  padding-bottom: 20px;
}

.label {
  opacity: 0.7;
  font-size: 14px;
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
}

.image {
  width: 100px;
  height: 100px;
  background: #eee;
  margin-right: 20px;
}

.imageContainer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.removeUploadButton {
  display: flex;
  cursor: pointer;
}

.addButton {
  display: flex;
  cursor: pointer;
}

.removeButton {
  display: flex;
  left: -6px;
  cursor: pointer;
}

.inputContainer {
  display: flex;
  padding: 10px 20px;
  margin: 10px 0px;
  background: #eee;
  width: 100%;
  border-radius: 4px;
  font-size: 22px;
}

.input {
  flex: 1;
}

.input::placeholder, .inputPrefix {
  color: #aaa;
}

.legend {
  text-transform: uppercase;
  font-size: 22px;
  opacity: 0.7;
}

.imageContent {
  display: flex;
  align-items: center;
}

.button {
  cursor: pointer;
  padding: 10px 20px;
  color: white;
  background: #777;
  border-radius: 4px;
  text-align: center;
  width: 200px;
}

Any ideas?

@iggyfisk
Copy link

Experiencing the same thing in our app, started happening recently. We haven't changed any styles or markup on that page nor updated the library, so it's either a browser change (happens in Chrome/FF/Safari though) or something new in React.

@Andarist
Copy link
Owner

Please always try to share a repro case in a runnable form - either by providing a git repository to clone or a codesandbox. OSS maintainers usually can't afford the time to set up the repro, even if exact steps are given.

@aspnetde
Copy link

Just browsing by, not having a repro at hand either (sorry). But I can confirm a similar behavior: Randomly the initial height seems ~double of what it should be. When the textarea gets the focus, it immediately "snaps back" to the expected height. It also occured out of the blue a couple of weeks/months ago.

@m4cn4ir
Copy link

m4cn4ir commented Dec 2, 2021

I can confirm this behavior as well and I just installed the package without any sort of customization.
I start with no styling, minRows:1, maxRows: 4. It will display the textArea as intended, with one row.
Next step, I set a font size and then it will already lead to the issue. The textArea will be displayed with 2 rows and, on input, revert to 1

@m4cn4ir
Copy link

m4cn4ir commented Dec 2, 2021

The bug can be reproduced when I set the font size with classnames and in rem.
It wont occur if I set the size in react inline (either rem or px) or with classnames + px.

@aspnetde
Copy link

The bug can be reproduced when I set the font size with classnames and in rem. It wont occur if I set the size in react inline (either rem or px) or with classnames + px.

In my case, the font size alone doesn't change the behavior. It's a combination of multiple styles applied to the element (cannot figure out a pattern yet, though).

@raduflp
Copy link

raduflp commented Jan 28, 2022

@Andarist could not reproduce it for simple scenarios, but was able to reproduce it with @headlessui/react Transition
Inspecting the broken scenarios you can see that the textarea style doesn't have a height set compared to the working scenarios.
Hope that helps

https://codesandbox.io/s/clever-shtern-uw09l-uw09l

@Andarist
Copy link
Owner

@raduflp that is... somewhat helpful but I also believe that this might be a different issue than the issue reported originally here.

In this codesandbox, our useLayoutEffect that calls resizeTextarea is called when the rendered textarea is not yet attached to the DOM. I suspect that this is some kind of deferred mounting implemented in one of the components coming from the @headlessui/react.

My bet is that this code is related:
https://github.com/tailwindlabs/headlessui/blob/3bc754516985db9fa094e364ff10be4de34a473b/packages/%40headlessui-react/src/components/portal/portal.tsx#L71-L96

It looks like doing exactly that - mounting to a different container and then attaching/detaching it manually from within an effect. The problem is that... effects are called from the bottom to the top so our effect gets called first and we can't measure what the height should be when the textarea isn't mounted in the DOM.

@raduflp
Copy link

raduflp commented Jan 29, 2022

Thanks @Andarist for the explanation. Makes sense, but I'm not sure what's the best way to go about fixing this long term, not too familiar with the internals of react-textarea-autosize. I'll try to revisit this issue whenever I have more time at hand.

As a workaround now I have a thin wrapper that triggers a re-render on mount. That's less than ideal, but works for my use-cases.

export const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
  (props, ref) => {
    const [, setIsRerendered] = useState(false);
    useLayoutEffect(() => setIsRerendered(true), []);
    return <ReactTextareaAutosize {...props} ref={ref} />;
  }
);

@Andarist
Copy link
Owner

I'm not sure what's the best way to go about fixing this long term

Me neither 😉 In a way - this is a clash between the internals of those two libs. I don't plan to make any changes here to accommodate this use case - unless those would be really small changes. For instance, this could be solved with a MutationObserver but I don't plan on using it because it would complicate the codebase and add more code than I'm comfortable with. If anyone figures out how to improve this - I'm open to discussion but I won't certainly be looking for an answer myself.

@SonOfCrypto
Copy link

SonOfCrypto commented Feb 12, 2022

Have the same issue with @headlessui/react when putting the textarea inside a modal

thx @raduflp for the workaround 👍🏻

using it like this

const [isRerendered, setIsRerendered] = useState(false)
useLayoutEffect(() => setIsRerendered(true), [])
{isRerendered && <TextareaAutosize />}

@nielskrijger
Copy link

The workaround works for me as well.

In my setup the logic was as follows:

<div className="advanced-search">
  <AdvancedSearch onClick={onShowAdvanced} expanded={showAdvanced} />

  {showAdvanced && (
    <CustomTextarea label="Text" maxRows={5} />
  )}
</div>

where advanced search is a simple button toggling the "advanced section".

Removing the conditional {showAdvanced && ( ... )} and <CustomTextArea /> renders the correct TextArea height. With the condition however the height is too low, by the looks of it it only calculates the padding but excludes the font-size (a simple 16px).

The re-render trick made it render the correct height.

@mablin7
Copy link

mablin7 commented May 12, 2022

Not sure if it's the same bug, but I was having issues with the default height being off by 3px, but only on Chrome. I debugged it and it turns out that in Chrome (I couldn't test Safari rn) on the hidden text area the line height can differ from the line height of the actual text area. I can't figure out why, but explicitly setting a line height like 1.3 on the text area fixed the issue for me.

@dherault
Copy link

IMO This is due to the internal getHeight function that returns integers not decimal values.

@stasuello
Copy link

17.02.2023
But still no solution, except SonOfCrypto's solution. it is sad...

@Macil
Copy link

Macil commented Sep 26, 2023

@raduflp that is... somewhat helpful but I also believe that this might be a different issue than the issue reported originally here.

In this codesandbox, our useLayoutEffect that calls resizeTextarea is called when the rendered textarea is not yet attached to the DOM. I suspect that this is some kind of deferred mounting implemented in one of the components coming from the @headlessui/react.

My bet is that this code is related: https://github.com/tailwindlabs/headlessui/blob/3bc754516985db9fa094e364ff10be4de34a473b/packages/%40headlessui-react/src/components/portal/portal.tsx#L71-L96

It looks like doing exactly that - mounting to a different container and then attaching/detaching it manually from within an effect. The problem is that... effects are called from the bottom to the top so our effect gets called first and we can't measure what the height should be when the textarea isn't mounted in the DOM.

This is a good explanation. Here's some notes of mine about solving an equivalent issue before, that are more relevant to outside projects than this project:

I've run into and fixed a similar issue before with a library I wrote (react-float-anchor) that functioned similar to that portal component. The issue was that an input with an autofocus prop inside of a portal wasn't being focused when the input first showed up, and it had the same root cause as this issue: when the contents of the portal had their mount/useLayoutEffects callbacks run, the contents weren't actually present in the DOM yet and couldn't be focused (or measured). It wasn't until the react-float-anchor component had its mount/useLayoutEffects callbacks run did it actually attach the React portal element into the document, and parents have their mount/useLayoutEffects callbacks called after their children.

This can be solved by using useInsertionEffect instead of useLayoutEffect in the portal component for attaching the portal element to the document. React runs the useInsertionEffect callbacks for parents before children have their mount/useLayoutEffects callbacks run, so it seems perfect for the use-case of attaching a portal element to the document before the children mount. (The React documentation does say it's mainly intended for CSS-in-JS systems, which have similar requirements to this use-case: they need to be able to inject styles before any of the children have done mount callbacks and potentially measured themselves, and they don't need to interact with state or component refs.)

Older alternative solution

An alternative solution, which I used in the current release version of react-float-anchor because it's still a class-based React component, is to make the portal-handling component render a dummy child component (before all props-provided children) which attaches the portal element to the document during its own mount callback. I do this here with the "LifecycleHelper" component:

      floatPortal = (
        <FloatAnchorContext.Provider value={this._childContext}>
          <LifecycleHelper onMount={this._mountPortalEl} />
          {createPortal(float, portalEl)}
        </FloatAnchorContext.Provider>
      );

where this._mountPortalEl is a callback which attaches the portal element to the document, and LifecycleHelper is just this:

export default class LifecycleHelper extends React.Component {
  componentDidMount() {
    this.props.onMount();
  }
  render() {
    return null;
  }
}

So now the portal being attached to the DOM happens during a child's mount callbacks instead of during the parent's mount callbacks, and it runs before the other children's mount callbacks because it's an earlier child than them.


However, all that being said, I just discovered the not directly related bug #389, and it turns out the fix for it would happen to work around this issue in the separate portal library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests