Skip to content

Commit

Permalink
[material-ui][TextareaAutosize] Temporarily disconnect ResizeObserver…
Browse files Browse the repository at this point in the history
… to avoid loop error (#44540)
  • Loading branch information
mj12albert authored Jan 21, 2025
1 parent 8a4eaf6 commit cce1222
Showing 1 changed file with 55 additions and 48 deletions.
103 changes: 55 additions & 48 deletions packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ type TextareaStyles = {
overflowing: boolean;
};

function isObjectEmpty(object: TextareaStyles) {
// eslint-disable-next-line
for (const _ in object) {
return false;
}
return true;
}

function isEmpty(obj: TextareaStyles) {
return (
obj === undefined ||
obj === null ||
Object.keys(obj).length === 0 ||
(obj.outerHeightStyle === 0 && !obj.overflowing)
);
return isObjectEmpty(obj) || (obj.outerHeightStyle === 0 && !obj.overflowing);
}

/**
Expand All @@ -62,16 +65,21 @@ 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<HTMLTextAreaElement>(null);
const handleRef = useForkRef(forwardedRef, inputRef);
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const handleRef = useForkRef(forwardedRef, textareaRef);
const heightRef = React.useRef<number>(null);
const shadowRef = React.useRef<HTMLTextAreaElement>(null);
const hiddenTextareaRef = React.useRef<HTMLTextAreaElement>(null);

const calculateTextareaStyles = React.useCallback(() => {
const input = inputRef.current!;
const textarea = textareaRef.current;
const hiddenTextarea = hiddenTextareaRef.current;

const containerWindow = ownerWindow(input);
const computedStyle = containerWindow.getComputedStyle(input);
if (!textarea || !hiddenTextarea) {
return undefined;
}

const containerWindow = ownerWindow(textarea);
const computedStyle = containerWindow.getComputedStyle(textarea);

// If input's width is shrunk and it's not visible, don't sync height.
if (computedStyle.width === '0px') {
Expand All @@ -81,15 +89,13 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
};
}

const inputShallow = shadowRef.current!;

inputShallow.style.width = computedStyle.width;
inputShallow.value = input.value || props.placeholder || 'x';
if (inputShallow.value.slice(-1) === '\n') {
hiddenTextarea.style.width = computedStyle.width;
hiddenTextarea.value = textarea.value || props.placeholder || 'x';
if (hiddenTextarea.value.slice(-1) === '\n') {
// Certain fonts which overflow the line height will cause the textarea
// to report a different scrollHeight depending on whether the last line
// is empty. Make it non-empty to avoid this issue.
inputShallow.value += ' ';
hiddenTextarea.value += ' ';
}

const boxSizing = computedStyle.boxSizing;
Expand All @@ -99,11 +105,11 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
getStyleValue(computedStyle.borderBottomWidth) + getStyleValue(computedStyle.borderTopWidth);

// The height of the inner content
const innerHeight = inputShallow.scrollHeight;
const innerHeight = hiddenTextarea.scrollHeight;

// Measure height of a textarea with a single row
inputShallow.value = 'x';
const singleRowHeight = inputShallow.scrollHeight;
hiddenTextarea.value = 'x';
const singleRowHeight = hiddenTextarea.scrollHeight;

// The height of the outer content
let outerHeight = innerHeight;
Expand All @@ -124,54 +130,55 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
}, [maxRows, minRows, props.placeholder]);

const syncHeight = React.useCallback(() => {
const textarea = textareaRef.current;
const textareaStyles = calculateTextareaStyles();

if (isEmpty(textareaStyles)) {
if (!textarea || !textareaStyles || isEmpty(textareaStyles)) {
return;
}

const outerHeightStyle = textareaStyles.outerHeightStyle;
const input = inputRef.current!;
if (heightRef.current !== outerHeightStyle) {
heightRef.current = outerHeightStyle;
input.style.height = `${outerHeightStyle}px`;
textarea.style.height = `${outerHeightStyle}px`;
}
input.style.overflow = textareaStyles.overflowing ? 'hidden' : '';
textarea.style.overflow = textareaStyles.overflowing ? 'hidden' : '';
}, [calculateTextareaStyles]);

const frameRef = React.useRef(-1);

useEnhancedEffect(() => {
const handleResize = () => {
syncHeight();
};
// Workaround a "ResizeObserver loop completed with undelivered notifications" error
// in test.
// Note that we might need to use this logic in production per https://github.com/WICG/resize-observer/issues/38
// Also see https://github.com/mui/mui-x/issues/8733
let rAF: any;
const rAFHandleResize = () => {
cancelAnimationFrame(rAF);
rAF = requestAnimationFrame(() => {
handleResize();
});
};
const debounceHandleResize = debounce(handleResize);
const input = inputRef.current!;
const containerWindow = ownerWindow(input);
const debounceHandleResize = debounce(() => syncHeight());
const textarea = textareaRef?.current;

if (!textarea) {
return undefined;
}

const containerWindow = ownerWindow(textarea);

containerWindow.addEventListener('resize', debounceHandleResize);

let resizeObserver: ResizeObserver;

if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(
process.env.NODE_ENV === 'test' ? rAFHandleResize : handleResize,
);
resizeObserver.observe(input);
resizeObserver = new ResizeObserver(() => {
// avoid "ResizeObserver loop completed with undelivered notifications" error
// by temporarily unobserving the textarea element while manipulating the height
// and reobserving one frame later
resizeObserver.unobserve(textarea);
cancelAnimationFrame(frameRef.current);
syncHeight();
frameRef.current = requestAnimationFrame(() => {
resizeObserver.observe(textarea);
});
});
resizeObserver.observe(textarea);
}

return () => {
debounceHandleResize.clear();
cancelAnimationFrame(rAF);
cancelAnimationFrame(frameRef.current);
containerWindow.removeEventListener('resize', debounceHandleResize);
if (resizeObserver) {
resizeObserver.disconnect();
Expand Down Expand Up @@ -208,7 +215,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
aria-hidden
className={props.className}
readOnly
ref={shadowRef}
ref={hiddenTextareaRef}
tabIndex={-1}
style={{
...styles.shadow,
Expand Down

0 comments on commit cce1222

Please sign in to comment.