From 87de571befd6a2cb552bbc4b496c19ee4c538141 Mon Sep 17 00:00:00 2001 From: Helen Chapman Date: Wed, 27 Mar 2024 15:16:42 +0000 Subject: [PATCH] Youtube consent - front-end (#183) * Youtube consent front-end * Focus into video after accepting tcs and cs so screenreaders can play the video * Don't focus on video unless you've just given consent, sort focus state, fix cookie in safari * update docs --- docs/front-end/lite-youtube.md | 7 + docs/front-end/sustainability.md | 2 + mkdocs.yml | 1 + .../patterns/atoms/sprites/sprites.html | 4 + .../streamfield/blocks/embed_block.html | 49 +++--- .../streamfield/blocks/embed_block.yaml | 2 + .../javascript/components/youtube-embed.js | 54 +++--- tbx/static_src/javascript/main.js | 3 +- tbx/static_src/sass/components/_promo.scss | 1 - .../sass/components/_youtube-embed.scss | 157 ++++++++++++++++++ .../sass/components/_youtube_embed.scss | 66 -------- tbx/static_src/sass/config/_mixins.scss | 2 +- tbx/static_src/sass/config/_variables.scss | 2 + tbx/static_src/sass/main.scss | 5 +- 14 files changed, 229 insertions(+), 126 deletions(-) create mode 100644 docs/front-end/lite-youtube.md create mode 100644 tbx/static_src/sass/components/_youtube-embed.scss delete mode 100644 tbx/static_src/sass/components/_youtube_embed.scss diff --git a/docs/front-end/lite-youtube.md b/docs/front-end/lite-youtube.md new file mode 100644 index 000000000..5f58045c7 --- /dev/null +++ b/docs/front-end/lite-youtube.md @@ -0,0 +1,7 @@ +# Lite youtube + +We use the https://www.npmjs.com/package/lite-youtube-embed package to avoid setting youtube cookies, along with https://theorangeone.net/projects/wagtail-lite-youtube-embed/ + +When youtube videos are first loaded the user must accept the youtube Ts and Cs. They can opt not to show them again - this sets a cookie to hide the consent message in future. + +This functionality is controlled by `tbx/static_src/javascript/components/youtube-embed.js`. diff --git a/docs/front-end/sustainability.md b/docs/front-end/sustainability.md index 013a84e13..5ab0b3ce1 100644 --- a/docs/front-end/sustainability.md +++ b/docs/front-end/sustainability.md @@ -9,3 +9,5 @@ We use dark mode by default. All images should be renedered in the template file as webp format using the `format-webp` attribute - see https://docs.wagtail.org/en/v5.2.2/advanced_topics/images/image_file_formats.html. We have a custom saturation filter that allows us to apply a slight desaturation to all images using `saturation-0.6` - this makes the file size of images smaller overall, as well as creating a cohesive look for photographic images. + +Youtube videos are not loaded by default. Instead they show a placeholder image and a play button, and only load when the user clicks 'play'. diff --git a/mkdocs.yml b/mkdocs.yml index 166998c7e..9483e5dcf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - front-end/placeholder-images.md - front-end/utility-classes.md - 'Markdown block and codehilite': 'front-end/markdown-codehilite.md' + - front-end/lite-youtube.md - 'Navigation': 'navigation.md' - 'Custom features': - 'Migration-friendly StreamFields': 'custom-features/migration-friendly-streamfields.md' diff --git a/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html b/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html index 0ac0116b3..c54677405 100644 --- a/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html +++ b/tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html @@ -68,4 +68,8 @@ + + + + diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.html b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.html index c527633e9..ae32bf1c8 100644 --- a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.html +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.html @@ -1,35 +1,36 @@ {% load static %} -
- {% if is_youtube %} -
+{% if is_youtube %} +
+
{% if thumbnail_url %} - Video Thumbnail + Video Thumbnail {% else %} - {% comment %} - We need a placeholder here just in case we fail to get the thumbnail_url for some reason - {% endcomment %} + {# Fallback if we can't fetch a preview image from youtube #} +
{% include "patterns/atoms/icons/icon.html" with name="youtube" classname="youtube-embed__logo" %}
{% endif %} -
- -
- - +
+
- +{% else %} +
{{ value }} - {% endif %} -
+
+{% endif %} + diff --git a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.yaml b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.yaml index 16c2a7c39..420b2238f 100644 --- a/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.yaml +++ b/tbx/project_styleguide/templates/patterns/molecules/streamfield/blocks/embed_block.yaml @@ -1,2 +1,4 @@ context: value: + is_youtube: True + thumbnail_url: https://i.ytimg.com/vi/g9kZwNEHmdw/hqdefault.jpg diff --git a/tbx/static_src/javascript/components/youtube-embed.js b/tbx/static_src/javascript/components/youtube-embed.js index 5657ba63f..983855ba6 100644 --- a/tbx/static_src/javascript/components/youtube-embed.js +++ b/tbx/static_src/javascript/components/youtube-embed.js @@ -5,65 +5,55 @@ import Cookies from 'js-cookie'; */ class YouTubeConsentManager { static selector() { - return '.grid__embed.streamfield__embed.youtube-video-container'; + return '[data-youtube-embed]'; } /** * Create a new YouTubeConsentManager. */ constructor(node) { - this.consentButtonClass = 'consent-button'; - this.dontAskAgainButtonClass = 'dont-ask-again-button'; - this.videoPlaceholderClass = 'youtube-video-placeholder'; - this.youtubeEmbedClass = 'youtube-embed'; - - this.youtubeEmbedContainer = node; - this.consentButton = this.youtubeEmbedContainer.querySelector( - `.${this.consentButtonClass}`, - ); - this.dontAskAgainButton = this.youtubeEmbedContainer.querySelector( - `.${this.dontAskAgainButtonClass}`, + this.youtubeEmbedNode = node; + this.consentButton = this.youtubeEmbedNode.querySelector( + '[data-youtube-consent-button]', ); - this.videoPlaceholder = this.youtubeEmbedContainer.querySelector( - `.${this.videoPlaceholderClass}`, + this.dontAskAgainCheckbox = this.youtubeEmbedNode.querySelector( + '[data-youtube-save-prefs]', ); - this.youtubeEmbed = this.youtubeEmbedContainer.querySelector( - `.${this.youtubeEmbedClass}`, + this.embedContainer = this.youtubeEmbedNode.querySelector( + '[data-youtube-embed-container]', ); + this.bindEvents(); + } - // Bind event handlers - this.consentButton.addEventListener( - 'click', - this.handleconsentClick.bind(this), - ); - this.dontAskAgainButton.addEventListener( - 'click', - this.handleDontAskAgainClick.bind(this), - ); + bindEvents() { + this.consentButton.addEventListener('click', () => { + this.handleconsentClick(); + }); // Check if consent has been given previously this.checkConsent(); } loadYouTubeEmbed() { - // Hide the video placeholder - this.videoPlaceholder.style.display = 'none'; - // Show the YouTube embed container - this.youtubeEmbed.style.display = 'block'; + // Hide the video placeholder and show the YouTube embed container + this.youtubeEmbedNode.classList.add('loaded'); } handleconsentClick() { + if (this.dontAskAgainCheckbox.checked) { + this.handleDontAskAgainClick(); + } this.loadYouTubeEmbed(); + this.embedContainer.querySelector('button').focus(); } handleDontAskAgainClick() { // Set a cookie to remember the user's choice not to ask again Cookies.set('youtube_consent', 'true', { - expires: 7, - secure: true, + expires: 365, sameSite: 'Lax', }); - this.dontAskAgainButton.style.display = 'none'; + this.loadYouTubeEmbed(); } diff --git a/tbx/static_src/javascript/main.js b/tbx/static_src/javascript/main.js index ac1a12fa1..370548016 100755 --- a/tbx/static_src/javascript/main.js +++ b/tbx/static_src/javascript/main.js @@ -12,7 +12,8 @@ import foreachPolyfill from './polyfills/foreach-polyfill'; import closestPolyfill from './polyfills/closest-polyfill'; import '../sass/main.scss'; -import 'lite-youtube-embed/src/lite-yt-embed.css'; + +// Third party imports import 'lite-youtube-embed/src/lite-yt-embed'; foreachPolyfill(); diff --git a/tbx/static_src/sass/components/_promo.scss b/tbx/static_src/sass/components/_promo.scss index d1829f4ff..9d5f37ece 100644 --- a/tbx/static_src/sass/components/_promo.scss +++ b/tbx/static_src/sass/components/_promo.scss @@ -50,7 +50,6 @@ &__button { width: fit-content; color: var(--color--light-background-text); - background-color: transparent; border-color: var(--color--light-background-text); &:hover, diff --git a/tbx/static_src/sass/components/_youtube-embed.scss b/tbx/static_src/sass/components/_youtube-embed.scss new file mode 100644 index 000000000..15459886a --- /dev/null +++ b/tbx/static_src/sass/components/_youtube-embed.scss @@ -0,0 +1,157 @@ +@use 'config' as *; + +.youtube-embed { + $root: &; + position: relative; + // allow room for overlay to be positioned absolutely + padding-bottom: $spacer-large; + // for high contrast mode + border: 1px solid transparent; + + @include media-query(medium) { + padding-bottom: 0; + } + + &__placeholder { + position: relative; + width: 100%; + + #{$root}.loaded & { + display: none; + } + + @media (scripting: none) { + display: none; + } + } + + // fallback if there is no thumbnail + &__fallback { + // same as youtube placeholder image + aspect-ratio: 16 / 12; + width: 100%; + background-color: var(--color--white); + display: flex; + justify-content: center; + padding-top: $spacer-mini; + + @include media-query(medium) { + padding-top: $spacer-medium; + } + } + + // only shows in the fallback div + &__logo { + width: 100px; + height: 23px; + // for high contrast mode + background-color: var(--color--white); + + @include media-query(medium) { + width: 200px; + height: 45px; + } + } + + &__thumbnail-image { + width: 100%; + } + + &__overlay { + position: absolute; + // at mobile the overlay would cover the whole placeholder + // image so shift it down + bottom: -$spacer-large; + left: 0; + right: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + + @include media-query(medium) { + inset: 0; + } + } + + &__consent-banner { + @include font-size(body); + color: var(--color--heading); + width: 100%; + background-color: rgba($color--mid-grey, 0.5); + background-color: var(--color--streamfield-background); + padding: $spacer-mini; + text-align: center; + // for high contrast mode + border: 1px solid transparent; + + @include media-query(large) { + padding: $spacer-small-plus; + } + } + + &__link { + @include link-styles( + $color: var(--color--heading), + $interaction-color: var(--color--heading) + ); + } + + &__button-container { + @include font-size(supporting); + display: flex; + flex-direction: column; + justify-content: center; + margin-top: $spacer-mini; + gap: $spacer-mini-plus; + + @include media-query(medium) { + flex-direction: row; + } + } + + &__button { + margin: 0 auto; + + @include media-query(medium) { + margin: 0; + } + } + + &__checkbox-wrapper { + display: flex; + align-items: center; + gap: 5px; + margin: 0 auto; + + @include media-query(medium) { + margin: 0; + } + } + + &__checkbox { + width: 20px; + height: 20px; + } + + &__label { + @include font-size(supporting); + } + + &__container { + display: none; + + #{$root}.loaded & { + display: block; + } + + @media (scripting: none) { + display: block; + } + + button:focus { + @include focus-style(); + outline-offset: -2px; + } + } +} diff --git a/tbx/static_src/sass/components/_youtube_embed.scss b/tbx/static_src/sass/components/_youtube_embed.scss deleted file mode 100644 index 455f3b55c..000000000 --- a/tbx/static_src/sass/components/_youtube_embed.scss +++ /dev/null @@ -1,66 +0,0 @@ -@use 'config' as *; - -.youtube-video-container { - position: relative; -} - -.youtube-video-placeholder { - position: relative; - width: 100%; - - img { - width: 100%; - height: auto; - } -} - -.youtube-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - .consent-banner { - color: $color--white; - padding: 10px; - text-align: center; - margin-bottom: 10px; - - a { - color: $color--white; - text-decoration: underline; - transition: color 0.3s ease; - - &:hover { - color: $color--grey-20; - } - } - } - - .button-container { - display: flex; - - .consent-button, - .dont-ask-again-button { - margin: 5px; - padding: 10px 20px; - background-color: $color--nebuline-10; - color: $color--mid-grey; - border: none; - border-radius: 5px; - cursor: pointer; - display: flex; - justify-content: center; - - &:hover { - background-color: $color--grey-5; - } - } - } -} diff --git a/tbx/static_src/sass/config/_mixins.scss b/tbx/static_src/sass/config/_mixins.scss index 253ea6307..b5df66b85 100755 --- a/tbx/static_src/sass/config/_mixins.scss +++ b/tbx/static_src/sass/config/_mixins.scss @@ -336,7 +336,7 @@ text-align: center; border: 1px solid var(--color--link); color: var(--color--link); - background-color: var(--color--background); + background-color: transparent; padding: 12px $spacer-small-plus; transition: color $transition-quick, background-color $transition-quick; font-weight: $weight--semibold; diff --git a/tbx/static_src/sass/config/_variables.scss b/tbx/static_src/sass/config/_variables.scss index c0b05a76c..8dca531ea 100755 --- a/tbx/static_src/sass/config/_variables.scss +++ b/tbx/static_src/sass/config/_variables.scss @@ -56,6 +56,8 @@ $color--mid-grey: #282e30; // in figma this colour is created with a very transp --color--border: #{color.adjust($color--grey-50, $alpha: -0.5)}; --color--overlay: #{color.adjust($color--black, $alpha: -0.6)}; --color--light-grey-background: #{$color--grey-5}; + // where we always want white + --color--white: #{$color--white}; } // Coral theme, dark mode - also by default diff --git a/tbx/static_src/sass/main.scss b/tbx/static_src/sass/main.scss index e80ee9cd9..04c7b183f 100755 --- a/tbx/static_src/sass/main.scss +++ b/tbx/static_src/sass/main.scss @@ -1,3 +1,6 @@ +// CSS from external vendors (available as npm) +@use '../../../node_modules/lite-youtube-embed/src/lite-yt-embed.css'; + // Base @use 'base/base'; @use 'base/typography'; @@ -55,7 +58,7 @@ @use 'components/work-hero'; @use 'components/work-page-author'; @use 'components/work-sections'; -@use 'components/youtube_embed'; +@use 'components/youtube-embed'; // Navigation @use 'components/navigation/breadcrumbs-nav';