Skip to content

Commit

Permalink
- Fix with GLOBAL flag in API
Browse files Browse the repository at this point in the history
  • Loading branch information
lmartorella committed Feb 3, 2025
1 parent 9723f03 commit eff21d6
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 8 deletions.
28 changes: 24 additions & 4 deletions packages/platform-browser/src/dom/dom_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
*/
const REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT = true;

/**
* The default value for the `ISOLATED_SHADOW_DOM` DI token.
*/
const ISOLATED_SHADOW_DOM_DEFAULT = false;

/**
* A DI token that indicates whether styles
* of destroyed components should be removed from DOM.
Expand All @@ -70,6 +75,20 @@ export const REMOVE_STYLES_ON_COMPONENT_DESTROY = new InjectionToken<boolean>(
},
);

/**
* A [DI token](guide/glossary#di-token "DI token definition") that indicates whether the style
* of components that are using ShadowDom as encapsulation must remain isolated from other
* components instances styles and/or global styles.
*
* By default, the value is set to `false`.
* @publicApi
*/
export const ISOLATED_SHADOW_DOM =
new InjectionToken<boolean>(ngDevMode ? 'IsolatedShadowDom' : '', {
providedIn: 'root',
factory: () => ISOLATED_SHADOW_DOM_DEFAULT,
});

export function shimContentAttribute(componentShortId: string): string {
return CONTENT_ATTR.replace(COMPONENT_REGEX, componentShortId);
}
Expand Down Expand Up @@ -142,6 +161,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
private readonly sharedStylesHost: SharedStylesHost,
@Inject(APP_ID) private readonly appId: string,
@Inject(REMOVE_STYLES_ON_COMPONENT_DESTROY) private removeStylesOnCompDestroy: boolean,
@Inject(ISOLATED_SHADOW_DOM) private readonly isolatedShadowDom: boolean,
@Inject(DOCUMENT) private readonly doc: Document,
@Inject(PLATFORM_ID) readonly platformId: Object,
readonly ngZone: NgZone,
Expand Down Expand Up @@ -212,7 +232,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
case ViewEncapsulation.ShadowDom:
return new ShadowDomRenderer(
eventManager,
sharedStylesHost,
this.isolatedShadowDom ? null : sharedStylesHost,
element,
type,
doc,
Expand Down Expand Up @@ -487,7 +507,7 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {

constructor(
eventManager: EventManager,
private sharedStylesHost: SharedStylesHost,
private sharedStylesHost: SharedStylesHost | null,
private hostEl: any,
component: RendererType2,
doc: Document,
Expand All @@ -498,7 +518,7 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
) {
super(eventManager, doc, ngZone, platformIsServer, tracingService);
this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'});
this.sharedStylesHost.addHost(this.shadowRoot);
this.sharedStylesHost?.addHost(this.shadowRoot);
let styles = component.styles;
if (ngDevMode) {
// We only do this in development, as for production users should not add CSS sourcemaps to components.
Expand Down Expand Up @@ -555,7 +575,7 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
}

override destroy() {
this.sharedStylesHost.removeHost(this.shadowRoot);
this.sharedStylesHost?.removeHost(this.shadowRoot);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/platform-browser/src/platform-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export {Meta, MetaDefinition} from './browser/meta';
export {Title} from './browser/title';
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';
export {By} from './dom/debug/by';
export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer';
export {REMOVE_STYLES_ON_COMPONENT_DESTROY, ISOLATED_SHADOW_DOM} from './dom/dom_renderer';
export {EVENT_MANAGER_PLUGINS, EventManager, EventManagerPlugin} from './dom/events/event_manager';
export {
HAMMER_GESTURE_CONFIG,
Expand Down
111 changes: 108 additions & 3 deletions packages/platform-browser/test/dom/shadow_dom_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Component, NgModule, ViewEncapsulation} from '@angular/core';
import {Component, inject, NgModule, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserModule, ISOLATED_SHADOW_DOM} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';

describe('ShadowDOM Support', () => {
Expand Down Expand Up @@ -80,6 +80,80 @@ describe('ShadowDOM Support', () => {
expect(articleContent.assignedSlot).toBe(articleSlot);
expect(articleSubcontent.assignedSlot).toBe(articleSlot);
});

it('should inject None shared styles in web elements', () => {
const comp = TestBed.createComponent(ShadowInjectedComponent);
const compEl = comp.nativeElement as HTMLElement;
const div = compEl.shadowRoot!.querySelector('div.green')!;
// Not set before creating a sibling component
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
// Add NoneStyleComponent
const compInstance = comp.componentInstance;
const viewContainerRef = compInstance.viewContainerRef;
viewContainerRef.createComponent(NoneStyleComponent);
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 128, 0)'); // green
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(2); // two <style> elements
});

it('should inject Emulated shared styles in web elements', () => {
const comp = TestBed.createComponent(ShadowInjectedComponent);
const compEl = comp.nativeElement as HTMLElement;
const div = compEl.shadowRoot!.querySelector('div.yellow')!;
// Not set before creating a sibling component
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
// Add EmulatedStyleComponent
const compInstance = comp.componentInstance;
const viewContainerRef = compInstance.viewContainerRef;
viewContainerRef.createComponent(EmulatedStyleComponent);
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(2); // two <style> elements
});

describe('should not inject shared styles in shadow dom when `ISOLATED_SHADOW_DOM` is `true`', () => {
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [BrowserModule],
declarations: [StyledShadowComponent, NoneStyleComponent, EmulatedStyleComponent, ShadowInjectedComponent],
providers: [
{
provide: ISOLATED_SHADOW_DOM,
useValue: true,
},
],
});
});

it('should not inject None shared styles in web elements', () => {
const comp = TestBed.createComponent(ShadowInjectedComponent);
const compEl = comp.nativeElement as HTMLElement;
const div = compEl.shadowRoot!.querySelector('div.green')!;
// Not set before creating a sibling component
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
// Add NoneStyleComponent
const compInstance = comp.componentInstance;
const viewContainerRef = compInstance.viewContainerRef;
viewContainerRef.createComponent(NoneStyleComponent);
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1);
});

it('should not inject Emulated shared styles in web elements', () => {
const comp = TestBed.createComponent(ShadowInjectedComponent);
const compEl = comp.nativeElement as HTMLElement;
const div = compEl.shadowRoot!.querySelector('div.yellow')!;
// Not set before creating a sibling component
expect(window.getComputedStyle(div).color).toEqual('rgb(0, 0, 0)');
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1); // one <style> element
// Add EmulatedStyleComponent
const compInstance = comp.componentInstance;
const viewContainerRef = compInstance.viewContainerRef;
viewContainerRef.createComponent(EmulatedStyleComponent);
expect(compEl.shadowRoot!.querySelectorAll('style').length).toEqual(1);
});
});
});

@Component({
Expand Down Expand Up @@ -116,9 +190,40 @@ class ShadowSlotComponent {}
})
class ShadowSlotsComponent {}

@Component({
selector: 'shadow-inj-comp',
template: '<div class="green yellow"></div>',
styles: [`.green { background-color: green; } .yellow { background-color: yellow; }`],
encapsulation: ViewEncapsulation.ShadowDom,
standalone: false,
})
class ShadowInjectedComponent {
viewContainerRef = inject(ViewContainerRef);
}

@Component({
selector: 'none-style-comp',
template:
'<div class="green"></div>',
styles: [`.green { color: green; }`],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
class NoneStyleComponent {}

@Component({
selector: 'emulated-style-comp',
template:
'<div class="yellow"></div>',
styles: [`.yellow { color: yellow; }`],
encapsulation: ViewEncapsulation.Emulated,
standalone: false,
})
class EmulatedStyleComponent {}

@NgModule({
imports: [BrowserModule],
declarations: [ShadowComponent, ShadowSlotComponent, ShadowSlotsComponent, StyledShadowComponent],
declarations: [ShadowComponent, ShadowSlotComponent, ShadowSlotsComponent, StyledShadowComponent, NoneStyleComponent, EmulatedStyleComponent, ShadowInjectedComponent],
})
class TestModule {
ngDoBootstrap() {}
Expand Down

0 comments on commit eff21d6

Please sign in to comment.