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

Modal functionality #76 #256

Merged
merged 12 commits into from
Nov 27, 2023
104 changes: 97 additions & 7 deletions common/modal/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,32 @@
left: 0;
width: 100vw;
height: 100vh;
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
background: white;
background: var(--background-color);
color: var(--text-color);
z-index: 1051;
transition: opacity 0.15s linear, visibility 0.15s linear;
transition: opacity var(--duration-small) var(--easing-entrance),
visibility var(--duration-small) var(--easing-entrance);
opacity: 1;

--background-color: var(--c-white);
--top-bar-height: 64px;
--text-color: var(--c-black);
}

@supports (height: 1svh) {
.modal-background {
height: 100svh;
}
}

.modal--black {
--background-color: var(--c-black);
--text-color: var(--c-white);
--color-icon: var(--c-white);
}

.modal--gray {
--background-color: var(--c-grey-50);
}

.modal-hidden {
Expand All @@ -29,6 +51,63 @@
background-color: var(--c-grey-1);
}

.modal-content--wide {
margin: 0;
overflow: auto;
height: calc(100% - var(--top-bar-height));
background-color: var(--background-color);
}

.modal-top-bar {
display: flex;
justify-content: center;
}

.modal-top-bar-content {
display: flex;
justify-content: space-between;
margin: 0 16px;
gap: 10px;
min-height: var(--top-bar-height);
align-items: center;
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
width: 100%;
padding: 32px 0;
}

.modal-top-bar-heading {
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
font-family: var(--ff-volvo-novum);
font-size: var(--f-heading-5-font-size);
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
line-height: var(--f-heading-5-line-height);
margin: 0;
}

.modal-top-bar .modal-close-button {
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
padding: 0;
margin: 0;
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
border: 0;
background: inherit;
display: flex;
cogniSyb marked this conversation as resolved.
Show resolved Hide resolved
border-radius: 1px;
min-height: 44px;
min-width: 44px;
justify-content: center;
align-items: center;
}

.modal-top-bar .modal-close-button:focus {
outline: 0;
}

.modal-top-bar .modal-close-button:focus-visible {
outline: 2px solid var(--light-border-focus);
outline-offset: 5px;
}

.modal-top-bar svg {
height: 24px;
width: 24px;
}

.modal-content .modal-video {
width: 100%;
height: 100%;
Expand All @@ -38,7 +117,7 @@
.modal-before-banner {
display: flex;
justify-content: center;
background: white;
background: var(--background-color);
}

.modal-before-banner button.modal-close-button {
Expand All @@ -52,24 +131,35 @@
}

@media (min-width: 768px) {
.modal-content {
:root:not(.redesign-v2) .modal-content {
margin: 100px auto;
width: 726px;
}
}

@media (min-width: 992px) {
.modal-content {
:root:not(.redesign-v2) .modal-content {
width: 930px;
}
}

@media (min-width: 1300px) {
.modal-content {
:root:not(.redesign-v2) .modal-content {
width: 1170px;
}
}

@media (min-width: 744px) {
.modal-top-bar-content {
margin: 0 32px;
}
}

@media (min-width: 1200px) {
.modal-top-bar-content {
max-width: 1170px;
}
}

/* adjustments for soundcloud variant of modal, e.g. https://www.volvotrucks.us/trucks/powertrain/i-torque/ */
.modal-content .modal-soundcloud {
Expand All @@ -81,7 +171,7 @@
gap: 2%;
}

.modal-content:has(.modal-soundcloud) {
.modal-content:has(.modal-soundcloud) {
aspect-ratio: unset;
}

Expand Down
100 changes: 90 additions & 10 deletions common/modal/modal.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createElement, getTextLabel } from '../../scripts/common.js';
import { loadCSS } from '../../scripts/lib-franklin.js';
// eslint-disable-next-line import/no-cycle
import { createIframe, isLowResolutionVideoUrl } from '../../scripts/video-helper.js';
Expand All @@ -7,11 +8,33 @@ const styles$ = new Promise((r) => {
});

const HIDE_MODAL_CLASS = 'modal-hidden';
let currentModalClasses = null;
let currentInvokeContext = null;

const createModalTopBar = (parentEl) => {
const topBar = document.createRange().createContextualFragment(`
<div class="modal-top-bar">
<div class="modal-top-bar-content">
<h2 class="modal-top-bar-heading" id="modal-heading"></h2>
<button class="modal-close-button" aria-label=${getTextLabel('close')}>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.97979 4.97979C5.17505 4.78453 5.49163 4.78453 5.6869 4.97979L16 15.2929L26.3131 4.97979C26.5084 4.78453 26.825 4.78453 27.0202 4.97979C27.2155 5.17505 27.2155 5.49163 27.0202 5.6869L16.7071 16L27.0202 26.3131C27.2155 26.5084 27.2155 26.825 27.0202 27.0202C26.825 27.2155 26.5084 27.2155 26.3131 27.0202L16 16.7071L5.6869 27.0202C5.49163 27.2155 5.17505 27.2155 4.97979 27.0202C4.78453 26.825 4.78453 26.5084 4.97979 26.3131L15.2929 16L4.97979 5.6869C4.78453 5.49163 4.78453 5.17505 4.97979 4.97979Z" fill="var(--color-icon, #000)"/>
</svg>
</button>
</div>
</div>
`);

parentEl.prepend(...topBar.children);
// eslint-disable-next-line no-use-before-define
parentEl.querySelector('.modal-close-button').addEventListener('click', () => hideModal());
parentEl.querySelector('.modal-top-bar').addEventListener('click', (event) => event.stopPropagation());
};

const createModal = () => {
const modalBackground = document.createElement('div');
const modalBackground = createElement('div', { classes: ['modal-background', HIDE_MODAL_CLASS] });
modalBackground.setAttribute('role', 'dialog');

modalBackground.classList.add('modal-background', HIDE_MODAL_CLASS);
modalBackground.addEventListener('click', () => {
// eslint-disable-next-line no-use-before-define
hideModal();
Expand All @@ -24,8 +47,8 @@ const createModal = () => {
}
};

const modalContent = document.createElement('div');
modalContent.classList.add('modal-content');
const modalContent = createElement('div', { classes: ['modal-content'] });
createModalTopBar(modalBackground);
modalBackground.appendChild(modalContent);
// preventing initial animation when added to DOM
modalBackground.style = 'display: none';
Expand All @@ -36,25 +59,66 @@ const createModal = () => {
event.stopPropagation();
});

async function showModal(newUrl, beforeBanner, beforeIframe) {
const clearModalContent = () => {
modalContent.innerHTML = '';
modalContent.className = 'modal-content';
modalBackground.querySelector('.modal-top-bar-heading').textContent = '';
};

const handleNewContent = (newContent) => {
clearModalContent();

const firstSection = newContent[0];

// checking if the first section contains one heading only
if (
firstSection.children.length === 1
&& firstSection.children[0].children.length === 1
&& /^H[1-6]$/.test(newContent[0].children[0].children[0].tagName)
) {
const headingContent = firstSection.children[0].children[0].textContent;

modalBackground.querySelector('.modal-top-bar-heading').textContent = headingContent;
firstSection.style.display = 'none';
modalBackground.setAttribute('aria-labelledby', 'modal-heading');
} else {
modalBackground.removeAttribute('aria-labelledby');
}

modalContent.classList.add('modal-content--wide');
modalContent.append(...newContent);
};

async function showModal(newContent, {
beforeBanner, beforeIframe, modalClasses = [], invokeContext,
}) {
currentInvokeContext = invokeContext;
// disabling focus for header, footer and main elements when modal is open
document.querySelectorAll('header, footer, main').forEach((el) => {
el.setAttribute('inert', 'inert');
});
await styles$;
modalBackground.style = '';
modalBackground.classList.add(...modalClasses);
currentModalClasses = modalClasses;
window.addEventListener('keydown', keyDownAction);

if (newUrl) {
if (newContent && (typeof newContent !== 'string')) {
handleNewContent(newContent);
} else if (newContent) {
let videoOrIframe = null;
if (isLowResolutionVideoUrl(newUrl)) {
if (isLowResolutionVideoUrl(newContent)) {
// We can't use the iframe for videos, because if the Content-Type
// `application/octet-stream` is returned instead of `video/mp4`, the
// file is downloaded instead of displayed. So we use the video element instead.
videoOrIframe = document.createElement('video');
videoOrIframe.setAttribute('src', newUrl);
videoOrIframe.setAttribute('src', newContent);
videoOrIframe.setAttribute('controls', '');
videoOrIframe.setAttribute('autoplay', '');
videoOrIframe.classList.add('modal-video');
modalContent.append(videoOrIframe);
} else {
videoOrIframe = createIframe(newUrl, { parentEl: modalContent, classes: 'modal-video' });
videoOrIframe = createIframe(newContent, { parentEl: modalContent, classes: 'modal-video' });
}

if (beforeBanner) {
Expand Down Expand Up @@ -82,19 +146,35 @@ const createModal = () => {

modalContent.classList.add('modal-content-fade-in');
modalBackground.classList.remove(HIDE_MODAL_CLASS);
modalBackground.querySelector('.modal-top-bar .modal-close-button').focus();
modalBackground.setAttribute('aria-hidden', 'false');

// disable page scrolling
document.body.classList.add('disable-scroll');
}

function hideModal() {
// restoring focus for header, footer and main elements when modal is close
document.querySelectorAll('header, footer, main').forEach((el) => {
el.removeAttribute('inert');
});
if (currentInvokeContext) {
currentInvokeContext.focus();
}
currentInvokeContext = null;
modalBackground.classList.add(HIDE_MODAL_CLASS);
modalContent.classList.remove('modal-content-fade-in');
window.removeEventListener('keydown', keyDownAction);
document.body.classList.remove('disable-scroll');
modalContent.querySelector('iframe, video').remove();
modalContent.querySelector('iframe, video')?.remove();
modalContent.querySelector('.modal-before-banner')?.remove();
modalContent.querySelector('.modal-before-iframe')?.remove();
modalBackground.setAttribute('aria-hidden', 'true');

if (currentModalClasses.length) {
modalBackground.classList.remove(currentModalClasses);
currentModalClasses = null;
}
}

return {
Expand Down
24 changes: 14 additions & 10 deletions placeholder.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"total": 30,
"total": 36,
"offset": 0,
"limit": 30,
"limit": 36,
"data": [
{
"Key": "Low resolution video message",
Expand Down Expand Up @@ -54,19 +54,19 @@
{
"Key": "vinlabel",
"Text": "Enter your 17-digit VIN (Vehicle Identification Number)"
},
},
{
"Key": "submit",
"Text": "Submit"
},
},
{
"Key": "vinformat",
"Text": "The VIN entered is not valid for Volvo Trucks, Nova Bus, or Prevost vehicles"
},
},
{
"Key": "result text",
"Text": "${count} recalls available for \"${vin}\" VIN"
},
},
{
"Key": "recalls",
"Text": "Recalls"
Expand All @@ -86,7 +86,7 @@
{
"Key": "remedy_description",
"Text": "Remedy"
},
},
{
"Key": "published_info",
"Text": "Information last updated"
Expand All @@ -102,11 +102,11 @@
{
"Key": "recall-incomplete",
"Text": "Recall Incomplete"
},
},
{
"Key": "recall-incomplete-no-remedy",
"Text": "Recall In Complete, remedy not available"
},
},
{
"Key": "loading recalls",
"Text": " Loading Recalls ....."
Expand All @@ -118,7 +118,7 @@
{
"Key": "mfr_notes",
"Text": "Next Steps"
},
},
{
"Key": "mfr_recall_number",
"Text": "Brand Recall Number"
Expand All @@ -142,6 +142,10 @@
{
"Key": "vinformat-length",
"Text": "Please fill out this field"
},
{
"Key": "Close",
"Text": "Close"
}
],
":type": "sheet"
Expand Down
Loading