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 59eb31e748..7b3dcdeafe 100644 --- a/packages/text-area/src/vaadin-text-area-mixin.d.ts +++ b/packages/text-area/src/vaadin-text-area-mixin.d.ts @@ -71,6 +71,14 @@ export declare class TextAreaMixinClass { */ minRows: number; + /** + * Maximum number of rows to expand to before the text area starts scrolling. This effectively sets a max-height + * on the `input-field` part. By default, it is not set, and the text area grows with the content without + * constraints. + * @attr {number} max-rows + */ + maxRows: number | null | undefined; + /** * 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 0f6cfde835..9aa77c1b80 100644 --- a/packages/text-area/src/vaadin-text-area-mixin.js +++ b/packages/text-area/src/vaadin-text-area-mixin.js @@ -53,6 +53,16 @@ export const TextAreaMixin = (superClass) => value: 2, observer: '__minRowsChanged', }, + + /** + * Maximum number of rows to expand to before the text area starts scrolling. This effectively sets a max-height + * on the `input-field` part. By default, it is not set, and the text area grows with the content without + * constraints. + * @attr {number} max-rows + */ + maxRows: { + type: Number, + }, }; } @@ -65,7 +75,7 @@ export const TextAreaMixin = (superClass) => } static get observers() { - return ['__updateMinHeight(minRows, inputElement)']; + return ['__updateMinHeight(minRows, inputElement)', '__updateMaxHeight(maxRows, inputElement, _inputField)']; } /** @@ -192,6 +202,10 @@ export const TextAreaMixin = (superClass) => inputField.style.removeProperty('display'); inputField.style.removeProperty('height'); inputField.scrollTop = scrollTop; + + // Update max height in case this update was triggered by style changes + // affecting line height, paddings or margins. + this.__updateMaxHeight(this.maxRows); } /** @private */ @@ -209,6 +223,36 @@ export const TextAreaMixin = (superClass) => } } + /** @private */ + __updateMaxHeight(maxRows) { + if (!this._inputField || !this.inputElement) { + return; + } + + if (maxRows) { + // For maximum height, we need to constrain the height of the input + // container to prevent it from growing further. For this we take the + // line height of the native textarea times the number of rows, and add + // other properties affecting the height of the input container. + const inputStyle = getComputedStyle(this.inputElement); + const inputFieldStyle = getComputedStyle(this._inputField); + + const lineHeight = parseFloat(inputStyle.lineHeight); + const contentHeight = lineHeight * maxRows; + const marginsAndPaddings = + parseFloat(inputStyle.paddingTop) + + parseFloat(inputStyle.paddingBottom) + + parseFloat(inputStyle.marginTop) + + parseFloat(inputStyle.marginBottom) + + parseFloat(inputFieldStyle.paddingTop) + + parseFloat(inputFieldStyle.paddingBottom); + const maxHeight = Math.ceil(contentHeight + marginsAndPaddings); + this._inputField.style.setProperty('max-height', `${maxHeight}px`); + } else { + this._inputField.style.removeProperty('max-height'); + } + } + /** * @private */ diff --git a/packages/text-area/test/text-area.common.js b/packages/text-area/test/text-area.common.js index 1b75466302..403916806b 100644 --- a/packages/text-area/test/text-area.common.js +++ b/packages/text-area/test/text-area.common.js @@ -358,23 +358,28 @@ describe('text-area', () => { ); }); - describe('min rows', () => { - const lineHeight = 20; + describe('min / max rows', () => { + let lineHeight; let consoleWarn; beforeEach(async () => { + lineHeight = 20; const fixture = fixtureSync(`
`); textArea = fixture.querySelector('vaadin-text-area'); await nextUpdate(textArea); + native = textArea.querySelector('textarea'); consoleWarn = sinon.stub(console, 'warn'); }); @@ -443,6 +448,69 @@ describe('text-area', () => { expect(textArea.clientHeight).to.be.above(80); }); + + it('should use max-height based on maximum rows', async () => { + textArea.maxRows = 4; + textArea.value = Array(400).join('400'); + await nextUpdate(textArea); + + expect(textArea.clientHeight).to.equal(lineHeight * 4); + }); + + it('should include margins and paddings when calculating max-height', async () => { + const native = textArea.querySelector('textarea'); + const inputContainer = textArea.shadowRoot.querySelector('[part="input-field"]'); + native.style.paddingTop = '5px'; + native.style.paddingBottom = '10px'; + native.style.marginTop = '15px'; + native.style.marginBottom = '20px'; + inputContainer.style.paddingTop = '25px'; + inputContainer.style.paddingBottom = '30px'; + + textArea.maxRows = 4; + textArea.value = Array(400).join('400'); + await nextUpdate(textArea); + + expect(textArea.clientHeight).to.equal(lineHeight * 4 + 5 + 10 + 15 + 20 + 25 + 30); + }); + + it('should shrink below max-height defined by maximum rows', async () => { + textArea.maxRows = 4; + textArea.value = 'value'; + await nextUpdate(textArea); + + expect(textArea.clientHeight).to.be.below(lineHeight * 4); + }); + + it('should update max-height when component is resized', async () => { + textArea.maxRows = 4; + textArea.value = Array(400).join('400'); + await nextUpdate(textArea); + + // Change the line height to observe a max-height change + lineHeight = 30; + native.style.setProperty('line-height', `${lineHeight}px`); + + // Trigger a resize event + textArea._onResize(); + + expect(textArea.clientHeight).to.equal(lineHeight * 4); + }); + + it('should update max-height when value changes', async () => { + textArea.maxRows = 4; + textArea.value = Array(400).join('400'); + await nextUpdate(textArea); + + // Change the line height to observe a max-height change + lineHeight = 30; + native.style.setProperty('line-height', `${lineHeight}px`); + + // Trigger a value change + textArea.value += 'change'; + + expect(textArea.clientHeight).to.equal(lineHeight * 4); + }); }); describe('--_text-area-vertical-scroll-position CSS variable', () => { diff --git a/packages/text-area/test/visual/lumo/screenshots/text-area/baseline/max-rows.png b/packages/text-area/test/visual/lumo/screenshots/text-area/baseline/max-rows.png new file mode 100644 index 0000000000..0b7fac59d0 Binary files /dev/null and b/packages/text-area/test/visual/lumo/screenshots/text-area/baseline/max-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 eb331ed404..cc09d18af4 100644 --- a/packages/text-area/test/visual/lumo/text-area.test.js +++ b/packages/text-area/test/visual/lumo/text-area.test.js @@ -124,4 +124,10 @@ describe('text-area', () => { element.minRows = 4; await visualDiff(div, 'min-rows'); }); + + it('max-rows', async () => { + element.value = Array(10).join('value\n'); + element.maxRows = 4; + await visualDiff(div, 'max-rows'); + }); }); diff --git a/packages/text-area/test/visual/material/screenshots/text-area/baseline/max-rows.png b/packages/text-area/test/visual/material/screenshots/text-area/baseline/max-rows.png new file mode 100644 index 0000000000..5615b068cb Binary files /dev/null and b/packages/text-area/test/visual/material/screenshots/text-area/baseline/max-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 35c53ce300..6993018577 100644 --- a/packages/text-area/test/visual/material/text-area.test.js +++ b/packages/text-area/test/visual/material/text-area.test.js @@ -124,4 +124,10 @@ describe('text-area', () => { element.minRows = 4; await visualDiff(div, 'min-rows'); }); + + it('max-rows', async () => { + element.value = Array(10).join('value\n'); + element.maxRows = 4; + await visualDiff(div, 'max-rows'); + }); });