Skip to content

Commit 262e796

Browse files
authored
[TextareaAutosize] Fix ResizeObserver causing infinite selectionchange loop (#45351) (#45498)
1 parent 107bdf3 commit 262e796

File tree

2 files changed

+59
-14
lines changed

2 files changed

+59
-14
lines changed

packages/mui-material/src/TextareaAutosize/TextareaAutosize.test.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,33 @@ describe('<TextareaAutosize />', () => {
445445
backgroundColor: 'rgb(255, 255, 0)',
446446
});
447447
});
448+
449+
// edge case: https://github.com/mui/material-ui/issues/45307
450+
it('should not infinite loop document selectionchange', async function test() {
451+
// document selectionchange event doesn't fire in JSDOM
452+
if (/jsdom/.test(window.navigator.userAgent)) {
453+
this.skip();
454+
}
455+
456+
const handleSelectionChange = spy();
457+
458+
function App() {
459+
React.useEffect(() => {
460+
document.addEventListener('selectionchange', handleSelectionChange);
461+
return () => {
462+
document.removeEventListener('selectionchange', handleSelectionChange);
463+
};
464+
}, []);
465+
466+
return (
467+
<TextareaAutosize defaultValue="some long text that makes the input start with multiple rows" />
468+
);
469+
}
470+
471+
await render(<App />);
472+
await sleep(100);
473+
// when the component mounts and idles this fires 3 times in browser tests
474+
// and 2 times in a real browser
475+
expect(handleSelectionChange.callCount).to.lessThanOrEqual(3);
476+
});
448477
});

packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx

+30-14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
unstable_debounce as debounce,
66
unstable_useForkRef as useForkRef,
77
unstable_useEnhancedEffect as useEnhancedEffect,
8+
unstable_useEventCallback as useEventCallback,
89
unstable_ownerWindow as ownerWindow,
910
} from '@mui/utils';
1011
import { TextareaAutosizeProps } from './TextareaAutosize.types';
@@ -129,6 +130,19 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
129130
return { outerHeightStyle, overflowing };
130131
}, [maxRows, minRows, props.placeholder]);
131132

133+
const didHeightChange = useEventCallback(() => {
134+
const textarea = textareaRef.current;
135+
const textareaStyles = calculateTextareaStyles();
136+
137+
if (!textarea || !textareaStyles || isEmpty(textareaStyles)) {
138+
return false;
139+
}
140+
141+
const outerHeightStyle = textareaStyles.outerHeightStyle;
142+
143+
return heightRef.current != null && heightRef.current !== outerHeightStyle;
144+
});
145+
132146
const syncHeight = React.useCallback(() => {
133147
const textarea = textareaRef.current;
134148
const textareaStyles = calculateTextareaStyles();
@@ -148,7 +162,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
148162
const frameRef = React.useRef(-1);
149163

150164
useEnhancedEffect(() => {
151-
const debounceHandleResize = debounce(() => syncHeight());
165+
const debouncedHandleResize = debounce(syncHeight);
152166
const textarea = textareaRef?.current;
153167

154168
if (!textarea) {
@@ -157,34 +171,36 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
157171

158172
const containerWindow = ownerWindow(textarea);
159173

160-
containerWindow.addEventListener('resize', debounceHandleResize);
174+
containerWindow.addEventListener('resize', debouncedHandleResize);
161175

162176
let resizeObserver: ResizeObserver;
163177

164178
if (typeof ResizeObserver !== 'undefined') {
165179
resizeObserver = new ResizeObserver(() => {
166-
// avoid "ResizeObserver loop completed with undelivered notifications" error
167-
// by temporarily unobserving the textarea element while manipulating the height
168-
// and reobserving one frame later
169-
resizeObserver.unobserve(textarea);
170-
cancelAnimationFrame(frameRef.current);
171-
syncHeight();
172-
frameRef.current = requestAnimationFrame(() => {
173-
resizeObserver.observe(textarea);
174-
});
180+
if (didHeightChange()) {
181+
// avoid "ResizeObserver loop completed with undelivered notifications" error
182+
// by temporarily unobserving the textarea element while manipulating the height
183+
// and reobserving one frame later
184+
resizeObserver.unobserve(textarea);
185+
cancelAnimationFrame(frameRef.current);
186+
syncHeight();
187+
frameRef.current = requestAnimationFrame(() => {
188+
resizeObserver.observe(textarea);
189+
});
190+
}
175191
});
176192
resizeObserver.observe(textarea);
177193
}
178194

179195
return () => {
180-
debounceHandleResize.clear();
196+
debouncedHandleResize.clear();
181197
cancelAnimationFrame(frameRef.current);
182-
containerWindow.removeEventListener('resize', debounceHandleResize);
198+
containerWindow.removeEventListener('resize', debouncedHandleResize);
183199
if (resizeObserver) {
184200
resizeObserver.disconnect();
185201
}
186202
};
187-
}, [calculateTextareaStyles, syncHeight]);
203+
}, [calculateTextareaStyles, syncHeight, didHeightChange]);
188204

189205
useEnhancedEffect(() => {
190206
syncHeight();

0 commit comments

Comments
 (0)