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
2 changes: 1 addition & 1 deletion blocks/v2-stories-carousel/v2-stories-carousel.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
}

/* Full width block */
main .section.v2-stories-carousel-container .v2-stories-carousel-wrapper {
body .section.v2-stories-carousel-container .v2-stories-carousel-wrapper {
margin: 0;
padding-left: 0;
padding-right: 0;
Expand Down
7 changes: 6 additions & 1 deletion blocks/v2-truck-lineup/v2-truck-lineup.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
}

/* Full width block */
main .section.v2-truck-lineup-container .v2-truck-lineup-wrapper {
body .section.v2-truck-lineup-container .v2-truck-lineup-wrapper {
margin: 0;
padding: 0;
width: 100%;
Expand Down Expand Up @@ -303,12 +303,14 @@ ul.v2-truck-lineup__navigation {
max-width: 931px;
}

div:where([role="dialog"]):not(.truck-lineup-content-center) .v2-truck-lineup__content .default-content-wrapper,
main:not(.truck-lineup-content-center) .v2-truck-lineup__content .default-content-wrapper {
align-items: flex-start;
display: grid;
grid-template-columns: 1fr 567px 80px 430px 1fr;
}

div:where([role="dialog"]):not(.truck-lineup-content-center) .v2-truck-lineup__text,
main:not(.truck-lineup-content-center) .v2-truck-lineup__text {
width: 567px;
max-width: none;
Expand All @@ -318,11 +320,14 @@ ul.v2-truck-lineup__navigation {
grid-column: 2;
}

div:where([role="dialog"]):not(.truck-lineup-content-center) .v2-truck-lineup__buttons-container,
main:not(.truck-lineup-content-center) .v2-truck-lineup__buttons-container,
div:where([role="dialog"]):not(.truck-lineup-content-center) .v2-truck-lineup__buttons-container .button-container,
main:not(.truck-lineup-content-center) .v2-truck-lineup__buttons-container .button-container {
margin-top: 0;
}

div:where([role="dialog"]):not(.truck-lineup-content-center) .v2-truck-lineup__buttons-container,
main:not(.truck-lineup-content-center) .v2-truck-lineup__buttons-container {
justify-content: start;
grid-column: 4;
Expand Down
2 changes: 1 addition & 1 deletion blocks/v2-video/v2-video.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
margin-top: -40px;
}

main .section.v2-video-container .v2-video-wrapper {
body .section.v2-video-container .v2-video-wrapper {
padding: 0;
color: var(--c-white);
margin: 0;
Expand Down
112 changes: 105 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,65 @@
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: flex-start;
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-subtitle-1-font-size);
line-height: var(--f-subtitle-1-line-height);
letter-spacing: var(--f-subtitle-1-letter-spacing);
align-self: center;
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 +119,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 +133,41 @@
}

@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;
}

.modal-top-bar-heading {
font-size: var(--f-heading-6-font-size);
line-height: var(--f-heading-6-line-height);
letter-spacing: var(--f-heading-6-letter-spacing);
}
}

@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 +179,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
Loading