Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions src/cdk/text-field/autosize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,63 @@ describe('CdkTextareaAutosize', () => {

expect(textarea.hasAttribute('placeholder')).toBe(false);
});

// Regression for #32178. Issue mentions zoom, but we can't change browser zoom here;
// instead we simulate the kind of rounding/truncation that can occur in that environment.
it('should not show a scrollbar when line-height causes sub-pixel rounding', () => {
const fixture = TestBed.createComponent(AutosizeTextAreaWithDecimalLineHeight);
const textarea = fixture.nativeElement.querySelector('textarea') as HTMLTextAreaElement;
const autosize = fixture.debugElement
.query(By.css('textarea'))!
.injector.get<CdkTextareaAutosize>(CdkTextareaAutosize);

fixture.detectChanges();
textarea.style.width = '400px';
textarea.style.fontSize = '14px';

const longLine = 'a'.repeat(80);
textarea.value = `${longLine}\n${longLine}\n${longLine}a`;

fixture.detectChanges();
const actualNeededScrollHeight = textarea.scrollHeight;

// Simulate fractional pixel rounding/truncation:
// - During measurement, the measuring class adds 4px padding; if we had an extra 0.5px in
// layout but it gets truncated, the value we read can be 1px smaller than the "real" need.
const scrollHeightDuringMeasurement = Math.floor(actualNeededScrollHeight + 4.5);
Comment on lines +396 to +398
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the 0.5 px coming from the layout ? Is this just simulating the extra 110% browser zoom? And it's just an example right?

And I am also not sure if I understand that it can be 1px smaller than the "real" need. Isn't it 0.5px smaller? Or maybe you mean it can be UP to 1px smaller than the real need - since it rounds down.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure either 😭. I asked cursor to make a regression test for it (because of the contributing guidelines) and this is what it came up for it after a bunch of attempts. I do know that this solution works, why it works and I can provide screenshot proof and mathematical proof why it works.
The addition of 0.5 does not really add 0.5, it is 0.5 to be more palatable, internally, the 0.5 is rounded up to get us an extra pixel of scroll height.

const actualNeededWithFractional = actualNeededScrollHeight + 0.5;

Object.defineProperty(textarea, 'scrollHeight', {
get: function () {
if (this.classList.contains('cdk-textarea-autosize-measuring')) {
return scrollHeightDuringMeasurement;
}
return Math.ceil(actualNeededWithFractional);
},
configurable: true,
});

try {
autosize.resizeToFitContent();
fixture.detectChanges();
} finally {
// Ensure we always restore the native property, even if the expectations fail.
delete (textarea as any).scrollHeight;
}

const actualScrollHeight = textarea.scrollHeight;
const actualClientHeight = textarea.clientHeight;
const setHeight = parseFloat(textarea.style.height);
const heightWithBuffer = scrollHeightDuringMeasurement - 3.5;

expect(actualClientHeight)
.withContext(`Expected no scrollbar with decimal line-height`)
.toBe(actualScrollHeight);

expect(setHeight)
.withContext(`Set height (${setHeight}px) should be at least ${heightWithBuffer}px`)
.toBeGreaterThanOrEqual(heightWithBuffer);
});
});

// Styles to reset padding and border to make measurement comparisons easier.
Expand Down Expand Up @@ -414,3 +471,21 @@ class AutosizeTextareaWithNgModel {
class AutosizeTextareaWithoutAutosize {
content: string = '';
}

const textareaStyleWithDecimalLineHeight = `
textarea {
padding: 0;
border: none;
overflow: auto;
line-height: 1.15;
}`;

@Component({
template: `
<textarea cdkTextareaAutosize #autosize="cdkTextareaAutosize"></textarea>`,
styles: textareaStyleWithDecimalLineHeight,
imports: [FormsModule, TextFieldModule],
})
class AutosizeTextAreaWithDecimalLineHeight {
@ViewChild('autosize') autosize!: CdkTextareaAutosize;
}
7 changes: 5 additions & 2 deletions src/cdk/text-field/autosize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,11 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
// Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
element.classList.add(measuringClass);
// The measuring class includes a 2px padding to workaround an issue with Chrome,
// so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
const scrollHeight = element.scrollHeight - 4;
// so we account for that extra space here. We subtract 3.5 (2px top + 2px bottom - 0.5px) to account
// for fractional pixel truncation that can occur with decimal line-height values. When the browser
// truncates fractional pixels (e.g., 20.4px becomes 20px), subtracting 3.5 instead of 4
// provides a small margin that prevents scrollbars from appearing. See issue #32178.
const scrollHeight = element.scrollHeight - 3.5;
element.classList.remove(measuringClass);

if (needsMarginFiller) {
Expand Down