Skip to content

Commit a3ce8fd

Browse files
committed
fix(elements): added flag to adhere to style encapsulation
Added new API to let ShadowDom encapsulation to be isolated and avoid injection of shared styles
1 parent 58f52f6 commit a3ce8fd

File tree

6 files changed

+151
-8
lines changed

6 files changed

+151
-8
lines changed

goldens/public-api/platform-browser/index.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ export enum HydrationFeatureKind {
164164
NoHttpTransferCache = 0
165165
}
166166

167+
// @public
168+
export const ISOLATED_SHADOW_DOM: InjectionToken<boolean>;
169+
167170
// @public
168171
export class Meta {
169172
constructor(_doc: any);

packages/core/test/acceptance/renderer_factory_spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ function getRendererFactory2(document: Document): RendererFactory2 {
401401
new ɵSharedStylesHost(document, appId),
402402
appId,
403403
true,
404+
false,
404405
document,
405406
isNode ? PLATFORM_SERVER_ID : PLATFORM_BROWSER_ID,
406407
fakeNgZone,

packages/core/test/render3/imported_renderer2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function getRendererFactory2(document: any): RendererFactory2 {
5959
new ɵSharedStylesHost(document, appId),
6060
appId,
6161
true,
62+
false,
6263
document,
6364
isNode ? PLATFORM_SERVER_ID : PLATFORM_BROWSER_ID,
6465
fakeNgZone,

packages/platform-browser/src/dom/dom_renderer.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
5656
*/
5757
const REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT = true;
5858

59+
/**
60+
* The default value for the `ISOLATED_SHADOW_DOM` DI token.
61+
*/
62+
const ISOLATED_SHADOW_DOM_DEFAULT = false;
63+
5964
/**
6065
* A DI token that indicates whether styles
6166
* of destroyed components should be removed from DOM.
@@ -71,6 +76,22 @@ export const REMOVE_STYLES_ON_COMPONENT_DESTROY = new InjectionToken<boolean>(
7176
},
7277
);
7378

79+
/**
80+
* A [DI token](guide/glossary#di-token "DI token definition") that indicates whether the style
81+
* of components that are using ShadowDom as encapsulation must remain isolated from other
82+
* components instances styles and/or global styles.
83+
*
84+
* By default, the value is set to `false`.
85+
* @publicApi
86+
*/
87+
export const ISOLATED_SHADOW_DOM = new InjectionToken<boolean>(
88+
ngDevMode ? 'IsolatedShadowDom' : '',
89+
{
90+
providedIn: 'root',
91+
factory: () => ISOLATED_SHADOW_DOM_DEFAULT,
92+
},
93+
);
94+
7495
export function shimContentAttribute(componentShortId: string): string {
7596
return CONTENT_ATTR.replace(COMPONENT_REGEX, componentShortId);
7697
}
@@ -143,6 +164,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
143164
private readonly sharedStylesHost: SharedStylesHost,
144165
@Inject(APP_ID) private readonly appId: string,
145166
@Inject(REMOVE_STYLES_ON_COMPONENT_DESTROY) private removeStylesOnCompDestroy: boolean,
167+
@Inject(ISOLATED_SHADOW_DOM) private readonly isolatedShadowDom: boolean,
146168
@Inject(DOCUMENT) private readonly doc: Document,
147169
@Inject(PLATFORM_ID) readonly platformId: Object,
148170
readonly ngZone: NgZone,
@@ -217,7 +239,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
217239
case ViewEncapsulation.ShadowDom:
218240
return new ShadowDomRenderer(
219241
eventManager,
220-
sharedStylesHost,
242+
this.isolatedShadowDom ? null : sharedStylesHost,
221243
element,
222244
type,
223245
doc,
@@ -498,7 +520,7 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
498520

499521
constructor(
500522
eventManager: EventManager,
501-
private sharedStylesHost: SharedStylesHost,
523+
private sharedStylesHost: SharedStylesHost | null,
502524
private hostEl: any,
503525
component: RendererType2,
504526
doc: Document,
@@ -515,7 +537,7 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
515537
// element life-cycle (after a disconnect/reconnect)
516538
this.shadowRoot.innerHTML = '';
517539
}
518-
this.sharedStylesHost.addHost(this.shadowRoot);
540+
this.sharedStylesHost?.addHost(this.shadowRoot);
519541
let styles = component.styles;
520542
if (ngDevMode) {
521543
// We only do this in development, as for production users should not add CSS sourcemaps to components.
@@ -572,7 +594,7 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
572594
}
573595

574596
override destroy() {
575-
this.sharedStylesHost.removeHost(this.shadowRoot);
597+
this.sharedStylesHost?.removeHost(this.shadowRoot);
576598
}
577599
}
578600

packages/platform-browser/src/platform-browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export {Meta, MetaDefinition} from './browser/meta';
1919
export {Title} from './browser/title';
2020
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';
2121
export {By} from './dom/debug/by';
22-
export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer';
22+
export {REMOVE_STYLES_ON_COMPONENT_DESTROY, ISOLATED_SHADOW_DOM} from './dom/dom_renderer';
2323
export {EVENT_MANAGER_PLUGINS, EventManager, EventManagerPlugin} from './dom/events/event_manager';
2424
export {
2525
HAMMER_GESTURE_CONFIG,

packages/platform-browser/test/dom/shadow_dom_spec.ts

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Component, NgModule, ViewEncapsulation} from '@angular/core';
9+
import {Component, inject, NgModule, ViewContainerRef, ViewEncapsulation} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {BrowserModule} from '../../index';
11+
import {BrowserModule, ISOLATED_SHADOW_DOM} from '../../index';
1212
import {expect} from '@angular/private/testing/matchers';
1313
import {isNode} from '@angular/private/testing';
1414

@@ -81,6 +81,85 @@ describe('ShadowDOM Support', () => {
8181
expect(articleContent.assignedSlot).toBe(articleSlot);
8282
expect(articleSubcontent.assignedSlot).toBe(articleSlot);
8383
});
84+
85+
it('should inject None shared styles in web elements', () => {
86+
const comp = TestBed.createComponent(ShadowInjectedComponent);
87+
const compEl = comp.nativeElement as HTMLElement;
88+
const div = compEl.shadowRoot!.querySelector('div.green')!;
89+
// Not set before creating a sibling component
90+
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
91+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
92+
// Add NoneStyleComponent
93+
const compInstance = comp.componentInstance;
94+
const viewContainerRef = compInstance.viewContainerRef;
95+
viewContainerRef.createComponent(NoneStyleComponent);
96+
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 128, 0)'); // green
97+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(2); // two <style> elements
98+
});
99+
100+
it('should inject Emulated shared styles in web elements', () => {
101+
const comp = TestBed.createComponent(ShadowInjectedComponent);
102+
const compEl = comp.nativeElement as HTMLElement;
103+
const div = compEl.shadowRoot!.querySelector('div.yellow')!;
104+
// Not set before creating a sibling component
105+
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
106+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
107+
// Add EmulatedStyleComponent
108+
const compInstance = comp.componentInstance;
109+
const viewContainerRef = compInstance.viewContainerRef;
110+
viewContainerRef.createComponent(EmulatedStyleComponent);
111+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(2); // two <style> elements
112+
});
113+
114+
describe('should not inject shared styles in shadow dom when `ISOLATED_SHADOW_DOM` is `true`', () => {
115+
beforeEach(() => {
116+
TestBed.resetTestingModule();
117+
TestBed.configureTestingModule({
118+
imports: [BrowserModule],
119+
declarations: [
120+
StyledShadowComponent,
121+
NoneStyleComponent,
122+
EmulatedStyleComponent,
123+
ShadowInjectedComponent,
124+
],
125+
providers: [
126+
{
127+
provide: ISOLATED_SHADOW_DOM,
128+
useValue: true,
129+
},
130+
],
131+
});
132+
});
133+
134+
it('should not inject None shared styles in web elements', () => {
135+
const comp = TestBed.createComponent(ShadowInjectedComponent);
136+
const compEl = comp.nativeElement as HTMLElement;
137+
const div = compEl.shadowRoot!.querySelector('div.green')!;
138+
// Not set before creating a sibling component
139+
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
140+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
141+
// Add NoneStyleComponent
142+
const compInstance = comp.componentInstance;
143+
const viewContainerRef = compInstance.viewContainerRef;
144+
viewContainerRef.createComponent(NoneStyleComponent);
145+
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
146+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1);
147+
});
148+
149+
it('should not inject Emulated shared styles in web elements', () => {
150+
const comp = TestBed.createComponent(ShadowInjectedComponent);
151+
const compEl = comp.nativeElement as HTMLElement;
152+
const div = compEl.shadowRoot!.querySelector('div.yellow')!;
153+
// Not set before creating a sibling component
154+
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
155+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
156+
// Add EmulatedStyleComponent
157+
const compInstance = comp.componentInstance;
158+
const viewContainerRef = compInstance.viewContainerRef;
159+
viewContainerRef.createComponent(EmulatedStyleComponent);
160+
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1);
161+
});
162+
});
84163
});
85164

86165
@Component({
@@ -117,9 +196,46 @@ class ShadowSlotComponent {}
117196
})
118197
class ShadowSlotsComponent {}
119198

199+
@Component({
200+
selector: 'shadow-inj-comp',
201+
template: '<div class="green yellow"></div>',
202+
styles: [`.green { background-color: green; } .yellow { background-color: yellow; }`],
203+
encapsulation: ViewEncapsulation.ShadowDom,
204+
standalone: false,
205+
})
206+
class ShadowInjectedComponent {
207+
viewContainerRef = inject(ViewContainerRef);
208+
}
209+
210+
@Component({
211+
selector: 'none-style-comp',
212+
template: '<div class="green"></div>',
213+
styles: [`.green { color: green; }`],
214+
encapsulation: ViewEncapsulation.None,
215+
standalone: false,
216+
})
217+
class NoneStyleComponent {}
218+
219+
@Component({
220+
selector: 'emulated-style-comp',
221+
template: '<div class="yellow"></div>',
222+
styles: [`.yellow { color: yellow; }`],
223+
encapsulation: ViewEncapsulation.Emulated,
224+
standalone: false,
225+
})
226+
class EmulatedStyleComponent {}
227+
120228
@NgModule({
121229
imports: [BrowserModule],
122-
declarations: [ShadowComponent, ShadowSlotComponent, ShadowSlotsComponent, StyledShadowComponent],
230+
declarations: [
231+
ShadowComponent,
232+
ShadowSlotComponent,
233+
ShadowSlotsComponent,
234+
StyledShadowComponent,
235+
NoneStyleComponent,
236+
EmulatedStyleComponent,
237+
ShadowInjectedComponent,
238+
],
123239
})
124240
class TestModule {
125241
ngDoBootstrap() {}

0 commit comments

Comments
 (0)