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');
+ });
});