diff --git a/src/components/banner/_banner.scss b/src/components/banner/_banner.scss new file mode 100644 index 0000000..28a64c3 --- /dev/null +++ b/src/components/banner/_banner.scss @@ -0,0 +1,113 @@ +.banner { + min-height: 16rem; + position: relative; +} + +.banner--has-image { + background-size: 100%; + background-position: center; +} + +.banner__video-container { + width: 100%; + height: 100%; + max-width: 100%; + position: absolute; +} + +.banner--has-image, +.banner__video-container { + &::before { + content: ''; + top: 0; + left: 0; + z-index: 1; + width: 100%; + height: 100%; + opacity: 0.75; + display: block; + position: absolute; + pointer-events: none; + mix-blend-mode: multiply; + background-color: var(--color-primary-dark); + } +} + +.banner__video { + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; +} + +.banner__video-controls { + z-index: 3; + position: absolute; + bottom: var(--spacing-xl); + right: var(--spacing-xl); +} + +.banner__toggle { + cursor: pointer; + border-radius: 50%; + width: var(--spacing-xl); + height: var(--spacing-xl); + border: 3px solid var(--color-white); + background-color: var(--color-primary-lighter); + + svg { + width: 100%; + height: 100%; + } +} + +.banner__content { + z-index: 2; + width: 100%; + display: flex; + position: relative; + flex-direction: column; + color: var(--color-white); + padding: var(--spacing-2xl) var(--spacing-xl); + + .heading { + @include heading-large; + + margin: 0 0 calc(var(--spacing-lg) * 1.5); + } + + p { + margin: 0 0 var(--spacing-lg); + font-size: var(--font-size-h5); + + &:is(p:last-child) { + margin-bottom: 0; + } + } +} + +.banner__action { + margin: var(--spacing-xl) 0 0; + + .button, + .button:hover, + .button:focus { + color: var(--color-black); + font-size: var(--font-size-small); + border-color: var(--color-primary-lighter); + background-color: var(--color-primary-lighter); + } +} + +.banner-list { + display: flex; + flex-direction: column; + row-gap: var(--spacing-xl); +} + +/* Variants -- Content centered */ +.banner__content--center { + align-items: center; +} diff --git a/src/components/banner/banner.component.yml b/src/components/banner/banner.component.yml new file mode 100644 index 0000000..bb38d2f --- /dev/null +++ b/src/components/banner/banner.component.yml @@ -0,0 +1,74 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json + +name: Banner +group: Components +status: stable +props: + type: object + required: + - banner__title + - banner__content + - banner__button_text + - banner__button_url + properties: + banner__title: + type: string + title: Title + description: 'Specifies the title of the banner' + data: 'This is the banner title' + banner__content: + type: string + title: Content + description: 'Specifies the main content of the banner' + data: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + banner__button_text: + type: string + title: Button Text + description: 'Specifies the text displayed on the button' + data: 'Learn More' + banner__button_url: + type: string + title: Button URL + description: 'Specifies the URL the button will link to' + data: '#' + banner__background_image: + type: string + title: Background Image + description: 'Specifies the URL of the background image for the banner. Either this or banner__video should be provided.' + data: 'https://example.com/path/to/background-image.jpg' + banner__video: + type: string + title: Video URL + description: 'Specifies the URL of the video to be displayed. Either this or banner__background_image should be provided.' + data: '' + banner__alignment: + type: string + title: Alignment + description: 'Specifies the alignment of the content within the banner. Options include: left, center, right.' + enum: + - left + - center + data: 'center' + example: + - banner__title: 'THIS IS A BANNER HEADING' + banner__content: '
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
' + banner__button_text: 'this is a link' + banner__button_url: '#' + banner__background_image: '' + - banner__title: 'THIS IS A BANNER HEADING' + banner__content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
' + banner__button_text: 'this is a link' + banner__button_url: '#' + banner__video: '' + - banner__title: 'THIS IS A BANNER HEADING' + banner__content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
' + banner__button_text: 'this is a link' + banner__button_url: '#' + banner__background_image: '' + banner__alignment: 'center' + - banner__title: 'THIS IS A BANNER HEADING' + banner__content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
' + banner__button_text: 'this is a link' + banner__button_url: '#' + banner__video: '' + banner__alignment: 'center' diff --git a/src/components/banner/banner.js b/src/components/banner/banner.js new file mode 100644 index 0000000..ba4bece --- /dev/null +++ b/src/components/banner/banner.js @@ -0,0 +1,109 @@ +Drupal.behaviors.banners = { + attach(context) { + const banners = context.querySelectorAll('.banner'); + + /** + * getBannerReferences + * + * @description Returns references to banner elements. + * @param {HTMLElement} banner Banner element. + * @returns {Object} References to label, button, video, pause, and play elements. + */ + function getBannerReferences(banner) { + return { + label: banner.querySelector('.banner__toggle_label'), + button: banner.querySelector('.banner__toggle'), + video: banner.querySelector('.banner__video'), + pause: banner.querySelector('.banner__pause'), + play: banner.querySelector('.banner__play'), + }; + } + + /** + * playVideo + * + * @description Starts video playback and updates UI to reflect the playing state. + * @param {Object} refs Object containing element references. + * @returns {Promise} Resolves when the video starts playing or rejects with an error. + */ + function playVideo(refs) { + return new Promise((resolve, reject) => { + refs.video + .play() + .then(() => { + refs.pause.classList.remove('visually-hidden'); + refs.play.classList.add('visually-hidden'); + refs.label.textContent = 'Pause video'; + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * pauseVideo + * + * @description Pauses video playback and updates UI to reflect the paused state. + * @param {Object} refs Object containing element references. + */ + function pauseVideo(refs) { + refs.play.classList.remove('visually-hidden'); + refs.pause.classList.add('visually-hidden'); + refs.label.textContent = 'Play video'; + refs.video.pause(); + } + + /** + * toggleVideo + * + * @description Toggles video playback state and updates UI accordingly. + * @param {Object} refs Object containing element references. + */ + function toggleVideo(refs) { + if (refs.video.paused) { + playVideo(refs); + } else { + pauseVideo(refs); + } + } + + /** + * setInitialState + * + * @description Sets the initial state for a video element and performs associated actions. + * @param {Object} refs Object containing element references. + * @throws {DOMException} - If an error occurs while attempting to play the video. + */ + async function setInitialState(refs) { + const reduceMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ); + + // Check if the user disabled 'prefer reduced motion' on their system. + if (!reduceMotion.matches) { + // Check if the user's browser allows video autoplay. + try { + await playVideo(refs); + refs.actions[0].classList.add('hidden'); + } catch (err) { + if (err.name === 'NotAllowedError') { + pauseVideo(refs); + } + } + } else { + pauseVideo(refs); + } + } + + banners?.forEach((banner) => { + const refs = getBannerReferences(banner); + + if (refs.button && refs.video) { + refs.button.addEventListener('click', () => toggleVideo(refs)); + setInitialState(refs); + } + }); + }, +}; diff --git a/src/components/banner/banner.stories.js b/src/components/banner/banner.stories.js new file mode 100644 index 0000000..d6a5ece --- /dev/null +++ b/src/components/banner/banner.stories.js @@ -0,0 +1,29 @@ +import bannerTwig from './banner.twig'; +import { props } from './banner.component.yml'; +import bannerVideo from '../../media/video-placeholder.mp4'; +import bannerImage from '../../images/example/banner-image.jpg'; +import './banner'; + +export default { + title: 'Components/Banner', + decorators: [ + (story) => + `