Skip to content

Commit

Permalink
MWPW-141022 [Project PEP] Prompt Dismissal + Tie-in with App Launcher…
Browse files Browse the repository at this point in the history
… UX (#2392)

* Added a way to mock entitlements in non prod environments for testing purposes

* Added a pulsing animation after dismissing the PEP modal

* Added the ability to debug pep in prod; added skipPepEntitlements option

* fixed url for loading the pep dismissal animation

* Another url fix for the pep dismissal animation css file

* slowed down the default animation

* Added the tooltip

* re-enabled tracking dismissed prompts

* added tooltip that uses data attributes; cleaned up ring animation slightly

* styled the tooltip in accordance with the new figma spec sheet

* Changed the color of the tooltip

* Pick up pep dismissal action config from section metadata; fixed lowercase url issue

* Updated unit tests

* updated an incomplete pep test

* Fixed dismissal actions firing when redirecting

* Removed a comment

* added tests for running dismissal actions

* Tooltip and animation are now cleared if the app switcher is clicked

* added a missing semicolon

* small refactor of remove animation/tooltip on click logic

* A bit of cleanup

* removed some unnecessary code

* Fixed a bug introduced by one of the previous fixes

* fixed a classname

* Formatting of an html string

* replaced all usage of right in absolute positioning with left

* Added CONFIG default values to dismissal action function parameter lists

* renamed a variable

* Removed an unused variable

* Grouped together common css rules in tooltip.css

* Combined dismissal css with the general webapp-prompt css

* Removed an unused import

* Added a unit test

* Removed some unneeded newlines

* Moved the tooltip down slightly

* Added a missing curly brace

* added a missing semicolon

on line 92 of libs/features/webapp-prompt/webapp-prompt.js

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* made the dismissal config its own entity

* added some variables to the focus animation css

* removed a redundant style

* cleaned up the padding css for the tooltip

* minor refactoring

* Changed the tooltip fontfamily to use the milo styles variable defined in root

* Removed an unnecessary css rule

* Replaced tabs with spaces in webapp-prompt.css

* Fixed the PEP unit tests

* clock cleanup in tests

* fixed an issue with the redirect

* small change

* Fixed eslint error by making a method static

* Fixed failing tests

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
sharmrj and github-actions[bot] authored Jul 8, 2024
1 parent 5f2f3e2 commit 15f8c9f
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 42 deletions.
115 changes: 115 additions & 0 deletions libs/features/webapp-prompt/webapp-prompt.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,118 @@
}
}
}

/* DISMISSAL TOOLTIP */

[data-pep-dismissal-tooltip]::after {
content: attr(data-pep-dismissal-tooltip);
display: inline-flex;
z-index: 3;
height: fit-content;
width: 8.875rem;
top: 125%;
left: -300%;
word-break: break-word;
border-radius: 7px;

padding-inline: 0.5625rem;
padding-block: 0.25rem 0.3125rem;

font-family: var(--body-font-family);
font-size: 0.75rem;
line-height: 0.9375rem;
color: white;
}

[data-pep-dismissal-tooltip]::before {
content: '';
z-index: 2;
width: 0.44rem;
height: 0.44rem;
border-radius: 0.05rem;
left: calc(50% - 0.22rem);
top: 115%;

transform: rotate(45deg);
}

[data-pep-dismissal-tooltip]::before,
[data-pep-dismissal-tooltip]::after {
background-color: #3B63FB;
position: absolute;
pointer-events: none;
transition: opacity 0.5s;
}

@media (min-width: 1520px) {
[data-pep-dismissal-tooltip]::after {
left: calc(50% - 5rem);
}
}

/* DISMISSAL ANIMATION */

.coach-indicator {
--coach-indicator-ring-default-color: rgba(56,146,243);
--coach-indicator-ring-diameter: 1.25rem;
--coach-indicator-ring-border-size: 2px;
--coach-indicator-ring-inline-size: var(--coach-indicator-ring-diameter);
--coach-indicator-ring-block-size: var(--coach-indicator-ring-diameter);
--coach-indicator-first-ring-delay-fraction: 0;
--coach-indicator-second-ring-delay-fraction: 0.33;
--coach-indicator-third-ring-delay-fraction: 0.66;
--animation-duration: 3000ms;
}

@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0;
}

50% {
transform: scale(1.5);
opacity: 1;
}

100% {
transform: scale(2);
opacity: 0;
}
}

.coach-indicator .coach-indicator-ring {
display: block;
position: absolute;
top: 14%;
left: 13%;

border-style: solid;
border-width: var(--coach-indicator-ring-border-size);
border-color: var(--coach-indicator-ring-default-color);

inline-size: var(--coach-indicator-ring-inline-size);
block-size: var(--coach-indicator-ring-block-size);
animation: pulse var(--animation-duration) linear;
animation-fill-mode: both;

border-radius: 5px;
}

.coach-indicator .coach-indicator-ring:nth-child(1) {
animation-delay: calc(var(--animation-duration)*var(--coach-indicator-first-ring-delay-fraction));
}

.coach-indicator .coach-indicator-ring:nth-child(2) {
animation-delay: calc(var(--animation-duration)*var(--coach-indicator-second-ring-delay-fraction));
}

.coach-indicator .coach-indicator-ring:nth-child(3) {
animation-delay: calc(var(--animation-duration)*var(--coach-indicator-third-ring-delay-fraction));
}

@media (prefers-reduced-motion: reduce) {
.coach-indicator .coach-indicator-ring {
animation: none;
}
}
95 changes: 87 additions & 8 deletions libs/features/webapp-prompt/webapp-prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import {
import { getConfig, decorateSVG } from '../../utils/utils.js';
import { replaceKey, replaceText } from '../placeholders.js';

export const DISMISSAL_CONFIG = {
animationCount: 2,
animationDuration: 2500,
tooltipMessage: 'Use the App Switcher to quickly find apps.',
tooltipDuration: 5000,
};

const CONFIG = {
selectors: { prompt: '.appPrompt' },
delay: 7000,
loaderColor: '#EB1000',
...DISMISSAL_CONFIG,
};

const getElemText = (elem) => elem?.textContent?.trim().toLowerCase();
const getElemText = (elem) => elem?.textContent?.trim();

const getMetadata = (el) => [...el.childNodes].reduce((acc, row) => {
if (row.children?.length === 2) {
Expand All @@ -35,6 +43,54 @@ const getIcon = (content) => {
return icons.company;
};

const showTooltip = (
element,
message = CONFIG.tooltipMessage,
time = CONFIG.tooltipDuration,
) => {
element.setAttribute('data-pep-dismissal-tooltip', message);
const cleanup = () => element.removeAttribute('data-pep-dismissal-tooltip');
const timeoutID = setTimeout(cleanup, time);
element.addEventListener('click', () => {
cleanup();
clearTimeout(timeoutID);
}, { once: true });
};

const playFocusAnimation = (
element,
iterationCount = CONFIG.animationCount,
animationDuration = CONFIG.animationDuration,
) => {
element.classList.add('coach-indicator');
element.style.setProperty('--animation-duration', `${animationDuration}ms`);
const rings = [];
const createRing = () => toFragment`
<div
class="coach-indicator-ring"
style="animation-iteration-count: ${iterationCount};">
</div>`;
for (let i = 0; i < 3; i += 1) {
const ring = createRing();
element.insertAdjacentElement('afterbegin', ring);
rings.push(ring);
}
// The cleanup function is added to the event queue
// some time after the end of the animation because
// the cleanup isn't high priority but it should be done
// eventually. (Animation truly ends slightly after
// animationDuration * iterationCount due to animation-delay)
const cleanup = () => {
rings.forEach((ring) => ring.remove());
element.classList.remove('coach-indicator');
};
const timeoutID = setTimeout(cleanup, (iterationCount + 1) * animationDuration);
element.addEventListener('click', () => {
cleanup();
clearTimeout(timeoutID);
}, { once: true });
};

const modalsActive = () => !!document.querySelector('.dialog-modal');

const waitForClosedModalsThen = (loadPEP) => {
Expand All @@ -59,7 +115,10 @@ class AppPrompt {
() => waitForClosedModalsThen(this.init),
{ once: true },
);
} else this.init();
this.initializationQueued = true;
return;
}
this.initializationQueued = false;
}

init = async () => {
Expand Down Expand Up @@ -162,6 +221,10 @@ class AppPrompt {
const metadata = getMetadata(content.querySelector('.section-metadata'));
metadata['loader-duration'] = parseInt(metadata['loader-duration'] || CONFIG.delay, 10);
metadata['loader-color'] = metadata['loader-color'] || CONFIG.loaderColor;
metadata['dismissal-animation-count'] = parseInt(metadata['dismissal-animation-count'] ?? CONFIG.animationCount, 10);
metadata['dismissal-animation-duration'] = parseInt(metadata['dismissal-animation-duration'] ?? CONFIG.animationDuration, 10);
metadata['dismissal-tooltip-message'] ??= CONFIG.tooltipMessage;
metadata['dismissal-tooltip-duration'] = parseInt(metadata['dismissal-tooltip-duration'] ?? CONFIG.tooltipDuration, 10);
this.options = metadata;
};

Expand All @@ -181,7 +244,7 @@ class AppPrompt {
: '';

return toFragment`<div
daa-state="true" daa-im="true" daa-lh="PEP Modal_${this.options['product-name']}"
daa-state="true" daa-im="true" daa-lh="PEP Modal_${this.options['product-name']?.toLowerCase()}"
class="appPrompt" style="margin: 0 ${this.offset}px">
${this.elements.closeIcon}
<div class="appPrompt-icon">
Expand All @@ -200,7 +263,7 @@ class AppPrompt {
};

addEventListeners = () => {
this.anchor?.addEventListener('click', this.close);
this.anchor?.addEventListener('click', () => this.close({ dismissalActions: false }));
document.addEventListener('keydown', this.handleKeyDown);

[this.elements.closeIcon, this.elements.cta]
Expand All @@ -211,9 +274,11 @@ class AppPrompt {
if (event.key === 'Escape') this.close();
};

static redirectTo = (url) => window.location.assign(url);

initRedirect = () => setTimeout(() => {
this.close({ saveDismissal: false });
window.location.assign(this.options['redirect-url']);
this.close({ saveDismissal: false, dismissalActions: false });
this.redirectTo(this.options['redirect-url']);
}, this.options['loader-duration']);

isDismissedPrompt = () => AppPrompt.getDismissedPrompts().includes(this.id);
Expand All @@ -224,14 +289,27 @@ class AppPrompt {
document.cookie = `dismissedAppPrompts=${JSON.stringify([...dismissedPrompts])};path=/`;
};

close = ({ saveDismissal = true } = {}) => {
close = ({ saveDismissal = true, dismissalActions = true } = {}) => {
const appPromptElem = document.querySelector(CONFIG.selectors.prompt);
appPromptElem?.remove();
clearTimeout(this.redirectFn);
if (saveDismissal) this.setDismissedPrompt();
document.removeEventListener('keydown', this.handleKeyDown);
this.anchor?.focus();
this.anchor?.removeEventListener('click', this.close);

if (dismissalActions) {
playFocusAnimation(
this.anchor,
this.options['dismissal-animation-count'],
this.options['dismissal-animation-duration'],
);
showTooltip(
this.anchor,
this.options['dismissal-tooltip-message'],
this.options['dismissal-tooltip-duration'],
);
}
};

static getDismissedPrompts = () => {
Expand All @@ -246,7 +324,8 @@ class AppPrompt {

export default async function init(config) {
try {
const appPrompt = await new AppPrompt(config);
const appPrompt = new AppPrompt(config);
if (!appPrompt.initializationQueued) await appPrompt.init();
return appPrompt;
} catch (e) {
lanaLog({ message: 'Could not initialize PEP', e, tags: 'errorType=error,module=pep' });
Expand Down
27 changes: 26 additions & 1 deletion test/features/webapp-prompt/mocks/pep-prompt-content.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
export default ({ color, loaderDuration, redirectUrl, productName }) => `<div>
export default ({
color,
loaderDuration,
redirectUrl,
productName,
animationCount,
animationDuration,
tooltipMessage,
tooltipDuration,
}) => `<div>
<p>
<picture>
<source type="image/webp" srcset="http://localhost:2000/test/features/webapp-prompt/mocks/media-icon.png" media="(min-width: 600px)">
Expand Down Expand Up @@ -27,5 +36,21 @@ export default ({ color, loaderDuration, redirectUrl, productName }) => `<div>
<div>product-name</div>
<div>${productName}</div>
</div>`}
${animationCount && `<div>
<div>dismissal-animation-count</div>
<div>${animationCount}</div>
</div>`}
${animationDuration && `<div>
<div>dismissal-animation-duration</div>
<div>${animationDuration}</div>
</div>`}
${tooltipMessage && `<div>
<div>dismissal-tooltip-message</div>
<div>${tooltipMessage}</div>
</div>`}
${tooltipDuration && `<div>
<div>dismissal-tooltip-duration</div>
<div>${tooltipDuration}</div>
</div>`}
</div>
</div>`;
13 changes: 11 additions & 2 deletions test/features/webapp-prompt/test-utilities.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { setViewport } from '@web/test-runner-commands';
import sinon from 'sinon';
import init from '../../../libs/features/webapp-prompt/webapp-prompt.js';
import init, { DISMISSAL_CONFIG } from '../../../libs/features/webapp-prompt/webapp-prompt.js';
import { viewports, mockRes as importedMockRes } from '../../blocks/global-navigation/test-utilities.js';
import { setUserProfile } from '../../../libs/blocks/global-navigation/utilities/utilities.js';
import { getConfig, loadStyle, setConfig, updateConfig } from '../../../libs/utils/utils.js';

export const allSelectors = {
Expand All @@ -17,13 +18,16 @@ export const allSelectors = {
progressWrapper: '.appPrompt-progressWrapper',
progress: '.appPrompt-progress',
appSwitcher: '#unav-app-switcher',
indicatorRing: '.coach-indicator-ring',
tooltip: '[data-pep-dismissal-tooltip]',
};

export const defaultConfig = {
color: '#b30b00',
loaderDuration: 7500,
redirectUrl: 'https://www.adobe.com/?pep=true',
productName: 'photoshop',
...DISMISSAL_CONFIG,
};

export const mockRes = importedMockRes;
Expand All @@ -38,13 +42,18 @@ export const initPep = async ({ entName = 'firefly-web-usage', isAnchorOpen = fa
await setViewport(viewports.desktop);
await loadStyle('../../../libs/features/webapp-prompt/webapp-prompt.css');

setUserProfile({});
const pep = await init({
promptPath: 'https://pep-mocks.test/pep-prompt-content.plain.html',
getAnchorState: getAnchorStateMock || (async () => ({ id: 'unav-app-switcher', isOpen: isAnchorOpen })),
entName,
parent: document.querySelector('div.feds-utilities'),
});

sinon.stub(pep, 'initRedirect').callsFake(() => null);
Object.setPrototypeOf(pep, {
...Object.getPrototypeOf(pep),
redirectTo: sinon.stub().returns({}),
});

return pep;
};
Loading

0 comments on commit 15f8c9f

Please sign in to comment.