Skip to content

Commit d83e5cf

Browse files
authored
fix(cdk/text-field): avoid page jump on firefox (#23296)
When we measure the size of the autosize `textarea`, we make it temporarily smaller which can cause the scroll position to shift on Firefox. These changes add a workaround that assign a temporary `margin-bottom` while measuring. Fixes #23233.
1 parent 6c50423 commit d83e5cf

File tree

1 file changed

+34
-11
lines changed

1 file changed

+34
-11
lines changed

src/cdk/text-field/autosize.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
105105
/** Used to reference correct document/window */
106106
protected _document?: Document;
107107

108-
/** Class that should be applied to the textarea while it's being measured. */
109-
private _measuringClass: string;
108+
private _hasFocus: boolean;
110109

111110
private _isViewInited = false;
112111

@@ -118,9 +117,6 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
118117
this._document = document;
119118

120119
this._textareaElement = this._elementRef.nativeElement as HTMLTextAreaElement;
121-
this._measuringClass = _platform.FIREFOX ?
122-
'cdk-textarea-autosize-measuring-firefox' :
123-
'cdk-textarea-autosize-measuring';
124120
}
125121

126122
/** Sets the minimum height of the textarea as determined by minRows. */
@@ -147,7 +143,6 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
147143
if (this._platform.isBrowser) {
148144
// Remember the height which we started with in case autosizing is disabled
149145
this._initialHeight = this._textareaElement.style.height;
150-
151146
this.resizeToFitContent();
152147

153148
this._ngZone.runOutsideAngular(() => {
@@ -156,6 +151,9 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
156151
fromEvent(window, 'resize')
157152
.pipe(auditTime(16), takeUntil(this._destroyed))
158153
.subscribe(() => this.resizeToFitContent(true));
154+
155+
this._textareaElement.addEventListener('focus', this._handleFocusEvent);
156+
this._textareaElement.addEventListener('blur', this._handleFocusEvent);
159157
});
160158

161159
this._isViewInited = true;
@@ -164,6 +162,8 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
164162
}
165163

166164
ngOnDestroy() {
165+
this._textareaElement.removeEventListener('focus', this._handleFocusEvent);
166+
this._textareaElement.removeEventListener('blur', this._handleFocusEvent);
167167
this._destroyed.next();
168168
this._destroyed.complete();
169169
}
@@ -212,13 +212,32 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
212212
}
213213

214214
private _measureScrollHeight(): number {
215+
const element = this._textareaElement;
216+
const previousMargin = element.style.marginBottom || '';
217+
const isFirefox = this._platform.FIREFOX;
218+
const needsMarginFiller = isFirefox && this._hasFocus;
219+
const measuringClass = isFirefox ?
220+
'cdk-textarea-autosize-measuring-firefox' :
221+
'cdk-textarea-autosize-measuring';
222+
223+
// In some cases the page might move around while we're measuring the `textarea` on Firefox. We
224+
// work around it by assigning a temporary margin with the same height as the `textarea` so that
225+
// it occupies the same amount of space. See #23233.
226+
if (needsMarginFiller) {
227+
element.style.marginBottom = `${element.clientHeight}px`;
228+
}
229+
215230
// Reset the textarea height to auto in order to shrink back to its default size.
216231
// Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
217-
this._textareaElement.classList.add(this._measuringClass);
232+
element.classList.add(measuringClass);
218233
// The measuring class includes a 2px padding to workaround an issue with Chrome,
219234
// so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
220-
const scrollHeight = this._textareaElement.scrollHeight - 4;
221-
this._textareaElement.classList.remove(this._measuringClass);
235+
const scrollHeight = element.scrollHeight - 4;
236+
element.classList.remove(measuringClass);
237+
238+
if (needsMarginFiller) {
239+
element.style.marginBottom = previousMargin;
240+
}
222241

223242
return scrollHeight;
224243
}
@@ -239,6 +258,11 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
239258
this._textareaElement.value = value;
240259
}
241260

261+
/** Handles `focus` and `blur` events. */
262+
private _handleFocusEvent = (event: FocusEvent) => {
263+
this._hasFocus = event.type === 'focus';
264+
}
265+
242266
ngDoCheck() {
243267
if (this._platform.isBrowser) {
244268
this.resizeToFitContent();
@@ -329,15 +353,14 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
329353
*/
330354
private _scrollToCaretPosition(textarea: HTMLTextAreaElement) {
331355
const {selectionStart, selectionEnd} = textarea;
332-
const document = this._getDocument();
333356

334357
// IE will throw an "Unspecified error" if we try to set the selection range after the
335358
// element has been removed from the DOM. Assert that the directive hasn't been destroyed
336359
// between the time we requested the animation frame and when it was executed.
337360
// Also note that we have to assert that the textarea is focused before we set the
338361
// selection range. Setting the selection range on a non-focused textarea will cause
339362
// it to receive focus on IE and Edge.
340-
if (!this._destroyed.isStopped && document.activeElement === textarea) {
363+
if (!this._destroyed.isStopped && this._hasFocus) {
341364
textarea.setSelectionRange(selectionStart, selectionEnd);
342365
}
343366
}

0 commit comments

Comments
 (0)