Skip to content

Commit 476a90b

Browse files
authored
fix(cdk/text-field): autosize text areas using the placeholder (#22197)
Fixes a bug with CdkTextareaAutosize where the textarea would not be autosized when using long placeholders Fixes #22042 Cache the height with the placeholder do calculation better fix comment Stop caching the placeholder because I can't make the caching approach work with view-engine go back to the caching approach Account for the input tests fix lint error
1 parent 08bbd50 commit 476a90b

File tree

5 files changed

+103
-21
lines changed

5 files changed

+103
-21
lines changed

scripts/check-mdc-tests-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const config = {
7777
'should calculate the outline gaps inside the shadow DOM',
7878
'should be legacy appearance if no default options provided',
7979
'should be legacy appearance if empty default options provided',
80-
'should not calculate wrong content height due to long placeholders',
80+
'should adjust height due to long placeholders',
8181
'should work in a tab',
8282
'should work in a step'
8383
],

src/cdk/text-field/autosize.spec.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('CdkTextareaAutosize', () => {
5050
it('should resize the textarea based on its content', () => {
5151
let previousHeight = textarea.clientHeight;
5252

53-
fixture.componentInstance.content = `
53+
textarea.value = `
5454
Once upon a midnight dreary, while I pondered, weak and weary,
5555
Over many a quaint and curious volume of forgotten lore—
5656
While I nodded, nearly napping, suddenly there came a tapping,
@@ -68,7 +68,7 @@ describe('CdkTextareaAutosize', () => {
6868
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
6969

7070
previousHeight = textarea.clientHeight;
71-
fixture.componentInstance.content += `
71+
textarea.value += `
7272
Ah, distinctly I remember it was in the bleak December;
7373
And each separate dying ember wrought its ghost upon the floor.
7474
Eagerly I wished the morrow;—vainly I had sought to borrow
@@ -85,6 +85,38 @@ describe('CdkTextareaAutosize', () => {
8585
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
8686
});
8787

88+
it('should keep the placeholder size if the value is shorter than the placeholder', () => {
89+
fixture = TestBed.createComponent(AutosizeTextAreaWithContent);
90+
91+
textarea = fixture.nativeElement.querySelector('textarea');
92+
autosize = fixture.debugElement.query(By.css('textarea'))!
93+
.injector.get<CdkTextareaAutosize>(CdkTextareaAutosize);
94+
95+
fixture.componentInstance.placeholder = `
96+
Once upon a midnight dreary, while I pondered, weak and weary,
97+
Over many a quaint and curious volume of forgotten lore—
98+
While I nodded, nearly napping, suddenly there came a tapping,
99+
As of some one gently rapping, rapping at my chamber door.
100+
“’Tis some visitor,” I muttered, “tapping at my chamber door—
101+
Only this and nothing more.”`;
102+
103+
fixture.detectChanges();
104+
105+
expect(textarea.clientHeight)
106+
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
107+
108+
let previousHeight = textarea.clientHeight;
109+
110+
textarea.value = 'a';
111+
112+
// Manually call resizeToFitContent instead of faking an `input` event.
113+
fixture.detectChanges();
114+
autosize.resizeToFitContent();
115+
116+
expect(textarea.clientHeight)
117+
.toBe(previousHeight, 'Expected textarea height not to have changed');
118+
});
119+
88120
it('should set a min-height based on minRows', () => {
89121
expect(textarea.style.minHeight).toBeFalsy();
90122

@@ -161,7 +193,7 @@ describe('CdkTextareaAutosize', () => {
161193
});
162194

163195
it('should calculate the proper height based on the specified amount of max rows', () => {
164-
fixture.componentInstance.content = [1, 2, 3, 4, 5, 6, 7, 8].join('\n');
196+
textarea.value = [1, 2, 3, 4, 5, 6, 7, 8].join('\n');
165197
fixture.detectChanges();
166198
autosize.resizeToFitContent();
167199

@@ -196,6 +228,27 @@ describe('CdkTextareaAutosize', () => {
196228
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
197229
});
198230

231+
it('should properly resize to placeholder on init', () => {
232+
// Manually create the test component in this test, because in this test the first change
233+
// detection should be triggered after a multiline placeholder is set.
234+
fixture = TestBed.createComponent(AutosizeTextAreaWithContent);
235+
textarea = fixture.nativeElement.querySelector('textarea');
236+
autosize = fixture.debugElement.query(By.css('textarea'))!
237+
.injector.get<CdkTextareaAutosize>(CdkTextareaAutosize);
238+
239+
fixture.componentInstance.placeholder = `
240+
Line
241+
Line
242+
Line
243+
Line
244+
Line`;
245+
246+
fixture.detectChanges();
247+
248+
expect(textarea.clientHeight)
249+
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
250+
});
251+
199252
it('should resize when an associated form control value changes', fakeAsync(() => {
200253
const fixtureWithForms = TestBed.createComponent(AutosizeTextareaWithNgModel);
201254
textarea = fixtureWithForms.nativeElement.querySelector('textarea');
@@ -298,14 +351,15 @@ const textareaStyleReset = `
298351
@Component({
299352
template: `
300353
<textarea cdkTextareaAutosize [cdkAutosizeMinRows]="minRows" [cdkAutosizeMaxRows]="maxRows"
301-
#autosize="cdkTextareaAutosize">{{content}}</textarea>`,
354+
#autosize="cdkTextareaAutosize" [placeholder]="placeholder">{{content}}</textarea>`,
302355
styles: [textareaStyleReset],
303356
})
304357
class AutosizeTextAreaWithContent {
305358
@ViewChild('autosize') autosize: CdkTextareaAutosize;
306359
minRows: number | null = null;
307360
maxRows: number | null = null;
308361
content: string = '';
362+
placeholder: string = '';
309363
}
310364

311365
@Component({

src/cdk/text-field/autosize.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,19 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
8888
}
8989
}
9090

91+
@Input()
92+
get placeholder(): string { return this._textareaElement.placeholder; }
93+
set placeholder(value: string) {
94+
this._cachedPlaceholderHeight = undefined;
95+
this._textareaElement.placeholder = value;
96+
this._cacheTextareaPlaceholderHeight();
97+
}
98+
99+
91100
/** Cached height of a textarea with a single row. */
92101
private _cachedLineHeight: number;
102+
/** Cached height of a textarea with only the placeholder. */
103+
private _cachedPlaceholderHeight?: number;
93104

94105
/** Used to reference correct document/window */
95106
protected _document?: Document;
@@ -195,6 +206,30 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
195206
this._setMaxHeight();
196207
}
197208

209+
private _measureScrollHeight(): number {
210+
// Reset the textarea height to auto in order to shrink back to its default size.
211+
// Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
212+
this._textareaElement.classList.add(this._measuringClass);
213+
// The measuring class includes a 2px padding to workaround an issue with Chrome,
214+
// so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
215+
const scrollHeight = this._textareaElement.scrollHeight - 4;
216+
this._textareaElement.classList.remove(this._measuringClass);
217+
218+
return scrollHeight;
219+
}
220+
221+
private _cacheTextareaPlaceholderHeight(): void {
222+
if (this._cachedPlaceholderHeight) {
223+
return;
224+
}
225+
226+
const value = this._textareaElement.value;
227+
228+
this._textareaElement.value = this._textareaElement.placeholder;
229+
this._cachedPlaceholderHeight = this._measureScrollHeight();
230+
this._textareaElement.value = value;
231+
}
232+
198233
ngDoCheck() {
199234
if (this._platform.isBrowser) {
200235
this.resizeToFitContent();
@@ -213,6 +248,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
213248
}
214249

215250
this._cacheTextareaLineHeight();
251+
this._cacheTextareaPlaceholderHeight();
216252

217253
// If we haven't determined the line-height yet, we know we're still hidden and there's no point
218254
// in checking the height of the textarea.
@@ -228,24 +264,14 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
228264
return;
229265
}
230266

231-
const placeholderText = textarea.placeholder;
232-
233-
// Reset the textarea height to auto in order to shrink back to its default size.
234-
// Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
235-
// Long placeholders that are wider than the textarea width may lead to a bigger scrollHeight
236-
// value. To ensure that the scrollHeight is not bigger than the content, the placeholders
237-
// need to be removed temporarily.
238-
textarea.classList.add(this._measuringClass);
239-
textarea.placeholder = '';
267+
const scrollHeight = this._measureScrollHeight();
240268

241269
// The measuring class includes a 2px padding to workaround an issue with Chrome,
242270
// so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
243-
const height = textarea.scrollHeight - 4;
271+
const height = Math.max(scrollHeight, this._cachedPlaceholderHeight || 0);
244272

245273
// Use the scrollHeight to know how large the textarea *would* be if fit its entire value.
246274
textarea.style.height = `${height}px`;
247-
textarea.classList.remove(this._measuringClass);
248-
textarea.placeholder = placeholderText;
249275

250276
this._ngZone.runOutsideAngular(() => {
251277
if (typeof requestAnimationFrame !== 'undefined') {

src/material/input/input.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,7 +1719,7 @@ describe('MatFormField default options', () => {
17191719
});
17201720

17211721
describe('MatInput with textarea autosize', () => {
1722-
it('should not calculate wrong content height due to long placeholders', () => {
1722+
it('should adjust height due to long placeholders', () => {
17231723
const fixture = createComponent(AutosizeTextareaWithLongPlaceholder);
17241724
fixture.detectChanges();
17251725

@@ -1735,8 +1735,8 @@ describe('MatInput with textarea autosize', () => {
17351735

17361736
autosize.resizeToFitContent(true);
17371737

1738-
expect(textarea.clientHeight).toBe(heightWithLongPlaceholder,
1739-
'Expected the textarea height to be the same with a long placeholder.');
1738+
expect(textarea.clientHeight).toBeLessThan(heightWithLongPlaceholder,
1739+
'Expected the textarea height to be shorter with a long placeholder.');
17401740
});
17411741

17421742
it('should work in a tab', () => {

tools/public_api_guard/cdk/text-field.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export declare class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDe
3131
set maxRows(value: number);
3232
get minRows(): number;
3333
set minRows(value: number);
34+
get placeholder(): string;
35+
set placeholder(value: string);
3436
constructor(_elementRef: ElementRef<HTMLElement>, _platform: Platform, _ngZone: NgZone,
3537
document?: any);
3638
_noopInputHandler(): void;
@@ -44,7 +46,7 @@ export declare class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDe
4446
static ngAcceptInputType_enabled: BooleanInput;
4547
static ngAcceptInputType_maxRows: NumberInput;
4648
static ngAcceptInputType_minRows: NumberInput;
47-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkTextareaAutosize, "textarea[cdkTextareaAutosize]", ["cdkTextareaAutosize"], { "minRows": "cdkAutosizeMinRows"; "maxRows": "cdkAutosizeMaxRows"; "enabled": "cdkTextareaAutosize"; }, {}, never>;
49+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkTextareaAutosize, "textarea[cdkTextareaAutosize]", ["cdkTextareaAutosize"], { "minRows": "cdkAutosizeMinRows"; "maxRows": "cdkAutosizeMaxRows"; "enabled": "cdkTextareaAutosize"; "placeholder": "placeholder"; }, {}, never>;
4850
static ɵfac: i0.ɵɵFactoryDef<CdkTextareaAutosize, [null, null, null, { optional: true; }]>;
4951
}
5052

0 commit comments

Comments
 (0)