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-140481: Delayed modal #1877

Closed
wants to merge 10 commits into from
42 changes: 42 additions & 0 deletions libs/blocks/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="2
</svg>`;
const MOBILE_MAX = 599;
const TABLET_MAX = 1199;
export const DISPLAY_MODE = {
oncePerPageLoad: 'pageload',
oncePerSession: 'session',
};
let messageAbortController;
let resizeAbortController;

Expand Down Expand Up @@ -246,3 +250,41 @@ window.addEventListener('hashchange', (e) => {
if (details) getModal(details);
}
});

export const decorateDelayedModalAnchor = ({ a, hash, pathname }) => {
if (!a || !hash || !pathname) return;
a.setAttribute('href', hash);
a.setAttribute('data-modal-hash', hash);
a.setAttribute('data-modal-path', pathname);
a.setAttribute('style', 'display: none');
a.classList.add('modal');
a.classList.add('link-block');
};

export const defineDelayedModalParams = (search) => {
if (!search) return {};
const urlParams = new URLSearchParams(search);
const delay = Number(urlParams?.get('delay'));
const displayMode = urlParams?.get('display');
return {
...(Number.isInteger(delay) && delay > 0 && { delay: delay * 1000 }),
...((displayMode === DISPLAY_MODE.oncePerPageLoad
|| displayMode === DISPLAY_MODE.oncePerSession) && { displayMode }),
};
};

export function showModalWithDelay({ delay, displayMode, hash }) {
if (!delay || !displayMode || !hash) return;
if (displayMode === DISPLAY_MODE.oncePerPageLoad) {
setTimeout(() => {
window.location.hash = hash;
}, delay);
} else if (displayMode === DISPLAY_MODE.oncePerSession) {
if (!window.sessionStorage.getItem('wasDelayedModalShown')) {
setTimeout(() => {
window.location.hash = hash;
window.sessionStorage.setItem('wasDelayedModalShown', true);
}, delay);
}
}
}
30 changes: 24 additions & 6 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createTag, getConfig, loadLink, loadScript, localizeLink, updateConfig,
} from '../../utils/utils.js';
import { getEntitlementMap } from './entitlements.js';
import { defineDelayedModalParams, decorateDelayedModalAnchor, showModalWithDelay } from '../../blocks/modal/modal.js';

/* c20 ignore start */
const PHONE_SIZE = window.screen.width < 768 || window.screen.height < 768;
Expand Down Expand Up @@ -105,18 +106,35 @@ export const preloadManifests = ({ targetManifests = [], persManifests = [] }) =

export const getFileName = (path) => path?.split('/').pop();

const createFrag = (el, url, manifestId) => {
let href = url;
export const parseUrl = (url) => {
if (!url) return {};
const parsed = {};
try {
const { pathname, search, hash } = new URL(url);
href = `${pathname}${search}${hash}`;
parsed.href = `${pathname}${search}${hash}`;
parsed.pathname = pathname;
parsed.hash = hash;
parsed.search = search;
} catch {
// ignore
// if target has this format: '/fragments/somepath'
parsed.href = url;
}
return parsed;
};

export const createFrag = (el, url, manifestId) => {
const { href, pathname, hash, search } = parseUrl(url);
const a = createTag('a', { href }, url);
if (manifestId) a.dataset.manifestId = manifestId;
let frag = createTag('p', undefined, a);
const isSection = el.parentElement.nodeName === 'MAIN';
const { delay, displayMode } = defineDelayedModalParams(search);
let frag = createTag('p', undefined, a);

if (delay && displayMode) {
frag = a;
decorateDelayedModalAnchor({ a, hash, pathname });
showModalWithDelay({ delay, displayMode, hash });
}
if (manifestId) a.dataset.manifestId = manifestId;
if (isSection) {
frag = createTag('div', undefined, frag);
}
Expand Down
99 changes: 99 additions & 0 deletions test/features/personalization/delayedModal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect } from '@esm-bundle/chai';
import { parseUrl } from '../../../libs/features/personalization/personalization.js';
import { defineDelayedModalParams, decorateDelayedModalAnchor, showModalWithDelay, DISPLAY_MODE } from '../../../libs/blocks/modal/modal.js';
import { delay, waitForElement } from '../../helpers/waitfor.js';

const hash = '#dm';

it('parses URL properly', () => {
expect(parseUrl()).to.deep.equal({});
expect(parseUrl('https://www.adobe.com/')).to.deep.equal({
hash: '',
href: '/',
pathname: '/',
search: '',
});
expect(parseUrl('/fragments/testpage')).to.deep.equal({ href: '/fragments/testpage' });
expect(parseUrl('https://www.adobe.com/testpage/?delay=1&display=pageload#dm')).to.deep.equal({
hash,
href: '/testpage/?delay=1&display=pageload#dm',
pathname: '/testpage/',
search: '?delay=1&display=pageload',
});
});

it('takes the delayed modal parameters from the URL', () => {
expect(defineDelayedModalParams()).to.deep.equal({});
expect(defineDelayedModalParams('?delay=invalid&display=invalid')).to.deep.equal({});
expect(defineDelayedModalParams('?delay=-1&display=pageload')).to.deep.equal({ displayMode: DISPLAY_MODE.oncePerPageLoad });
expect(defineDelayedModalParams('?delay=1&display=pageload')).to.deep.equal({
delay: 1000,
displayMode: DISPLAY_MODE.oncePerPageLoad,
});
});

it('add proper attributes and class names to the link', () => {
const a = document.createElement('a');
decorateDelayedModalAnchor({
a,
hash,
pathname: '/testpage/',
});
expect(a.getAttribute('href')).to.equal(hash);
expect(a.getAttribute('data-modal-hash')).to.equal(hash);
expect(a.getAttribute('data-modal-path')).to.equal('/testpage/');
expect(a.getAttribute('style')).to.equal('display: none');
expect(a.classList.contains('modal')).to.be.true;
expect(a.classList.contains('link-block')).to.be.true;
a.remove();
});

it('creates and opens the delayed modal', async () => {
const a = document.createElement('a');
decorateDelayedModalAnchor({
a,
hash,
pathname: '/testpage',
});
document.body.appendChild(a);
showModalWithDelay({
delay: '1',
displayMode: DISPLAY_MODE.oncePerPageLoad,
hash,
});
const delayedModal = await waitForElement(hash);
expect(delayedModal).to.exist;
delayedModal.remove();
a.remove();
window.location.hash = '';
});

it('does not open a delayed modal if it should be displayed once per session and was already displayed', async () => {
const a = document.createElement('a');
window.sessionStorage.removeItem('wasDelayedModalShown');
decorateDelayedModalAnchor({
a,
hash,
pathname: '/testpage',
});
document.body.appendChild(a);
showModalWithDelay({
delay: '1',
displayMode: DISPLAY_MODE.oncePerSession,
hash,
});
const delayedModal = await waitForElement(hash);
expect(delayedModal).to.exist;
expect(window.sessionStorage.getItem('wasDelayedModalShown')).to.equal('true');
delayedModal.remove();

showModalWithDelay({
delay: '1',
displayMode: DISPLAY_MODE.oncePerSession,
hash,
});
await delay(900);
expect(document.querySelector(hash)).to.not.exist;
a.remove();
window.sessionStorage.removeItem('wasDelayedModalShown');
});
26 changes: 25 additions & 1 deletion test/features/personalization/personalization.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { readFile } from '@web/test-runner-commands';
import { stub } from 'sinon';
import { getConfig, setConfig, loadBlock } from '../../../libs/utils/utils.js';
import initFragments from '../../../libs/blocks/fragment/fragment.js';
import { applyPers, normalizePath } from '../../../libs/features/personalization/personalization.js';
import { waitForElement } from '../../helpers/waitfor.js';
import { applyPers, createFrag, normalizePath } from '../../../libs/features/personalization/personalization.js';

document.head.innerHTML = await readFile({ path: './mocks/metadata.html' });
document.body.innerHTML = await readFile({ path: './mocks/personalization.html' });
Expand Down Expand Up @@ -31,6 +32,29 @@ describe('Functional Test', () => {
};
});

it('should create a fragment, and show a modal with delay', async () => {
const hash = '#testhash';
const url = 'https://adobe.com/testpage/?delay=1&display=pageload#testhash';
const manifestId = 'testManifestId';
const parentElement = document.createElement('div');
const el = document.createElement('div');
parentElement.appendChild(el);
document.body.appendChild(parentElement);
const frag = createFrag(el, url, manifestId);
expect(frag.nodeName).to.equal('A');
expect(frag.getAttribute('href')).to.equal(hash);
expect(frag.getAttribute('data-modal-hash')).to.equal(hash);
expect(frag.getAttribute('data-modal-path')).to.equal('/testpage/');
expect(frag.getAttribute('style')).to.equal('display: none');
expect(frag.classList.contains('modal')).to.be.true;
expect(frag.classList.contains('link-block')).to.be.true;
el.appendChild(frag);
const delayedModal = await waitForElement(hash);
expect(delayedModal).to.exist;
delayedModal.remove();
parentElement.remove();
});

it('replaceContent should replace an element with a fragment', async () => {
let manifestJson = await readFile({ path: './mocks/manifestReplace.json' });
manifestJson = JSON.parse(manifestJson);
Expand Down
Loading