Skip to content

Commit 554047b

Browse files
authored
fix(dialtone-tokens): NO-JIRA fix shadow DOM setAttribute crash in theme setters (#1271)
1 parent 6087db9 commit 554047b

2 files changed

Lines changed: 45 additions & 53 deletions

File tree

packages/dialtone-tokens/tests/config.test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,15 @@ describe('themes/config.js', () => {
184184
});
185185
});
186186

187-
it.skip('host-with-shadowRoot writes attribute on shadowRoot — pending bug fix', () => {});
187+
it('host-with-shadowRoot: sets data-dt-* attributes on host, injects styles into shadowRoot', () => {
188+
const { host, shadowRoot } = setupShadowHost();
189+
initDialtoneTheme(dpStub, 'light', host);
190+
expect(host.getAttribute('data-dt-mode')).toBe('light');
191+
expect(host.getAttribute('data-dt-brand')).toBe(dpStub.brand.name);
192+
expect(host.getAttribute('data-dt-contrast')).toBe('default');
193+
expect(shadowRoot.querySelector('#dialtone-css-core')).not.toBeNull();
194+
expect(shadowRoot.querySelector('#dialtone-css-brand-colors')).not.toBeNull();
195+
});
188196
});
189197

190198
describe('getBrandMaterial', () => {

packages/dialtone-tokens/themes/config.js

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,17 @@ export function setTheme (theme, rootNode = document.documentElement, contrastTh
118118
*/
119119
function _setThemeLegacy(theme, rootNode = document.documentElement, contrastTheme = null) {
120120
_setThemeAttributeOnRoot(theme.base.name, theme.brand.name, rootNode);
121-
if (rootNode?.shadowRoot) {
122-
rootNode = rootNode.shadowRoot;
123-
}
121+
const styleRoot = rootNode?.shadowRoot ?? rootNode;
124122
// Load css files
125-
_setStyleTag('dialtone-css-theme', theme.base.css, rootNode);
126-
_setStyleTag('dialtone-css-brand', theme.brand.css, rootNode);
123+
_setStyleTag('dialtone-css-theme', theme.base.css, styleRoot);
124+
_setStyleTag('dialtone-css-brand', theme.brand.css, styleRoot);
127125

128126
// Apply contrast layer
129127
if (contrastTheme) {
130-
_setStyleTag('dialtone-css-contrast', contrastTheme.css, rootNode);
128+
_setStyleTag('dialtone-css-contrast', contrastTheme.css, styleRoot);
131129
rootNode?.setAttribute('data-dt-contrast', 'high');
132130
} else {
133-
_removeStyleTag('dialtone-css-contrast', rootNode);
131+
_removeStyleTag('dialtone-css-contrast', styleRoot);
134132
rootNode?.setAttribute('data-dt-contrast', 'default');
135133
}
136134
}
@@ -139,30 +137,28 @@ function _setThemeLegacy(theme, rootNode = document.documentElement, contrastThe
139137
* Layered theme setter (new optimized system)
140138
*/
141139
function _setThemeLayered(theme, rootNode = document.documentElement) {
142-
if (rootNode?.shadowRoot) {
143-
rootNode = rootNode.shadowRoot;
144-
}
140+
const styleRoot = rootNode?.shadowRoot ?? rootNode;
145141

146142
// Load core tokens only once per JavaScript instance
147143
if (theme.core && !coreTokensLoaded) {
148-
_setStyleTag('dialtone-css-core', theme.core, rootNode);
144+
_setStyleTag('dialtone-css-core', theme.core, styleRoot);
149145
coreTokensLoaded = true;
150146
}
151147

152148
// Load base colors only once
153-
if (theme.baseColors && !rootNode?.querySelector('#dialtone-css-base-colors')) {
154-
_setStyleTag('dialtone-css-base-colors', theme.baseColors, rootNode);
149+
if (theme.baseColors && !styleRoot?.querySelector('#dialtone-css-base-colors')) {
150+
_setStyleTag('dialtone-css-base-colors', theme.baseColors, styleRoot);
155151
}
156152

157153
// Load brand colors (dp base is always loaded, others are overrides)
158154
if (theme.brand) {
159-
_setStyleTag('dialtone-css-brand-colors', theme.brand.css, rootNode);
155+
_setStyleTag('dialtone-css-brand-colors', theme.brand.css, styleRoot);
160156
rootNode?.setAttribute('data-dt-brand', theme.brand.name);
161157
}
162158

163159
// Apply contrast layer if provided
164160
if (theme.contrast) {
165-
_setStyleTag('dialtone-css-contrast', theme.contrast.css, rootNode);
161+
_setStyleTag('dialtone-css-contrast', theme.contrast.css, styleRoot);
166162
rootNode?.setAttribute('data-dt-contrast', theme.contrast.name);
167163
}
168164
}
@@ -249,10 +245,7 @@ export function setMode(mode, rootNode = document.documentElement) {
249245
'[Dialtone] You passed a ShadowRoot directly to setMode(). ' +
250246
'Please pass the host element instead. The function will access shadowRoot automatically.',
251247
);
252-
}
253-
254-
if (rootNode?.shadowRoot) {
255-
rootNode = rootNode.shadowRoot;
248+
return;
256249
}
257250

258251
rootNode?.setAttribute('data-dt-mode', mode);
@@ -315,13 +308,11 @@ export function setBrand(brandTheme, rootNode = document.documentElement) {
315308
'[Dialtone] You passed a ShadowRoot directly to setBrand(). ' +
316309
'Please pass the host element instead. The function will access shadowRoot automatically.',
317310
);
311+
return;
318312
}
319313

320-
if (rootNode?.shadowRoot) {
321-
rootNode = rootNode.shadowRoot;
322-
}
323-
324-
_setStyleTag('dialtone-css-brand-colors', brandTheme.brand.css, rootNode);
314+
const styleRoot = rootNode?.shadowRoot ?? rootNode;
315+
_setStyleTag('dialtone-css-brand-colors', brandTheme.brand.css, styleRoot);
325316
rootNode?.setAttribute('data-dt-brand', brandTheme.brand.name);
326317

327318
_applyBrandLockedMaterial(brandTheme, rootNode);
@@ -381,17 +372,15 @@ export function setContrast(contrastTheme, rootNode = document.documentElement)
381372
'[Dialtone] You passed a ShadowRoot directly to setContrast(). ' +
382373
'Please pass the host element instead. The function will access shadowRoot automatically.',
383374
);
375+
return;
384376
}
385377

386-
if (rootNode?.shadowRoot) {
387-
rootNode = rootNode.shadowRoot;
388-
}
389-
378+
const styleRoot = rootNode?.shadowRoot ?? rootNode;
390379
if (contrastTheme && contrastTheme.contrast) {
391-
_setStyleTag('dialtone-css-contrast', contrastTheme.contrast.css, rootNode);
380+
_setStyleTag('dialtone-css-contrast', contrastTheme.contrast.css, styleRoot);
392381
rootNode?.setAttribute('data-dt-contrast', contrastTheme.contrast.name);
393382
} else {
394-
_removeStyleTag('dialtone-css-contrast', rootNode);
383+
_removeStyleTag('dialtone-css-contrast', styleRoot);
395384
rootNode?.setAttribute('data-dt-contrast', 'default');
396385
}
397386
}
@@ -427,10 +416,7 @@ export function setMaterial (name, rootNode = document.documentElement) {
427416
'[Dialtone] You passed a ShadowRoot directly to setMaterial(). ' +
428417
'Please pass the host element instead. The function will access shadowRoot automatically.',
429418
);
430-
}
431-
432-
if (rootNode?.shadowRoot) {
433-
rootNode = rootNode.shadowRoot;
419+
return;
434420
}
435421

436422
const resolved = name || 'sandstone';
@@ -539,11 +525,10 @@ export function initDialtoneTheme(brandTheme, mode = 'light', rootNode = documen
539525
'Correct: initDialtoneTheme(brand, mode, hostElement)\n' +
540526
'Incorrect: initDialtoneTheme(brand, mode, hostElement.shadowRoot)',
541527
);
528+
return;
542529
}
543530

544-
if (rootNode?.shadowRoot) {
545-
rootNode = rootNode.shadowRoot;
546-
}
531+
const styleRoot = rootNode?.shadowRoot ?? rootNode;
547532

548533
// CRITICAL: Detect embedded app trying to use document.documentElement
549534
// This check MUST run on first init, before idempotency check
@@ -581,11 +566,11 @@ export function initDialtoneTheme(brandTheme, mode = 'light', rootNode = documen
581566
}
582567

583568
// Load core tokens (once per JavaScript instance)
584-
_setStyleTag('dialtone-css-core', Core.core, rootNode);
569+
_setStyleTag('dialtone-css-core', Core.core, styleRoot);
585570
coreTokensLoaded = true;
586571

587572
// Load base colors (once)
588-
_setStyleTag('dialtone-css-base-colors', Core.baseColors, rootNode);
573+
_setStyleTag('dialtone-css-base-colors', Core.baseColors, styleRoot);
589574

590575
// Set initial mode
591576
setMode(mode, rootNode);
@@ -649,8 +634,7 @@ export function hasBrandMaterialLock(brandTheme) {
649634
* }
650635
*/
651636
export function resetTheme(rootNode = document.documentElement) {
652-
// Access shadowRoot if present
653-
const actualRoot = rootNode?.shadowRoot || rootNode;
637+
const styleRoot = rootNode?.shadowRoot ?? rootNode;
654638

655639
// Clear initialization state (only one instance per app)
656640
initializationState = null;
@@ -659,15 +643,15 @@ export function resetTheme(rootNode = document.documentElement) {
659643
// Remove all theme style tags. Material no longer injects a style tag
660644
// (attribute-driven), but resetTheme should still scrub any pre-existing
661645
// injection from older code paths.
662-
_removeStyleTag('dialtone-css-core', actualRoot);
663-
_removeStyleTag('dialtone-css-base-colors', actualRoot);
664-
_removeStyleTag('dialtone-css-material', actualRoot);
665-
_removeStyleTag('dialtone-css-brand-colors', actualRoot);
666-
_removeStyleTag('dialtone-css-contrast', actualRoot);
667-
668-
// Remove theme attributes
669-
actualRoot?.removeAttribute('data-dt-mode');
670-
actualRoot?.removeAttribute('data-dt-brand');
671-
actualRoot?.removeAttribute('data-dt-contrast');
672-
actualRoot?.removeAttribute('data-dt-material');
646+
_removeStyleTag('dialtone-css-core', styleRoot);
647+
_removeStyleTag('dialtone-css-base-colors', styleRoot);
648+
_removeStyleTag('dialtone-css-material', styleRoot);
649+
_removeStyleTag('dialtone-css-brand-colors', styleRoot);
650+
_removeStyleTag('dialtone-css-contrast', styleRoot);
651+
652+
// Remove theme attributes from the host element (not ShadowRoot — it has no removeAttribute)
653+
rootNode?.removeAttribute('data-dt-mode');
654+
rootNode?.removeAttribute('data-dt-brand');
655+
rootNode?.removeAttribute('data-dt-contrast');
656+
rootNode?.removeAttribute('data-dt-material');
673657
}

0 commit comments

Comments
 (0)