diff --git a/assets/icons/pause.svg b/assets/icons/pause.svg new file mode 100644 index 0000000..5bb17c7 --- /dev/null +++ b/assets/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/play.svg b/assets/icons/play.svg new file mode 100644 index 0000000..c54977e --- /dev/null +++ b/assets/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/banner/banner.component.yml b/src/components/banner/banner.component.yml new file mode 100644 index 0000000..1c83fc0 --- /dev/null +++ b/src/components/banner/banner.component.yml @@ -0,0 +1,43 @@ +$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__heading + - banner__text + properties: + banner__heading: + type: string + title: Title + description: 'Specifies the title of the banner' + data: 'THIS IS A BANNER HEADING' + banner__text: + type: string + title: Content + description: 'Specifies the main content of the banner' + data: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

' + banner__link: + type: string + title: Link Content + description: 'Specifies the text displayed on the link' + data: 'this is a link' + banner__image: + type: string + title: Image's source + description: The image's source of the banner. + data: 'https://picsum.photos/1480/360' + banner__video: + type: string + title: Video URL + description: 'Specifies the URL of the video to be displayed. Either this or banner__image should be provided.' + data: + - 'https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4' + - 'https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-webm-file.webm' + banner__alignment: + type: string + title: Alignment + description: 'Specifies the alignment of the content within the banner. Options include: left, center, right.' + data: 'left' diff --git a/src/components/banner/banner.scss b/src/components/banner/banner.scss new file mode 100644 index 0000000..a85a100 --- /dev/null +++ b/src/components/banner/banner.scss @@ -0,0 +1,123 @@ +@use '../../foundation/typography/typography' as *; + +@mixin overlay-darken($color: var(--color-primary-dark), $opacity: 0.75, $blend-mode: multiply) { + &::before { + content: ''; + top: 0; + left: 0; + z-index: 2; + width: 100%; + height: 100%; + opacity: $opacity; + display: block; + position: absolute; + pointer-events: none; + mix-blend-mode: $blend-mode; + background-color: $color; + } +} + +.banner { + z-index: 1; + min-height: 16rem; + position: relative; +} + +.banner__media { + @include overlay-darken; + + & { + position: absolute; + top: 0; + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-size: cover; + z-index: 0; + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.banner__video { + @include overlay-darken; + + & { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 0; + } + + video { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.banner__playback { + z-index: 5; + position: absolute; + bottom: var(--spacing-xl); + right: var(--spacing-xl); +} + +.banner__content { + z-index: 2; + display: flex; + position: relative; + flex-direction: column; + color: var(--color-white); + row-gap: var(--spacing-xl); + padding: var(--spacing-2xl) 0; + + [data-banner-alignment='center'] & { + align-items: center; + } +} + +.banner__heading { + @include h2; + + & { + margin: 0; + } +} + +.banner__text { + @include h5; + + & { + font-weight: initial; + margin: 0; + } + + p { + margin: 0 0 var(--spacing-lg); + + &:last-child { + margin-bottom: 0; + } + } +} + +.banner__links { + display: flex; + flex-direction: row; + row-gap: var(--spacing); +} + +.banner-list { + display: flex; + flex-direction: column; + row-gap: var(--spacing-xl); + margin: var(--spacing-lg) 0; +} diff --git a/src/components/banner/banner.stories.js b/src/components/banner/banner.stories.js new file mode 100644 index 0000000..400dde1 --- /dev/null +++ b/src/components/banner/banner.stories.js @@ -0,0 +1,66 @@ +import bannerTwig from './banner.twig'; +import { props } from './banner.component.yml'; + +import '../video/playback'; + +const bannerData = props.properties; + +export default { + title: 'Components/Banner', + argTypes: { + type: { + name: 'Media Type', + control: { type: 'select' }, + options: { + Image: 'image', + Video: 'video', + }, + }, + alignment: { + name: 'Banner Alignment', + type: 'select', + options: { + 'Left': 'left', + 'Center': 'center', + }, + }, + heading: { + name: 'Banner Heading', + type: 'string', + }, + text: { + name: 'Banner Text', + type: 'string', + }, + link: { + name: 'Banner Link', + type: 'string', + }, + }, + args: { + type: 'image', + alignment: bannerData.banner__alignment.data, + heading: bannerData.banner__heading.data, + text: bannerData.banner__text.data, + link: bannerData.banner__link.data, + }, +}; + +export const Banner = ({ + type, + alignment, + heading, + text, + link, +}) => bannerTwig({ + banner__image: bannerData.banner__image.data, + banner__video: bannerData.banner__video.data, + banner__media: type, + banner__alignment: alignment, + banner__heading: heading, + banner__text: text, + banner__link_items: [{ + url: '#', + content: link, + }] +}); diff --git a/src/components/banner/banner.twig b/src/components/banner/banner.twig new file mode 100644 index 0000000..4fc92dd --- /dev/null +++ b/src/components/banner/banner.twig @@ -0,0 +1,114 @@ +{# + # Available Variables: + # - banner__heading: The title of the banner. + # - banner__text: The main content of the banner. + # - banner__link_items: links or buttons below the main content. + # + # - banner__image: Decorative background image. + # - 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). + # - banner__attributes: HTML attributes for the banner container. + # + # Available Blocks: + # - banner_media + # - banner__media_caption +#} + +{% set banner__base_class = 'banner' %} +{% set banner__modifiers = banner__modifiers|default([]) %} +{% set banner__attributes = banner__attributes|default({}) %} + +{% set banner__attributes = banner__attributes|merge({ + 'class': bem(banner__base_class, banner__modifiers, banner__blockname), + 'data-banner-alignment': banner__alignment|default('left'), +}) %} + +{% set banner_media %} +
+ {% block banner__media %} + {% if banner__media == 'image' %} + This is the banner image alt text + {% else %} + {% set video_id = 'video-' ~ random('1234567890') ~ random('1234567890') ~ random('1234567890') ~ random('1234567890') %} + + {% include "@components/video/video.twig" with { + video__type: 'html5', + video__urls: banner__video, + video__show_placeholder: false, + video__blockname: banner__base_class, + video_object__attributes: { + 'id': video_id, + 'preload': 'true', + 'muted': 'true', + 'playsinline': 'true', + 'webkit-playsinline': 'true', + 'aria-hidden': 'true', + 'loop': 'true', + 'autoplay': 'true', + }, + } %} + + {% include "@components/video/playback-button.twig" with { + playback__blockname: banner__base_class, + playback__video_id: video_id, + } %} + {% endif %} + {% endblock %} +
+{% block banner__media_caption %}{% endblock %} +{% endset %} + +{% set banner %} +
+
+ {% if banner__heading %} + {% include "@components/typography/heading/heading.twig" with { + heading: banner__heading, + heading__level: banner__heading__level|default('4'), + heading__blockname: banner__base_class, + } %} + {% endif %} + + {% if banner__text %} + {% include "@components/typography/text/text.twig" with { + text__content: banner__text, + text__blockname: banner__base_class, + } %} + {% endif %} + + {% if banner__link_items|length > 0 %} +
+ {% for item in banner__link_items %} + {% include "@components/button/button.twig" with { + button__element: 'a', + button__style: 'primary', + button__content: item.content, + button__attributes: { + 'href': item.url, + } + } %} + {% endfor %} +
+ {% endif %} +
+
+{% endset %} + +{# Render the banner within a generic container #} +{% embed "@layout/container/container.twig" with { + container__component_width: banner__width|default('compressed'), + container__modifiers: banner__container_modifiers, + container__component_alignment: 'center', + container__theme: banner__theme|default('inverse'), +}%} + {% block container__content %} + {{ banner }} + {% endblock %} + + {% block container__suffix %} + {{ banner_media }} + {% endblock %} +{% endembed %} + diff --git a/src/components/icons/_icon.twig b/src/components/icons/_icon.twig index bd93c9b..2f2668b 100644 --- a/src/components/icons/_icon.twig +++ b/src/components/icons/_icon.twig @@ -10,7 +10,7 @@ * - icon__desc - a11y description */ #} -{% set icon__base_class = icon_base_class|default('icon') %} +{% set icon__base_class = icon__base_class|default('icon') %} {% set icon__attributes = icon__attributes|default([]) %} {# If `directory` is defined, set the path relative for Drupal. # Otherwise, set the path relative to the Component Library. #} diff --git a/src/components/icons/icons.md b/src/components/icons/icons.md index e821a7e..b8bc85c 100644 --- a/src/components/icons/icons.md +++ b/src/components/icons/icons.md @@ -34,8 +34,8 @@ Complex (BEM classes): ``` {% include "@atoms/04-images/icons/_icon.twig" with { - icon_base_class: 'toggle', - icon_blockname: 'main-nav', + icon__base_class: 'toggle', + icon__blockname: 'main-nav', icon__name: 'menu', } %} ``` diff --git a/src/components/video/playback-button.twig b/src/components/video/playback-button.twig new file mode 100644 index 0000000..f711eb8 --- /dev/null +++ b/src/components/video/playback-button.twig @@ -0,0 +1,37 @@ +{# + # Available variables: + # - playback__base_class - the base classname + # - playback__video_id - id from video tag + # - playback__modifiers + # - playback__blockname + # + # Available blocks: + # - none +#} + +{% set playback__base_class = playback__base_class|default('playback') %} +{% set playback__attributes = playback__attributes|default({}) %} +{% set playback__modifiers = playback__modifiers|default([]) %} + +{% set playback__attributes = playback__attributes|merge({ + 'class': bem(playback__base_class, playback__modifiers, playback__blockname), + 'data-video-id': playback__video_id, +}) %} + +
+ +
diff --git a/src/components/video/playback.js b/src/components/video/playback.js new file mode 100644 index 0000000..8aaf0c4 --- /dev/null +++ b/src/components/video/playback.js @@ -0,0 +1,113 @@ +Drupal.behaviors.playbackButtons = { + attach(context) { + const buttons = context.querySelectorAll('.playback__button'); + + /** + * fetchButtonRefs + * + * @description Returns references to playback button elements. + * @param {HTMLElement} button button element. + * @return {Object} References to label, button, video, pause, and play elements. + */ + function fetchButtonRefs(button) { + const { videoId } = button.parentElement.dataset; + + return { + button, + video: context.querySelector(`#${videoId}`), + label: button.querySelector('.playback__label'), + pause: button.querySelector('.playback__pause'), + play: button.querySelector('.playback__play'), + }; + } + + /** + * playVideo + * + * @description Starts video playback and updates UI to reflect the playing state. + * @param {Object} refs Object containing element references. + * @return {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); + } + } + + buttons?.forEach((button) => { + const refs = fetchButtonRefs(button); + + console.log(refs); + + if (refs.button && refs.video) { + refs.button.addEventListener('click', () => toggleVideo(refs)); + setInitialState(refs); + } + }); + }, +}; diff --git a/src/components/video/video.scss b/src/components/video/video.scss index e49e1f9..e5a4314 100644 --- a/src/components/video/video.scss +++ b/src/components/video/video.scss @@ -16,3 +16,17 @@ overflow: hidden; position: relative; } + +.playback__button { + 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%; + } +}