Skip to content

Commit

Permalink
[TextareaAutosize] Simplify logic and add test
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Aug 30, 2023
1 parent 7e2e737 commit a2e2a3f
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 42 deletions.
67 changes: 67 additions & 0 deletions packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx
Expand Up @@ -4,6 +4,8 @@ import sinon, { spy, stub } from 'sinon';
import {
describeConformanceUnstyled,
act,
screen,
waitFor,
createMount,
createRenderer,
fireEvent,
Expand All @@ -15,6 +17,12 @@ function getStyleValue(value: string) {
return parseInt(value, 10) || 0;
}

function sleep(time: number): Promise<void> {
return new Promise<void>((res) => {
setTimeout(res, time);
});
}

describe('<TextareaAutosize />', () => {
const { clock, render } = createRenderer();
const mount = createMount();
Expand All @@ -36,6 +44,65 @@ describe('<TextareaAutosize />', () => {
],
}));

// 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<any>((resolve) => {
setTimeout(() => {
resolve({
default: () => <div>LazyRoute</div>,
});
}, 0);
});
});

function App() {
const [toggle, setToggle] = React.useState(false);

return (
<React.Suspense fallback={null}>
<button onClick={() => setToggle((r) => !r)}>Toggle</button>
{toggle ? <LazyRoute /> : <TextareaAutosize />}
</React.Suspense>
);
}

render(<App />);
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<any>) => {
setValue(event.target.value);
};

return <TextareaAutosize value={value} onChange={handleChange} />;
}
const { container } = render(<App />);
const input = container.querySelector<HTMLTextAreaElement>('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<Element, Partial<CSSStyleDeclaration>>();
function setLayout(
Expand Down
65 changes: 23 additions & 42 deletions packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx
Expand Up @@ -163,71 +163,52 @@ 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);
resizeObserver.observe(input);
}

return () => {
handleResizeWindow.clear();
containerWindow.removeEventListener('resize', handleResizeWindow);
debounceHandleResize.clear();
containerWindow.removeEventListener('resize', debounceHandleResize);
if (resizeObserver) {
resizeObserver.disconnect();
}
};
});
}, [getUpdatedState]);

useEnhancedEffect(() => {
syncHeight();
Expand Down

0 comments on commit a2e2a3f

Please sign in to comment.