Skip to content

Commit 2dcf795

Browse files
committed
fix(autocomplete): incorrectly detecting shadow DOM when inserted through an embedded view
When an autocomplete is inserted, we try to figure out whether it's in the shadow DOM so that we can handle outside clicks properly. It seems like our logic can run too early in some cases, causing it to be detected incorrectly. These changes move the logic later in the process, right before the overlay is attached to the DOM. Fixes #19330.
1 parent eb218e5 commit 2dcf795

File tree

3 files changed

+68
-23
lines changed

3 files changed

+68
-23
lines changed

src/material/autocomplete/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ ng_test_library(
6868
"//src/material/core",
6969
"//src/material/form-field",
7070
"//src/material/input",
71+
"@npm//@angular/common",
7172
"@npm//@angular/forms",
7273
"@npm//@angular/platform-browser",
7374
"@npm//rxjs",

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
228228
const window = this._getWindow();
229229

230230
if (typeof window !== 'undefined') {
231-
this._zone.runOutsideAngular(() => {
232-
window.addEventListener('blur', this._windowBlurHandler);
233-
});
234-
235-
this._isInsideShadowRoot = !!_getShadowRoot(this._element.nativeElement);
231+
this._zone.runOutsideAngular(() => window.addEventListener('blur', this._windowBlurHandler));
236232
}
237233
}
238234

@@ -619,6 +615,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
619615
throw getMatAutocompleteMissingPanelError();
620616
}
621617

618+
// We want to resolve this once, as late as possible so that we can be
619+
// sure that the element has been moved into its final place in the DOM.
620+
if (this._isInsideShadowRoot == null) {
621+
this._isInsideShadowRoot = !!_getShadowRoot(this._element.nativeElement);
622+
}
623+
622624
let overlayRef = this._overlayRef;
623625

624626
if (!overlayRef) {

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ViewChild,
2525
ViewChildren,
2626
ViewEncapsulation,
27+
TemplateRef,
2728
} from '@angular/core';
2829
import {
2930
async,
@@ -39,6 +40,7 @@ import {MatOption, MatOptionSelectionChange} from '@angular/material/core';
3940
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
4041
import {By} from '@angular/platform-browser';
4142
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
43+
import {CommonModule} from '@angular/common';
4244
import {EMPTY, Observable, Subject, Subscription} from 'rxjs';
4345
import {map, startWith} from 'rxjs/operators';
4446

@@ -69,7 +71,8 @@ describe('MatAutocomplete', () => {
6971
MatInputModule,
7072
FormsModule,
7173
ReactiveFormsModule,
72-
NoopAnimationsModule
74+
NoopAnimationsModule,
75+
CommonModule
7376
],
7477
declarations: [component],
7578
providers: [
@@ -538,28 +541,55 @@ describe('MatAutocomplete', () => {
538541
}));
539542

540543
it('should not close the panel when clicking on the input inside shadow DOM', fakeAsync(() => {
541-
// This test is only relevant for Shadow DOM-capable browsers.
542-
if (!_supportsShadowDom()) {
543-
return;
544-
}
544+
// This test is only relevant for Shadow DOM-capable browsers.
545+
if (!_supportsShadowDom()) {
546+
return;
547+
}
545548

546-
const fixture = createComponent(SimpleAutocompleteShadowDom);
547-
fixture.detectChanges();
548-
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
549+
const fixture = createComponent(SimpleAutocompleteShadowDom);
550+
fixture.detectChanges();
551+
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
549552

550-
dispatchFakeEvent(input, 'focusin');
551-
fixture.detectChanges();
552-
zone.simulateZoneExit();
553+
dispatchFakeEvent(input, 'focusin');
554+
fixture.detectChanges();
555+
zone.simulateZoneExit();
553556

554-
expect(fixture.componentInstance.trigger.panelOpen)
555-
.toBe(true, 'Expected panel to be opened on focus.');
557+
expect(fixture.componentInstance.trigger.panelOpen)
558+
.toBe(true, 'Expected panel to be opened on focus.');
556559

557-
input.click();
558-
fixture.detectChanges();
560+
input.click();
561+
fixture.detectChanges();
559562

560-
expect(fixture.componentInstance.trigger.panelOpen)
561-
.toBe(true, 'Expected panel to remain opened after clicking on the input.');
562-
}));
563+
expect(fixture.componentInstance.trigger.panelOpen)
564+
.toBe(true, 'Expected panel to remain opened after clicking on the input.');
565+
}));
566+
567+
it('should not close the panel when clicking inside the shadow DOM when inserted ' +
568+
'through an embedded view', fakeAsync(() => {
569+
// This test is only relevant for Shadow DOM-capable browsers.
570+
if (!_supportsShadowDom()) {
571+
return;
572+
}
573+
574+
const fixture = createComponent(AutocompleteInShadowDomEmbeddedView);
575+
fixture.detectChanges();
576+
fixture.componentInstance.currentTemplate = fixture.componentInstance.template;
577+
fixture.detectChanges();
578+
579+
const input = fixture.nativeElement.querySelector('input');
580+
dispatchFakeEvent(input, 'focusin');
581+
fixture.detectChanges();
582+
zone.simulateZoneExit();
583+
584+
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel'))
585+
.toBeTruthy('Expected panel to be opened on focus.');
586+
587+
input.click();
588+
fixture.detectChanges();
589+
590+
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel'))
591+
.toBeTruthy('Expected panel to remain opened after clicking on the input.');
592+
}));
563593

564594
it('should have the correct text direction in RTL', () => {
565595
const rtlFixture = createComponent(SimpleAutocomplete, [
@@ -2669,6 +2699,18 @@ class SimpleAutocomplete implements OnDestroy {
26692699
class SimpleAutocompleteShadowDom extends SimpleAutocomplete {
26702700
}
26712701

2702+
@Component({
2703+
template: `
2704+
<ng-template #template>${SIMPLE_AUTOCOMPLETE_TEMPLATE}</ng-template>
2705+
<ng-container [ngTemplateOutlet]="currentTemplate"></ng-container>
2706+
`,
2707+
encapsulation: ViewEncapsulation.ShadowDom
2708+
})
2709+
class AutocompleteInShadowDomEmbeddedView extends SimpleAutocomplete {
2710+
@ViewChild('template') template: TemplateRef<any>;
2711+
currentTemplate: TemplateRef<any>;
2712+
}
2713+
26722714
@Component({
26732715
template: `
26742716
<mat-form-field *ngIf="isVisible">

0 commit comments

Comments
 (0)