Skip to content

Commit 0b72e73

Browse files
committed
refactor(platform): consolidate and expose shadow dom detection utilities
Over time we had accumulated the same utility for finding the shadow root of an element in multiple places. These changes move it into a common place under `cdk/platform`. I also decided to remove the underscore from `_supportsShadowDom` so it's officially a public API. It was already exposed under `cdk/platform` so there was nothing stopping people from using it, but the underscore looked weird. The API should be stable enough, since we've had it for more than a year in multiple components and we haven't had issues with it.
1 parent 3d35180 commit 0b72e73

File tree

10 files changed

+38
-66
lines changed

10 files changed

+38
-66
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
2525
import {DOCUMENT} from '@angular/common';
2626
import {ViewportRuler, ScrollingModule} from '@angular/cdk/scrolling';
27-
import {_supportsShadowDom} from '@angular/cdk/platform';
27+
import {supportsShadowDom} from '@angular/cdk/platform';
2828
import {of as observableOf} from 'rxjs';
2929

3030
import {DragDropModule} from '../drag-drop-module';
@@ -4578,7 +4578,7 @@ describe('CdkDrag', () => {
45784578

45794579
it('should be able to drop into a new container inside the Shadow DOM', fakeAsync(() => {
45804580
// This test is only relevant for Shadow DOM-supporting browsers.
4581-
if (!_supportsShadowDom()) {
4581+
if (!supportsShadowDom()) {
45824582
return;
45834583
}
45844584

@@ -4612,7 +4612,7 @@ describe('CdkDrag', () => {
46124612
it('should be able to drop into a new container inside the Shadow DOM and ngIf',
46134613
fakeAsync(() => {
46144614
// This test is only relevant for Shadow DOM-supporting browsers.
4615-
if (!_supportsShadowDom()) {
4615+
if (!supportsShadowDom()) {
46164616
return;
46174617
}
46184618

src/cdk/drag-drop/drop-list-ref.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {ElementRef, NgZone} from '@angular/core';
1010
import {Direction} from '@angular/cdk/bidi';
1111
import {coerceElement} from '@angular/cdk/coercion';
1212
import {ViewportRuler} from '@angular/cdk/scrolling';
13-
import {_supportsShadowDom} from '@angular/cdk/platform';
13+
import {supportsShadowDom, getShadowRoot} from '@angular/cdk/platform';
1414
import {Subject, Subscription, interval, animationFrameScheduler} from 'rxjs';
1515
import {takeUntil} from 'rxjs/operators';
1616
import {moveItemInArray} from './drag-utils';
@@ -907,7 +907,8 @@ export class DropListRef<T = any> {
907907
*/
908908
private _getShadowRoot(): DocumentOrShadowRoot {
909909
if (!this._cachedShadowRoot) {
910-
this._cachedShadowRoot = getShadowRoot(coerceElement(this.element)) || this._document;
910+
const shadowRoot = getShadowRoot(coerceElement(this.element)) as ShadowRoot | null;
911+
this._cachedShadowRoot = shadowRoot || this._document;
911912
}
912913

913914
return this._cachedShadowRoot;
@@ -1102,16 +1103,3 @@ function getElementScrollDirections(element: HTMLElement, clientRect: ClientRect
11021103

11031104
return [verticalScrollDirection, horizontalScrollDirection];
11041105
}
1105-
1106-
/** Gets the shadow root of an element, if any. */
1107-
function getShadowRoot(element: HTMLElement): DocumentOrShadowRoot | null {
1108-
if (_supportsShadowDom()) {
1109-
const rootNode = element.getRootNode ? element.getRootNode() : null;
1110-
1111-
if (rootNode instanceof ShadowRoot) {
1112-
return rootNode;
1113-
}
1114-
}
1115-
1116-
return null;
1117-
}

src/cdk/platform/features/shadow-dom.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,24 @@
99
let shadowDomIsSupported: boolean;
1010

1111
/** Checks whether the user's browser support Shadow DOM. */
12-
export function _supportsShadowDom(): boolean {
12+
export function supportsShadowDom(): boolean {
1313
if (shadowDomIsSupported == null) {
1414
const head = typeof document !== 'undefined' ? document.head : null;
1515
shadowDomIsSupported = !!(head && ((head as any).createShadowRoot || head.attachShadow));
1616
}
1717

1818
return shadowDomIsSupported;
1919
}
20+
21+
/** Gets the shadow root of an element, if supported and the element is inside the Shadow DOM. */
22+
export function getShadowRoot(element: HTMLElement): Node | null {
23+
if (supportsShadowDom()) {
24+
const rootNode = element.getRootNode ? element.getRootNode() : null;
25+
26+
if (rootNode instanceof ShadowRoot) {
27+
return rootNode;
28+
}
29+
}
30+
31+
return null;
32+
}

src/dev-app/focus-trap/focus-trap-demo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
QueryList,
1818
} from '@angular/core';
1919
import {MatDialog} from '@angular/material/dialog';
20-
import {_supportsShadowDom} from '@angular/cdk/platform';
20+
import {supportsShadowDom} from '@angular/cdk/platform';
2121

2222
@Component({
2323
selector: 'shadow-dom-demo',
@@ -39,7 +39,7 @@ export class FocusTrapDemo implements AfterViewInit {
3939
@ViewChildren(CdkTrapFocus)
4040
private _focusTraps: QueryList<CdkTrapFocus>;
4141

42-
_supportsShadowDom = _supportsShadowDom();
42+
_supportsShadowDom = supportsShadowDom();
4343

4444
constructor(public dialog: MatDialog) {}
4545

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ScrollStrategy,
1818
ConnectedPosition,
1919
} from '@angular/cdk/overlay';
20-
import {_supportsShadowDom} from '@angular/cdk/platform';
20+
import {getShadowRoot} from '@angular/cdk/platform';
2121
import {TemplatePortal} from '@angular/cdk/portal';
2222
import {ViewportRuler} from '@angular/cdk/scrolling';
2323
import {DOCUMENT} from '@angular/common';
@@ -230,14 +230,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
230230
window.addEventListener('blur', this._windowBlurHandler);
231231
});
232232

233-
if (_supportsShadowDom()) {
234-
const element = this._element.nativeElement;
235-
const rootNode = element.getRootNode ? element.getRootNode() : null;
236-
237-
// We need to take the `ShadowRoot` off of `window`, because the built-in types are
238-
// incorrect. See https://github.com/Microsoft/TypeScript/issues/27929.
239-
this._isInsideShadowRoot = rootNode instanceof (window as any).ShadowRoot;
240-
}
233+
this._isInsideShadowRoot = !!getShadowRoot(this._element.nativeElement);
241234
}
242235
}
243236

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Directionality} from '@angular/cdk/bidi';
22
import {DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
33
import {Overlay, OverlayContainer} from '@angular/cdk/overlay';
4-
import {_supportsShadowDom} from '@angular/cdk/platform';
4+
import {supportsShadowDom} from '@angular/cdk/platform';
55
import {ScrollDispatcher} from '@angular/cdk/scrolling';
66
import {
77
MockNgZone,
@@ -539,7 +539,7 @@ describe('MatAutocomplete', () => {
539539

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

src/material/input/input.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Platform, PlatformModule, _supportsShadowDom} from '@angular/cdk/platform';
1+
import {Platform, PlatformModule, supportsShadowDom} from '@angular/cdk/platform';
22
import {
33
createFakeEvent,
44
dispatchFakeEvent,
@@ -1519,7 +1519,7 @@ describe('MatInput with appearance', () => {
15191519

15201520

15211521
it('should calculate the outline gaps inside the shadow DOM', fakeAsync(() => {
1522-
if (!_supportsShadowDom()) {
1522+
if (!supportsShadowDom()) {
15231523
return;
15241524
}
15251525

src/material/progress-spinner/progress-spinner.spec.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {TestBed, async, inject} from '@angular/core/testing';
22
import {Component, ViewEncapsulation, ViewChild, ElementRef} from '@angular/core';
33
import {By} from '@angular/platform-browser';
4-
import {Platform} from '@angular/cdk/platform';
4+
import {Platform, getShadowRoot} from '@angular/cdk/platform';
55
import {CommonModule} from '@angular/common';
6-
import {_getShadowRoot} from './progress-spinner';
76
import {
87
MatProgressSpinnerModule,
98
MatProgressSpinner,
@@ -366,7 +365,7 @@ describe('MatProgressSpinner', () => {
366365
fixture.detectChanges();
367366

368367
const spinner = fixture.debugElement.query(By.css('mat-progress-spinner'))!.nativeElement;
369-
const shadowRoot = _getShadowRoot(spinner, document) as HTMLElement;
368+
const shadowRoot = getShadowRoot(spinner) as HTMLElement;
370369

371370
expect(shadowRoot.querySelector('style[mat-spinner-animation="27"]')).toBeTruthy();
372371

@@ -387,7 +386,7 @@ describe('MatProgressSpinner', () => {
387386
fixture.detectChanges();
388387

389388
const spinner = fixture.debugElement.query(By.css('mat-progress-spinner'))!.nativeElement;
390-
const shadowRoot = _getShadowRoot(spinner, document) as HTMLElement;
389+
const shadowRoot = getShadowRoot(spinner) as HTMLElement;
391390

392391
expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="39"]').length).toBe(1);
393392

@@ -418,7 +417,7 @@ describe('MatProgressSpinner', () => {
418417
fixture.detectChanges();
419418

420419
const spinner = fixture.componentInstance.spinner.nativeElement;
421-
const shadowRoot = _getShadowRoot(spinner, document) as HTMLElement;
420+
const shadowRoot = getShadowRoot(spinner) as HTMLElement;
422421

423422
expect(shadowRoot.querySelector('style[mat-spinner-animation="27"]')).toBeTruthy();
424423

src/material/progress-spinner/progress-spinner.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
10-
import {Platform} from '@angular/cdk/platform';
10+
import {Platform, getShadowRoot} from '@angular/cdk/platform';
1111
import {DOCUMENT} from '@angular/common';
1212
import {
1313
ChangeDetectionStrategy,
@@ -218,7 +218,7 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
218218
// Note that we need to look up the root node in ngOnInit, rather than the constructor, because
219219
// Angular seems to create the element outside the shadow root and then moves it inside, if the
220220
// node is inside an `ngIf` and a ShadowDom-encapsulated component.
221-
this._styleRoot = _getShadowRoot(element, this._document) || this._document.head;
221+
this._styleRoot = getShadowRoot(element) || this._document.head;
222222
this._attachStyleNode();
223223

224224
// On IE and Edge, we can't animate the `stroke-dashoffset`
@@ -333,26 +333,3 @@ export class MatSpinner extends MatProgressSpinner {
333333
this.mode = 'indeterminate';
334334
}
335335
}
336-
337-
338-
/** Gets the shadow root of an element, if supported and the element is inside the Shadow DOM. */
339-
export function _getShadowRoot(element: HTMLElement, _document: Document): Node | null {
340-
// TODO(crisbeto): see whether we should move this into the CDK
341-
// feature detection utilities once #15616 gets merged in.
342-
if (typeof window !== 'undefined') {
343-
const head = _document.head;
344-
345-
// Check whether the browser supports Shadow DOM.
346-
if (head && ((head as any).createShadowRoot || head.attachShadow)) {
347-
const rootNode = element.getRootNode ? element.getRootNode() : null;
348-
349-
// We need to take the `ShadowRoot` off of `window`, because the built-in types are
350-
// incorrect. See https://github.com/Microsoft/TypeScript/issues/27929.
351-
if (rootNode instanceof (window as any).ShadowRoot) {
352-
return rootNode;
353-
}
354-
}
355-
}
356-
357-
return null;
358-
}

tools/public_api_guard/cdk/platform.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
export declare function _supportsShadowDom(): boolean;
2-
31
export declare function getRtlScrollAxisType(): RtlScrollAxisType;
42

3+
export declare function getShadowRoot(element: HTMLElement): Node | null;
4+
55
export declare function getSupportedInputTypes(): Set<string>;
66

77
export declare function normalizePassiveListenerOptions(options: AddEventListenerOptions): AddEventListenerOptions | boolean;
@@ -35,3 +35,5 @@ export declare const enum RtlScrollAxisType {
3535
export declare function supportsPassiveEventListeners(): boolean;
3636

3737
export declare function supportsScrollBehavior(): boolean;
38+
39+
export declare function supportsShadowDom(): boolean;

0 commit comments

Comments
 (0)