From f533f3d12b3ea3bd2edb3409d7060abb0ab2604e Mon Sep 17 00:00:00 2001 From: Mira Date: Mon, 11 Mar 2024 16:09:00 +0100 Subject: [PATCH] fixed height auto adjustment and iframe styles in modal --- libs/blocks/modal/modal.css | 20 ++-- libs/blocks/modal/modal.js | 69 +++---------- libs/blocks/modal/modal.merch.js | 71 +++++++++++++ test/blocks/modals/mocks/iframe.plain.html | 19 ++++ test/blocks/modals/modal.merch.test.js | 114 +++++++++++++++++++++ test/blocks/modals/modals.test.js | 110 +------------------- 6 files changed, 230 insertions(+), 173 deletions(-) create mode 100644 libs/blocks/modal/modal.merch.js create mode 100644 test/blocks/modals/mocks/iframe.plain.html create mode 100644 test/blocks/modals/modal.merch.test.js diff --git a/libs/blocks/modal/modal.css b/libs/blocks/modal/modal.css index a5b2deed5f8..79b57243258 100644 --- a/libs/blocks/modal/modal.css +++ b/libs/blocks/modal/modal.css @@ -25,11 +25,6 @@ z-index: 103; } -.dialog-modal.commerce-frame .fragment, -.dialog-modal.commerce-frame .section { - height: 100vh; -} - .dialog-modal.upgrade-flow-modal { height: 100%; width: 100%; @@ -193,7 +188,7 @@ } } -@media (max-width: 1200px) { +@media (max-width: 1199px) { .dialog-modal.commerce-frame { width: 100%; max-width: 100%; @@ -203,7 +198,11 @@ .dialog-modal.upgrade-flow-modal { border-radius: 0; } - + + .dialog-modal.commerce-frame .fragment, + .dialog-modal.commerce-frame .section { + height: 100vh; + } } @media (min-width: 1200px) { @@ -221,6 +220,11 @@ height: 850px; } + .dialog-modal.commerce-frame .fragment, + .dialog-modal.commerce-frame .section { + height: 100%; + } + .dialog-modal.commerce-frame .milo-iframe { padding-bottom: 0; height: 100%; @@ -237,7 +241,7 @@ max-height: 845px; } - .dialog-modal.commerce-frame.height-fit-content .milo-iframe iframe { + .dialog-modal.commerce-frame .milo-iframe iframe { height: 0%; } diff --git a/libs/blocks/modal/modal.js b/libs/blocks/modal/modal.js index ca4f8a61886..759ed52952c 100644 --- a/libs/blocks/modal/modal.js +++ b/libs/blocks/modal/modal.js @@ -9,10 +9,6 @@ const CLOSE_ICON = ` `; -const MOBILE_MAX = 599; -const TABLET_MAX = 1199; -let messageAbortController; -let resizeAbortController; export function findDetails(hash, el) { const id = hash.replace('#', ''); @@ -39,9 +35,6 @@ export function closeModal(modal) { const localeModal = id?.includes('locale-modal') ? 'localeModal' : 'milo'; const analyticsEventName = window.location.hash ? window.location.hash.replace('#', '') : localeModal; const closeEventAnalytics = new Event(`${analyticsEventName}:modalClose:buttonClose`); - // removing the 'message' and 'resize' event listener set for commerce modals - messageAbortController?.abort(); - resizeAbortController?.abort(); sendAnalytics(closeEventAnalytics); @@ -99,44 +92,6 @@ async function getPathModal(path, dialog) { await getFragment(block); } -function sendViewportDimensionsToiFrame(source) { - const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); - source.postMessage({ mobileMax: MOBILE_MAX, tabletMax: TABLET_MAX, viewportWidth }, '*'); -} - -export function sendViewportDimensionsOnRequest({ messageInfo, debounce }) { - const { data, source } = messageInfo || {}; - if (data !== 'viewportWidth' || !source || !debounce) return; - resizeAbortController = new AbortController(); - sendViewportDimensionsToiFrame(source); - window.addEventListener('resize', debounce(() => sendViewportDimensionsToiFrame(source), 10), { signal: resizeAbortController.signal }); -} - -/** For the modal height adjustment to work the following conditions must be met: - * 1. The modal must have classes 'commerce-frame height-fit-content'; - * 2. The iframe inside must send a postMessage with the contentHeight (a number of px or '100%); - */ -function adjustModalHeight({ contentHeight, dialog }) { - const iframe = dialog?.querySelector('iframe'); - const iframeWrapper = dialog?.querySelector('.milo-iframe'); - if (!contentHeight || !iframe || !iframeWrapper) return; - if (contentHeight === '100%') { - // the initial iframe height was set to 0 in CSS for the content height to be measured properly - iframe.style.height = '100%'; - iframeWrapper.style.height = contentHeight; - dialog.style.height = contentHeight; - } else { - const verticalMargins = 20; - const clientHeight = document.documentElement.clientHeight - verticalMargins; - if (clientHeight <= 0) return; - const newHeight = contentHeight > clientHeight ? clientHeight : contentHeight; - // the initial iframe height was set to 0 in CSS for the content height to be measured properly - iframe.style.height = '100%'; - iframeWrapper.style.height = `${newHeight}px`; - dialog.style.height = `${newHeight}px`; - } -} - export async function getModal(details, custom) { if (!(details?.path || custom)) return null; const { id } = details || custom; @@ -205,19 +160,19 @@ export async function getModal(details, custom) { [...document.querySelectorAll('header, main, footer')] .forEach((element) => element.setAttribute('aria-disabled', 'true')); } - if (dialog.classList.contains('commerce-frame')) { - const { debounce } = await import('../../utils/action.js'); - messageAbortController = new AbortController(); - window.addEventListener('message', (messageInfo) => { - if (dialog.classList.contains('height-fit-content')) { - adjustModalHeight({ contentHeight: messageInfo?.data?.contentHeight, dialog }); - } - /* If the page inside iFrame comes from another domain, it won't be able to retrieve - the viewport dimensions, so it sends a request to receive the viewport dimensions - from the parent window. */ - sendViewportDimensionsOnRequest({ debounce, messageInfo }); - }, { signal: messageAbortController.signal }); + + const iframe = dialog.querySelector('iframe'); + if (iframe) { + if (dialog.classList.contains('commerce-frame')) { + const { default: enableCommerceFrameFeatures } = await import('./modal.merch.js'); + await enableCommerceFrameFeatures({ dialog, iframe }); + } else { + /* Initially iframe height is set to 0% in CSS for the height auto adjustment feature. + For modals without the 'commerce-frame' class height auto adjustment is not applicable */ + iframe.style.height = '100%'; + } } + return dialog; } diff --git a/libs/blocks/modal/modal.merch.js b/libs/blocks/modal/modal.merch.js new file mode 100644 index 00000000000..cfb76125107 --- /dev/null +++ b/libs/blocks/modal/modal.merch.js @@ -0,0 +1,71 @@ +import { debounce } from '../../utils/action.js'; + +export const MOBILE_MAX = 599; +export const TABLET_MAX = 1199; + +export function adjustModalHeight(contentHeight) { + if (!window.location.hash) return; + const dialog = document.querySelector(window.location.hash); + const iframe = dialog?.querySelector('iframe'); + const iframeWrapper = dialog?.querySelector('.milo-iframe'); + if (!contentHeight || !iframe || !iframeWrapper) return; + if (contentHeight === '100%') { + iframe.style.height = '100%'; + iframeWrapper.style.removeProperty('height'); + dialog.style.removeProperty('height'); + } else { + const verticalMargins = 20; + const clientHeight = document.documentElement.clientHeight - verticalMargins; + if (clientHeight <= 0) return; + const newHeight = contentHeight > clientHeight ? clientHeight : contentHeight; + iframe.style.height = '100%'; + iframeWrapper.style.height = `${newHeight}px`; + dialog.style.height = `${newHeight}px`; + } +} + +export function sendViewportDimensionsToIframe(source) { + const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); + source.postMessage({ mobileMax: MOBILE_MAX, tabletMax: TABLET_MAX, viewportWidth }, '*'); +} + +export function sendViewportDimensionsOnRequest(source) { + sendViewportDimensionsToIframe(source); + window.addEventListener('resize', debounce(() => sendViewportDimensionsToIframe(source), 10)); +} + +function reactToMessage({ data, source }) { + if (data === 'viewportWidth' && source) { + /* If the page inside iframe comes from another domain, it won't be able to retrieve + the viewport dimensions, so it sends a request to receive the viewport dimensions + from the parent window. */ + sendViewportDimensionsOnRequest(source); + } + + if (data?.contentHeight) { + /* If the page inside iframe sends the postMessage with its content height, + we activate the height auto adjustment to eliminate the blank space at the bottom of the modal. + For this we set the iframe height to 0% in CSS to let the page inside iframe + to measure its content height properly. + Then we set the modal height to be the same as the content height we received. + For the modal height adjustment to work the following conditions must be met: + 1. The modal must have the class 'commerce-frame'; + 2. The page inside iframe must send a postMessage with the contentHeight (in px, or '100%); */ + adjustModalHeight(data?.contentHeight); + } +} + +export function adjustStyles({ dialog, iframe }) { + const isAutoHeightAdjustment = /\/mini-plans\/.*mid=ft.*web=1/.test(iframe.src); // matches e.g. https://www.adobe.com/mini-plans/photoshop.html?mid=ft&web=1 + if (isAutoHeightAdjustment) { + dialog.classList.add('height-fit-content'); + } else { + iframe.style.height = '100%'; + } +} + +export default async function enableCommerceFrameFeatures({ dialog, iframe }) { + if (!dialog || !iframe) return; + adjustStyles({ dialog, iframe }); + window.addEventListener('message', reactToMessage); +} diff --git a/test/blocks/modals/mocks/iframe.plain.html b/test/blocks/modals/mocks/iframe.plain.html new file mode 100644 index 00000000000..c6a6be46943 --- /dev/null +++ b/test/blocks/modals/mocks/iframe.plain.html @@ -0,0 +1,19 @@ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
diff --git a/test/blocks/modals/modal.merch.test.js b/test/blocks/modals/modal.merch.test.js new file mode 100644 index 00000000000..ea27cc862a3 --- /dev/null +++ b/test/blocks/modals/modal.merch.test.js @@ -0,0 +1,114 @@ +import { readFile, setViewport } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import enableCommerceFrameFeatures, { MOBILE_MAX, TABLET_MAX } from '../../../libs/blocks/modal/modal.merch.js'; +import { delay } from '../../helpers/waitfor.js'; + +document.body.innerHTML = await readFile({ path: './mocks/iframe.plain.html' }); +const dialog = document.querySelector('#first-dialog'); +const iframeWrapper = dialog.querySelector('.milo-iframe'); +const iframe = dialog.querySelector('iframe'); +const secondDialog = document.querySelector('#second-dialog'); +const secondIframeWrapper = secondDialog.querySelector('.milo-iframe'); +const secondIframe = secondDialog.querySelector('iframe'); + +describe('Modal dialog with a `commerce-frame` class', () => { + it('adjustStyles: sets the iframe height to 100% if height adjustment is not applicable', () => { + const originalSrc = iframe.getAttribute('src'); + iframe.setAttribute('src', 'https://www.adobe.com/somepage.html'); + enableCommerceFrameFeatures({ dialog, iframe }); + expect(dialog.classList.contains('height-fit-content')).to.be.false; + expect(iframe.style.height).to.equal('100%'); + iframe.style.removeProperty('height'); + iframe.setAttribute('src', originalSrc); + }); + + it('adjustStyles: sets the `height-fit-content` class if height adjustment is applicable', () => { + enableCommerceFrameFeatures({ dialog, iframe }); + expect(dialog.classList.contains('height-fit-content')).to.be.true; + expect(iframe.style.height).to.equal(''); + }); + + it('sends viewport dimensions upon request, and then on every resize', async () => { + sinon.spy(window, 'postMessage'); + enableCommerceFrameFeatures({ dialog, iframe }); + window.postMessage('viewportWidth'); + await delay(10); + expect(window.postMessage.calledWith({ + mobileMax: MOBILE_MAX, + tabletMax: TABLET_MAX, + viewportWidth: 800, + })).to.be.true; + + document.documentElement.setAttribute('style', 'width: 1200px'); + window.dispatchEvent(new Event('resize')); + await delay(10); + expect(window.postMessage.calledWith({ + mobileMax: MOBILE_MAX, + tabletMax: TABLET_MAX, + viewportWidth: 1200, + })).to.be.true; + document.documentElement.removeAttribute('style'); + }); + + it('adjusts modal height if height auto adjustment is applicable', async () => { + const contentHeight = { + desktop: 714, + mobile: '100%', + }; + await setViewport({ width: 1200, height: 1000 }); + window.location.hash = '#first-dialog'; + window.postMessage({ contentHeight: contentHeight.desktop }, '*'); + await delay(50); + expect(iframe.clientHeight).to.equal(contentHeight.desktop); + expect(iframeWrapper.clientHeight).to.equal(contentHeight.desktop); + expect(dialog.clientHeight).to.equal(contentHeight.desktop); + + await setViewport({ width: 320, height: 600 }); + window.postMessage({ contentHeight: contentHeight.mobile }, '*'); + await delay(50); + expect(iframe.style.height).to.equal(contentHeight.mobile); + expect(iframeWrapper.style.height).to.equal(''); + expect(dialog.style.height).to.equal(''); + window.location.hash = ''; + }); + + it('properly adjusts the modal height when there are two modals on the page', async () => { + const contentHeight = { + desktop1: 714, + desktop2: 600, + mobile: '100%', + }; + await setViewport({ width: 1200, height: 1000 }); + window.location.hash = '#first-dialog'; + window.postMessage({ contentHeight: contentHeight.desktop1 }, '*'); + await delay(50); + expect(iframe.clientHeight).to.equal(contentHeight.desktop1); + expect(iframeWrapper.clientHeight).to.equal(contentHeight.desktop1); + expect(dialog.clientHeight).to.equal(contentHeight.desktop1); + + await setViewport({ width: 320, height: 600 }); + window.postMessage({ contentHeight: contentHeight.mobile }, '*'); + await delay(50); + expect(iframe.style.height).to.equal(contentHeight.mobile); + expect(iframeWrapper.style.height).to.equal(''); + expect(dialog.style.height).to.equal(''); + window.location.hash = ''; + + await setViewport({ width: 1200, height: 1000 }); + window.location.hash = '#second-dialog'; + window.postMessage({ contentHeight: contentHeight.desktop2 }, '*'); + await delay(50); + expect(secondIframe.clientHeight).to.equal(contentHeight.desktop2); + expect(secondIframeWrapper.clientHeight).to.equal(contentHeight.desktop2); + expect(secondDialog.clientHeight).to.equal(contentHeight.desktop2); + + await setViewport({ width: 320, height: 600 }); + window.postMessage({ contentHeight: contentHeight.mobile }, '*'); + await delay(50); + expect(iframe.style.height).to.equal(contentHeight.mobile); + expect(iframeWrapper.style.height).to.equal(''); + expect(dialog.style.height).to.equal(''); + window.location.hash = ''; + }); +}); diff --git a/test/blocks/modals/modals.test.js b/test/blocks/modals/modals.test.js index 5643a61fd18..9a825b29535 100644 --- a/test/blocks/modals/modals.test.js +++ b/test/blocks/modals/modals.test.js @@ -1,9 +1,9 @@ -import { readFile, sendKeys, setViewport } from '@web/test-runner-commands'; +import { readFile, sendKeys } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; import { delay, waitForElement, waitForRemoval } from '../../helpers/waitfor.js'; +import init, { getModal } from '../../../libs/blocks/modal/modal.js'; document.body.innerHTML = await readFile({ path: './mocks/body.html' }); -const { default: init, getModal, sendViewportDimensionsOnRequest, closeModal } = await import('../../../libs/blocks/modal/modal.js'); describe('Modals', () => { it('Doesnt load modals on page load with no hash', async () => { @@ -154,23 +154,6 @@ describe('Modals', () => { await waitForRemoval('#title'); }); - it('checks if dialog modal has the 100% screen width when screen with is less than 1200', async () => { - await setViewport({ width: 600, height: 100 }); - window.location.hash = '#milo'; - await waitForElement('#milo'); - await getModal({ id: 'animate', path: '/cc-shared/fragments/trial-modals/animate', isHash: true }); - sendViewportDimensionsOnRequest({ data: 'viewportWidth', source: window }); - const dialogmodal = document.getElementsByClassName('dialog-modal')[0]; - dialogmodal.classList.add('commerce-frame'); - expect(window.innerWidth).to.equal(dialogmodal.offsetWidth); - }); - - it('checks if dialog modal is less than screen size if it does not have commerce frame class and screen size is less than 1200', async () => { - const dialogmodal = document.getElementsByClassName('dialog-modal')[0]; - dialogmodal.classList.remove('commerce-frame'); - expect(window.innerWidth).not.equal(dialogmodal.offsetWidth); - }); - it('does not error for a modal with a non-querySelector compliant hash', async () => { window.location.hash = '#milo=&'; @@ -186,93 +169,4 @@ describe('Modals', () => { // Test passing, means there was no error thrown await hashChangeTriggered; }); - - it('adjusts the modal height upon request', async () => { - const contentHeightDesktop = 714; - const contentHeightMobile = '100%'; - const content = new DocumentFragment(); - const iframeWrapper = document.createElement('div'); - const iframe = document.createElement('iframe'); - iframeWrapper.appendChild(iframe); - iframeWrapper.classList.add('milo-iframe'); - iframeWrapper.classList.add('modal'); - content.append(iframeWrapper); - getModal(null, { class: 'commerce-frame', id: 'modal-with-iframe', content, closeEvent: 'closeModal' }); - window.location.hash = '#modal-with-iframe'; - const modalWithIFrame = document.querySelector('#modal-with-iframe'); - modalWithIFrame.classList.add('height-fit-content'); - - await setViewport({ width: 1200, height: 1000 }); - window.postMessage({ contentHeight: contentHeightDesktop }, '*'); - await delay(50); - expect(iframe.clientHeight).to.equal(contentHeightDesktop); - expect(iframeWrapper.clientHeight).to.equal(contentHeightDesktop); - - await setViewport({ width: 320, height: 600 }); - window.postMessage({ contentHeight: contentHeightMobile }, '*'); - await delay(50); - expect(iframe.style.height).to.equal(contentHeightMobile); - expect(iframeWrapper.style.height).to.equal(contentHeightMobile); - - closeModal(modalWithIFrame); - }); - - it('properly adjusts the modal height when there are two modals on the page', async () => { - const firstContentHeight = 714; - const secondContentHeight = 600; - const contentHeightMobile = '100%'; - - const content = new DocumentFragment(); - const iframeWrapper = document.createElement('div'); - const iframe = document.createElement('iframe'); - iframeWrapper.appendChild(iframe); - iframeWrapper.classList.add('milo-iframe'); - iframeWrapper.classList.add('modal'); - content.append(iframeWrapper); - getModal(null, { class: 'commerce-frame', id: 'modal-with-iframe', content, closeEvent: 'closeModal' }); - window.location.hash = '#modal-with-iframe'; - const modalWithIFrame = document.querySelector('#modal-with-iframe'); - modalWithIFrame.classList.add('height-fit-content'); - - await setViewport({ width: 1200, height: 1000 }); - window.postMessage({ contentHeight: firstContentHeight }, '*'); - await delay(50); - expect(iframe.clientHeight).to.equal(firstContentHeight); - expect(iframeWrapper.clientHeight).to.equal(firstContentHeight); - - await setViewport({ width: 320, height: 600 }); - window.postMessage({ contentHeight: contentHeightMobile }, '*'); - await delay(50); - expect(iframe.style.height).to.equal(contentHeightMobile); - expect(iframeWrapper.style.height).to.equal(contentHeightMobile); - - closeModal(modalWithIFrame); - - const secondContent = new DocumentFragment(); - const secondIframeWrapper = document.createElement('div'); - const secondIframe = document.createElement('iframe'); - secondIframeWrapper.appendChild(secondIframe); - secondIframeWrapper.classList.add('milo-iframe'); - secondIframeWrapper.classList.add('modal'); - secondContent.append(secondIframeWrapper); - - getModal(null, { class: 'commerce-frame', id: 'modal-with-iframe-2', content: secondContent, closeEvent: 'closeModal' }); - window.location.hash = '#modal-with-iframe-2'; - const secondModalWithIFrame = document.querySelector('#modal-with-iframe-2'); - secondModalWithIFrame.classList.add('height-fit-content'); - - await setViewport({ width: 1200, height: 1000 }); - window.postMessage({ contentHeight: secondContentHeight }, '*'); - await delay(50); - expect(secondIframe.clientHeight).to.equal(secondContentHeight); - expect(secondIframeWrapper.clientHeight).to.equal(secondContentHeight); - - await setViewport({ width: 320, height: 600 }); - window.postMessage({ contentHeight: contentHeightMobile }, '*'); - await delay(50); - expect(secondIframe.style.height).to.equal(contentHeightMobile); - expect(secondIframeWrapper.style.height).to.equal(contentHeightMobile); - - closeModal(secondModalWithIFrame); - }); });