From 28c1876027e3605ca0ee9887c925ec40c9ad035b Mon Sep 17 00:00:00 2001 From: originalajitest Date: Sat, 3 Jan 2026 18:02:57 -0800 Subject: [PATCH 1/3] fix(cdk/autosize): fixed auto-size line-height bug at 110% zoom --- src/cdk/text-field/autosize.spec.ts | 77 +++++++++++++++++++++++++++++ src/cdk/text-field/autosize.ts | 7 ++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/cdk/text-field/autosize.spec.ts b/src/cdk/text-field/autosize.spec.ts index 4181fdd50f55..ea29c3154a2f 100644 --- a/src/cdk/text-field/autosize.spec.ts +++ b/src/cdk/text-field/autosize.spec.ts @@ -372,6 +372,65 @@ describe('CdkTextareaAutosize', () => { expect(textarea.hasAttribute('placeholder')).toBe(false); }); + + it('should not show scrollbar with decimal line-height values at 110% zoom', () => { + // Test for issue #32178: verifies that subtraction of 3.5 (not 4) prevents scrollbars + // when line-height has decimal values and scrollHeight has fractional pixels that get truncated. + const fixture = TestBed.createComponent(AutosizeTextAreaWithDecimalLineHeight); + const textarea = fixture.nativeElement.querySelector('textarea'); + const autosize = fixture.debugElement + .query(By.css('textarea'))! + .injector.get(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 truncation: scrollHeight with measuring class would be + // actualNeededScrollHeight + 4.5px, but gets truncated to actualNeededScrollHeight + 4px + let measurementCallCount = 0; + const scrollHeightDuringMeasurement = Math.floor(actualNeededScrollHeight + 4.5); + const actualNeededWithFractional = actualNeededScrollHeight + 0.5; + + Object.defineProperty(textarea, 'scrollHeight', { + get: function () { + measurementCallCount++; + if ( + measurementCallCount <= 10 && + this.classList.contains('cdk-textarea-autosize-measuring') + ) { + return scrollHeightDuringMeasurement; + } + return Math.ceil(actualNeededWithFractional); + }, + configurable: true, + }); + + autosize.resizeToFitContent(); + fixture.detectChanges(); + + delete (textarea as any).scrollHeight; + void textarea.offsetHeight; + + const actualScrollHeight = textarea.scrollHeight; + const actualClientHeight = textarea.clientHeight; + const setHeight = parseFloat(textarea.style.height); + const heightWithSubtraction3_5 = 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 ${heightWithSubtraction3_5}px`) + .toBeGreaterThanOrEqual(heightWithSubtraction3_5); + }); }); // Styles to reset padding and border to make measurement comparisons easier. @@ -414,3 +473,21 @@ class AutosizeTextareaWithNgModel { class AutosizeTextareaWithoutAutosize { content: string = ''; } + +const textareaStyleWithDecimalLineHeight = ` + textarea { + padding: 0; + border: none; + overflow: auto; + line-height: 1.15; + }`; + +@Component({ + template: ` + `, + styles: textareaStyleWithDecimalLineHeight, + imports: [FormsModule, TextFieldModule], +}) +class AutosizeTextAreaWithDecimalLineHeight { + @ViewChild('autosize') autosize!: CdkTextareaAutosize; +} diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 81ee7726a75f..27f11e2e7551 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -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 (instead of 4) 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) { From b7b83a4801841858bb026ebcce20a34b13c18931 Mon Sep 17 00:00:00 2001 From: originalajitest Date: Sat, 3 Jan 2026 23:57:55 -0800 Subject: [PATCH 2/3] fix(cdk/autosize): minor test changes --- src/cdk/text-field/autosize.spec.ts | 38 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/cdk/text-field/autosize.spec.ts b/src/cdk/text-field/autosize.spec.ts index ea29c3154a2f..13a0dde06b8a 100644 --- a/src/cdk/text-field/autosize.spec.ts +++ b/src/cdk/text-field/autosize.spec.ts @@ -373,11 +373,11 @@ describe('CdkTextareaAutosize', () => { expect(textarea.hasAttribute('placeholder')).toBe(false); }); - it('should not show scrollbar with decimal line-height values at 110% zoom', () => { - // Test for issue #32178: verifies that subtraction of 3.5 (not 4) prevents scrollbars - // when line-height has decimal values and scrollHeight has fractional pixels that get truncated. + // 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'); + const textarea = fixture.nativeElement.querySelector('textarea') as HTMLTextAreaElement; const autosize = fixture.debugElement .query(By.css('textarea'))! .injector.get(CdkTextareaAutosize); @@ -392,19 +392,15 @@ describe('CdkTextareaAutosize', () => { fixture.detectChanges(); const actualNeededScrollHeight = textarea.scrollHeight; - // Simulate fractional pixel truncation: scrollHeight with measuring class would be - // actualNeededScrollHeight + 4.5px, but gets truncated to actualNeededScrollHeight + 4px - let measurementCallCount = 0; + // 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); const actualNeededWithFractional = actualNeededScrollHeight + 0.5; Object.defineProperty(textarea, 'scrollHeight', { get: function () { - measurementCallCount++; - if ( - measurementCallCount <= 10 && - this.classList.contains('cdk-textarea-autosize-measuring') - ) { + if (this.classList.contains('cdk-textarea-autosize-measuring')) { return scrollHeightDuringMeasurement; } return Math.ceil(actualNeededWithFractional); @@ -412,24 +408,26 @@ describe('CdkTextareaAutosize', () => { configurable: true, }); - autosize.resizeToFitContent(); - fixture.detectChanges(); - - delete (textarea as any).scrollHeight; - void textarea.offsetHeight; + 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 heightWithSubtraction3_5 = scrollHeightDuringMeasurement - 3.5; + 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 ${heightWithSubtraction3_5}px`) - .toBeGreaterThanOrEqual(heightWithSubtraction3_5); + .withContext(`Set height (${setHeight}px) should be at least ${heightWithBuffer}px`) + .toBeGreaterThanOrEqual(heightWithBuffer); }); }); From 239af9d3e458abc8c3afc8f14c8240ba984df860 Mon Sep 17 00:00:00 2001 From: originalajitest <125171962+originalajitest@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:57:17 -0800 Subject: [PATCH 3/3] resolving comments --- src/cdk/text-field/autosize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 27f11e2e7551..e6fdda57b4db 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -253,8 +253,8 @@ 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. We subtract 3.5 (instead of 4) to account for - // fractional pixel truncation that can occur with decimal line-height values. When the browser + // 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;