diff --git a/components/video/README.md b/components/video/README.md new file mode 100644 index 00000000..f15dd8b7 --- /dev/null +++ b/components/video/README.md @@ -0,0 +1,48 @@ +# Video + +Produces a YouTube embed from a URL stored in a Drupal Link field. It's basically a reimplementation of [lite-youtube-embed](https://github.com/paulirish/lite-youtube-embed) by Paul Irish, but in Twig. + +This component has been tested over the years on multiple World Humanitarian Day campaigns and has met the following requirements: + +- **Performant:** loads minimal code/data up front, waits for user interaction before downloading video. +- **Accessible:** has been tested to ensure it meets our standards for screen-reader UX and keyboard nav. +- **Translatable:** all necessary labels can be translated by Drupal. +- **Trackable:** integrates with GTM out of the box by allowing the necessary permissions within the iframe that gets generated by JS. +- **Less spammy:** once the video has finished playing, it will only recommend videos from the same channel. + +## Outline of Progressive Enhancement + +1. When the page loads, it displays a responsive preview image with a YouTube play button superimposed over the preview. The video transcript is totally optional, but can be used to supplement the video for screen-reader users and requires no additional JS to be read. It uses the `
`/`` HTML elements and their built-in interactivity. +2. When the cursor hovers over the image, it will preconnect to YouTube. +3. When the user finally clicks the image, it will load the actual video embed and attempt to autoplay. + +## Caveats + +The `video_slug`/`video_url` properties are rarely entered and stored directly in Drupal. Rather, we often have Editors enter a YouTube URL as displayed in the browser, and preprocess it into what we need. This example preprocess is intended for a Paragraph with machine name `video` which contains a Link field named `field_video_url`. + +```php +/** + * Implements hook_preprocess_paragraph__type(). + * + * Example preprocess hook that converts a Link field that contains a + * fully-qualified URL pointing to YouTube into the video slug/url. + * + * Example: https://www.youtube.com/watch?v=ScMzIvxBSi4 + */ +function THEME_preprocess_paragraph__video(&$variables) { + /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ + $paragraph = $variables['paragraph']; + + if (!$paragraph->field_video_url->isEmpty()) { + // Extract video slug from YouTube. + $qs = parse_url($paragraph->get('field_video_url')->first()->uri, PHP_URL_QUERY); + parse_str($qs, $params); + $variables['video_slug'] = $params['v']; + $variables['video_url'] = $paragraph->get('field_video_url')->first()->uri; + } +} +``` + +## Future TODOs + +- Once `sizes="auto"` is supported in evergreen browsers, we should use it. For the time being, we used some simple defaults that match the CD layout container. diff --git a/components/video/button-youtube.png b/components/video/button-youtube.png new file mode 100644 index 00000000..3fbe82f8 Binary files /dev/null and b/components/video/button-youtube.png differ diff --git a/components/video/video.component.yml b/components/video/video.component.yml new file mode 100644 index 00000000..a4da5cc0 --- /dev/null +++ b/components/video/video.component.yml @@ -0,0 +1,53 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json +name: Video +status: experimental +props: + type: object + required: + - attributes + - title + - video_url + - video_slug + properties: + attributes: + title: Drupal attributes + type: Drupal\Core\Template\Attribute + classes: + title: Extra classnames + type: array + items: + type: string + title: + title: Title + type: string + description: The title of the Video. + examples: + - Humanitarians are \#NotATarget + video_url: + title: Video URL + type: string + description: The publicly-viewable canonical URL of the Video. + examples: + - https://www.youtube.com/watch?v=p5zzrlNtmLo + video_slug: + title: Video Slug + type: string + description: The unique identifier extracted from the Video URL. Look in the README to see how this is provided to the Drupal template which implements this component. Compare example slug value to the `video_url` example. + examples: + - p5zzrlNtmLo + video_transcript: + title: Video transcript + type: string + description: A plaintext transcript of the video, or including a written description of any critical content within the video. If you send HTML, the tags will be visible in the output. + examples: + - | + A man is shown handing out supplies to a group of children. The camera zooms in towards his face. + + Narrator: While bringing food, relief supplies, and medicine, we are at risk of being looted, harassed and even killed. + + Humanitarian workers are never the enemy. + Face world leaders to say civilians are \#NotATarget + World Humanitarian Day + 19. August 2018 + WorldHumanitarianDay.org + diff --git a/components/video/video.css b/components/video/video.css new file mode 100644 index 00000000..f81fe5d3 --- /dev/null +++ b/components/video/video.css @@ -0,0 +1,86 @@ +/** + * Video Embed + */ +.video { + --video-padding: 0; + --video-margin: 1rem; + + position: relative; + overflow: hidden; + + /* Legacy layout (IE) */ + width: 100%; + height: 0; + padding-bottom: 56.25%; +} + +/* Modern layout */ +@supports (--css-vars: true) { + .video { + aspect-ratio: 16 / 9; + width: 100%; + height: unset; + margin-block-start: 1rem; + padding: var(--video-padding); + border-radius: 5px; + } +} + +.video__link { + display: block; +} + +.video__img { + width: 100%; + height: 100%; +} + +.video__button { + position: absolute; + z-index: 10; + display: block; + width: 73.4px; + height: 51.8px; + cursor: pointer; + transform: translate(-50%, -50%); + border: none; + border-radius: 13px; + background: transparent url('button-youtube.png') no-repeat 50% 50%; + background-size: contain; + inset: 50%; +} + +.video__button:focus-visible { + outline: 3px solid var(--cd-white); + outline-offset: -2px; +} + +.video__iframe { + width: 100%; + height: 100%; +} + +.video__transcript { + margin-block-start: 1rem; + border-radius: 5px; + background: var(--brand-grey); +} + +/* normalize.css removes built-in browser affordance for so we are +resetting its display */ +.video__transcript-label { + display: list-item; + cursor: pointer; + border-radius: 5px; + padding-block: .25rem; + padding-inline: 1rem; + font-weight: 700; +} + +.video__transcript-label:focus-visible { + outline: 3px solid var(--cd-black); +} + +.video__transcript-text { + padding: 1rem; +} diff --git a/components/video/video.js b/components/video/video.js new file mode 100644 index 00000000..836c6fcd --- /dev/null +++ b/components/video/video.js @@ -0,0 +1,58 @@ +(function ($) { + 'use strict'; + + // The videos get delivered as links to YouTube with image tags inside. Each + // one needs to be prepped by JS with the following steps: + // - Add a JS button to dynamically load the video when clicked + // - Setup the listener for the button click + const videos = document.querySelectorAll('.video'); + videos.forEach((thisVideo) => { + // Create basic button with contents/styling + const thisButton = document.createElement('button'); + thisButton.classList.add('video__button'); + thisButton.innerHTML = '' + thisVideo.dataset.videoButtonLabel + ''; + + // Listen for interaction and load video + thisButton.addEventListener('click', (ev) => { + prepVideo(thisVideo); + }); + + // Insert button into DOM. + thisVideo.appendChild(thisButton); + + // Prevent link from leading away now, and override with video + const originalLink = thisVideo.querySelector('a'); + originalLink.addEventListener('click', (ev) => { + prepVideo(thisVideo); + ev.preventDefault(); + }); + + // Wait for cursor to hover and warm the connection when we find it. + originalLink.addEventListener('pointerover', (ev) => { + if (thisVideo.dataset.videoConnected === 'true') { + return; + } + + addPrefetch('preconnect', 'https://www.youtube.com'); + addPrefetch('preconnect', 'https://www.google.com'); + thisVideo.dataset.videoConnected = 'true'; + }); + + }); + + // Accepts a DOM container and replaces its contents with a YouTube iframe. + function prepVideo(video) { + video.innerHTML = ''; + } + + // Warm TCP connections by adding s to the . + function addPrefetch(kind, url, as) { + const linkEl = document.createElement('link'); + linkEl.rel = kind; + linkEl.href = url; + if (as) { + linkEl.as = as; + } + document.head.append(linkEl); + } +})(); diff --git a/components/video/video.twig b/components/video/video.twig new file mode 100644 index 00000000..404b6895 --- /dev/null +++ b/components/video/video.twig @@ -0,0 +1,33 @@ + +

{{ title }}

+
+ + {{ 'Preview of the video from YouTube'|t }} + +
+ + {% if video_transcript is not empty %} +
+ {{ 'Video transcript'|t }} +
+ {{ video_transcript }} +
+
+ {% endif %} + diff --git a/templates/layout/page--demo.html.twig b/templates/layout/page--demo.html.twig index 9cf818f1..41a8deb9 100644 --- a/templates/layout/page--demo.html.twig +++ b/templates/layout/page--demo.html.twig @@ -149,6 +149,7 @@
  • Title List
  • Table of contents
  • Typography
  • +
  • Video
  • @@ -330,6 +331,26 @@ {% include '@cd-components/cd-typography/cd-typography.html.twig' %} +

    cd-video + {% include '@cd-components/cd-back-to-toc/cd-back-to-toc.html.twig' %}

    + + {{ include('common_design:video', { + attributes: create_attribute(), + classes: ['video--youtube'], + title: 'Demo Video Title', + video_slug: 'p5zzrlNtmLo', + video_url: 'https://www.youtube.com/watch?v=p5zzrlNtmLo', + video_transcript: 'A man is shown handing out supplies to a group of children. The camera zooms in towards his face. + +Narrator: While bringing food, relief supplies, and medicine, we are at risk of being looted, harassed and even killed. + +Humanitarian workers are never the enemy. +Face world leaders to say civilians are \#NotATarget +World Humanitarian Day +19. August 2018 +https://www.WorldHumanitarianDay.org', + }) }} + {% endif %} {# /.cd-demo-content #}