Notes:

  • To use the dark version of the slice, add a classname ps-video-player--dark to the slice container as shown in the markup for the dark version.

Interactivity Requirements:

In order for the script to enhance the list of links into the video player:

  • The video player and playlist need to be wrapped in a container with data-video-player on it.
  • The video itself (an iframe) needs to be wrapped in a container with data-video on it. Initially, it’s not shown and will be shown using JS because the widget is only interactive when JS is enabled anyway. So a hidden attribute is set on it.
  • The video playlist is wrapped in a container with data-playlist on it.

The only thing that the user needs to provide is the list of links to videos.

  • The script turns the list of links into a list of “tabs” as far as screen readers as concerned.
  • Each tab has role="tab" and a unique ID.
  • The container of the tabs has role="tablist".
  • Normally, each tab would control its own panel, but in this case they all control one panel.
  • The content panel gets role="tabpanel".
  • When a tab is selected, aria-selected="true" is set on it, and the content panel (the video container) gets aria-labelledby="#tabID", where tabID is the ID of the tab currently selected.
  • The above roles we used will tell a screen reader that this is a tabbed interface, and the user will have certain expectations for interacting with it. They will expect to be able to navigate the tabs using arrow keys (up and down), and then press SPACE or ENTER to activate a tab. This keyboard support is adding in the JS.
  • Focus management is therefore in order. When a user tabs to the component, focus should move to the currently selected tab only. The other tabs would only be accessible using arrow keys. This means that only the currently selected tab can have tabindex="0". When a tab is not selected, it has tabindex="-1".
  • When the user tabs away from the currently selected tab, the focus moves to the tabpanel, and then to any focusable content inside of it.
  • When the user tabs away from the tabpanel, it should get a tabindex="-1" so that keyboard focus continues to work as expected. All the keyboard focus management is handled in the JavaScript.
<section class="ps ps-video-player">
    <div class="ps__wrap">
        <div class="ps__head">
            <header class="ps__header">
                <span class="ps__kicker">Feature</span>
                <h2 class="ps__title" aria-level="">Video Highlights</h2>
            </header>
            <div class="ps__desc">
                <p>
                    These are some awesome videos that teach you how to use Prismic Slices to quickly create a great Web site.
                </p>
            </div>
        </div>

        <div class="ps__main">
            <div class="ps__video-player" data-video-player>
                <div class="ps__video-player__playlist span-9-12" data-playlist>
                    <ul role="list" class="ps__video-player__playlist__list">
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/GVt3LO1Wjwc">Setup a blog with Gatsby and Prismic in
                                less than 10min</a>
                        </li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/yrOYLNiYtBQ">Prismic &amp; Gatsby - Initial setup /
                                custom type / first document</a></li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/3SKii_OLrHs">Why Ueno Chose Prismic</a></li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/are7ZZgA86I">CSS Best Practices: Introduction to The
                                Cascade</a></li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/huvysaySBrw">Why Next.js is An Effective
                                Framework</a></li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/HE2R86EZPMA">Three Things I wish I knew When I
                                started Working on the Web</a></li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/huvysaySBrw">Why Accessibility is Important</a></li>
                        <li class="ps__video-player__playlist__item"><a class="ps__video-player__playlist__link" href="https://www.youtube.com/embed/huvysaySBrw">Implementing a More Effective Web Font
                                Loading Strategy</a>
                        </li>
                    </ul>
                </div>
                <div class="ps__video-container span-1-8" data-video hidden>
                    <iframe title="Video Player" width="560" height="315" src="" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
                </div>
            </div>
        </div>
    </div>
</section>
  • Content:
    var util = {
      keyCodes: {
        UP: 38,
        DOWN: 40,
        LEFT: 37,
        RIGHT: 39,
        HOME: 36,
        END: 35,
        ENTER: 13,
        SPACE: 32,
        DELETE: 46,
        TAB: 9
      },
      generateID: function (base) {
        return base + Math.floor(Math.random() * 999);
      }
    };
    
    (function (w, doc, undefined) {
      var video_player_wrapper = document.querySelector("[data-video-player]"),
        video_container = document.querySelector("[data-video-player] [data-video]"),
        video_player = video_container.querySelector("iframe"),
        video_playlist_container = document.querySelector("[data-video-player] [data-playlist]"),
        video_playlist = video_playlist_container.querySelector("ul"),
        video_playlist_items = Array.from(video_playlist.querySelectorAll("li")),
        video_playlist_sources = Array.from(video_playlist.querySelectorAll("a")),
        video_id = util.generateID('ps__video-player-');
    
      video_player_wrapper.setAttribute('id', video_id);
    
      var init = function () {
        video_player_wrapper.classList.add('js-video-player');
        setupTabList();
        setupTabs();
        setupTabPanel();
      };
      // enhance links to tabs
      // show video player and set the src of the iframe to the first link
    
      // tab list <ul> gets role "tablist" and must have a label
      var setupTabList = function () {
        video_playlist.setAttribute("role", "tablist");
        video_playlist.setAttribute("aria-label", "Video Playlist");
        video_playlist.setAttribute("aria-orientation", "vertical");
      }
      // LI's get role presentation
      // the <a>s get role tab
      var setupTabs = function () {
        video_playlist_items.forEach((item, index) => {
          item.setAttribute('role', 'presentation');
        });
    
        // each link is set to become a tab
        // the href needs to be removed so it doesn't act like link anymore and isn't styled as a link in 
        video_playlist_sources.forEach((tab, index) => {
          tab.setAttribute('role', 'tab');
          tab.setAttribute('data-href', tab.getAttribute('href'));
          tab.setAttribute('href', '#');
          // each tab needs an ID that will be used to label its corresponding panel
          tab.setAttribute('id', video_id + util.generateID('__tab-'));
    
    
          // first tab is initially active
          if (index === 0) {
            selectTab(tab);
          }
    
          tab.addEventListener('click', (e) => {
            e.preventDefault();
            selectTab(tab);
          }, false);
    
          tab.addEventListener('keydown', (e) => {
            tabKeyboardRespond(e, tab);
          }, false);
        });
      }
    
      var selectTab = function (tab) {
    
        // unselect all other tabs
        video_playlist_sources.forEach(tab => {
          tab.setAttribute('aria-selected', 'false');
          tab.setAttribute('tabindex', '-1');
        });
        //select current tab
        tab.setAttribute('aria-selected', 'true');
        tab.setAttribute('tabindex', '0');
    
        // activate corresponding panel accordingly
        activatePanel(tab);
      }
    
      // data-video is shown
      // it has role tabpanel
      var setupTabPanel = function () {
        video_container.removeAttribute('hidden');
        video_container.setAttribute('role', 'tabpanel');
        video_container.setAttribute('tabindex', '-1');
    
        video_container.addEventListener('keydown', (e) => {
          panelKeyboardRespond(e);
        }, false);
    
        video_container.addEventListener("blur", () => {
          video_container.removeAttribute('tabindex');
        }, false);
    
      }
    
      var panelKeyboardRespond = function (e) {
        var keyCode = e.keyCode || e.which;
    
        switch (keyCode) {
          case util.keyCodes.TAB:
            video_container.removeAttribute('tabindex');
            break;
    
          default:
            break;
        }
      }
    
      // tabpanel has aria-labelledby set to the currently selected tab
      // currently selected tab is set with aria-selected="true"
      // iframe in the tabpanel has src 
    
      var activatePanel = function (tab) {
        let vidTitle = tab.innerText;
    
        video_container.setAttribute('aria-labelledby', tab.getAttribute('id'));
        video_player.setAttribute('src', tab.getAttribute('data-href'));
        video_player.setAttribute('title', 'Video Player: ' + vidTitle);
        video_container.setAttribute('tabindex', '0');
      }
    
    
      // keyboard interactions
      var tabKeyboardRespond = function (e, tab) {
        var nextTab = tab.parentNode.nextElementSibling ? tab.parentNode.nextElementSibling.querySelector("[role='tab']") : false,
          previousTab = tab.parentNode.previousElementSibling ? tab.parentNode.previousElementSibling.querySelector("[role='tab']") : false,
          firstTab = video_playlist_sources[0],
          lastTab = video_playlist_sources[video_playlist_sources.length - 1];
    
        var keyCode = e.keyCode || e.which;
    
        switch (keyCode) {
          case util.keyCodes.UP:
            e.preventDefault();
            if (!previousTab) {
              lastTab.focus(); // keep focus within component
            } else {
              previousTab.focus();
            }
            break;
    
          case util.keyCodes.DOWN:
            e.preventDefault();
    
            if (!nextTab) {
              firstTab.focus(); // keep focus within component
            } else {
              nextTab.focus();
            }
            break;
    
          case util.keyCodes.ENTER:
          case util.keyCodes.SPACE:
            e.preventDefault();
            selectTab(tab);
            break;
    
          case util.keyCodes.TAB:
            video_container.setAttribute('tabindex', '0');
            break;
    
          case util.keyCodes.HOME:
            e.preventDefault();
            firstTab.focus();
            break;
    
          case util.keyCodes.END:
            e.preventDefault();
            lastTab.focus();
            break;
        }
    
      }
    
      init();
    })();
    
    
  • URL: /components/raw/video-player/video-player.js
  • Filesystem Path: src/components/video-player/video-player.js
  • Size: 5.5 KB
  • Content:
    .ps-video-player--dark {
      background-color: black;
      color: #fff;
    }
    
    .ps__video-player {
      display: flex;
      flex-direction: column-reverse;
    
      @media all and (min-width: 50em) {
        display: grid;
        grid-template-columns: repeat(12, 1fr);
        grid-column-gap: var(--h-padding);
      }
    }
    
    .ps__video-player__playlist__list {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    
    .ps__video-container {
      height: 0;
      width: 100%;
      padding-top: 56.2%;
      position: relative;
      margin-bottom: var(--c-margin);
    
      grid-row: 1;
    
      @media all and (min-width: 50em) {
        margin-bottom: 0;
      }
    
      &:focus {
        outline-color: var(--color-text-grey);
      }
    
      iframe {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
    
        &:focus {
          outline-color: var(--color-text-grey);
        }
      }
    }
    
    
    .ps__video-player__playlist__item {
      border-top: 1px solid #e5e5e5;
    
      &:last-of-type {
        border-bottom: 1px solid #e5e5e5;
      }
    
      @media all and (max-width: 50em) {
        &:first-of-type {
          border-top: none;
        }
      }
    
      .ps-video-player--dark & {
        border-color: var(--color-grey-20);
      }
    }
    
    .ps__video-player__playlist__link {
      position: relative;
      z-index: 1;
      display: block;
      cursor: pointer;
      font-weight: normal;
      color: inherit;
    
      text-align: left;
      text-decoration: none;
      text-overflow: ellipsis;
      /* Required for text-overflow to do anything */
      white-space: nowrap;
      overflow: hidden;
    
    
      padding: .75em;
      padding-left: .5em;
      padding-right: 3em;
      background-size: 1em 1em;
      background-repeat: no-repeat;
      background-position: right center;
    
      .js-video-player & {
        background-image: url("../img/video-play-icon--grey.svg");
      }
    
      &:hover {
        color: inherit;
      }
    
      .ps-video-player--dark & {
        color: var(--color-text-grey);
    
        .js-video-player & {
          background-image: url("../img/video-play-icon--dark-grey.svg");
        }
    
        &:visited {
          color: var(--color-text-grey);
        }
      }
    
      &[aria-selected="true"] {
        .js-video-player & {
          background-image: url("../img/video-play-icon--black.svg");
        }
    
        font-weight: bold;
      }
    
      .ps-video-player--dark &[aria-selected="true"] {
        .js-video-player & {
          background-image: url("../img/video-play-icon--white.svg");
        }
    
        color: #fff;
        font-weight: normal;
      }
    
      &[href]:focus {
        background-repeat: no-repeat; // for a weird firefox bug on click
        outline-color: var(--color-text-grey);
    
        outline-offset: 5px;
      }
    
      &:visited {
        color: inherit;
      }
    
    }
  • URL: /components/raw/video-player/video-player.scss
  • Filesystem Path: src/components/video-player/video-player.scss
  • Size: 2.5 KB
  • Content:
    title: Video Player
    status: ready
    # notes: "You can have a note here; it will override the README.md notes"
    context:
      text: 
    
  • URL: /components/raw/video-player/video-player.yml
  • Filesystem Path: src/components/video-player/video-player.yml
  • Size: 126 Bytes