-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(textarea): add md-autosize directive (#1846)
- Loading branch information
Showing
7 changed files
with
228 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import {Component} from '@angular/core'; | ||
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; | ||
import {By} from '@angular/platform-browser'; | ||
import {MdInputModule} from './input'; | ||
import {MdTextareaAutosize} from './autosize'; | ||
|
||
|
||
describe('MdTextareaAutosize', () => { | ||
let fixture: ComponentFixture<AutosizeTextAreaWithContent>; | ||
let textarea: HTMLTextAreaElement; | ||
let autosize: MdTextareaAutosize; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [MdInputModule], | ||
declarations: [AutosizeTextAreaWithContent, AutosizeTextAreaWithValue], | ||
}); | ||
|
||
TestBed.compileComponents(); | ||
})); | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(AutosizeTextAreaWithContent); | ||
fixture.detectChanges(); | ||
|
||
textarea = fixture.nativeElement.querySelector('textarea'); | ||
autosize = fixture.debugElement.query( | ||
By.directive(MdTextareaAutosize)).injector.get(MdTextareaAutosize); | ||
}); | ||
|
||
it('should resize the textarea based on its content', () => { | ||
let previousHeight = textarea.offsetHeight; | ||
|
||
fixture.componentInstance.content = ` | ||
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.”`; | ||
|
||
// Manually call resizeToFitContent instead of faking an `input` event. | ||
fixture.detectChanges(); | ||
autosize.resizeToFitContent(); | ||
|
||
expect(textarea.offsetHeight) | ||
.toBeGreaterThan(previousHeight, 'Expected textarea to have grown with added content.'); | ||
expect(textarea.offsetHeight) | ||
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight'); | ||
|
||
previousHeight = textarea.offsetHeight; | ||
fixture.componentInstance.content += ` | ||
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 | ||
From my books surcease of sorrow—sorrow for the lost Lenore— | ||
For the rare and radiant maiden whom the angels name Lenore— | ||
Nameless here for evermore.`; | ||
|
||
fixture.detectChanges(); | ||
autosize.resizeToFitContent(); | ||
|
||
expect(textarea.offsetHeight) | ||
.toBeGreaterThan(previousHeight, 'Expected textarea to have grown with added content.'); | ||
expect(textarea.offsetHeight) | ||
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight'); | ||
}); | ||
|
||
it('should set a min-width based on minRows', () => { | ||
expect(textarea.style.minHeight).toBeFalsy(); | ||
|
||
fixture.componentInstance.minRows = 4; | ||
fixture.detectChanges(); | ||
|
||
expect(textarea.style.minHeight).toBeDefined('Expected a min-height to be set via minRows.'); | ||
|
||
let previousMinHeight = parseInt(textarea.style.minHeight); | ||
fixture.componentInstance.minRows = 6; | ||
fixture.detectChanges(); | ||
|
||
expect(parseInt(textarea.style.minHeight)) | ||
.toBeGreaterThan(previousMinHeight, 'Expected increased min-height with minRows increase.'); | ||
}); | ||
|
||
it('should set a max-width based on maxRows', () => { | ||
expect(textarea.style.maxHeight).toBeFalsy(); | ||
|
||
fixture.componentInstance.maxRows = 4; | ||
fixture.detectChanges(); | ||
|
||
expect(textarea.style.maxHeight).toBeDefined('Expected a max-height to be set via maxRows.'); | ||
|
||
let previousMaxHeight = parseInt(textarea.style.maxHeight); | ||
fixture.componentInstance.maxRows = 6; | ||
fixture.detectChanges(); | ||
|
||
expect(parseInt(textarea.style.maxHeight)) | ||
.toBeGreaterThan(previousMaxHeight, 'Expected increased max-height with maxRows increase.'); | ||
}); | ||
}); | ||
|
||
|
||
// Styles to reset padding and border to make measurement comparisons easier. | ||
const textareaStyleReset = ` | ||
textarea { | ||
padding: 0; | ||
border: none; | ||
overflow: auto; | ||
}`; | ||
|
||
@Component({ | ||
template: `<textarea md-autosize [minRows]="minRows" [maxRows]="maxRows">{{content}}</textarea>`, | ||
styles: [textareaStyleReset], | ||
}) | ||
class AutosizeTextAreaWithContent { | ||
minRows: number = null; | ||
maxRows: number = null; | ||
content: string = ''; | ||
} | ||
|
||
@Component({ | ||
template: `<textarea md-autosize [value]="value"></textarea>`, | ||
styles: [textareaStyleReset], | ||
}) | ||
class AutosizeTextAreaWithValue { | ||
value: string = ''; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import {Directive, ElementRef, Input, OnInit} from '@angular/core'; | ||
|
||
|
||
/** | ||
* Directive to automatically resize a textarea to fit its content. | ||
*/ | ||
@Directive({ | ||
selector: 'textarea[md-autosize]', | ||
host: { | ||
'(input)': 'resizeToFitContent()', | ||
'[style.min-height]': '_minHeight', | ||
'[style.max-height]': '_maxHeight', | ||
}, | ||
}) | ||
export class MdTextareaAutosize implements OnInit { | ||
/** Minimum number of rows for this textarea. */ | ||
@Input() minRows: number; | ||
|
||
/** Maximum number of rows for this textarea. */ | ||
@Input() maxRows: number; | ||
|
||
/** Cached height of a textarea with a single row. */ | ||
private _cachedLineHeight: number; | ||
|
||
constructor(private _elementRef: ElementRef) { } | ||
|
||
/** The minimum height of the textarea as determined by minRows. */ | ||
get _minHeight() { | ||
return this.minRows ? `${this.minRows * this._cachedLineHeight}px` : null; | ||
} | ||
|
||
/** The maximum height of the textarea as determined by maxRows. */ | ||
get _maxHeight() { | ||
return this.maxRows ? `${this.maxRows * this._cachedLineHeight}px` : null; | ||
} | ||
|
||
ngOnInit() { | ||
this._cacheTextareaLineHeight(); | ||
this.resizeToFitContent(); | ||
} | ||
|
||
/** | ||
* Cache the hight of a single-row textarea. | ||
* | ||
* We need to know how large a single "row" of a textarea is in order to apply minRows and | ||
* maxRows. For the initial version, we will assume that the height of a single line in the | ||
* textarea does not ever change. | ||
*/ | ||
private _cacheTextareaLineHeight(): void { | ||
let textarea = this._elementRef.nativeElement as HTMLTextAreaElement; | ||
|
||
// Use a clone element because we have to override some styles. | ||
let textareaClone = textarea.cloneNode(false) as HTMLTextAreaElement; | ||
textareaClone.rows = 1; | ||
|
||
// Use `position: absolute` so that this doesn't cause a browser layout and use | ||
// `visibility: hidden` so that nothing is rendered. Clear any other styles that | ||
// would affect the height. | ||
textareaClone.style.position = 'absolute'; | ||
textareaClone.style.visibility = 'hidden'; | ||
textareaClone.style.border = 'none'; | ||
textareaClone.style.padding = ''; | ||
textareaClone.style.height = ''; | ||
textareaClone.style.minHeight = ''; | ||
textareaClone.style.maxHeight = ''; | ||
|
||
textarea.parentNode.appendChild(textareaClone); | ||
this._cachedLineHeight = textareaClone.offsetHeight; | ||
textarea.parentNode.removeChild(textareaClone); | ||
} | ||
|
||
/** Resize the textarea to fit its content. */ | ||
resizeToFitContent() { | ||
let textarea = this._elementRef.nativeElement as HTMLTextAreaElement; | ||
// Reset the textarea height to auto in order to shrink back to its default size. | ||
textarea.style.height = 'auto'; | ||
|
||
// Use the scrollHeight to know how large the textarea *would* be if fit its entire value. | ||
textarea.style.height = `${textarea.scrollHeight}px`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters