Skip to content

Commit

Permalink
Merge pull request #452 from UN-OCHA/CD-483-sdc-video
Browse files Browse the repository at this point in the history
[CD-483] Video SDC
  • Loading branch information
rupl authored Jan 4, 2024
2 parents 9cda016 + 3946ea8 commit 099726d
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 0 deletions.
48 changes: 48 additions & 0 deletions components/video/README.md
Original file line number Diff line number Diff line change
@@ -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 `<details>`/`<summary>` 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.
Binary file added components/video/button-youtube.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions components/video/video.component.yml
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions components/video/video.css
Original file line number Diff line number Diff line change
@@ -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 <summary> 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;
}
58 changes: 58 additions & 0 deletions components/video/video.js
Original file line number Diff line number Diff line change
@@ -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 = '<span class="visually-hidden">' + thisVideo.dataset.videoButtonLabel + '</span>';

// 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 = '<iframe class="video__iframe" src="https://www.youtube.com/embed/' + video.dataset.videoSlug + '?autoplay=1&playsinline=1&rel=0&enablejsapi=1&origin=' + encodeURIComponent(video.dataset.videoOrigin) + '" frameborder="0" allow="autoplay; fullscreen" sandbox="allow-same-origin allow-scripts"></iframe>';
}

// Warm TCP connections by adding <link>s to the <head>.
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);
}
})();
33 changes: 33 additions & 0 deletions components/video/video.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<article{{ attributes.addClass(classes) }} aria-label="{{ 'Video'|t }}">
<h3 class="visually-hidden">{{ title }}</h3>
<div class="video"
data-video-slug="{{ video_slug }}"
data-video-button-label="{{ 'Load video'|t }}"
data-video-origin="{{ url('<front>')|render|trim('/') }}">
<a href="{{ video_url }}"
target="_blank"
rel="noopener"
class="video__link">
<img
class="video__img"
loading="lazy"
src="https://img.youtube.com/vi/{{ video_slug }}/maxresdefault.jpg"
srcset="https://img.youtube.com/vi/{{ video_slug }}/mqdefault.jpg 320w,
https://img.youtube.com/vi/{{ video_slug }}/hqdefault.jpg 480w,
https://img.youtube.com/vi/{{ video_slug }}/sddefault.jpg 640w,
https://img.youtube.com/vi/{{ video_slug }}/maxresdefault.jpg 1280w"
sizes="(min-width: 1220px) 1200px,
calc(100% - 1.5rem)"
alt="{{ 'Preview of the video from YouTube'|t }}">
</a>
</div>

{% if video_transcript is not empty %}
<details class="video__transcript">
<summary class="video__transcript-label">{{ 'Video transcript'|t }}</summary>
<div class="video__transcript-text">
{{ video_transcript }}
</div>
</details>
{% endif %}
</article>
21 changes: 21 additions & 0 deletions templates/layout/page--demo.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
<li><a href="#cd-title-list">Title List</a></li>
<li><a href="#cd-toc">Table of contents</a></li>
<li><a href="#cd-typography">Typography</a></li>
<li><a href="#cd-video">Video</a></li>
</ol>
</div>

Expand Down Expand Up @@ -330,6 +331,26 @@

{% include '@cd-components/cd-typography/cd-typography.html.twig' %}

<h3 class="cd-styleguide"><a id="cd-video">cd-video</a>
{% include '@cd-components/cd-back-to-toc/cd-back-to-toc.html.twig' %}</h3>

{{ 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',
}) }}</h3>

{% endif %}

</div>{# /.cd-demo-content #}
Expand Down

0 comments on commit 099726d

Please sign in to comment.