@@ -36,13 +36,16 @@ type TextareaStyles = {
36
36
overflowing : boolean ;
37
37
} ;
38
38
39
+ function isObjectEmpty ( object : TextareaStyles ) {
40
+ // eslint-disable-next-line
41
+ for ( const _ in object ) {
42
+ return false ;
43
+ }
44
+ return true ;
45
+ }
46
+
39
47
function isEmpty ( obj : TextareaStyles ) {
40
- return (
41
- obj === undefined ||
42
- obj === null ||
43
- Object . keys ( obj ) . length === 0 ||
44
- ( obj . outerHeightStyle === 0 && ! obj . overflowing )
45
- ) ;
48
+ return isObjectEmpty ( obj ) || ( obj . outerHeightStyle === 0 && ! obj . overflowing ) ;
46
49
}
47
50
48
51
/**
@@ -62,16 +65,21 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
62
65
const { onChange, maxRows, minRows = 1 , style, value, ...other } = props ;
63
66
64
67
const { current : isControlled } = React . useRef ( value != null ) ;
65
- const inputRef = React . useRef < HTMLTextAreaElement > ( null ) ;
66
- const handleRef = useForkRef ( forwardedRef , inputRef ) ;
68
+ const textareaRef = React . useRef < HTMLTextAreaElement > ( null ) ;
69
+ const handleRef = useForkRef ( forwardedRef , textareaRef ) ;
67
70
const heightRef = React . useRef < number > ( null ) ;
68
- const shadowRef = React . useRef < HTMLTextAreaElement > ( null ) ;
71
+ const hiddenTextareaRef = React . useRef < HTMLTextAreaElement > ( null ) ;
69
72
70
73
const calculateTextareaStyles = React . useCallback ( ( ) => {
71
- const input = inputRef . current ! ;
74
+ const textarea = textareaRef . current ;
75
+ const hiddenTextarea = hiddenTextareaRef . current ;
72
76
73
- const containerWindow = ownerWindow ( input ) ;
74
- const computedStyle = containerWindow . getComputedStyle ( input ) ;
77
+ if ( ! textarea || ! hiddenTextarea ) {
78
+ return undefined ;
79
+ }
80
+
81
+ const containerWindow = ownerWindow ( textarea ) ;
82
+ const computedStyle = containerWindow . getComputedStyle ( textarea ) ;
75
83
76
84
// If input's width is shrunk and it's not visible, don't sync height.
77
85
if ( computedStyle . width === '0px' ) {
@@ -81,15 +89,13 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
81
89
} ;
82
90
}
83
91
84
- const inputShallow = shadowRef . current ! ;
85
-
86
- inputShallow . style . width = computedStyle . width ;
87
- inputShallow . value = input . value || props . placeholder || 'x' ;
88
- if ( inputShallow . value . slice ( - 1 ) === '\n' ) {
92
+ hiddenTextarea . style . width = computedStyle . width ;
93
+ hiddenTextarea . value = textarea . value || props . placeholder || 'x' ;
94
+ if ( hiddenTextarea . value . slice ( - 1 ) === '\n' ) {
89
95
// Certain fonts which overflow the line height will cause the textarea
90
96
// to report a different scrollHeight depending on whether the last line
91
97
// is empty. Make it non-empty to avoid this issue.
92
- inputShallow . value += ' ' ;
98
+ hiddenTextarea . value += ' ' ;
93
99
}
94
100
95
101
const boxSizing = computedStyle . boxSizing ;
@@ -99,11 +105,11 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
99
105
getStyleValue ( computedStyle . borderBottomWidth ) + getStyleValue ( computedStyle . borderTopWidth ) ;
100
106
101
107
// The height of the inner content
102
- const innerHeight = inputShallow . scrollHeight ;
108
+ const innerHeight = hiddenTextarea . scrollHeight ;
103
109
104
110
// Measure height of a textarea with a single row
105
- inputShallow . value = 'x' ;
106
- const singleRowHeight = inputShallow . scrollHeight ;
111
+ hiddenTextarea . value = 'x' ;
112
+ const singleRowHeight = hiddenTextarea . scrollHeight ;
107
113
108
114
// The height of the outer content
109
115
let outerHeight = innerHeight ;
@@ -124,54 +130,55 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
124
130
} , [ maxRows , minRows , props . placeholder ] ) ;
125
131
126
132
const syncHeight = React . useCallback ( ( ) => {
133
+ const textarea = textareaRef . current ;
127
134
const textareaStyles = calculateTextareaStyles ( ) ;
128
135
129
- if ( isEmpty ( textareaStyles ) ) {
136
+ if ( ! textarea || ! textareaStyles || isEmpty ( textareaStyles ) ) {
130
137
return ;
131
138
}
132
139
133
140
const outerHeightStyle = textareaStyles . outerHeightStyle ;
134
- const input = inputRef . current ! ;
135
141
if ( heightRef . current !== outerHeightStyle ) {
136
142
heightRef . current = outerHeightStyle ;
137
- input . style . height = `${ outerHeightStyle } px` ;
143
+ textarea . style . height = `${ outerHeightStyle } px` ;
138
144
}
139
- input . style . overflow = textareaStyles . overflowing ? 'hidden' : '' ;
145
+ textarea . style . overflow = textareaStyles . overflowing ? 'hidden' : '' ;
140
146
} , [ calculateTextareaStyles ] ) ;
141
147
148
+ const frameRef = React . useRef ( - 1 ) ;
149
+
142
150
useEnhancedEffect ( ( ) => {
143
- const handleResize = ( ) => {
144
- syncHeight ( ) ;
145
- } ;
146
- // Workaround a "ResizeObserver loop completed with undelivered notifications" error
147
- // in test.
148
- // Note that we might need to use this logic in production per https://github.com/WICG/resize-observer/issues/38
149
- // Also see https://github.com/mui/mui-x/issues/8733
150
- let rAF : any ;
151
- const rAFHandleResize = ( ) => {
152
- cancelAnimationFrame ( rAF ) ;
153
- rAF = requestAnimationFrame ( ( ) => {
154
- handleResize ( ) ;
155
- } ) ;
156
- } ;
157
- const debounceHandleResize = debounce ( handleResize ) ;
158
- const input = inputRef . current ! ;
159
- const containerWindow = ownerWindow ( input ) ;
151
+ const debounceHandleResize = debounce ( ( ) => syncHeight ( ) ) ;
152
+ const textarea = textareaRef ?. current ;
153
+
154
+ if ( ! textarea ) {
155
+ return undefined ;
156
+ }
157
+
158
+ const containerWindow = ownerWindow ( textarea ) ;
160
159
161
160
containerWindow . addEventListener ( 'resize' , debounceHandleResize ) ;
162
161
163
162
let resizeObserver : ResizeObserver ;
164
163
165
164
if ( typeof ResizeObserver !== 'undefined' ) {
166
- resizeObserver = new ResizeObserver (
167
- process . env . NODE_ENV === 'test' ? rAFHandleResize : handleResize ,
168
- ) ;
169
- resizeObserver . observe ( input ) ;
165
+ 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
+ } ) ;
175
+ } ) ;
176
+ resizeObserver . observe ( textarea ) ;
170
177
}
171
178
172
179
return ( ) => {
173
180
debounceHandleResize . clear ( ) ;
174
- cancelAnimationFrame ( rAF ) ;
181
+ cancelAnimationFrame ( frameRef . current ) ;
175
182
containerWindow . removeEventListener ( 'resize' , debounceHandleResize ) ;
176
183
if ( resizeObserver ) {
177
184
resizeObserver . disconnect ( ) ;
@@ -208,7 +215,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(
208
215
aria-hidden
209
216
className = { props . className }
210
217
readOnly
211
- ref = { shadowRef }
218
+ ref = { hiddenTextareaRef }
212
219
tabIndex = { - 1 }
213
220
style = { {
214
221
...styles . shadow ,
0 commit comments