diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx index 1740aa8eb6b37b..8b4d2777919e17 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx @@ -4,6 +4,8 @@ import sinon, { spy, stub } from 'sinon'; import { describeConformanceUnstyled, act, + screen, + waitFor, createMount, createRenderer, fireEvent, @@ -15,6 +17,12 @@ function getStyleValue(value: string) { return parseInt(value, 10) || 0; } +function sleep(time: number): Promise { + return new Promise((res) => { + setTimeout(res, time); + }); +} + describe('', () => { const { clock, render } = createRenderer(); const mount = createMount(); @@ -36,6 +44,65 @@ describe('', () => { ], })); + // For https://github.com/mui/material-ui/pull/33238 + it('should not crash when unmounting with Suspense', async () => { + const LazyRoute = React.lazy(() => { + // Force react to show fallback suspense + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + default: () =>
LazyRoute
, + }); + }, 0); + }); + }); + + function App() { + const [toggle, setToggle] = React.useState(false); + + return ( + + + {toggle ? : } + + ); + } + + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + await waitFor(() => { + expect(screen.queryByText('LazyRoute')).not.to.equal(null); + }); + }); + + // For https://github.com/mui/material-ui/pull/33253 + it('should update height without an infinite rendering loop', async () => { + function App() { + const [value, setValue] = React.useState('Controlled'); + + const handleChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + }; + + return ; + } + const { container } = render(); + const input = container.querySelector('textarea')!; + act(() => { + input.focus(); + }); + const activeElement = document.activeElement!; + // set the value of the input to be 1 larger than its content width + fireEvent.change(activeElement, { + target: { value: 'Controlled\n' }, + }); + await sleep(0); + fireEvent.change(activeElement, { + target: { value: 'Controlled\n\n' }, + }); + }); + describe('layout', () => { const getComputedStyleStub = new Map>(); function setLayout( diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx index 164dbd478a2bde..0d8606914db45f 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx @@ -163,57 +163,38 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( return; } - setState((prevState) => { - return updateState(prevState, newState); - }); + setState((prevState) => updateState(prevState, newState)); }, [getUpdatedState]); - const syncHeightWithFlushSync = () => { - const newState = getUpdatedState(); + useEnhancedEffect(() => { + const syncHeightWithFlushSync = () => { + const newState = getUpdatedState(); - if (isEmpty(newState)) { - return; - } + if (isEmpty(newState)) { + return; + } - // In React 18, state updates in a ResizeObserver's callback are happening after the paint which causes flickering - // when doing some visual updates in it. Using flushSync ensures that the dom will be painted after the states updates happen - // Related issue - https://github.com/facebook/react/issues/24331 - ReactDOM.flushSync(() => { - setState((prevState) => { - return updateState(prevState, newState); + // In React 18, state updates in a ResizeObserver's callback are happening after + // the paint, this leads to an infinite rendering. + // + // Using flushSync ensures that the states is updated before the next pain. + // Related issue - https://github.com/facebook/react/issues/24331 + ReactDOM.flushSync(() => { + setState((prevState) => updateState(prevState, newState)); }); - }); - }; + }; - React.useEffect(() => { const handleResize = () => { renders.current = 0; - - // If the TextareaAutosize component is replaced by Suspense with a fallback, the last - // ResizeObserver's handler that runs because of the change in the layout is trying to - // access a dom node that is no longer there (as the fallback component is being shown instead). - // See https://github.com/mui/material-ui/issues/32640 - if (inputRef.current) { - syncHeightWithFlushSync(); - } + syncHeightWithFlushSync(); }; - const handleResizeWindow = debounce(() => { - renders.current = 0; - - // If the TextareaAutosize component is replaced by Suspense with a fallback, the last - // ResizeObserver's handler that runs because of the change in the layout is trying to - // access a dom node that is no longer there (as the fallback component is being shown instead). - // See https://github.com/mui/material-ui/issues/32640 - if (inputRef.current) { - syncHeightWithFlushSync(); - } - }); - let resizeObserver: ResizeObserver; - + const debounceHandleResize = debounce(handleResize); const input = inputRef.current!; const containerWindow = ownerWindow(input); - containerWindow.addEventListener('resize', handleResizeWindow); + containerWindow.addEventListener('resize', debounceHandleResize); + + let resizeObserver: ResizeObserver; if (typeof ResizeObserver !== 'undefined') { resizeObserver = new ResizeObserver(handleResize); @@ -221,13 +202,13 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( } return () => { - handleResizeWindow.clear(); - containerWindow.removeEventListener('resize', handleResizeWindow); + debounceHandleResize.clear(); + containerWindow.removeEventListener('resize', debounceHandleResize); if (resizeObserver) { resizeObserver.disconnect(); } }; - }); + }, [getUpdatedState]); useEnhancedEffect(() => { syncHeight();