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

[base-ui][TextareaAutosize] Fix resizing instability #41638

Conversation

ZeeshanTamboli
Copy link
Member

@ZeeshanTamboli ZeeshanTamboli commented Mar 25, 2024

Similar to mui/base-ui#189. This issue needs fixing here as it's a regression. It also affects the TextField component with the multiline prop which uses TextareaAutosize. Additionally, it needs to be released from the master branch under v5.

It doesn't need to be synced with base-ui repo since it will be deprecated there - see mui/base-ui#189 (comment). I'll close that PR once this gets merged.

Before: https://mui.com/base-ui/react-textarea-autosize/
After: https://deploy-preview-41638--material-ui.netlify.app/base-ui/react-textarea-autosize/

@ZeeshanTamboli ZeeshanTamboli added bug 🐛 Something doesn't work component: TextareaAutosize The React component. package: base-ui Specific to @mui/base regression A bug, but worse labels Mar 25, 2024
@mui-bot
Copy link

mui-bot commented Mar 25, 2024

Netlify deploy preview

https://deploy-preview-41638--material-ui.netlify.app/

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against 112315b

@DiegoAndai
Copy link
Member

DiegoAndai commented Mar 26, 2024

Thanks for working on this @ZeeshanTamboli. The after demo looks like it's working properly 🎉.

I read mui/base-ui#168 (comment) and reviewed the code, but I failed to understand the issue and solution. May I ask you to explain it to me? Thanks in advance.

@ZeeshanTamboli
Copy link
Member Author

ZeeshanTamboli commented Mar 27, 2024

I read mui/base-ui#168 (comment) and reviewed the code, but I failed to understand the issue and solution. May I ask you to explain it to me?

@DiegoAndai Sure.

Quoting mui/base-ui#168 (comment):

The calculateTextareaStyles function's return value for outerHeightStyle is always the same. As a result, resizing causes the height to momentarily revert to its original value, leading to unstable behavior. (https://github.com/mui/base-ui/blob/master/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx#L70-L124)

The calculateTextareaStyles function always returns the same outerHeightStyle when the component is resized (by dragging). This occurs because the calculation relies on the height of a shadow/hidden textarea (the outerHeightStyle style), which remains constant during resizing. The outerHeightStyle only changes when the textarea's content triggers a height adjustment.

Previously, we were updating the height everytime even when the outerHeightStyle remained unchanged. This caused the height to momentarily revert to its original value, resulting in a glitch. The calculateTextareaStyles function is called whenever a resizing event occurs: https://github.com/mui/material-ui/blob/next/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx#L139-L141.

This behavior, which involves always getting the same value, has been present since before the fix for #40789, when the function was called getUpdatedState.
It appears that the height was being changed by maintaining height in the state and passing it as style props. However, in reality, the value of state.outerHeightStyle has always remained unchanged. (https://github.com/mui/material-ui/pull/40789/files#diff-4bad88270b0417529b9c97204bcd993e4117f43e6ba590c8c20ddc49157d09a2L256)

This indicates that previously, when state was utilized (before I removed it in #40789), the outerHeightStyle remained constant even when resized. At that time, the height was stored in the React state and passed to the style prop. You might wonder why this didn't cause a glitch back then. It was because, before that, the number of re-renders was limited to 20. You can find more details at #40789 (comment).


So the solution here I believe is to store the height in a ref and update it only when it is different from the previous value, as mentioned in mui/base-ui#189 (comment).


Marija added the "PR: needs test" label. I'm uncertain about how to add a test for this. I expressed the same concern in mui/base-ui#189 (comment). Do you have any suggestions?


I hope it helps.

@DiegoAndai
Copy link
Member

Thanks for the detailed explanation @ZeeshanTamboli. The solution makes more sense now 👍🏼

Part of the confusion was due to the inner/outer concepts. If I understand correctly inner is the shadow, and outerHeight refers to the final height after min and max height restrictions. Renaming inner* to shadow* and outerHeight to height might help, either in this PR or in a follow-up.


Something I noticed is that with this change, the maxRows prop stops working after resizing by dragging:

Screen.Recording.2024-03-27.at.09.16.04.mov

Which was not the case previously. Although I think this isn't correct either:

Screen.Recording.2024-03-27.at.09.16.29.mov

I would handle this in a separate issue/PR


Marija added the "PR: needs test" label. I'm uncertain about how to add a test for this. I expressed the same concern in mui/base-ui#189 (comment). Do you have any suggestions?

By providing a ref to the component, would it be possible to spy on how many times and with which values the input.style.height attribute is set? That might be a way to do it, but I'm not sure it's possible


Heads up: I'll be on vacation starting tomorrow (March 28th) until April 8th, so I won't attend to notifications during this time. To move this PR forward, please reach out to @michaldudak 😊

From my perspective, this fix makes sense, and it should be merged. Let's try to figure out a way to test it first.

@ZeeshanTamboli
Copy link
Member Author

ZeeshanTamboli commented Mar 28, 2024

Part of the confusion was due to the inner/outer concepts. If I understand correctly inner is the shadow, and outerHeight refers to the final height after min and max height restrictions. Renaming inner* to shadow* and outerHeight to height might help, either in this PR or in a follow-up.

Correct. We can do this in a follow-up PR.


Something I noticed is that with this change, the maxRows prop stops working after resizing by dragging

If resizing is done by dragging, which is a native feature of the textarea HTML element, it shouldn't consider maxRows or minRows. Essentially, it behaves like a regular textarea element. So, the current behavior in this PR should be correct.


By providing a ref to the component, would it be possible to spy on how many times and with which values the input.style.height attribute is set? That might be a way to do it, but I'm not sure it's possible

I believe we should find a way to test resizing the component by dragging and then verify that it doesn't glitch by ensuring the height value isn't being set each time.

@DiegoAndai
Copy link
Member

If resizing is done by dragging, which is a native feature of the textarea HTML element, it shouldn't consider maxRows or minRows. Essentially, it behaves like a regular textarea element. So, the current behavior in this PR should be correct.

This makes sense 👍🏼

I believe we should find a way to test resizing the component by dragging and then verify that it doesn't glitch by ensuring the height value isn't being set each time.

Have you been able to explore testing this fix?

@ZeeshanTamboli
Copy link
Member Author

Have you been able to explore testing this fix?

No, I haven't. I'm not sure how to simulate dragging the textarea programmatically in a test.

@DiegoAndai
Copy link
Member

DiegoAndai commented Apr 9, 2024

@ZeeshanTamboli
Copy link
Member Author

We could try this: https://testing-library.com/docs/user-event/pointer#moving-a-pointer

But how do we target the bottom-right dragger of the textarea? Strangely, I haven't come across any test case examples for textarea dragging online.

@DiegoAndai
Copy link
Member

DiegoAndai commented Apr 10, 2024

But how do we target the bottom-right dragger of the textarea?

If we know the textarea's width and height we should be able to move the cursor to the bottom-right corner, right?

(I've never tested this either, just throwing ideas 😅)

@ZeeshanTamboli
Copy link
Member Author

But how do we target the bottom-right dragger of the textarea?

If we know the textarea's width and height we should be able to move the cursor to the bottom-right corner, right?

I've attempted the following approach:

  it.only('should not glitch when resizing textarea', function test() {
    if (/jsdom/.test(window.navigator.userAgent)) {
      this.skip();
    }

    const { container } = render(<TextareaAutosize />);
    const textarea = container.querySelector<HTMLTextAreaElement>('textarea')!;

    console.log('before textarea.style.height: ', textarea.style.height);

    // Get the element's dimensions
    const { top, left, width, height } = textarea.getBoundingClientRect();

    // Calculate coordinates of bottom-right corner
    const bottomRightX = left + width;
    const bottomRightY = top + height;

    fireEvent.mouseDown(textarea, { clientX: bottomRightX, clientY: bottomRightY });
    fireEvent.mouseMove(textarea, { clientX: bottomRightX + 50, clientY: bottomRightY + 50 });
    fireEvent.mouseUp(textarea);

    console.log('after textarea.style.height: ', textarea.style.height);
  });

But despite these efforts, the height remains unchanged. I'm testing it in the browser rather than JSDOM.

I've also reached out for assistance on Stack Overflow: https://stackoverflow.com/questions/78319726/how-to-resize-textarea-by-dragging-in-react-testing-library.

@DiegoAndai
Copy link
Member

The code you shared was exactly what I imagined 👌🏼 let's wait for a while to see if we get any luck on Stack Overflow.

Did you test with user event instead of fire event? https://testing-library.com/docs/user-event/pointer

@ZeeshanTamboli
Copy link
Member Author

Did you test with user event instead of fire event? https://testing-library.com/docs/user-event/pointer

Tried with it but getting the same results.

@ZeeshanTamboli ZeeshanTamboli removed the PR: needs test The pull request can't be merged label Apr 21, 2024
@ZeeshanTamboli
Copy link
Member Author

ZeeshanTamboli commented Apr 21, 2024

@DiegoAndai I tested it by adding an E2E test using Playwright! I think an E2E test is more suitable here for this case and gives higher confidence. Please review.

Copy link
Member

@DiegoAndai DiegoAndai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work figuring the test out @ZeeshanTamboli!

A couple minor questions:

test/e2e/index.test.ts Outdated Show resolved Hide resolved
test/e2e/index.test.ts Outdated Show resolved Hide resolved
Copy link
Member

@DiegoAndai DiegoAndai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job @ZeeshanTamboli 🙌🏼

@ZeeshanTamboli ZeeshanTamboli added cherry-pick The PR was cherry-picked from the newer alpha/beta/stable branch needs cherry-pick The PR should be cherry-picked to master after merge and removed cherry-pick The PR was cherry-picked from the newer alpha/beta/stable branch labels Apr 26, 2024
@ZeeshanTamboli ZeeshanTamboli merged commit f9a5847 into mui:next Apr 26, 2024
22 checks passed
@ZeeshanTamboli ZeeshanTamboli deleted the issue-textareaautosize-resizing-instabiity branch April 26, 2024 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: TextareaAutosize The React component. needs cherry-pick The PR should be cherry-picked to master after merge package: base-ui Specific to @mui/base regression A bug, but worse
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants