Skip to content

Commit c6f4af3

Browse files
authored
Merge pull request #59 from cloudadoption/issue-modal
modal block for fragments
2 parents e158a3b + 6ab4a81 commit c6f4af3

File tree

3 files changed

+170
-1
lines changed

3 files changed

+170
-1
lines changed

blocks/dynamic/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
/** Conditionally loads and runs dynamic blocks (e.g. tabs from section metadata). */
1+
/** Conditionally loads and runs dynamic blocks (e.g. tabs, modal). */
22
export default async function dynamicBlocks(main) {
3+
const { setupFragmentModal } = await import('../modal/modal.js');
4+
setupFragmentModal();
5+
36
const hasTabSections = main?.querySelectorAll('.section[data-tab-id]').length > 0;
47
if (!hasTabSections) return;
58

blocks/modal/modal.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* Modal – opens fragment links in a dialog */
2+
3+
.modal {
4+
position: fixed;
5+
inset: 0;
6+
z-index: 1400;
7+
display: flex;
8+
align-items: center;
9+
justify-content: center;
10+
padding: var(--space-s);
11+
}
12+
13+
.modal[hidden] {
14+
display: none;
15+
}
16+
17+
.modal .modal-backdrop {
18+
position: absolute;
19+
inset: 0;
20+
background: rgb(0 0 0 / 45%);
21+
}
22+
23+
.modal .modal-dialog {
24+
position: relative;
25+
width: min(920px, calc(100vw - var(--space-xl)));
26+
max-height: calc(100vh - var(--space-xl));
27+
margin: 0;
28+
overflow: auto;
29+
border-radius: 12px;
30+
background: light-dark(var(--color-light), var(--color-gray-900));
31+
box-shadow: 0 12px 30px rgb(0 0 0 / 26%);
32+
padding: var(--space-l);
33+
}
34+
35+
.modal .modal-close {
36+
position: sticky;
37+
top: 0;
38+
margin-left: auto;
39+
display: block;
40+
width: 32px;
41+
height: 32px;
42+
border: 0;
43+
border-radius: 999px;
44+
background: light-dark(var(--color-gray-100), var(--color-gray-700));
45+
color: inherit;
46+
font-size: 24px;
47+
line-height: 1;
48+
cursor: pointer;
49+
}
50+
51+
.modal .modal-close:hover {
52+
opacity: 0.9;
53+
}
54+
55+
.modal .modal-content .modal-main {
56+
display: block;
57+
min-height: 0;
58+
max-width: 100%;
59+
padding: 0;
60+
}
61+
62+
.modal .modal-content .modal-main > div,
63+
.modal .modal-content .modal-main div[data-status] {
64+
display: block;
65+
}
66+
67+
@media (width >= 600px) {
68+
.modal {
69+
padding: var(--space-m);
70+
}
71+
}

blocks/modal/modal.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Modal Block – opens fragment links in a dialog instead of navigating.
3+
*/
4+
5+
import { loadFragment } from '../fragment/fragment.js';
6+
import { loadCSS } from '../../scripts/aem.js';
7+
import { createTag } from '../../scripts/shared.js';
8+
import dynamicBlocks from '../dynamic/index.js';
9+
10+
const FRAGMENT_PREFIX = '/fragments/';
11+
12+
function getFragmentPath(href = '') {
13+
try {
14+
const url = new URL(href, window.location.origin);
15+
if (!url.pathname.startsWith(FRAGMENT_PREFIX)) return null;
16+
return `${url.pathname}${url.search}`;
17+
} catch {
18+
return null;
19+
}
20+
}
21+
22+
export function setupFragmentModal() {
23+
if (window.__fragmentModalReady) return;
24+
window.__fragmentModalReady = true;
25+
26+
loadCSS(`${window.hlx.codeBasePath}/blocks/modal/modal.css`);
27+
28+
const closeBtn = createTag('button', { type: 'button', class: 'modal-close', 'aria-label': 'Close dialog' }, '×');
29+
const content = createTag('div', { class: 'modal-content' });
30+
const dialog = createTag('div', {
31+
class: 'modal-dialog',
32+
role: 'dialog',
33+
'aria-modal': 'true',
34+
'aria-label': 'Dialog',
35+
}, [closeBtn, content]);
36+
const backdrop = createTag('div', { class: 'modal-backdrop', 'aria-hidden': 'true' });
37+
const root = createTag('div', { class: 'modal', hidden: 'true' }, [backdrop, dialog]);
38+
document.body.append(root);
39+
let previousOverflow = '';
40+
let previousFocus = null;
41+
42+
const close = () => {
43+
root.hidden = true;
44+
content.replaceChildren();
45+
document.body.style.overflow = previousOverflow;
46+
if (previousFocus?.focus) previousFocus.focus();
47+
};
48+
49+
const open = async (path) => {
50+
previousFocus = document.activeElement;
51+
root.hidden = false;
52+
previousOverflow = document.body.style.overflow;
53+
document.body.style.overflow = 'hidden';
54+
content.textContent = 'Loading...';
55+
closeBtn.focus();
56+
57+
try {
58+
const fragment = await loadFragment(path);
59+
if (fragment) {
60+
const main = createTag('main', { class: 'modal-main' });
61+
main.append(...fragment.childNodes);
62+
content.replaceChildren(main);
63+
await dynamicBlocks(main);
64+
} else {
65+
content.textContent = 'Unable to load this content right now.';
66+
}
67+
dialog.scrollTop = 0;
68+
} catch {
69+
content.textContent = 'Unable to load this content right now.';
70+
}
71+
};
72+
73+
closeBtn.addEventListener('click', close);
74+
backdrop.addEventListener('click', close);
75+
window.addEventListener('keydown', (e) => {
76+
if (e.key === 'Escape' && !root.hidden) close();
77+
});
78+
79+
document.addEventListener('click', (e) => {
80+
const link = e.target.closest('main a[href*="/fragments/"]');
81+
if (!link) return;
82+
if (link.closest('header, footer, nav, .modal')) return;
83+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
84+
if (link.target === '_blank') return;
85+
const path = getFragmentPath(link.href);
86+
if (!path) return;
87+
e.preventDefault();
88+
open(path);
89+
});
90+
}
91+
92+
export default function decorate(block) {
93+
setupFragmentModal();
94+
block.style.display = 'none';
95+
}

0 commit comments

Comments
 (0)