Skip to content

Commit df302ca

Browse files
shsteimerclaude
andcommitted
fix: make header block shadow DOM aware for embed compatibility
Header assumed it ran in the main document, causing theme toggle, dropdown menus, and mobile nav to break inside aem-embed shadow DOM. - Add getBlockContext() utility to shared.js for shadow DOM detection - Header uses correct body/eventRoot refs instead of document globals - Replace document.body.contains() with li.isConnected for mega sync - Unify localStorage key to 'diyfire-theme' (was 'color-scheme') - Header broadcasts aem-theme-change event; scripts.js and embed tester each handle theme application for their own context - Remove fixed body height constraint from aem-embed header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c4fe3f7 commit df302ca

File tree

5 files changed

+65
-34
lines changed

5 files changed

+65
-34
lines changed

blocks/header/header.js

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77
import { getMetadata } from '../../scripts/aem.js';
88
import { loadFragment } from '../fragment/fragment.js';
9+
import { getBlockContext } from '../../scripts/shared.js';
910

1011
const DESKTOP = window.matchMedia('(min-width: 900px)');
11-
const THEME_KEY = 'color-scheme';
12+
const THEME_KEY = 'diyfire-theme';
1213

1314
function getNavPath() {
1415
const meta = getMetadata('nav');
@@ -72,7 +73,7 @@ function decorateMega(li) {
7273

7374
// Position dropdown: full viewport width, arrow under trigger
7475
const sync = () => {
75-
if (!document.body.contains(li)) return;
76+
if (!li.isConnected) return;
7677
const trigger = li.querySelector(':scope > p');
7778
const menu = li.querySelector(':scope > ul');
7879
if (!trigger || !menu) return;
@@ -135,23 +136,22 @@ function initTheme(tools) {
135136
const get = () => {
136137
try {
137138
const s = localStorage.getItem(THEME_KEY);
138-
if (s === 'light-scheme' || s === 'dark-scheme') return s;
139+
if (s === 'light' || s === 'dark') return s;
139140
} catch (e) { /* ignore */ }
140-
return matchMedia('(prefers-color-scheme: dark)').matches ? 'dark-scheme' : 'light-scheme';
141+
return matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
141142
};
142143
const set = (s) => {
143-
if (s !== 'light-scheme' && s !== 'dark-scheme') return;
144-
document.body.classList.remove('light-scheme', 'dark-scheme');
145-
document.body.classList.add(s);
144+
if (s !== 'light' && s !== 'dark') return;
146145
try { localStorage.setItem(THEME_KEY, s); } catch (e) { /* ignore */ }
146+
window.dispatchEvent(new CustomEvent('aem-theme-change', { detail: { theme: s } }));
147147
};
148148

149149
btn.classList.add('nav-tool');
150150
btn.setAttribute('role', 'button');
151151
btn.setAttribute('tabindex', '0');
152-
const updateLabel = () => btn.setAttribute('aria-label', `Switch to ${get() === 'dark-scheme' ? 'light' : 'dark'} mode`);
152+
const updateLabel = () => btn.setAttribute('aria-label', `Switch to ${get() === 'dark' ? 'light' : 'dark'} mode`);
153153
const toggle = () => {
154-
set(get() === 'dark-scheme' ? 'light-scheme' : 'dark-scheme');
154+
set(get() === 'dark' ? 'light' : 'dark');
155155
updateLabel();
156156
};
157157
btn.addEventListener('click', toggle);
@@ -311,7 +311,7 @@ function decorateLanguageMenu(menu) {
311311
});
312312
}
313313

314-
function initLanguage(tools) {
314+
function initLanguage(tools, eventRoot) {
315315
const globe = tools.querySelector('.icon-globe')?.closest('p, button, a, div');
316316
const menu = globe ? findLangMenu(tools, globe) : null;
317317
if (!globe || !menu) return;
@@ -340,16 +340,16 @@ function initLanguage(tools) {
340340
};
341341

342342
globe.addEventListener('click', (e) => { e.preventDefault(); toggle(); });
343-
document.addEventListener('click', (e) => { if (!tools.contains(e.target)) { menu.hidden = true; globe.setAttribute('aria-expanded', 'false'); } });
344-
document.addEventListener('keydown', (e) => { if (e.code === 'Escape') { menu.hidden = true; globe.setAttribute('aria-expanded', 'false'); } });
343+
eventRoot.addEventListener('click', (e) => { if (!tools.contains(e.target)) { menu.hidden = true; globe.setAttribute('aria-expanded', 'false'); } });
344+
eventRoot.addEventListener('keydown', (e) => { if (e.code === 'Escape') { menu.hidden = true; globe.setAttribute('aria-expanded', 'false'); } });
345345
menu.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => { menu.hidden = true; globe.setAttribute('aria-expanded', 'false'); }));
346346

347347
decorateLanguageMenu(menu);
348348
}
349349

350-
function toggleMobile(nav, open) {
350+
function toggleMobile(nav, open, body) {
351351
const isOpen = open === undefined ? nav.getAttribute('aria-expanded') !== 'true' : open;
352-
document.body.style.overflowY = isOpen && !DESKTOP.matches ? 'hidden' : '';
352+
body.style.overflowY = isOpen && !DESKTOP.matches ? 'hidden' : '';
353353
nav.setAttribute('aria-expanded', isOpen);
354354
nav.querySelector('.nav-hamburger button')?.setAttribute('aria-label', isOpen ? 'Close navigation' : 'Open navigation');
355355
nav.querySelectorAll('.nav-drop').forEach((d) => d.setAttribute('tabindex', DESKTOP.matches ? '0' : '-1'));
@@ -358,6 +358,8 @@ function toggleMobile(nav, open) {
358358
const NAV_ITEMS = '.default-content-wrapper > ul > li';
359359

360360
export default async function decorate(block) {
361+
const { body, eventRoot } = getBlockContext(block);
362+
361363
// Load nav content (skip if aem-embed already provided content)
362364
if (block.textContent === '') {
363365
const fragment = await loadFragment(getNavPath());
@@ -406,17 +408,17 @@ export default async function decorate(block) {
406408
const hamburger = document.createElement('div');
407409
hamburger.className = 'nav-hamburger';
408410
hamburger.innerHTML = '<button type="button" aria-controls="nav" aria-label="Open navigation"><span class="nav-hamburger-icon"></span></button>';
409-
hamburger.onclick = () => toggleMobile(nav);
411+
hamburger.onclick = () => toggleMobile(nav, undefined, body);
410412

411-
document.addEventListener('click', (e) => {
413+
eventRoot.addEventListener('click', (e) => {
412414
if (!DESKTOP.matches && nav.getAttribute('aria-expanded') === 'true' && !nav.contains(e.target)) {
413-
toggleMobile(nav, false);
415+
toggleMobile(nav, false, body);
414416
}
415417
});
416-
document.addEventListener('keydown', (e) => {
418+
eventRoot.addEventListener('keydown', (e) => {
417419
if (e.code !== 'Escape') return;
418420
if (!DESKTOP.matches && nav.getAttribute('aria-expanded') === 'true') {
419-
toggleMobile(nav, false);
421+
toggleMobile(nav, false, body);
420422
} else if (DESKTOP.matches && nav.querySelector('.nav-drop[aria-expanded="true"]')) {
421423
collapseAll(nav);
422424
}
@@ -428,14 +430,14 @@ export default async function decorate(block) {
428430
wrapper.append(nav);
429431
block.append(wrapper);
430432

431-
toggleMobile(nav, false);
433+
toggleMobile(nav, false, body);
432434
collapseAll(nav);
433-
DESKTOP.addEventListener('change', () => toggleMobile(nav, false));
435+
DESKTOP.addEventListener('change', () => toggleMobile(nav, false, body));
434436

435437
if (tools) {
436438
initTheme(tools);
437439
initSearch(tools);
438-
initLanguage(tools);
440+
initLanguage(tools, eventRoot);
439441
hydrateTranslateFromCookie();
440442
}
441443

scripts/aem-embed.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ export class AEMEmbed extends HTMLElement {
7171

7272
block.dataset.blockStatus = 'loaded';
7373

74-
body.style.height = 'var(--nav-height)';
7574
body.classList.add('appear');
7675
}
7776

scripts/scripts.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,12 @@ import dynamicBlocks from '../blocks/dynamic/index.js';
1818

1919
const THEME_STORAGE_KEY = 'diyfire-theme';
2020

21-
function applyStoredThemePreference() {
22-
try {
23-
const storedTheme = localStorage.getItem(THEME_STORAGE_KEY);
24-
if (storedTheme !== 'light' && storedTheme !== 'dark') return;
25-
document.documentElement.dataset.theme = storedTheme;
26-
document.body.classList.remove('light-scheme', 'dark-scheme');
27-
document.body.classList.add(`${storedTheme}-scheme`);
28-
} catch (e) {
29-
// do nothing
30-
}
21+
function applyTheme(theme) {
22+
const t = theme ?? (() => { try { return localStorage.getItem(THEME_STORAGE_KEY); } catch (e) { return null; } })();
23+
if (t !== 'light' && t !== 'dark') return;
24+
document.documentElement.dataset.theme = t;
25+
document.body.classList.remove('light-scheme', 'dark-scheme');
26+
document.body.classList.add(`${t}-scheme`);
3127
}
3228

3329
const isYoutubeLink = (url) => ['youtube.com', 'www.youtube.com', 'youtu.be'].includes(url.hostname);
@@ -170,7 +166,7 @@ async function loadTemplate(main) {
170166
async function loadEager(doc) {
171167
document.documentElement.lang = 'en';
172168
decorateTemplateAndTheme();
173-
applyStoredThemePreference();
169+
applyTheme();
174170
const main = doc.querySelector('main');
175171
if (main) {
176172
if (window.isErrorPage) loadErrorPage(main);
@@ -274,4 +270,8 @@ if (!window.hlx?.suppressLoadPage) {
274270
if (!new URL(window.location.href).searchParams.get('dapreview')) return;
275271
import('https://da.live/scripts/dapreview.js').then(({ default: daPreview }) => daPreview(loadPage));
276272
}());
273+
274+
window.addEventListener('aem-theme-change', (e) => {
275+
applyTheme(e.detail?.theme);
276+
});
277277
}

scripts/shared.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,21 @@ export function createChart(canvas, config) {
259259
canvas.chart = chart;
260260
return chart;
261261
}
262+
263+
/**
264+
* Resolve DOM context for a block element.
265+
* Returns the correct body and event root whether the block runs
266+
* in the normal document or inside a shadow DOM (aem-embed).
267+
* @param {Element} block
268+
* @returns {{ root: Document|ShadowRoot, body: HTMLElement, eventRoot: Document|ShadowRoot, isEmbed: boolean }}
269+
*/
270+
export function getBlockContext(block) {
271+
const root = block.getRootNode();
272+
const isEmbed = root !== document;
273+
return {
274+
root,
275+
body: isEmbed ? root.querySelector('body') : document.body,
276+
eventRoot: isEmbed ? root : document,
277+
isEmbed,
278+
};
279+
}

tools/embed-tester/scripts.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ import {
88

99
const LIVE_URL = 'https://main--diyfire--cloudadoption.aem.live';
1010

11+
// Sync theme changes from header toggle to all embeds on this page
12+
window.addEventListener('aem-theme-change', (e) => {
13+
const { theme } = e.detail;
14+
document.querySelectorAll('aem-embed').forEach((embed) => {
15+
const body = embed.shadowRoot?.querySelector('body');
16+
if (body) {
17+
body.classList.remove('light-scheme', 'dark-scheme');
18+
body.classList.add(`${theme}-scheme`);
19+
}
20+
});
21+
});
22+
1123
function getBaseUrl() {
1224
const { origin } = window.location;
1325
if (origin.includes('localhost')) return origin;

0 commit comments

Comments
 (0)