From a9026f1f6b210bce31604d998bdc39bd90f4e827 Mon Sep 17 00:00:00 2001 From: Zeeshan Tamboli Date: Mon, 12 Feb 2024 19:23:38 +0530 Subject: [PATCH] [TextareaAutosize] Improve implementation (#40789) --- .../TextareaAutosize.test.tsx | 28 ----- .../src/TextareaAutosize/TextareaAutosize.tsx | 102 ++++-------------- 2 files changed, 20 insertions(+), 110 deletions(-) diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx index 76db8c8d003cd9..fe7d02eccb19e6 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx @@ -9,7 +9,6 @@ import { createMount, createRenderer, fireEvent, - strictModeDoubleLoggingSuppressed, } from '@mui-internal/test-utils'; import { TextareaAutosize } from '@mui/base/TextareaAutosize'; @@ -458,32 +457,5 @@ describe('', () => { // the input should be 2 lines expect(input.style).to.have.property('height', `${lineHeight * 2}px`); }); - - describe('warnings', () => { - it('warns if layout is unstable but not crash', () => { - const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]')!; - const shadow = container.querySelector('textarea[aria-hidden=true]')!; - let index = 0; - setLayout(input, shadow, { - getComputedStyle: { - boxSizing: 'content-box', - }, - scrollHeight: 100, - lineHeight: () => { - index += 1; - return index; - }, - }); - - expect(() => { - forceUpdate(); - }).toErrorDev([ - 'MUI: Too many re-renders.', - !strictModeDoubleLoggingSuppressed && 'MUI: Too many re-renders.', - !strictModeDoubleLoggingSuppressed && 'MUI: Too many re-renders.', - ]); - }); - }); }); }); diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx index 9ee44c2fb2d923..1ab4d9fa1fc232 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx @@ -1,7 +1,6 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import * as ReactDOM from 'react-dom'; import { unstable_debounce as debounce, unstable_useForkRef as useForkRef, @@ -10,11 +9,6 @@ import { } from '@mui/utils'; import { TextareaAutosizeProps } from './TextareaAutosize.types'; -type State = { - outerHeightStyle: number; - overflow?: boolean | undefined; -}; - function getStyleValue(value: string) { return parseInt(value, 10) || 0; } @@ -37,12 +31,17 @@ const styles: { }, }; -function isEmpty(obj: State) { +type TextareaStyles = { + outerHeightStyle: number; + overflowing: boolean; +}; + +function isEmpty(obj: TextareaStyles) { return ( obj === undefined || obj === null || Object.keys(obj).length === 0 || - (obj.outerHeightStyle === 0 && !obj.overflow) + (obj.outerHeightStyle === 0 && !obj.overflowing) ); } @@ -64,15 +63,11 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( const { onChange, maxRows, minRows = 1, style, value, ...other } = props; const { current: isControlled } = React.useRef(value != null); - const inputRef = React.useRef(null); + const inputRef = React.useRef(null); const handleRef = useForkRef(forwardedRef, inputRef); const shadowRef = React.useRef(null); - const renders = React.useRef(0); - const [state, setState] = React.useState({ - outerHeightStyle: 0, - }); - const getUpdatedState = React.useCallback(() => { + const calculateTextareaStyles = React.useCallback(() => { const input = inputRef.current!; const containerWindow = ownerWindow(input); @@ -82,6 +77,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( if (computedStyle.width === '0px') { return { outerHeightStyle: 0, + overflowing: false, }; } @@ -122,71 +118,26 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( // Take the box sizing into account for applying this value as a style. const outerHeightStyle = outerHeight + (boxSizing === 'border-box' ? padding + border : 0); - const overflow = Math.abs(outerHeight - innerHeight) <= 1; + const overflowing = Math.abs(outerHeight - innerHeight) <= 1; - return { outerHeightStyle, overflow }; + return { outerHeightStyle, overflowing }; }, [maxRows, minRows, props.placeholder]); - const updateState = (prevState: State, newState: State) => { - const { outerHeightStyle, overflow } = newState; - // Need a large enough difference to update the height. - // This prevents infinite rendering loop. - if ( - renders.current < 20 && - ((outerHeightStyle > 0 && - Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) || - prevState.overflow !== overflow) - ) { - renders.current += 1; - return { - overflow, - outerHeightStyle, - }; - } - if (process.env.NODE_ENV !== 'production') { - if (renders.current === 20) { - console.error( - [ - 'MUI: Too many re-renders. The layout is unstable.', - 'TextareaAutosize limits the number of renders to prevent an infinite loop.', - ].join('\n'), - ); - } - } - return prevState; - }; - const syncHeight = React.useCallback(() => { - const newState = getUpdatedState(); + const textareaStyles = calculateTextareaStyles(); - if (isEmpty(newState)) { + if (isEmpty(textareaStyles)) { return; } - setState((prevState) => updateState(prevState, newState)); - }, [getUpdatedState]); + const input = inputRef.current!; + input.style.height = `${textareaStyles.outerHeightStyle}px`; + input.style.overflow = textareaStyles.overflowing ? 'hidden' : ''; + }, [calculateTextareaStyles]); useEnhancedEffect(() => { - const syncHeightWithFlushSync = () => { - const newState = getUpdatedState(); - - if (isEmpty(newState)) { - return; - } - - // 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)); - }); - }; - const handleResize = () => { - renders.current = 0; - syncHeightWithFlushSync(); + syncHeight(); }; // Workaround a "ResizeObserver loop completed with undelivered notifications" error // in test. @@ -222,19 +173,13 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( resizeObserver.disconnect(); } }; - }, [getUpdatedState]); + }, [calculateTextareaStyles, syncHeight]); useEnhancedEffect(() => { syncHeight(); }); - React.useEffect(() => { - renders.current = 0; - }, [value]); - const handleChange = (event: React.ChangeEvent) => { - renders.current = 0; - if (!isControlled) { syncHeight(); } @@ -252,13 +197,6 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize( ref={handleRef} // Apply the rows prop to get a "correct" first SSR paint rows={minRows as number} - style={{ - height: state.outerHeightStyle, - // Need a large enough difference to allow scrolling. - // This prevents infinite rendering loop. - overflow: state.overflow ? 'hidden' : undefined, - ...style, - }} {...other} />