diff --git a/packages/mui-material/src/TextareaAutosize/TextareaAutosize.test.tsx b/packages/mui-material/src/TextareaAutosize/TextareaAutosize.test.tsx index dc182cb4ac29e4..97f247f3fc4f6c 100644 --- a/packages/mui-material/src/TextareaAutosize/TextareaAutosize.test.tsx +++ b/packages/mui-material/src/TextareaAutosize/TextareaAutosize.test.tsx @@ -503,4 +503,104 @@ describe('', () => { // and 2 times in a real browser expect(handleSelectionChange.callCount).to.lessThanOrEqual(3); }); + + it('should not lose textarea value during reflow workaround for controlled component', async () => { + function App() { + const [value, setValue] = React.useState( + 'some long text that makes the input start with multiple rows', + ); + + const handleChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + }; + + return ; + } + + render(); + const textarea = screen.getByRole('textbox', { + hidden: false, + }); + + const originalValue = textarea.value; + + act(() => { + textarea.focus(); + }); + + // Trigger textarea height changes + fireEvent.change(textarea, { + target: { value: 'some short text' }, + }); + await raf(); + + fireEvent.change(textarea, { + target: { value: originalValue }, + }); + await raf(); + + expect(textarea.value).to.equal(originalValue); + }); + it('should not lose textarea value during reflow workaround for uncontrolled component', async () => { + function App() { + return ( + + ); + } + + render(); + const textarea = screen.getByRole('textbox', { + hidden: false, + }); + + const originalValue = textarea.value; + + act(() => { + textarea.focus(); + }); + + // Trigger textarea height changes + fireEvent.change(textarea, { + target: { value: 'some short text' }, + }); + await raf(); + + fireEvent.change(textarea, { + target: { value: originalValue }, + }); + await raf(); + + expect(textarea.value).to.equal(originalValue); + }); + it('should not restore selection when textarea is not focused', async () => { + function App() { + const [value, setValue] = React.useState('Initial long text'); + const handleChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + }; + return ( +
+ + +
+ ); + } + + render(); + const textarea = screen.getByRole('textbox', { + hidden: false, + }); + const button = screen.getByRole('button'); + + // Don't focus the textarea + expect(document.activeElement).not.to.equal(textarea); + + // Change value programmatically + fireEvent.click(button); + await raf(); + + // Should update without error + expect(textarea.value).to.equal('Short'); + expect(document.activeElement).not.to.equal(textarea); + }); }); diff --git a/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx b/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx index b3d460a1d6eaef..3947fdfcf0a9fe 100644 --- a/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx +++ b/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx @@ -6,6 +6,7 @@ import useForkRef from '@mui/utils/useForkRef'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; import ownerWindow from '@mui/utils/ownerWindow'; +import ownerDocument from '@mui/utils/ownerDocument'; import { TextareaAutosizeProps } from './TextareaAutosize.types'; function getStyleValue(value: string) { @@ -153,6 +154,21 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( if (heightRef.current !== outerHeightStyle) { heightRef.current = outerHeightStyle; textarea.style.height = `${outerHeightStyle}px`; + + // This is a workaround for Safari/WebKit not reflowing text when the textarea height changes + // Force Safari to reflow the text by manipulating the textarea value + const containerDocument = ownerDocument(textarea); + const selectionStart = textarea.selectionStart; + const selectionEnd = textarea.selectionEnd; + const tempValue = textarea.value; + textarea.value = ''; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + textarea.offsetHeight; + textarea.value = tempValue; + // Restore selection position + if (containerDocument.activeElement === textarea) { + textarea.setSelectionRange(selectionStart, selectionEnd); + } } textarea.style.overflow = textareaStyles.overflowing ? 'hidden' : ''; }, [calculateTextareaStyles]);