ps-video-player--dark
to the slice container as shown in the markup for the dark version.In order for the script to enhance the list of links into the video player:
data-video-player
on it.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.data-playlist
on it.The only thing that the user needs to provide is the list of links to videos.
role="tab"
and a unique ID. role="tablist"
.role="tabpanel"
.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.tabindex="0"
. When a tab is not selected, it has tabindex="-1"
.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 & 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>
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();
})();
.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;
}
}
title: Video Player
status: ready
# notes: "You can have a note here; it will override the README.md notes"
context:
text: