Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-120020 - Event-driven Modals #403

Merged
merged 2 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions libs/blocks/card/card.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { decorateButtons } from '../../utils/decorate.js';
import { loadStyle, getConfig, createTag } from '../../utils/utils.js';
import { getSectionMetadata } from '../section-metadata/section-metadata.js';
import { getMetadata } from '../section-metadata/section-metadata.js';

const HALF = 'OneHalfCard';
const HALF_HEIGHT = 'HalfHeightCard';
Expand All @@ -19,8 +19,10 @@ const getCardType = (styles) => {
};

const getUpFromSectionMetadata = (section) => {
const sectionMetadata = getSectionMetadata(section.querySelector('.section-metadata'));
const styles = sectionMetadata.style?.split(', ').map((style) => style.replaceAll(' ', '-'));
const sectionMetadata = section.querySelector('.section-metadata');
if (!sectionMetadata) return null;
const metadata = getMetadata(sectionMetadata);
const styles = metadata.style?.text.split(', ').map((style) => style.replaceAll(' ', '-'));
return styles?.find((style) => style.includes('-up'));
};

Expand Down
11 changes: 3 additions & 8 deletions libs/blocks/fragment/fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const updateFragMap = (fragment, a, href) => {
}
};

export default async function init(a, parent) {
export default async function init(a) {
const relHref = localizeLink(a.href);
if (isCircularRef(relHref)) {
console.log(`ERROR: Fragment Circular Reference loading ${a.href}`);
Expand All @@ -46,14 +46,9 @@ export default async function init(a, parent) {

updateFragMap(fragment, a, relHref);

await loadArea(fragment);
a.parentElement.replaceChild(fragment, a);

if (parent) {
a.remove();
parent.append(fragment);
} else if (a.parentElement) {
a.parentElement.replaceChild(fragment, a);
}
await loadArea(fragment);
} else {
window.lana.log('Could not make fragment');
}
Expand Down
3 changes: 3 additions & 0 deletions libs/blocks/modal-metadata/modal-metadata.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.modal-metadata {
display: none;
}
9 changes: 9 additions & 0 deletions libs/blocks/modal-metadata/modal-metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getMetadata, handleStyle } from '../section-metadata/section-metadata.js';

export default function init(el) {
const modal = el.closest('.dialog-modal');
if (!modal) return;
const metadata = getMetadata(el);
if (metadata.style) handleStyle(metadata.style.text, modal);
if (metadata.curtain?.text === 'off') modal.classList.add('curtain-off');
}
131 changes: 65 additions & 66 deletions libs/blocks/modal/modal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createTag, getMetadata, localizeLink } from '../../utils/utils.js';

const FOCUSABLES = 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]';
const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g transform="translate(-10500 3403)">
<circle cx="10" cy="10" r="10" transform="translate(10500 -3403)" fill="#707070"/>
Expand All @@ -8,34 +9,26 @@ const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="2
</g>
</svg>`;

function getDetails(el) {
const details = { id: window.location.hash.replace('#', '') };
const a = el || document.querySelector(`a[data-modal-hash="${window.location.hash}"]`);
if (a) {
details.path = a.dataset.modalPath;
return details;
}
const metaPath = getMetadata(`-${details.id}`);
if (metaPath) {
details.path = localizeLink(metaPath);
return details;
}
return null;
function findDetails(hash, el) {
const id = hash.replace('#', '');
const a = el || document.querySelector(`a[data-modal-hash="${hash}"]`);
const path = a?.dataset.modalPath || localizeLink(getMetadata(`-${id}`));
return { id, path, isHash: hash === window.location.hash };
}

function closeModals(modals) {
const qModals = modals || Array.from(document.querySelectorAll('.dialog-modal'));
if (qModals?.length) {
const anchor = qModals.some((m) => m.classList.contains('anchor'));
qModals.forEach((modal) => {
if (modal.nextElementSibling?.classList.contains('modal-curtain')) {
modal.nextElementSibling.remove();
}
modal.remove();
document.querySelector(`[data-modal-hash="#${modal.id}"]`)?.focus();
});
if (anchor) { window.history.pushState('', document.title, `${window.location.pathname}${window.location.search}`); }
}
function closeModal(modal) {
const { id } = modal;

document.querySelectorAll(`#${id}`).forEach((mod) => {
if (mod.nextElementSibling?.classList.contains('modal-curtain')) {
mod.nextElementSibling.remove();
}
mod.remove();
document.querySelector(`[data-modal-hash="#${mod.id}"]`)?.focus();
});

const hashId = window.location.hash.replace('#', '');
if (hashId === modal.id) window.history.pushState('', document.title, `${window.location.pathname}${window.location.search}`);
}

function isElementInView(element) {
Expand All @@ -48,44 +41,39 @@ function isElementInView(element) {
);
}

function handleCustomModal(custom, dialog) {
dialog.id = custom.id;
dialog.classList.add(custom.class);
function getCustomModal(custom, dialog) {
if (custom.id) dialog.id = custom.id;
if (custom.class) dialog.classList.add(custom.class);
if (custom.closeEvent) {
dialog.addEventListener(custom.closeEvent, () => {
closeModals([dialog]);
closeModal(dialog);
});
}
return custom.content;
dialog.append(custom.content);
}

async function handleAnchorModal(el, dialog) {
const details = getDetails(el);
if (!details) return null;
async function getPathModal(path, dialog) {
const block = createTag('a', { href: path });
dialog.append(block);

dialog.id = details.id;
dialog.classList.add('anchor');
const { default: getFragment } = await import('../fragment/fragment.js');
await getFragment(block);
}

const linkBlock = document.createElement('a');
linkBlock.href = details.path;
export async function getModal(details, custom) {
if (!(details?.path || custom)) return null;

const { default: getFragment } = await import('../fragment/fragment.js');
await getFragment(linkBlock, dialog);
const { id } = details || custom;

return linkBlock;
}
const dialog = createTag('div', { class: 'dialog-modal', id });

export async function getModal(el, custom) {
const curtain = createTag('div', { class: 'modal-curtain is-open' });
const close = createTag('button', { class: 'dialog-close', 'aria-label': 'Close' }, CLOSE_ICON);
const dialog = document.createElement('div');
dialog.className = 'dialog-modal';
if (custom) getCustomModal(custom, dialog);
if (details) await getPathModal(details.path, dialog);

const content = custom ? handleCustomModal(custom, dialog) : await handleAnchorModal(el, dialog);
if (!content) return;
const close = createTag('button', { class: 'dialog-close', 'aria-label': 'Close' }, CLOSE_ICON);

const focusVisible = { focusVisible: true };
const focusablesOnLoad = [...dialog.querySelectorAll('a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]')];
const focusablesOnLoad = [...dialog.querySelectorAll(FOCUSABLES)];
const titleOnLoad = dialog.querySelector('h1, h2, h3, h4, h5');
let firstFocusable;

Expand Down Expand Up @@ -115,44 +103,55 @@ export async function getModal(el, custom) {
});

close.addEventListener('click', (e) => {
closeModals([dialog]);
closeModal(dialog);
e.preventDefault();
});

curtain.addEventListener('click', (e) => {
// on click outside of modal
if (e.target === curtain) {
closeModals([dialog]);
}
});

dialog.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeModals([dialog]);
closeModal(dialog);
}
});

dialog.append(close, content);
dialog.append(close);
document.body.append(dialog);
dialog.insertAdjacentElement('afterend', curtain);
firstFocusable.focus(focusVisible);

if (!dialog.classList.contains('curtain-off')) {
const curtain = createTag('div', { class: 'modal-curtain is-open' });
curtain.addEventListener('click', (e) => {
if (e.target === curtain) closeModal(dialog);
});
dialog.insertAdjacentElement('afterend', curtain);
}

return dialog;
}

// Deep link-based
export default function init(el) {
const { modalHash } = el.dataset;
if (window.location.hash === modalHash) {
return getModal(el);
const details = findDetails(window.location.hash, el);
if (details) return getModal(details);
}
return null;
}

// First import will cause this side effect (on purpose)
window.addEventListener('hashchange', () => {
// Event-based modal
window.addEventListener('modal:open', (e) => {
const details = findDetails(e.detail.hash);
if (details) getModal(details);
});

// Click-based modal
window.addEventListener('hashchange', (e) => {
if (!window.location.hash) {
closeModals();
const url = new URL(e.oldURL);
const dialog = document.querySelector(`.dialog-modal${url.hash}`);
if (dialog) closeModal(dialog);
} else {
getModal();
const details = findDetails(window.location.hash, null);
if (details) getModal(details);
}
});
8 changes: 8 additions & 0 deletions libs/blocks/section-metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Section Metadata

## Note
The card & modal-metadata blocks use this block. If making changes, please ensure there are no breaking changes for those use cases.

## Test pages
https://main--milo--adobecom.hlx.page/drafts/cmillar/section-container - Section Metadata
https://main--milo--adobecom.hlx.page/drafts/methomas/card - Card
51 changes: 19 additions & 32 deletions libs/blocks/section-metadata/section-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,34 @@ function handleBackground(div, section) {
}
}

function handleStyle(div, section) {
const value = div.textContent.toLowerCase();
const styles = value.split(', ').map((style) => style.replaceAll(' ', '-'));
export function handleStyle(text, section) {
if (section) {
if (!text) return;
const styles = text.split(', ').map((style) => style.replaceAll(' ', '-'));
section.classList.add(...styles);
}
}

function handleLayout(div, section) {
if (!(div || section)) return;
const layoutString = div.textContent.trim();
const layoutClass = `grid-template-columns-${layoutString.replaceAll(' | ', '-')}`;
function handleLayout(text, section) {
if (!(text || section)) return;
const layoutClass = `grid-template-columns-${text.replaceAll(' | ', '-')}`;
section.classList.add(layoutClass);
}

export const getSectionMetadata = (el) => {
if (!el) return {};
const metadata = {};
el.childNodes.forEach((node) => {
const key = node.children?.[0]?.textContent?.toLowerCase();
if (!key) return;
const val = node.children?.[1]?.textContent?.toLowerCase();
metadata[key] = val;
});
return metadata;
};
export const getMetadata = (el) => [...el.childNodes].reduce((rdx, row) => {
if (row.children) {
const key = row.children[0].textContent.trim().toLowerCase();
const content = row.children[1];
const text = content.textContent.trim().toLowerCase();
if (key && content) rdx[key] = { content, text };
}
return rdx;
}, {});

export default function init(el) {
const section = el.closest('.section');
if (!section) return;
const keyDivs = el.querySelectorAll(':scope > div > div:first-child');
keyDivs.forEach((div) => {
const valueDiv = div.nextElementSibling;
if (div.textContent === 'style' && valueDiv.textContent) {
handleStyle(valueDiv, section);
}
if (div.textContent === 'background') {
handleBackground(valueDiv, section);
}
if (div.textContent === 'layout') {
handleLayout(valueDiv, section);
}
});
const metadata = getMetadata(el);
if (metadata.style) handleStyle(metadata.style.text, section);
if (metadata.background) handleBackground(metadata.background.content, section);
if (metadata.layout) handleLayout(metadata.layout.text, section);
}
4 changes: 2 additions & 2 deletions libs/scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ const locales = {
};

const config = {
geoRouting: 'off',
fallbackRouting: 'off',
geoRouting: 'on',
fallbackRouting: 'on',
links: 'on',
imsClientId: 'milo',
codeRoot: '/libs',
Expand Down
1 change: 1 addition & 0 deletions libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const MILO_BLOCKS = [
'media',
'merch',
'modal',
'modal-metadata',
'pdf-viewer',
'quote',
'read-more',
Expand Down
8 changes: 0 additions & 8 deletions test/blocks/fragment/fragment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,6 @@ describe('Fragments', () => {
expect(h1).to.exist;
});

it('Loads a fragment into a parent', async () => {
const a = document.querySelector('.parent-link');
const parent = document.querySelector('.parent');
await getFragment(a, parent);
const h1 = document.querySelector('.parent h1');
expect(h1).to.exist;
});

it('Doesnt load a fragment', async () => {
const a = document.querySelector('a.bad');
await getFragment(a);
Expand Down
Loading