From 81551bc22a949555fca41676cfd3c6e899ce5c91 Mon Sep 17 00:00:00 2001 From: jadutter <4691511+jadutter@users.noreply.github.com> Date: Mon, 9 Oct 2023 09:42:48 -0400 Subject: [PATCH 01/11] - added debug levels to control logging verbosity - added debug messages and listeners to view events and their order - adjusted how focus is handled when the modal opens and closes - added some aria attributes to increase accessibility --- src/components/message/Message.js | 17 +- .../content/US-EZP/parts/ContentWrapper.jsx | 9 +- .../modal/content/US/parts/Content.jsx | 4 +- src/components/modal/lib/hooks/helpers.js | 1 + src/components/modal/lib/providers/scroll.js | 23 +- src/components/modal/lib/utils.js | 28 ++- src/components/modal/parts/Overlay.jsx | 8 +- src/components/modal/v2/lib/hooks/helpers.js | 1 + .../modal/v2/lib/providers/scroll.js | 6 +- .../modal/v2/lib/providers/transition.js | 6 +- src/components/modal/v2/lib/utils.js | 27 ++- src/components/modal/v2/parts/BodyContent.jsx | 32 ++- src/components/modal/v2/parts/Header.jsx | 2 + src/components/modal/v2/parts/Overlay.jsx | 8 +- src/library/controllers/message/interface.js | 7 +- src/library/controllers/message/setup.js | 12 +- src/library/controllers/modal/interface.js | 12 +- src/library/controllers/modal/setup.js | 5 +- src/library/zoid/message/component.js | 38 +++- .../zoid/message/containerTemplate.jsx | 30 ++- src/library/zoid/modal/component.js | 34 ++- src/library/zoid/modal/containerTemplate.jsx | 38 +++- src/library/zoid/modal/prerenderTemplate.jsx | 97 ++++++-- src/library/zoid/treatments/component.js | 23 +- src/utils/constants.js | 162 ++++++++++++++ src/utils/debug.js | 210 +++++++++++++++++- src/utils/elements.js | 3 +- src/utils/events.js | 11 +- src/utils/experiments.js | 5 +- src/utils/global.js | 5 + src/utils/miscellaneous.js | 6 +- src/utils/sdk.js | 17 ++ .../src/components/message/Message.test.js | 7 +- 33 files changed, 760 insertions(+), 134 deletions(-) diff --git a/src/components/message/Message.js b/src/components/message/Message.js index 3e6086d293..ae88a7f1dd 100644 --- a/src/components/message/Message.js +++ b/src/components/message/Message.js @@ -9,7 +9,8 @@ import { parseObjFromEncoding, getRequestDuration, getTsCookieFromStorage, - getOrCreateDeviceID + getOrCreateDeviceID, + MESSAGE_DOM_EVENT } from '../../utils'; const Message = function ({ markup, meta, parentStyles, warnings }) { @@ -56,9 +57,9 @@ const Message = function ({ markup, meta, parentStyles, warnings }) { button.setAttribute('type', 'button'); - button.addEventListener('click', handleClick); - button.addEventListener('mouseover', handleHover); - button.addEventListener('focus', handleHover); + button.addEventListener(MESSAGE_DOM_EVENT.CLICK, handleClick); + button.addEventListener(MESSAGE_DOM_EVENT.MOUSEOVER, handleHover); + button.addEventListener(MESSAGE_DOM_EVENT.FOCUS, handleHover); button.style.display = 'block'; button.style.background = 'transparent'; @@ -88,7 +89,8 @@ const Message = function ({ markup, meta, parentStyles, warnings }) { warnings: serverData.warnings }); - window.addEventListener('focus', () => { + window.addEventListener(MESSAGE_DOM_EVENT.FOCUS, () => { + // when the iframe for the message receives focus, put the focus on the button button.focus(); }); @@ -178,7 +180,10 @@ const Message = function ({ markup, meta, parentStyles, warnings }) { ) .slice(1); - ppDebug('Updating message with new props...', { inZoid: true }); + ppDebug('Updating message with new props...', { + debugObj: { index: window.xprops.index }, + inZoid: true + }); request('GET', `${window.location.origin}/credit-presentment/smart/message?${query}`).then( ({ data: resData }) => { diff --git a/src/components/modal/content/US-EZP/parts/ContentWrapper.jsx b/src/components/modal/content/US-EZP/parts/ContentWrapper.jsx index 716bc316f6..867a90019c 100644 --- a/src/components/modal/content/US-EZP/parts/ContentWrapper.jsx +++ b/src/components/modal/content/US-EZP/parts/ContentWrapper.jsx @@ -6,6 +6,7 @@ import Header from '../../../parts/Header'; import Container from '../../../parts/Container'; import Button from '../../../parts/Button'; import { useApplyNow, useTransitionState } from '../../../lib'; +import { MODAL_DOM_EVENT } from '../../../../../utils'; const ContentWrapper = () => { const contentWrapper = useRef(); @@ -23,12 +24,12 @@ const ContentWrapper = () => { const handleApplyNowShow = () => !showApplyNow && setApplyNow(true); const handleApplyNowHide = () => showApplyNow && setApplyNow(false); - window.addEventListener('apply-now-visible', handleApplyNowShow); - window.addEventListener('apply-now-hidden', handleApplyNowHide); + window.addEventListener(MODAL_DOM_EVENT.APPLY_NOW_VISIBLE, handleApplyNowShow); + window.addEventListener(MODAL_DOM_EVENT.APPLY_NOW_HIDDEN, handleApplyNowHide); return () => { - window.removeEventListener('apply-now-visible', handleApplyNowShow); - window.removeEventListener('apply-now-hidden', handleApplyNowHide); + window.removeEventListener(MODAL_DOM_EVENT.APPLY_NOW_VISIBLE, handleApplyNowShow); + window.removeEventListener(MODAL_DOM_EVENT.APPLY_NOW_HIDDEN, handleApplyNowHide); }; }, [showApplyNow]); diff --git a/src/components/modal/content/US/parts/Content.jsx b/src/components/modal/content/US/parts/Content.jsx index 60796bab20..3f687803ab 100644 --- a/src/components/modal/content/US/parts/Content.jsx +++ b/src/components/modal/content/US/parts/Content.jsx @@ -152,7 +152,9 @@ const Content = ({ headerRef, contentWrapper }) => {
Subject to credit approval.

-
{tabsContent}
+
1 ? 's' : ''}.`}> + {tabsContent} +
); }; diff --git a/src/components/modal/lib/hooks/helpers.js b/src/components/modal/lib/hooks/helpers.js index 723e705f9f..9df900bb51 100644 --- a/src/components/modal/lib/hooks/helpers.js +++ b/src/components/modal/lib/hooks/helpers.js @@ -1,5 +1,6 @@ import { useEffect, useLayoutEffect, useRef } from 'preact/hooks'; +// useAutoFocus unused? export function useAutoFocus() { const ref = useRef(); diff --git a/src/components/modal/lib/providers/scroll.js b/src/components/modal/lib/providers/scroll.js index 440861e2da..e8a5c63f62 100644 --- a/src/components/modal/lib/providers/scroll.js +++ b/src/components/modal/lib/providers/scroll.js @@ -1,7 +1,13 @@ /** @jsx h */ import { h, createContext } from 'preact'; import { useEffect, useState, useContext, useCallback } from 'preact/hooks'; -import { getEventListenerPassiveOptionIfSupported } from '../../../../utils'; +import { + getEventListenerPassiveOptionIfSupported, + canDebug, + ppDebug, + DEBUG_CONDITIONS, + MODAL_DOM_EVENT +} from '../../../../utils'; const ScrollContext = createContext({ addScrollCallback: () => {}, @@ -34,16 +40,21 @@ export const ScrollProvider = ({ children, containerRef }) => { }; useEffect(() => { - const handleScroll = event => callbacks.forEach(callback => callback(event)); + const handleScroll = event => { + if (canDebug(DEBUG_CONDITIONS.MOUSEOVER)) { + ppDebug(`EVENT.MODAL.${window?.xprops?.index}.SCROLL`, { debugObj: event }); + } + callbacks.forEach(callback => callback(event)); + }; const passiveOption = getEventListenerPassiveOptionIfSupported(); - containerRef.current.addEventListener('scroll', handleScroll, passiveOption); - containerRef.current.addEventListener('touchmove', handleScroll, passiveOption); + containerRef.current.addEventListener(MODAL_DOM_EVENT.SCROLL, handleScroll, passiveOption); + containerRef.current.addEventListener(MODAL_DOM_EVENT.TOUCHMOVE, handleScroll, passiveOption); return () => { - containerRef.current.removeEventListener('scroll', handleScroll, passiveOption); - containerRef.current.removeEventListener('touchmove', handleScroll, passiveOption); + containerRef.current.removeEventListener(MODAL_DOM_EVENT.SCROLL, handleScroll, passiveOption); + containerRef.current.removeEventListener(MODAL_DOM_EVENT.TOUCHMOVE, handleScroll, passiveOption); }; }, [callbacks]); diff --git a/src/components/modal/lib/utils.js b/src/components/modal/lib/utils.js index 328fa12521..f94eca0ea9 100644 --- a/src/components/modal/lib/utils.js +++ b/src/components/modal/lib/utils.js @@ -1,6 +1,6 @@ import arrayFrom from 'core-js-pure/stable/array/from'; import objectEntries from 'core-js-pure/stable/object/entries'; -import { request, memoize, ppDebug } from '../../../utils'; +import { request, memoize, canDebug, DEBUG_CONDITIONS, ppDebug, MODAL_DOM_EVENT } from '../../../utils'; export const getContent = memoize( ({ @@ -55,15 +55,31 @@ export function setupTabTrap() { const tabArray = arrayFrom(document.querySelectorAll(focusableElementsString)).filter( node => window.getComputedStyle(node).visibility === 'visible' ); + let nextElement; // SHIFT + TAB if (e.shiftKey && document.activeElement === tabArray[0]) { + nextElement = tabArray[tabArray.length - 1]; + } else if (document.activeElement === tabArray[tabArray.length - 1]) { + // eslint-disable-next-line prefer-destructuring + nextElement = tabArray[0]; + } + + if (typeof nextElement !== 'undefined') { e.preventDefault(); - tabArray[tabArray.length - 1].focus(); - } else if (!e.shiftKey && document.activeElement === tabArray[tabArray.length - 1]) { - e.preventDefault(); - tabArray[0].focus(); + nextElement.focus(); + } + + if (canDebug(DEBUG_CONDITIONS.DOM_EVENTS)) { + // give the document 10ms to update before printing a debug log + // showing the currently selected element + setTimeout(() => { + ppDebug(`EVENT.MODAL.${window?.xprops?.index}.KEYDOWN.${e.shiftKey ? 'SHIFT_TAB' : 'TAB'}`, { + inZoid: true, + debugObj: nextElement ?? document.activeElement + }); + }, 10); } } } - window.addEventListener('keydown', trapTabKey); + window.addEventListener(MODAL_DOM_EVENT.KEYDOWN, trapTabKey); } diff --git a/src/components/modal/parts/Overlay.jsx b/src/components/modal/parts/Overlay.jsx index 2637d31a2e..d6dddf60bb 100644 --- a/src/components/modal/parts/Overlay.jsx +++ b/src/components/modal/parts/Overlay.jsx @@ -3,6 +3,7 @@ import { h, Fragment } from 'preact'; import { useEffect } from 'preact/hooks'; import { useTransitionState } from '../lib'; +import { canDebug, DEBUG_CONDITIONS, MODAL_DOM_EVENT, ppDebug } from '../../../utils'; const Overlay = ({ contentMaxWidth, contentMaxHeight }) => { const [, handleClose] = useTransitionState(); @@ -10,13 +11,16 @@ const Overlay = ({ contentMaxWidth, contentMaxHeight }) => { useEffect(() => { const handleEscapeKeyPress = evt => { if (evt.key === 'Escape' || evt.key === 'Esc' || evt.charCode === 27) { + if (canDebug(DEBUG_CONDITIONS.DOM_EVENTS)) { + ppDebug(`EVENT.MODAL.${window?.xprops?.index}.KEYUP.ESCAPE`, { inZoid: true, debugObj: evt }); + } handleClose('Escape Key'); } }; - window.addEventListener('keyup', handleEscapeKeyPress); + window.addEventListener(MODAL_DOM_EVENT.KEYUP, handleEscapeKeyPress); - return () => window.removeEventListener('keyup', handleEscapeKeyPress); + return () => window.removeEventListener(MODAL_DOM_EVENT.KEYUP, handleEscapeKeyPress); }); // Overlay must be split because the content wrapper fills diff --git a/src/components/modal/v2/lib/hooks/helpers.js b/src/components/modal/v2/lib/hooks/helpers.js index 723e705f9f..9df900bb51 100644 --- a/src/components/modal/v2/lib/hooks/helpers.js +++ b/src/components/modal/v2/lib/hooks/helpers.js @@ -1,5 +1,6 @@ import { useEffect, useLayoutEffect, useRef } from 'preact/hooks'; +// useAutoFocus unused? export function useAutoFocus() { const ref = useRef(); diff --git a/src/components/modal/v2/lib/providers/scroll.js b/src/components/modal/v2/lib/providers/scroll.js index e9502db5bb..1b575909b0 100644 --- a/src/components/modal/v2/lib/providers/scroll.js +++ b/src/components/modal/v2/lib/providers/scroll.js @@ -1,7 +1,7 @@ /** @jsx h */ import { h, createContext } from 'preact'; import { useEffect, useRef, useContext, useCallback } from 'preact/hooks'; -import { getEventListenerPassiveOptionIfSupported } from '../../../../../utils'; +import { getEventListenerPassiveOptionIfSupported, MODAL_DOM_EVENT } from '../../../../../utils'; const ScrollContext = createContext({ addScrollCallback: () => {}, @@ -34,10 +34,10 @@ export const ScrollProvider = ({ children, containerRef }) => { const handleScroll = event => callbacksRef.current.forEach(callback => callback(event)); const passiveOption = getEventListenerPassiveOptionIfSupported(); - containerRef.current.addEventListener('scroll', handleScroll, passiveOption); + containerRef.current.addEventListener(MODAL_DOM_EVENT.SCROLL, handleScroll, passiveOption); return () => { - containerRef.current.removeEventListener('scroll', handleScroll, passiveOption); + containerRef.current.removeEventListener(MODAL_DOM_EVENT.SCROLL, handleScroll, passiveOption); }; }, [containerRef.current]); diff --git a/src/components/modal/v2/lib/providers/transition.js b/src/components/modal/v2/lib/providers/transition.js index 4b073e09f1..b622004e30 100644 --- a/src/components/modal/v2/lib/providers/transition.js +++ b/src/components/modal/v2/lib/providers/transition.js @@ -27,8 +27,10 @@ export const TransitionStateProvider = ({ children }) => { * Particularly useful for those using screen readers and other accessibility functions. */ const focusCloseBtnOnModalOpen = () => { - const btn = document.querySelector('.close'); - btn?.focus(); + // give the document time to update before trying to focus the close button + setTimeout(() => { + document.querySelector('.close')?.focus(); + }, 10); }; useEffect(() => { diff --git a/src/components/modal/v2/lib/utils.js b/src/components/modal/v2/lib/utils.js index b35702ce8f..85730bcb17 100644 --- a/src/components/modal/v2/lib/utils.js +++ b/src/components/modal/v2/lib/utils.js @@ -1,7 +1,7 @@ import objectEntries from 'core-js-pure/stable/object/entries'; import arrayFrom from 'core-js-pure/stable/array/from'; import { isIosWebview, isAndroidWebview } from '@krakenjs/belter/src'; -import { request, memoize, ppDebug } from '../../../../utils'; +import { request, memoize, DEBUG_CONDITIONS, canDebug, ppDebug, MODAL_DOM_EVENT } from '../../../../utils'; export const getContent = memoize( ({ @@ -54,7 +54,7 @@ export const getContent = memoize( ) .slice(1); - ppDebug('Updating modal with new props...', { inZoid: true }); + ppDebug('Updating modal with new props...', { inZoid: true, debugObj: { index: window?.xprops?.index } }); return request('GET', `${window.location.origin}/credit-presentment/modalContent?${query}`).then( ({ data }) => data @@ -80,17 +80,32 @@ export function setupTabTrap() { const tabArray = arrayFrom(document.querySelectorAll(focusableElementsString)).filter( node => window.getComputedStyle(node).visibility === 'visible' ); + let nextElement; // SHIFT + TAB if (e.shiftKey && document.activeElement === tabArray[0]) { - e.preventDefault(); - tabArray[tabArray.length - 1].focus(); + nextElement = tabArray[tabArray.length - 1]; } else if (document.activeElement === tabArray[tabArray.length - 1]) { + // eslint-disable-next-line prefer-destructuring + nextElement = tabArray[0]; + } + if (typeof nextElement !== 'undefined') { e.preventDefault(); - tabArray[0].focus(); + nextElement.focus(); + } + + if (canDebug(DEBUG_CONDITIONS.DOM_EVENTS)) { + // give the document 10ms to update before printing a debug log + // showing the currently selected element + setTimeout(() => { + ppDebug(`EVENT.MODAL.${window?.xprops?.index}.KEYDOWN.${e.shiftKey ? 'SHIFT_TAB' : 'TAB'}`, { + inZoid: true, + debugObj: nextElement ?? document.activeElement + }); + }, 10); } } } - window.addEventListener('keydown', trapTabKey); + window.addEventListener(MODAL_DOM_EVENT.KEYDOWN, trapTabKey); } export function formatDateByCountry(country) { diff --git a/src/components/modal/v2/parts/BodyContent.jsx b/src/components/modal/v2/parts/BodyContent.jsx index b2d0878194..52b41ca8c7 100644 --- a/src/components/modal/v2/parts/BodyContent.jsx +++ b/src/components/modal/v2/parts/BodyContent.jsx @@ -13,6 +13,15 @@ import { import Header from './Header'; import { LongTerm, ShortTerm, NoInterest, ProductList, PayIn1 } from './views'; +const VIEW_IDS = { + // TODO: add an error view in case we receive an invalid view? + PAYPAL_CREDIT_NO_INTEREST: 'PAYPAL_CREDIT_NO_INTEREST', + PAY_LATER_LONG_TERM: 'PAY_LATER_LONG_TERM', + PAY_LATER_PAY_IN_1: 'PAY_LATER_PAY_IN_1', + PAY_LATER_SHORT_TERM: 'PAY_LATER_SHORT_TERM', + PRODUCT_LIST: 'PRODUCT_LIST' +}; + const BodyContent = () => { const { views } = useServerData(); const { offer } = useXProps(); @@ -29,12 +38,12 @@ const BodyContent = () => { let defaultViewName; - const productViews = views.filter(view => view?.meta?.product !== 'PRODUCT_LIST'); - const hasProductList = views.find(view => view?.meta?.product === 'PRODUCT_LIST'); + const productViews = views.filter(view => view?.meta?.product !== VIEW_IDS.PRODUCT_LIST); + const hasProductList = views.find(view => view?.meta?.product === VIEW_IDS.PRODUCT_LIST); if (productViews?.length === 1) { defaultViewName = productViews[0]?.meta?.product; } else if (productViews?.length > 1 && hasProductList) { - defaultViewName = 'PRODUCT_LIST'; + defaultViewName = VIEW_IDS.PRODUCT_LIST; } else if (productViews?.length > 1 && !hasProductList) { defaultViewName = productViews[0]?.meta?.product; } @@ -49,7 +58,7 @@ const BodyContent = () => { const { headline, subheadline, qualifyingSubheadline = '', closeButtonLabel } = content; const isQualifying = productMeta?.qualifying; - const openProductList = () => setViewName('PRODUCT_LIST'); + const openProductList = () => setViewName(VIEW_IDS.PRODUCT_LIST); useDidUpdateEffect(() => { scrollTo(0); // Reset scroll position to top when view changes @@ -71,13 +80,13 @@ const BodyContent = () => { // Add views to viewComponents object where the keys are the product name and the values are the view component const viewComponents = { - PAYPAL_CREDIT_NO_INTEREST: , - PAY_LATER_LONG_TERM: , - PAY_LATER_PAY_IN_1: , - PAY_LATER_SHORT_TERM: ( + [VIEW_IDS.PAYPAL_CREDIT_NO_INTEREST]: , + [VIEW_IDS.PAY_LATER_LONG_TERM]: , + [VIEW_IDS.PAY_LATER_PAY_IN_1]: , + [VIEW_IDS.PAY_LATER_SHORT_TERM]: ( ), - PRODUCT_LIST: + [VIEW_IDS.PRODUCT_LIST]: }; // IMPORTANT: These elements cannot be nested inside of other elements. @@ -95,7 +104,10 @@ const BodyContent = () => { viewName={viewName} />
-
+
{viewComponents[viewName]}
diff --git a/src/components/modal/v2/parts/Header.jsx b/src/components/modal/v2/parts/Header.jsx index 2f0ba9700f..22ed4846d4 100644 --- a/src/components/modal/v2/parts/Header.jsx +++ b/src/components/modal/v2/parts/Header.jsx @@ -56,6 +56,8 @@ const Header = ({ aria-label={closeButtonLabel} type="button" id="close-btn" + autofocus + aria-keyshortcuts="escape" onClick={() => handleClose('Close Button')} > diff --git a/src/components/modal/v2/parts/Overlay.jsx b/src/components/modal/v2/parts/Overlay.jsx index 79644a41d9..31706caf52 100644 --- a/src/components/modal/v2/parts/Overlay.jsx +++ b/src/components/modal/v2/parts/Overlay.jsx @@ -2,6 +2,7 @@ import { h } from 'preact'; import { useEffect } from 'preact/hooks'; +import { canDebug, DEBUG_CONDITIONS, MODAL_DOM_EVENT, ppDebug } from '../../../../utils'; import { useTransitionState } from '../lib'; const Overlay = () => { @@ -10,13 +11,16 @@ const Overlay = () => { useEffect(() => { const handleEscapeKeyPress = evt => { if (evt.key === 'Escape' || evt.key === 'Esc' || evt.charCode === 27) { + if (canDebug(DEBUG_CONDITIONS.DOM_EVENTS)) { + ppDebug(`EVENT.MODAL.${window?.xprops?.index}.KEYUP.ESCAPE`, { inZoid: true, debugObj: evt }); + } handleClose('Escape Key'); } }; - window.addEventListener('keyup', handleEscapeKeyPress); + window.addEventListener(MODAL_DOM_EVENT.KEYUP, handleEscapeKeyPress); - return () => window.removeEventListener('keyup', handleEscapeKeyPress); + return () => window.removeEventListener(MODAL_DOM_EVENT.KEYUP, handleEscapeKeyPress); }); // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions diff --git a/src/library/controllers/message/interface.js b/src/library/controllers/message/interface.js index 705ee6ba1a..50a0fb710e 100644 --- a/src/library/controllers/message/interface.js +++ b/src/library/controllers/message/interface.js @@ -14,7 +14,8 @@ import { PERFORMANCE_MEASURE_KEYS, globalEvent, ppDebug, - awaitTreatments + awaitTreatments, + GLOBAL_EVENT } from '../../../utils'; import { getMessageComponent } from '../../zoid/message'; @@ -167,7 +168,7 @@ export default (options = {}) => ({ ); return render(container) - .then(() => globalEvent.trigger('render')) + .then(() => globalEvent.trigger(GLOBAL_EVENT.RENDER)) .then(resolve); } @@ -193,7 +194,7 @@ export default (options = {}) => ({ ); return updateProps(updatedMessageProps) - .then(() => globalEvent.trigger('render')) + .then(() => globalEvent.trigger(GLOBAL_EVENT.RENDER)) .then(resolve); } catch (err) { // We only want console.warn to be called once diff --git a/src/library/controllers/message/setup.js b/src/library/controllers/message/setup.js index 1ad011a27f..bb816a01db 100644 --- a/src/library/controllers/message/setup.js +++ b/src/library/controllers/message/setup.js @@ -7,9 +7,12 @@ import { getPartnerAccount, getInsertionObserver, isZoidComponent, + canDebug, ppDebug, + DEBUG_CONDITIONS, getOverflowObserver, - ensureTreatments + ensureTreatments, + PARENT_DOM_EVENT } from '../../../utils'; import Messages from './adapter'; import { getMessageComponent } from '../../zoid/message'; @@ -84,12 +87,13 @@ export default function setup() { subtree: true, attributeFilter: ['data-pp-message'] }); - - ppDebug(`DOMContentLoaded at ${new Date().toLocaleString()}`); + if (canDebug(DEBUG_CONDITIONS.DOM_EVENTS)) { + ppDebug(`EVENT.GLOBAL.DOM_CONTENT_LOADED - ${new Date().toLocaleString()}`); + } }; if (document.readyState === 'loading') { - window.addEventListener('DOMContentLoaded', handleContentLoaded); + window.addEventListener(PARENT_DOM_EVENT.DOM_CONTENT_LOADED, handleContentLoaded); } else { handleContentLoaded(); } diff --git a/src/library/controllers/modal/interface.js b/src/library/controllers/modal/interface.js index f182c308e6..cbd8a6b35f 100644 --- a/src/library/controllers/modal/interface.js +++ b/src/library/controllers/modal/interface.js @@ -14,7 +14,9 @@ import { addPerformanceMeasure, PERFORMANCE_MEASURE_KEYS, globalEvent, - getTopWindow + getTopWindow, + MODAL_EVENT, + GLOBAL_EVENT } from '../../../utils'; import { getModalComponent } from '../../zoid/modal'; @@ -81,7 +83,7 @@ const memoizedModal = memoizeOnProps( Object.assign(zoidComponent, zoidComponent.clone()); } - const modalReady = new ZalgoPromise(resolve => zoidComponent.event.once('ready', resolve)); + const modalReady = new ZalgoPromise(resolve => zoidComponent.event.once(MODAL_EVENT.READY, resolve)); zoidComponent.state.renderStart = getCurrentTime(); @@ -101,7 +103,7 @@ const memoizedModal = memoizeOnProps( modalReady ]) ) - .then(() => globalEvent.trigger('modal-render')); + .then(() => globalEvent.trigger(GLOBAL_EVENT.MODAL_RENDER)); return renderProm; }; @@ -146,7 +148,7 @@ const memoizedModal = memoizeOnProps( // Tells containerTemplate to show the prerender modal as soon as possible if zoid has not // rendered anything yet and the show/hide events are not hooked up yet zoidComponent.state.open = true; - zoidComponent.event.trigger('modal-show'); + zoidComponent.event.trigger(MODAL_EVENT.MODAL_SHOW); return renderProm; }; @@ -154,7 +156,7 @@ const memoizedModal = memoizeOnProps( const hideModal = () => { renderProm = renderModal('body'); - zoidComponent.event.trigger('modal-hide'); + zoidComponent.event.trigger(MODAL_EVENT.MODAL_HIDE); return renderProm; }; diff --git a/src/library/controllers/modal/setup.js b/src/library/controllers/modal/setup.js index bf24513ce3..bb38508a2b 100644 --- a/src/library/controllers/modal/setup.js +++ b/src/library/controllers/modal/setup.js @@ -4,7 +4,8 @@ import { awaitDOMContentLoaded, getAllBySelector, objectMerge, - isZoidComponent + isZoidComponent, + MODAL_DOM_EVENT } from '../../../utils'; import Modal from './interface'; import { getModalComponent } from '../../zoid/modal'; @@ -45,7 +46,7 @@ export default function setup() { attachEls.forEach(el => { el.setAttribute('tabindex', 0); - el.addEventListener('click', () => modal.show(el)); + el.addEventListener(MODAL_DOM_EVENT.CLICK, () => modal.show(el)); }); } } diff --git a/src/library/zoid/message/component.js b/src/library/zoid/message/component.js index 626c09c1ba..e1a0f192bd 100644 --- a/src/library/zoid/message/component.js +++ b/src/library/zoid/message/component.js @@ -1,5 +1,4 @@ import stringStartsWith from 'core-js-pure/stable/string/starts-with'; -import { SDK_SETTINGS } from '@paypal/sdk-constants/src'; import { ZalgoPromise } from '@krakenjs/zalgo-promise/src'; import { uniqueID, getCurrentScriptUID } from '@krakenjs/belter/src'; import { create } from '@krakenjs/zoid/src'; @@ -24,11 +23,15 @@ import { getNonce, ppDebug, isScriptBeingDestroyed, - getScriptAttributes, + getPartnerAttributionId, getDevTouchpoint, getMerchantConfig, getLocalTreatments, - getTsCookieFromStorage + getTsCookieFromStorage, + MESSAGE_EVENT, + getDebugLevel, + canDebug, + DEBUG_CONDITIONS } from '../../../utils'; import validate from './validation'; import containerTemplate from './containerTemplate'; @@ -134,6 +137,9 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => const { onClick } = props; return ({ meta }) => { + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MESSAGE.${props.index}.onClick`, { debugObj: { meta } }); + } const { modal, index, @@ -193,6 +199,9 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => let hasHovered = false; return ({ meta }) => { + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MESSAGE.${index}.onHover`, { debugObj: { meta } }); + } if (!hasHovered) { hasHovered = true; logger.track({ @@ -214,6 +223,11 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => value: ({ props }) => { const { onReady } = props; return ({ meta, activeTags, ts, requestDuration, messageRequestId }) => { + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MESSAGE.${props.index}.onReady`, { + debugObj: { meta, activeTags, ts, requestDuration, messageRequestId } + }); + } const { account, merchantId, index, modal, getContainer } = props; const { trackingDetails, offerType, ppDebugId } = meta; const partnerClientId = merchantId && account.slice(10); // slice is to remove the characters 'client-id:' from account name @@ -286,10 +300,13 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => const { onMarkup } = props; return ({ styles, warnings, ...rest }) => { + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MESSAGE.${props.index}.onMarkup`); + } const { getContainer } = props; if (typeof styles !== 'undefined') { - event.trigger('styles', { styles }); + event.trigger(MESSAGE_EVENT.STYLES, { styles }); } if (warnings) { @@ -313,6 +330,9 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => // Handle moving the iframe around the DOM return () => { + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MESSAGE.${props.index}.onDestroy`); + } const CLEAN_UP_DELAY = 0; const { getContainer } = props; const { messagesMap } = getGlobalState(); @@ -432,10 +452,10 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => } }, debug: { - type: 'boolean', + type: 'number', queryParam: 'pp_debug', required: false, - value: () => (/(\?|&)pp_debug=true(&|$)/.test(window.location.search) ? true : undefined) + value: getDebugLevel }, messageLocation: { type: 'string', @@ -453,10 +473,8 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () => type: 'string', queryParam: true, required: false, - value: () => (getScriptAttributes() ?? {})[SDK_SETTINGS.PARTNER_ATTRIBUTION_ID] ?? null, - debug: ppDebug( - `Partner Attribution ID: ${(getScriptAttributes() ?? {})[SDK_SETTINGS.PARTNER_ATTRIBUTION_ID]}` - ) + value: getPartnerAttributionId, + debug: ppDebug(`Partner Attribution ID: ${getPartnerAttributionId()}`) }, devTouchpoint: { type: 'boolean', diff --git a/src/library/zoid/message/containerTemplate.jsx b/src/library/zoid/message/containerTemplate.jsx index 3f6b762465..cce9bc9fdb 100644 --- a/src/library/zoid/message/containerTemplate.jsx +++ b/src/library/zoid/message/containerTemplate.jsx @@ -1,18 +1,36 @@ /** @jsx node */ import { node, dom } from '@krakenjs/jsx-pragmatic/src'; -import { EVENT } from '@krakenjs/zoid/src'; -import { getOverflowObserver, createTitleGenerator } from '../../../utils'; +import { + getOverflowObserver, + createTitleGenerator, + canDebug, + ppDebug, + DEBUG_CONDITIONS, + MESSAGE_EVENT +} from '../../../utils'; const getTitle = createTitleGenerator(); export default ({ uid, frame, prerenderFrame, doc, event, props, container }) => { - event.on(EVENT.RENDERED, () => { + if (canDebug(DEBUG_CONDITIONS.PROPS)) { + ppDebug(`EVENT.MESSAGE.${props.index}.PROPS`, { debugObj: props }); + } + if (canDebug(DEBUG_CONDITIONS.EVENT_EMITTERS)) { + ppDebug(`EVENT_EMITTER.MESSAGE.${props.index}`, { debugObj: event }); + } + if (canDebug(DEBUG_CONDITIONS.ZOID_EVENTS) && typeof event?.on !== 'undefined') { + Object.entries(MESSAGE_EVENT).forEach(([eventId, eventName]) => + event.on(eventName, debugObj => ppDebug(`EVENT.MESSAGE.${props.index}.${eventId}`, { debugObj })) + ); + } + + event.on(MESSAGE_EVENT.RENDERED, () => { prerenderFrame.parentNode.removeChild(prerenderFrame); }); const setupAutoResize = el => { - event.on(EVENT.RESIZE, ({ width, height }) => { + event.on(MESSAGE_EVENT.RESIZE, ({ width, height }) => { if (width !== 0 || height !== 0) { if (props.style.layout === 'flex') { // Ensure height property does not exist for flex especially when swapping from text to flex @@ -33,7 +51,7 @@ export default ({ uid, frame, prerenderFrame, doc, event, props, container }) => if (el.__hasResizedBefore__) { // The styles event will fire first before the resize event for the initial render - event.once('styles', () => { + event.once(MESSAGE_EVENT.STYLES, () => { getOverflowObserver().then(observer => { observer.observe(el); // The observer will immediately check the element once, then unsubscribe }); @@ -60,7 +78,7 @@ export default ({ uid, frame, prerenderFrame, doc, event, props, container }) => } `; - event.on('styles', ({ styles }) => { + event.on(MESSAGE_EVENT.STYLES, ({ styles }) => { if (typeof styles === 'string') { const style = container.querySelector(`#${uid} style`); diff --git a/src/library/zoid/modal/component.js b/src/library/zoid/modal/component.js index 294cae1436..17b94bf999 100644 --- a/src/library/zoid/modal/component.js +++ b/src/library/zoid/modal/component.js @@ -25,7 +25,11 @@ import { ppDebug, getStandardProductOffer, getDevTouchpoint, - getTsCookieFromStorage + getTsCookieFromStorage, + MODAL_EVENT, + canDebug, + getDebugLevel, + DEBUG_CONDITIONS } from '../../../utils'; import validate from '../message/validation'; import containerTemplate from './containerTemplate'; @@ -146,6 +150,9 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => return ({ linkName, src }) => { const { index, refIndex } = props; + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MODAL.${index}.onClick`, { debugObj: { refIndex, linkName, src } }); + } logger.track({ index, @@ -174,6 +181,9 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => return ({ value }) => { const { index, refIndex } = props; + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MODAL.${index}.onCalculate`, { debugObj: { refIndex, value } }); + } logger.track({ index, @@ -200,6 +210,12 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => return () => { const { index, refIndex, src = 'show' } = props; + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MODAL.${index}.onShow`, { + debugObj: { refIndex, src, focus: document.activeElement } + }); + } + logger.track({ index, refIndex, @@ -227,8 +243,11 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => if (typeof linkName === 'undefined') return; const { index, refIndex } = props; + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MODAL.${index}.onClose`, { debugObj: { refIndex, linkName } }); + } - event.trigger('modal-hide'); + event.trigger(MODAL_EVENT.MODAL_HIDE); logger.track({ index, @@ -252,6 +271,11 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => // Fired anytime we fetch new content (e.g. amount change) return ({ products, meta, ts }) => { const { index, offer, merchantId, account, refIndex, messageRequestId } = props; + if (canDebug(DEBUG_CONDITIONS.PROP_EVENTS)) { + ppDebug(`EVENT.MODAL.${index}.onReady`, { + debugObj: { offer, merchantId, account, refIndex, messageRequestId, products, meta, ts } + }); + } const { renderStart, show, hide } = state; const { trackingDetails, ppDebugId } = meta; const partnerClientId = merchantId && account.slice(10); // slice is to remove the characters 'client-id:' from account name @@ -321,7 +345,7 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => // to determine if a modal is able to be displayed or not. // Primary use-case is a standalone modal state.products = Array.isArray(products) && products.map(getStandardProductOffer); // eslint-disable-line no-param-reassign - event.trigger('ready'); + event.trigger(MODAL_EVENT.READY); }; } }, @@ -397,10 +421,10 @@ export default createGlobalVariableGetter('__paypal_credit_modal__', () => } }, debug: { - type: 'boolean', + type: 'number', queryParam: 'pp_debug', required: false, - value: () => (/(\?|&)pp_debug=true(&|$)/.test(window.location.search) ? true : undefined) + value: getDebugLevel }, stageTag: { type: 'string', diff --git a/src/library/zoid/modal/containerTemplate.jsx b/src/library/zoid/modal/containerTemplate.jsx index 70961092a6..18bb7eb169 100644 --- a/src/library/zoid/modal/containerTemplate.jsx +++ b/src/library/zoid/modal/containerTemplate.jsx @@ -4,14 +4,32 @@ import { destroyElement } from '@krakenjs/belter/src'; import { node, dom } from '@krakenjs/jsx-pragmatic/src'; import { ZalgoPromise } from '@krakenjs/zalgo-promise/src'; -import { EVENT } from '@krakenjs/zoid/src'; -import { createTitleGenerator, viewportHijack } from '../../../utils'; +import { + createTitleGenerator, + viewportHijack, + canDebug, + DEBUG_CONDITIONS, + ppDebug, + MODAL_EVENT, + MODAL_DOM_EVENT +} from '../../../utils'; const TRANSITION_DELAY = 300; const getTitle = createTitleGenerator(); -export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNonce }, context }) => { +export default ({ uid, frame, prerenderFrame, doc, event, state, props, context }) => { + const { cspNonce } = props; + if (canDebug(DEBUG_CONDITIONS.PROPS)) { + ppDebug(`EVENT.MODAL.${props.index}.PROPS`, { debugObj: props }); + } + if (canDebug(DEBUG_CONDITIONS.ZOID_EVENTS) && typeof event?.on !== 'undefined') { + // this `event` bus is identical to the one received by the prerender modal, + // so we don't need a debug statement for it + Object.entries(MODAL_EVENT).forEach(([eventId, eventName]) => + event.on(eventName, data => ppDebug(`EVENT.MODAL.${props.index}.${eventId}`, { debugObj: { props, data } })) + ); + } // We render the modal as a popup when attempting to render the modal inside another IFrame. // In this scenario we can skip creating container elements and transitions since we // cannot overlay across the entire screen @@ -47,6 +65,7 @@ export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNon replaceViewport(); setTimeout(() => { wrapper.classList.add(CLASS.HIDDEN); + wrapper.querySelector('iframe')?.blur(); }, TRANSITION_DELAY); }; @@ -60,17 +79,20 @@ export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNon ZalgoPromise.delay(TRANSITION_DELAY) .then(() => overlay.classList.add(CLASS.TRANSITION)) .then(() => ZalgoPromise.delay(TRANSITION_DELAY)) - .then(() => destroyElement(prerenderFrame)); + .then(() => destroyElement(prerenderFrame)) + .then(() => event.trigger(MODAL_EVENT.PRERENDER_MODAL_DESTROY)) + .then(() => wrapper.querySelector('iframe')?.focus()); }; + // When the show function was called before zoid had a chance to render if (state.open) { handleShow(); } - event.on('modal-show', handleShow); - event.on('modal-hide', handleHide); - event.on(EVENT.RENDERED, handleTransition); - window.addEventListener('keyup', handleEscape); + event.on(MODAL_EVENT.MODAL_SHOW, handleShow); + event.on(MODAL_EVENT.MODAL_HIDE, handleHide); + event.on(MODAL_EVENT.RENDERED, handleTransition); + window.addEventListener(MODAL_DOM_EVENT.KEYUP, handleEscape); }; const fullScreen = position => diff --git a/src/library/zoid/modal/prerenderTemplate.jsx b/src/library/zoid/modal/prerenderTemplate.jsx index 50690e6dac..6efe6818a9 100644 --- a/src/library/zoid/modal/prerenderTemplate.jsx +++ b/src/library/zoid/modal/prerenderTemplate.jsx @@ -1,15 +1,36 @@ -/* eslint-disable eslint-comments/disable-enable-pair */ -/* eslint-disable no-param-reassign */ -/* eslint-disable react/self-closing-comp */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/control-has-associated-label */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ /** @jsx node */ import { node, dom } from '@krakenjs/jsx-pragmatic/src'; import { Spinner } from '@paypal/common-components'; import { ZalgoPromise } from '@krakenjs/zalgo-promise/src'; +import { canDebug, ppDebug, DEBUG_CONDITIONS, MODAL_EVENT } from '../../../utils'; export default ({ doc, props, event }) => { + if (canDebug(DEBUG_CONDITIONS.EVENT_EMITTERS)) { + ppDebug(`EVENT_EMITTER.MODAL.${props.index}`, { debugObj: event }); + } + if (canDebug(DEBUG_CONDITIONS.ZOID_EVENTS) && typeof event?.on !== 'undefined') { + /** + * Because the prerender modal and modal use the same event bus, + * we cannot call `event.reset()` to clear the debug listeners for + * the prerender modal without clearing the listeners for the modal; + * instead, we'll use this variable to stop prerender modal logs from + * firing after the PRERENDER_MODAL_DESTROY event. + * + * Moreover, belter doesn't appear to provide a `removeListeners` method + * @see {@link https://github.com/krakenjs/belter/blob/main/src/util.js#L798 belter EventEmitter} + */ + let exists = false; + Object.entries(MODAL_EVENT).forEach(([eventId, eventName]) => { + event.on(eventName, data => { + if (exists) { + ppDebug(`EVENT.PRERENDER_MODAL.${props.index}.${eventId}`, { debugObj: data }); + } + }); + }); + event.on(MODAL_EVENT.PRERENDER_MODAL_DESTROY, () => { + exists = true; + }); + } const ERROR_DELAY = 15000; const styles = ` @font-face { @@ -84,7 +105,6 @@ export default ({ doc, props, event }) => { position: relative !important; } .close-button > button { - background-image: url("data:image/svg+xml,%3Csvg width='48' height='48' viewBox='0 0 44 44' fill='transparent' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0L0 12' transform='translate(12 12)' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M0 0L12 12' transform='translate(12 12)' stroke='white' stroke-width='2' stroke-linecap='round' /%3E%3C/svg%3E"); background-color: transparent; width: 48px; height: 48px; @@ -95,6 +115,13 @@ export default ({ doc, props, event }) => { position: absolute; z-index: 50; right: 0; + margin-right: 2px; + margin-top: 2px; + } + + .close-button > button > svg { + width: 48px; + height: 48px; } .error{ @@ -120,19 +147,26 @@ export default ({ doc, props, event }) => { `; - const closeModal = () => event.trigger('modal-hide'); + const closeModal = () => event.trigger(MODAL_EVENT.MODAL_HIDE); const checkForErrors = element => { ZalgoPromise.delay(ERROR_DELAY).then(() => { + const errorElement = element.querySelector('#errMsg'); // check to see if modal content class exists - if (element.querySelector('.error')) { + if (errorElement) { // looks like there is an error if modal content class does not exist. // assign variable to state and access in UI - element.querySelector('.error').style.display = 'block'; - element.querySelector('.error').textContent = 'Error loading Modal'; + errorElement.style.display = 'block'; + errorElement.textContent = 'Error loading Modal'; + // TODO: should we report this failure to our log endpoint? } }); }; - + const focusCloseButton = element => { + window.requestAnimationFrame(() => { + // TODO: determine how to get this to re-focus if the prerender is dismissed and re-opened + element.focus(); + }); + }; return ( @@ -141,14 +175,45 @@ export default ({ doc, props, event }) => { -