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) => + `
${story()}
`, + ], +}; + +function getBannerData(data) { + const newData = Object.assign({}, data); + if (data && typeof data === 'object' && 'banner__video' in data) { + newData.banner__video = bannerVideo; + } + if (data && typeof data === 'object' && 'banner__background_image' in data) { + newData.banner__background_image = bannerImage; + } + return newData; +} + +export const Banner = () => + ``; diff --git a/src/components/banner/banner.twig b/src/components/banner/banner.twig new file mode 100644 index 0000000..51d2ba3 --- /dev/null +++ b/src/components/banner/banner.twig @@ -0,0 +1,82 @@ +{# +/** + * Available Variables: + * - banner__title: The title of the banner. + * - banner__content: The main content of the banner. + * - banner__button_text: The text displayed on the banner button. + * - banner__button_url: The URL that the banner button links to. + * - banner__background_image: The URL of the background image for the banner. + * - banner__video: The URL of the video to be displayed. If provided, background_image will be ignored. + * - banner__alignment: Specifies the alignment of the content within the banner. Options include 'left', 'center'. + * - banner__variant: Specifies the style variant for the banner component (e.g., default, light, dark). + * - additional_attributes: Additional HTML attributes for the banner container. + */ +#} +{{ attach_library('emulsify/banner') }} + +{% set banner__base_class = 'banner' %} +{% set banner__alignment = banner__alignment|default('left') %} +{% set additional_attributes = additional_attributes|default() %} +{% set has_image = not banner__video and banner__background_image %} +{% set banner__modifiers = banner__modifiers|default([])|merge([banner__alignment]) %} + +{% if has_image %} + {% set banner__modifiers = banner__modifiers|merge(['has-image']) %} +{% endif %} + +
+ {# Render the video background #} + {% if banner__video %} +
+ +
+ +
+
+ {% endif %} + {# Render the main content #} +
+ {% if banner__title %} + {% include "@components/text/headings/_heading.twig" with { + heading__level: banner__heading__level|default('4'), + heading: banner__title, + } %} + {% endif %} + {% if banner__content %} +
{{ banner__content }}
+ {% endif %} + {# Render the action button #} + {% if banner__button_text and banner__button_url %} +
+ {% include "@components/button/button.twig" with { + button__tag: 'a', + button__content: banner__button_text, + button__attributes: { + 'href': banner__button_url, + } + } %} +
+ {% endif %} +
+