Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cdk/text-field): autosize text areas using the placeholder #22197

Merged
merged 1 commit into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/check-mdc-tests-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const config = {
'should calculate the outline gaps inside the shadow DOM',
'should be legacy appearance if no default options provided',
'should be legacy appearance if empty default options provided',
'should not calculate wrong content height due to long placeholders',
'should adjust height due to long placeholders',
'should work in a tab',
'should work in a step'
],
Expand Down
62 changes: 58 additions & 4 deletions src/cdk/text-field/autosize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('CdkTextareaAutosize', () => {
it('should resize the textarea based on its content', () => {
let previousHeight = textarea.clientHeight;

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

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

it('should keep the placeholder size if the value is shorter than the placeholder', () => {
fixture = TestBed.createComponent(AutosizeTextAreaWithContent);

textarea = fixture.nativeElement.querySelector('textarea');
autosize = fixture.debugElement.query(By.css('textarea'))!
.injector.get<CdkTextareaAutosize>(CdkTextareaAutosize);

fixture.componentInstance.placeholder = `
Once upon a midnight dreary, while I pondered, weak and weary,
Over many a quaint and curious volume of forgotten lore—
While I nodded, nearly napping, suddenly there came a tapping,
As of some one gently rapping, rapping at my chamber door.
“’Tis some visitor,” I muttered, “tapping at my chamber door—
Only this and nothing more.”`;

fixture.detectChanges();

expect(textarea.clientHeight)
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');

let previousHeight = textarea.clientHeight;

textarea.value = 'a';

// Manually call resizeToFitContent instead of faking an `input` event.
fixture.detectChanges();
autosize.resizeToFitContent();

expect(textarea.clientHeight)
.toBe(previousHeight, 'Expected textarea height not to have changed');
});

it('should set a min-height based on minRows', () => {
expect(textarea.style.minHeight).toBeFalsy();

Expand Down Expand Up @@ -161,7 +193,7 @@ describe('CdkTextareaAutosize', () => {
});

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

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

it('should properly resize to placeholder on init', () => {
// Manually create the test component in this test, because in this test the first change
// detection should be triggered after a multiline placeholder is set.
fixture = TestBed.createComponent(AutosizeTextAreaWithContent);
textarea = fixture.nativeElement.querySelector('textarea');
autosize = fixture.debugElement.query(By.css('textarea'))!
.injector.get<CdkTextareaAutosize>(CdkTextareaAutosize);

fixture.componentInstance.placeholder = `
Line
Line
Line
Line
Line`;

fixture.detectChanges();

expect(textarea.clientHeight)
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
});

it('should resize when an associated form control value changes', fakeAsync(() => {
const fixtureWithForms = TestBed.createComponent(AutosizeTextareaWithNgModel);
textarea = fixtureWithForms.nativeElement.querySelector('textarea');
Expand Down Expand Up @@ -298,14 +351,15 @@ const textareaStyleReset = `
@Component({
template: `
<textarea cdkTextareaAutosize [cdkAutosizeMinRows]="minRows" [cdkAutosizeMaxRows]="maxRows"
#autosize="cdkTextareaAutosize">{{content}}</textarea>`,
#autosize="cdkTextareaAutosize" [placeholder]="placeholder">{{content}}</textarea>`,
styles: [textareaStyleReset],
})
class AutosizeTextAreaWithContent {
@ViewChild('autosize') autosize: CdkTextareaAutosize;
minRows: number | null = null;
maxRows: number | null = null;
content: string = '';
placeholder: string = '';
}

@Component({
Expand Down
50 changes: 38 additions & 12 deletions src/cdk/text-field/autosize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,19 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
}
}

@Input()
get placeholder(): string { return this._textareaElement.placeholder; }
set placeholder(value: string) {
this._cachedPlaceholderHeight = undefined;
this._textareaElement.placeholder = value;
this._cacheTextareaPlaceholderHeight();
}


/** Cached height of a textarea with a single row. */
private _cachedLineHeight: number;
/** Cached height of a textarea with only the placeholder. */
private _cachedPlaceholderHeight?: number;

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

private _measureScrollHeight(): number {
// Reset the textarea height to auto in order to shrink back to its default size.
// Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
this._textareaElement.classList.add(this._measuringClass);
// The measuring class includes a 2px padding to workaround an issue with Chrome,
// so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
const scrollHeight = this._textareaElement.scrollHeight - 4;
this._textareaElement.classList.remove(this._measuringClass);

return scrollHeight;
}

private _cacheTextareaPlaceholderHeight(): void {
if (this._cachedPlaceholderHeight) {
return;
}

const value = this._textareaElement.value;

this._textareaElement.value = this._textareaElement.placeholder;
this._cachedPlaceholderHeight = this._measureScrollHeight();
this._textareaElement.value = value;
}

ngDoCheck() {
if (this._platform.isBrowser) {
this.resizeToFitContent();
Expand All @@ -213,6 +248,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
}

this._cacheTextareaLineHeight();
this._cacheTextareaPlaceholderHeight();

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

const placeholderText = textarea.placeholder;

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

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

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

this._ngZone.runOutsideAngular(() => {
if (typeof requestAnimationFrame !== 'undefined') {
Expand Down
6 changes: 3 additions & 3 deletions src/material/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1719,7 +1719,7 @@ describe('MatFormField default options', () => {
});

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

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

autosize.resizeToFitContent(true);

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

it('should work in a tab', () => {
Expand Down
4 changes: 3 additions & 1 deletion tools/public_api_guard/cdk/text-field.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export declare class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDe
set maxRows(value: number);
get minRows(): number;
set minRows(value: number);
get placeholder(): string;
set placeholder(value: string);
constructor(_elementRef: ElementRef<HTMLElement>, _platform: Platform, _ngZone: NgZone,
document?: any);
_noopInputHandler(): void;
Expand All @@ -44,7 +46,7 @@ export declare class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDe
static ngAcceptInputType_enabled: BooleanInput;
static ngAcceptInputType_maxRows: NumberInput;
static ngAcceptInputType_minRows: NumberInput;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkTextareaAutosize, "textarea[cdkTextareaAutosize]", ["cdkTextareaAutosize"], { "minRows": "cdkAutosizeMinRows"; "maxRows": "cdkAutosizeMaxRows"; "enabled": "cdkTextareaAutosize"; }, {}, never>;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkTextareaAutosize, "textarea[cdkTextareaAutosize]", ["cdkTextareaAutosize"], { "minRows": "cdkAutosizeMinRows"; "maxRows": "cdkAutosizeMaxRows"; "enabled": "cdkTextareaAutosize"; "placeholder": "placeholder"; }, {}, never>;
static ɵfac: i0.ɵɵFactoryDef<CdkTextareaAutosize, [null, null, null, { optional: true; }]>;
}

Expand Down