Skip to content

Commit

Permalink
feat: configure maximum number of rows in text area (#8143)
Browse files Browse the repository at this point in the history
* feat: configure maximum number of rows in text area

* recalculate max-height on resize and value change

* avoid rounding individual height components
  • Loading branch information
sissbruecker authored Nov 18, 2024
1 parent 6ef77b9 commit f3f7ed5
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 3 deletions.
8 changes: 8 additions & 0 deletions packages/text-area/src/vaadin-text-area-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
46 changes: 45 additions & 1 deletion packages/text-area/src/vaadin-text-area-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
}

Expand All @@ -65,7 +75,7 @@ export const TextAreaMixin = (superClass) =>
}

static get observers() {
return ['__updateMinHeight(minRows, inputElement)'];
return ['__updateMinHeight(minRows, inputElement)', '__updateMaxHeight(maxRows, inputElement, _inputField)'];
}

/**
Expand Down Expand Up @@ -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 */
Expand All @@ -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
*/
Expand Down
72 changes: 70 additions & 2 deletions packages/text-area/test/text-area.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
<div>
<style>
vaadin-text-area textarea {
line-height: ${lineHeight}px;
}
vaadin-text-area::part(input-field) {
box-sizing: border-box;
}
</style>
<vaadin-text-area></vaadin-text-area>
</div>
`);
textArea = fixture.querySelector('vaadin-text-area');
await nextUpdate(textArea);
native = textArea.querySelector('textarea');

consoleWarn = sinon.stub(console, 'warn');
});
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/text-area/test/visual/lumo/text-area.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/text-area/test/visual/material/text-area.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

0 comments on commit f3f7ed5

Please sign in to comment.