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
52 changes: 52 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,39 @@ describe('CdkTextareaAutosize', () => {

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

// issue ticket: #32192
it('should correctly calculate height when textarea has padding and border-box sizing', fakeAsync(() => {
const fixture = TestBed.createComponent(AutosizeTextareaWithWidthSensitiveStyling);
const textarea = fixture.nativeElement.querySelector('textarea') as HTMLTextAreaElement;
const autosize = fixture.debugElement
.query(By.css('textarea'))!
.injector.get<CdkTextareaAutosize>(CdkTextareaAutosize);

fixture.detectChanges();
flush();

// Use many short words so wrapping is highly sensitive to the available content width.
// The width-sensitive styles ensure that switching `box-sizing`/padding during measurement
// would change wrapping, which would cause the textarea to underestimate its height.
textarea.value = Array(600).fill('word').join(' ');

fixture.detectChanges();
autosize.resizeToFitContent(true);
flush();
fixture.detectChanges();

const measuredHeight = textarea.clientHeight;
const requiredHeight = textarea.scrollHeight;
const heightDiff = requiredHeight - measuredHeight;

expect(heightDiff)
.withContext(
`Height should match scrollHeight when measuring does not change wrapping. ` +
`Required: ${requiredHeight}px, Measured: ${measuredHeight}px, Diff: ${heightDiff}px`,
)
.toBeLessThanOrEqual(5);
}));
});

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

@Component({
template: `<textarea cdkTextareaAutosize class="width-sensitive"></textarea>`,
styles: [
textareaStyleReset,
`
textarea.width-sensitive {
width: 120px;
box-sizing: border-box;
margin: 0;
padding: 0 24px;
word-wrap: break-word;
white-space: pre-wrap;
}
`,
],
imports: [FormsModule, TextFieldModule],
})
class AutosizeTextareaWithWidthSensitiveStyling {}
31 changes: 31 additions & 0 deletions src/cdk/text-field/autosize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {

/** Used to reference correct document/window */
protected _document = inject(DOCUMENT);
/** Cached reference to the current window (can be `null` in non-browser contexts). */
private _window = this._document.defaultView;

private _hasFocus = false;

Expand Down Expand Up @@ -242,6 +244,23 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
? 'cdk-textarea-autosize-measuring-firefox'
: 'cdk-textarea-autosize-measuring';

const previousWidth = element.style.width;
let contentWidth: number | null = null;

// Capture the *content* width (excluding horizontal padding) before we add the measuring class,
// because that class changes padding and box-sizing which in turn changes how text wraps and
// therefore the scrollHeight. (Issue: #32192.)
const computedStyle = this._window ? this._window.getComputedStyle(element) : null;

if (computedStyle) {
const paddingLeft = parseFloat(computedStyle.paddingLeft || '0') || 0;
const paddingRight = parseFloat(computedStyle.paddingRight || '0') || 0;
contentWidth = element.clientWidth - paddingLeft - paddingRight;
if (contentWidth <= 0) {
contentWidth = null;
}
}

// In some cases the page might move around while we're measuring the `textarea` on Firefox. We
// work around it by assigning a temporary margin with the same height as the `textarea` so that
// it occupies the same amount of space. See #23233.
Expand All @@ -252,11 +271,23 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
// Reset the textarea height to auto in order to shrink back to its default size.
// Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
element.classList.add(measuringClass);

// When measuring, CSS applies `box-sizing: content-box` and strips horizontal padding, which
// effectively increases the available text width. To keep wrapping identical to the rendered
// textarea, lock the measuring width to the original content width we captured above.
if (contentWidth !== null) {
element.style.width = `${contentWidth}px`;
}

// 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;
element.classList.remove(measuringClass);

if (contentWidth !== null) {
element.style.width = previousWidth;
}

if (needsMarginFiller) {
element.style.marginBottom = previousMargin;
}
Expand Down
Loading