From f9275b88c4caeae40752cc429a4a8c9c5697394c Mon Sep 17 00:00:00 2001 From: Daniel Kimmich Date: Wed, 16 Jul 2025 10:40:28 +0200 Subject: [PATCH] feat: allow custom root selector --- .../src/lib/material-css-vars.service.spec.ts | 28 +++++++++++++++++++ .../src/lib/material-css-vars.service.ts | 19 ++++++++++++- projects/material-css-vars/src/lib/model.ts | 6 ++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/projects/material-css-vars/src/lib/material-css-vars.service.spec.ts b/projects/material-css-vars/src/lib/material-css-vars.service.spec.ts index afe54f4..9af129d 100644 --- a/projects/material-css-vars/src/lib/material-css-vars.service.spec.ts +++ b/projects/material-css-vars/src/lib/material-css-vars.service.spec.ts @@ -218,4 +218,32 @@ describe("MaterialCssVarsService", () => { ); }); }); + + describe("_getRootElement", () => { + it("should return the html if no root selector is set", () => { + const rootElement = service["_getRootElement"](undefined); + + expect(rootElement).toBe(document.documentElement); + }); + + it("should return the element matching the root selector", () => { + const mockElement = document.createElement("app-root"); + const spy = spyOn(document, "querySelector").and.returnValue(mockElement); + + const rootElement = service["_getRootElement"]("app-root"); + + expect(rootElement).toBe(mockElement); + expect(spy).toHaveBeenCalledWith("app-root"); + }); + + it("should warn and fall back to the html element if the passed root selector cannot be found", () => { + spyOn(console, "warn"); + spyOn(document, "querySelector").and.returnValue(null); + + const rootElement = service["_getRootElement"]("non-existing"); + + expect(rootElement).toBe(document.documentElement); + expect(console.warn).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/projects/material-css-vars/src/lib/material-css-vars.service.ts b/projects/material-css-vars/src/lib/material-css-vars.service.ts index ff95af1..9970d4d 100644 --- a/projects/material-css-vars/src/lib/material-css-vars.service.ts +++ b/projects/material-css-vars/src/lib/material-css-vars.service.ts @@ -5,6 +5,7 @@ import { RendererFactory2, RendererStyleFlags2, DOCUMENT, + isDevMode, } from "@angular/core"; import { Numberify, RGBA, TinyColor } from "@ctrl/tinycolor"; import { @@ -58,7 +59,7 @@ export class MaterialCssVarsService { @Inject(MATERIAL_CSS_VARS_CFG) cfg: MaterialCssVariablesConfig, ) { this.renderer = rendererFactory.createRenderer(null, null); - this.ROOT = this.document.documentElement; + this.ROOT = this._getRootElement(cfg.rootSelector); this.cfg = { ...DEFAULT_MAT_CSS_CFG, @@ -433,4 +434,20 @@ export class MaterialCssVarsService { const darkest = Math.min(luminance1, luminance2); return (brightest + 0.05) / (darkest + 0.05); } + + private _getRootElement(rootSelector: string | undefined): HTMLElement { + if (typeof rootSelector !== "string") { + return this.document.documentElement; + } + const rootElement = document.querySelector(rootSelector); + if (rootElement) { + return rootElement; + } + if (isDevMode()) { + console.warn( + `[MaterialCssVars] Could not find root element: ${rootSelector}. Falling back to HTML element.`, + ); + } + return this.document.documentElement; + } } diff --git a/projects/material-css-vars/src/lib/model.ts b/projects/material-css-vars/src/lib/model.ts index 3192383..be38714 100644 --- a/projects/material-css-vars/src/lib/model.ts +++ b/projects/material-css-vars/src/lib/model.ts @@ -54,6 +54,12 @@ export interface MaterialCssVariablesConfig { darkThemeClass: string; lightThemeClass: string; + /** + * Selector to which the CSS variables are added. + * If not specified, the document's HTML element is used. + */ + rootSelector?: string; + colorMap: MaterialCssColorMapperEntry[]; sortedHues: HueValue[];