Skip to content

Commit

Permalink
fix(autocomplete): incorrectly detecting shadow DOM when inserted thr…
Browse files Browse the repository at this point in the history
…ough 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.
  • Loading branch information
crisbeto committed May 12, 2020
1 parent eb218e5 commit 2dcf795
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/material/autocomplete/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ ng_test_library(
"//src/material/core",
"//src/material/form-field",
"//src/material/input",
"@npm//@angular/common",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
"@npm//rxjs",
Expand Down
12 changes: 7 additions & 5 deletions src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
const window = this._getWindow();

if (typeof window !== 'undefined') {
this._zone.runOutsideAngular(() => {
window.addEventListener('blur', this._windowBlurHandler);
});

this._isInsideShadowRoot = !!_getShadowRoot(this._element.nativeElement);
this._zone.runOutsideAngular(() => window.addEventListener('blur', this._windowBlurHandler));
}
}

Expand Down Expand Up @@ -619,6 +615,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
throw getMatAutocompleteMissingPanelError();
}

// We want to resolve this once, as late as possible so that we can be
// sure that the element has been moved into its final place in the DOM.
if (this._isInsideShadowRoot == null) {
this._isInsideShadowRoot = !!_getShadowRoot(this._element.nativeElement);
}

let overlayRef = this._overlayRef;

if (!overlayRef) {
Expand Down
78 changes: 60 additions & 18 deletions src/material/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ViewChild,
ViewChildren,
ViewEncapsulation,
TemplateRef,
} from '@angular/core';
import {
async,
Expand All @@ -39,6 +40,7 @@ import {MatOption, MatOptionSelectionChange} from '@angular/material/core';
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {CommonModule} from '@angular/common';
import {EMPTY, Observable, Subject, Subscription} from 'rxjs';
import {map, startWith} from 'rxjs/operators';

Expand Down Expand Up @@ -69,7 +71,8 @@ describe('MatAutocomplete', () => {
MatInputModule,
FormsModule,
ReactiveFormsModule,
NoopAnimationsModule
NoopAnimationsModule,
CommonModule
],
declarations: [component],
providers: [
Expand Down Expand Up @@ -538,28 +541,55 @@ describe('MatAutocomplete', () => {
}));

it('should not close the panel when clicking on the input inside shadow DOM', fakeAsync(() => {
// This test is only relevant for Shadow DOM-capable browsers.
if (!_supportsShadowDom()) {
return;
}
// This test is only relevant for Shadow DOM-capable browsers.
if (!_supportsShadowDom()) {
return;
}

const fixture = createComponent(SimpleAutocompleteShadowDom);
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
const fixture = createComponent(SimpleAutocompleteShadowDom);
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;

dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
zone.simulateZoneExit();
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
zone.simulateZoneExit();

expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, 'Expected panel to be opened on focus.');
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, 'Expected panel to be opened on focus.');

input.click();
fixture.detectChanges();
input.click();
fixture.detectChanges();

expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, 'Expected panel to remain opened after clicking on the input.');
}));
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, 'Expected panel to remain opened after clicking on the input.');
}));

it('should not close the panel when clicking inside the shadow DOM when inserted ' +
'through an embedded view', fakeAsync(() => {
// This test is only relevant for Shadow DOM-capable browsers.
if (!_supportsShadowDom()) {
return;
}

const fixture = createComponent(AutocompleteInShadowDomEmbeddedView);
fixture.detectChanges();
fixture.componentInstance.currentTemplate = fixture.componentInstance.template;
fixture.detectChanges();

const input = fixture.nativeElement.querySelector('input');
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
zone.simulateZoneExit();

expect(overlayContainerElement.querySelector('.mat-autocomplete-panel'))
.toBeTruthy('Expected panel to be opened on focus.');

input.click();
fixture.detectChanges();

expect(overlayContainerElement.querySelector('.mat-autocomplete-panel'))
.toBeTruthy('Expected panel to remain opened after clicking on the input.');
}));

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

@Component({
template: `
<ng-template #template>${SIMPLE_AUTOCOMPLETE_TEMPLATE}</ng-template>
<ng-container [ngTemplateOutlet]="currentTemplate"></ng-container>
`,
encapsulation: ViewEncapsulation.ShadowDom
})
class AutocompleteInShadowDomEmbeddedView extends SimpleAutocomplete {
@ViewChild('template') template: TemplateRef<any>;
currentTemplate: TemplateRef<any>;
}

@Component({
template: `
<mat-form-field *ngIf="isVisible">
Expand Down

0 comments on commit 2dcf795

Please sign in to comment.