Skip to content

Commit

Permalink
MWPW-120020 - Event-driven Modals
Browse files Browse the repository at this point in the history
* Ability to open a modal with a custom event
* Ability to customize the look and behavior of a modal
* Turned GeoRouting on for Milo
  • Loading branch information
auniverseaway committed Jan 25, 2023
1 parent ec77160 commit 14e39da
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 142 deletions.
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
39 changes: 22 additions & 17 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 Expand Up @@ -179,23 +180,27 @@ function getExtension(path) {
}

export function localizeLink(href, originHostName = window.location.hostname) {
const url = new URL(href);
const relative = url.hostname === originHostName;
const processedHref = relative ? href.replace(url.origin, '') : href;
const { hash } = url;
if (hash === '#_dnt') return processedHref.split('#')[0];
const path = url.pathname;
const extension = getExtension(path);
const allowedExts = ['', 'html', 'json'];
if (!allowedExts.includes(extension)) return processedHref;
const { locale, locales, productionDomain } = getConfig();
if (!locale || !locales) return processedHref;
const isLocalizable = relative || productionDomain === url.hostname;
if (!isLocalizable) return processedHref;
const isLocalizedLink = path.startsWith(`/${LANGSTORE}`) || Object.keys(locales).some((loc) => loc !== '' && path.startsWith(`/${loc}/`));
if (isLocalizedLink) return processedHref;
const urlPath = `${locale.prefix}${path}${url.search}${hash}`;
return relative ? urlPath : `${url.origin}${urlPath}`;
try {
const url = new URL(href);
const relative = url.hostname === originHostName;
const processedHref = relative ? href.replace(url.origin, '') : href;
const { hash } = url;
if (hash === '#_dnt') return processedHref.split('#')[0];
const path = url.pathname;
const extension = getExtension(path);
const allowedExts = ['', 'html', 'json'];
if (!allowedExts.includes(extension)) return processedHref;
const { locale, locales, productionDomain } = getConfig();
if (!locale || !locales) return processedHref;
const isLocalizable = relative || productionDomain === url.hostname;
if (!isLocalizable) return processedHref;
const isLocalizedLink = path.startsWith(`/${LANGSTORE}`) || Object.keys(locales).some((loc) => loc !== '' && path.startsWith(`/${loc}/`));
if (isLocalizedLink) return processedHref;
const urlPath = `${locale.prefix}${path}${url.search}${hash}`;
return relative ? urlPath : `${url.origin}${urlPath}`;
} catch (e) {
return null;
}
}

export function loadStyle(href, callback) {
Expand Down
Loading

0 comments on commit 14e39da

Please sign in to comment.