diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 173023a9bea4..6cdb0432570e 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -529,7 +529,7 @@ export class MatFormField extends _MatFormFieldMixinBase } // If the element is not present in the DOM, the outline gap will need to be calculated // the next time it is checked and in the DOM. - if (!document.documentElement!.contains(this._elementRef.nativeElement)) { + if (!this._isAttachedToDOM()) { this._outlineGapCalculationNeededImmediately = true; return; } @@ -582,4 +582,20 @@ export class MatFormField extends _MatFormFieldMixinBase private _getStartEnd(rect: ClientRect): number { return this._previousDirection === 'rtl' ? rect.right : rect.left; } + + /** Checks whether the form field is attached to the DOM. */ + private _isAttachedToDOM(): boolean { + const element: HTMLElement = this._elementRef.nativeElement; + + if (element.getRootNode) { + const rootNode = element.getRootNode(); + // If the element is inside the DOM the root node will be either the document + // or the closest shadow root, otherwise it'll be the element itself. + return rootNode && rootNode !== element; + } + + // Otherwise fall back to checking if it's in the document. This doesn't account for + // shadow DOM, however browser that support shadow DOM should support `getRootNode` as well. + return document.documentElement!.contains(element); + } } diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 69a7d5948ed4..72772dc18253 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -1,4 +1,4 @@ -import {Platform, PlatformModule} from '@angular/cdk/platform'; +import {Platform, PlatformModule, _supportsShadowDom} from '@angular/cdk/platform'; import {wrappedErrorMessage, MockNgZone} from '@angular/cdk/private/testing'; import { createFakeEvent, @@ -12,6 +12,8 @@ import { Provider, NgZone, Directive, + ViewEncapsulation, + ElementRef, } from '@angular/core'; import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; import { @@ -1503,7 +1505,26 @@ describe('MatInput with appearance', () => { })); + it('should calculate the outline gaps inside the shadow DOM', fakeAsync(() => { + if (!_supportsShadowDom()) { + return; + } + fixture.destroy(); + TestBed.resetTestingModule(); + + const outlineFixture = createComponent(MatInputWithOutlineAppearanceInShadowDOM); + outlineFixture.detectChanges(); + flush(); + outlineFixture.detectChanges(); + + const formField = outlineFixture.componentInstance.formField.nativeElement; + const outlineStart = formField.querySelector('.mat-form-field-outline-start') as HTMLElement; + const outlineGap = formField.querySelector('.mat-form-field-outline-gap') as HTMLElement; + + expect(parseInt(outlineStart.style.width || '0')).toBeGreaterThan(0); + expect(parseInt(outlineGap.style.width || '0')).toBeGreaterThan(0); + })); }); @@ -2010,6 +2031,19 @@ class MatInputWithoutPlaceholder { }) class MatInputWithOutlineInsideInvisibleElement {} +@Component({ + template: ` + + Hello + + + `, + encapsulation: ViewEncapsulation.ShadowDom +}) +class MatInputWithOutlineAppearanceInShadowDOM { + @ViewChild('formField', {read: ElementRef, static: false}) formField: ElementRef; +} + // Styles to reset padding and border to make measurement comparisons easier. const textareaStyleReset = `