diff --git a/packages/text-area/src/vaadin-text-area-mixin.d.ts b/packages/text-area/src/vaadin-text-area-mixin.d.ts index 69b62411ac3..59eb31e7487 100644 --- a/packages/text-area/src/vaadin-text-area-mixin.d.ts +++ b/packages/text-area/src/vaadin-text-area-mixin.d.ts @@ -62,6 +62,15 @@ export declare class TextAreaMixinClass { */ pattern: string; + /** + * Minimum number of rows to show. Default is two rows, which is also the minimum value. + * + * When using a custom slotted textarea, the minimum number of rows are not applied for backwards compatibility. + * + * @attr {number} min-rows + */ + minRows: number; + /** * Scrolls the textarea to the start if it has a vertical scrollbar. */ diff --git a/packages/text-area/src/vaadin-text-area-mixin.js b/packages/text-area/src/vaadin-text-area-mixin.js index 533c0434c60..0f6cfde835e 100644 --- a/packages/text-area/src/vaadin-text-area-mixin.js +++ b/packages/text-area/src/vaadin-text-area-mixin.js @@ -40,6 +40,19 @@ export const TextAreaMixin = (superClass) => pattern: { type: String, }, + + /** + * Minimum number of rows to show. Default is two rows, which is also the minimum value. + * + * When using a custom slotted textarea, the minimum number of rows are not applied for backwards compatibility. + * + * @attr {number} min-rows + */ + minRows: { + type: Number, + value: 2, + observer: '__minRowsChanged', + }, }; } @@ -51,6 +64,10 @@ export const TextAreaMixin = (superClass) => return [...super.constraints, 'maxlength', 'minlength', 'pattern']; } + static get observers() { + return ['__updateMinHeight(minRows, inputElement)']; + } + /** * Used by `InputControlMixin` as a reference to the clear button element. * @protected @@ -77,14 +94,13 @@ export const TextAreaMixin = (superClass) => ready() { super.ready(); - this.addController( - new TextAreaController(this, (input) => { - this._setInputElement(input); - this._setFocusElement(input); - this.stateTarget = input; - this.ariaTarget = input; - }), - ); + this.__textAreaController = new TextAreaController(this, (input) => { + this._setInputElement(input); + this._setFocusElement(input); + this.stateTarget = input; + this.ariaTarget = input; + }); + this.addController(this.__textAreaController); this.addController(new LabelledInputController(this.inputElement, this._labelController)); this.addEventListener('animationend', this._onAnimationEnd); @@ -178,6 +194,30 @@ export const TextAreaMixin = (superClass) => inputField.scrollTop = scrollTop; } + /** @private */ + __updateMinHeight(minRows) { + if (!this.inputElement) { + return; + } + + // For minimum height, just set the number of rows on the native textarea, + // which causes the input container to grow as well. + // Do not override this on custom slotted textarea as number of rows may + // have been configured there. + if (this.inputElement === this.__textAreaController.defaultNode) { + this.inputElement.rows = Math.max(minRows, 2); + } + } + + /** + * @private + */ + __minRowsChanged(minRows) { + if (minRows < 2) { + console.warn(' minRows must be at least 2.'); + } + } + /** * Scrolls the textarea to the start if it has a vertical scrollbar. */ diff --git a/packages/text-area/test/dom/__snapshots__/text-area.test.snap.js b/packages/text-area/test/dom/__snapshots__/text-area.test.snap.js index ea2d1be8c91..385647e68d0 100644 --- a/packages/text-area/test/dom/__snapshots__/text-area.test.snap.js +++ b/packages/text-area/test/dom/__snapshots__/text-area.test.snap.js @@ -17,6 +17,7 @@ snapshots["vaadin-text-area host default"] = @@ -41,6 +42,7 @@ snapshots["vaadin-text-area host helper"] = @@ -77,6 +79,7 @@ snapshots["vaadin-text-area host error"] = aria-invalid="true" id="textarea-vaadin-text-area-3" invalid="" + rows="2" slot="textarea" > diff --git a/packages/text-area/test/text-area.common.js b/packages/text-area/test/text-area.common.js index f401006ae2e..1b754663024 100644 --- a/packages/text-area/test/text-area.common.js +++ b/packages/text-area/test/text-area.common.js @@ -358,6 +358,93 @@ describe('text-area', () => { ); }); + describe('min rows', () => { + const lineHeight = 20; + let consoleWarn; + + beforeEach(async () => { + const fixture = fixtureSync(` +
+ + +
+ `); + textArea = fixture.querySelector('vaadin-text-area'); + await nextUpdate(textArea); + + consoleWarn = sinon.stub(console, 'warn'); + }); + + afterEach(() => { + consoleWarn.restore(); + }); + + it('should use min-height of two rows by default', () => { + expect(textArea.clientHeight).to.equal(lineHeight * 2); + }); + + it('should use min-height based on minimum rows', async () => { + textArea.minRows = 4; + await nextUpdate(textArea); + + expect(textArea.clientHeight).to.equal(lineHeight * 4); + }); + + it('should not be possible to set min-height to less than two rows', async () => { + textArea.minRows = 1; + await nextUpdate(textArea); + + expect(textArea.clientHeight).to.closeTo(lineHeight * 2, 1); + }); + + it('should log warning when setting minRows to less than two rows', async () => { + textArea.minRows = 1; + await nextUpdate(textArea); + + expect(console.warn).to.be.calledWith(' minRows must be at least 2.'); + }); + + it('should not log warning when setting minRows to two rows or more', async () => { + textArea.minRows = 2; + await nextUpdate(textArea); + + expect(console.warn).not.to.be.called; + + textArea.minRows = 3; + await nextUpdate(textArea); + + expect(console.warn).not.to.be.called; + }); + + it('should not overwrite rows on custom slotted textarea', async () => { + const custom = document.createElement('textarea'); + custom.setAttribute('slot', 'textarea'); + custom.rows = 1; + textArea.appendChild(custom); + await nextUpdate(textArea); + + textArea.minRows = 4; + await nextUpdate(textArea); + + expect(custom.rows).to.equal(1); + expect(textArea.clientHeight).to.closeTo(lineHeight, 1); + }); + + it('should grow beyond the min-height defined by minimum rows', async () => { + textArea.minRows = 4; + await nextUpdate(textArea); + + textArea.value = Array(400).join('400'); + await nextUpdate(textArea); + + expect(textArea.clientHeight).to.be.above(80); + }); + }); + describe('--_text-area-vertical-scroll-position CSS variable', () => { function wheel({ element = inputField, deltaY = 0 }) { const e = new CustomEvent('wheel', { bubbles: true, cancelable: true }); diff --git a/packages/text-area/test/visual/lumo/screenshots/text-area/baseline/min-rows.png b/packages/text-area/test/visual/lumo/screenshots/text-area/baseline/min-rows.png new file mode 100644 index 00000000000..309aade3d34 Binary files /dev/null and b/packages/text-area/test/visual/lumo/screenshots/text-area/baseline/min-rows.png differ diff --git a/packages/text-area/test/visual/lumo/text-area.test.js b/packages/text-area/test/visual/lumo/text-area.test.js index ab636c07619..eb331ed4040 100644 --- a/packages/text-area/test/visual/lumo/text-area.test.js +++ b/packages/text-area/test/visual/lumo/text-area.test.js @@ -118,4 +118,10 @@ describe('text-area', () => { element.appendChild(span); await visualDiff(div, 'suffix'); }); + + it('min-rows', async () => { + element.value = 'value'; + element.minRows = 4; + await visualDiff(div, 'min-rows'); + }); }); diff --git a/packages/text-area/test/visual/material/screenshots/text-area/baseline/min-rows.png b/packages/text-area/test/visual/material/screenshots/text-area/baseline/min-rows.png new file mode 100644 index 00000000000..dfe9eabd1f5 Binary files /dev/null and b/packages/text-area/test/visual/material/screenshots/text-area/baseline/min-rows.png differ diff --git a/packages/text-area/test/visual/material/text-area.test.js b/packages/text-area/test/visual/material/text-area.test.js index 10e6adfc311..35c53ce3000 100644 --- a/packages/text-area/test/visual/material/text-area.test.js +++ b/packages/text-area/test/visual/material/text-area.test.js @@ -118,4 +118,10 @@ describe('text-area', () => { element.appendChild(span); await visualDiff(div, 'suffix'); }); + + it('min-rows', async () => { + element.value = 'value'; + element.minRows = 4; + await visualDiff(div, 'min-rows'); + }); });