Skip to content

Commit

Permalink
Update: Make media player web-accessible according to WCAG2.0 spec (#97)
Browse files Browse the repository at this point in the history
Main behavior updates are:
1. Functional elements (buttons, menu, scrubber) are focusable/tabbable
2. Tab-key (and sometimes arrow keys) signal visible borders to be
displayed on the focused element.
3. Tab order of elements reflects visual order
4. Key-events are contextual (e.g. up/down keys while focus is on
scrubber moves the handle in the scrubber)
5. ARIA properties are set on all functional elements, for screenreaders
  • Loading branch information
bhh1988 authored May 9, 2017
1 parent 2581b88 commit 687dba4
Show file tree
Hide file tree
Showing 22 changed files with 1,184 additions and 316 deletions.
4 changes: 4 additions & 0 deletions src/i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ media_play=Play
media_pause=Pause
# Label for Settings button in media player
media_settings=Settings
# Used in ARIA label for volume
volume=Volume
# Used in ARIA label for timescrubber progress
of=of


# 3D Preview
Expand Down
1 change: 1 addition & 0 deletions src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_DOCUMENT = 'bp-document-preload-w
export const CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_PRESENTATION = 'bp-presentation-preload-wrapper';
export const CLASS_BOX_PREVIEW_TOGGLE_OVERLAY = 'bp-toggle-overlay';
export const CLASS_BOX_PREVIEW_THEME_DARK = 'bp-theme-dark';
export const CLASS_ELEM_KEYBOARD_FOCUS = 'bp-has-keyboard-focus';
export const CLASS_FULLSCREEN = 'bp-is-fullscreen';
export const CLASS_INVISIBLE = 'bp-is-invisible';
export const CLASS_IS_VISIBLE = 'bp-is-visible';
Expand Down
61 changes: 57 additions & 4 deletions src/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export function openContentInsideIframe(content) {
*
* @param {Element} node - DOM node
* @param {string} template - HTML template
* @return {HTMLElement}
* @return {DocumentFragment}
*/
export function createFragment(node, template) {
const range = document.createRange();
Expand All @@ -201,14 +201,15 @@ export function createFragment(node, template) {
}

/**
* Inserts template string into DOM node
* Inserts template string into DOM node, before beforeNode. If beforeNode is null, inserts at end of child nodes
*
* @param {Element} node - DOM node
* @param {string} template html template
* @param {Element|void} beforeNode - DOM node
* @return {void}
*/
export function insertTemplate(node, template) {
node.appendChild(createFragment(node, template));
export function insertTemplate(node, template, beforeNode = null) {
node.insertBefore(createFragment(node, template), beforeNode);
}

/**
Expand Down Expand Up @@ -485,6 +486,12 @@ export function decodeKeydown(event) {
key = 'Space';
}

// Edge bug which outputs "Esc" instead of "Escape"
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5290772/
if (key === 'Esc') {
key = 'Escape';
}

// keyIdentifier spec does not prefix the word Arrow.
// Newer key spec does it automatically.
if (key === 'Right' || key === 'Left' || key === 'Down' || key === 'Up') {
Expand Down Expand Up @@ -589,3 +596,49 @@ export function setDimensions(element, width, height) {
element.style.height = `${height}px`;
/* eslint-enable no-param-reassign */
}

/**
* Wrapper around an event-handler that returns another event-handler which first checks that
* the event is a click, space-key, or enter-key before invoking the handler
*
* @param {Function} handler - Key activation handler
* @return {void}
*/
export function activationHandler(handler) {
return (event) => {
if (event.type === 'click') {
handler(event);
} else if (event.type === 'keydown') {
const key = decodeKeydown(event);
if (key === 'Space' || key === 'Enter') {
handler(event);
event.preventDefault();
event.stopPropagation();
}
}
};
}

/**
* Adds event listeners for click, space, and enter keys
*
* @param {HTMLElement} element - HTMLElement
* @param {Function} handler - Function to be invoked on click/space/enter
* @return {void}
*/
export function addActivationListener(element, handler) {
element.addEventListener('click', handler);
element.addEventListener('keydown', handler);
}

/**
* Removes event listeners added by addActivationListener
*
* @param {HTMLElement} element - HTMLElement
* @param {Function} handler - Function to be removed on click/space/enter
* @return {void}
*/
export function removeActivationListener(element, handler) {
element.removeEventListener('click', handler);
element.removeEventListener('keydown', handler);
}
2 changes: 1 addition & 1 deletion src/lib/viewers/doc/DocBaseViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ class DocBaseViewer extends BaseViewer {
event.preventDefault();
break;

case 'Esc':
case 'Escape':
this.hidePageNumInput();
this.docEl.focus();

Expand Down
1 change: 1 addition & 0 deletions src/lib/viewers/media/DashViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ class DashViewer extends VideoBaseViewer {

// Make media element visible after resize
this.showMedia();
this.mediaContainerEl.focus();
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/lib/viewers/media/MP3.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.bp-media-mp3 {
.bp-media-container {
height: 60px; // same as controls
outline: 0 none;
overflow: visible; // to show settings popup
width: 360px;
}
Expand All @@ -11,6 +12,11 @@
display: none;
}

.bp-media-gear-icon {
position: absolute;
right: 10px;
}

.bp-media-controls-wrapper {
background: $black;
opacity: 1;
Expand Down
54 changes: 46 additions & 8 deletions src/lib/viewers/media/MediaBaseViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import BaseViewer from '../BaseViewer';
import cache from '../../Cache';
import Browser from '../../Browser';
import MediaControls from './MediaControls';
import { CLASS_HIDDEN, CLASS_IS_BUFFERING, CLASS_IS_VISIBLE } from '../../constants';
import { CLASS_ELEM_KEYBOARD_FOCUS, CLASS_HIDDEN, CLASS_IS_BUFFERING, CLASS_IS_VISIBLE } from '../../constants';

const CSS_CLASS_MEDIA = 'bp-media';
const CSS_CLASS_MEDIA_CONTAINER = 'bp-media-container';
Expand All @@ -29,9 +29,12 @@ class MediaBaseViewer extends BaseViewer {

// Media Wrapper
this.mediaContainerEl = this.wrapperEl.appendChild(document.createElement('div'));
this.mediaContainerEl.setAttribute('tabindex', '-1');
this.mediaContainerEl.className = CSS_CLASS_MEDIA_CONTAINER;
this.mediaContainerEl.addEventListener('click', this.containerClickHandler);

this.loadTimeout = 100000;
this.oldVolume = DEFAULT_VOLUME;
}

/**
Expand Down Expand Up @@ -65,6 +68,7 @@ class MediaBaseViewer extends BaseViewer {

if (this.mediaContainerEl) {
this.mediaContainerEl.removeChild(this.mediaEl);
this.mediaContainerEl.removeEventListener(this.containerClickHandler);
}
} catch (e) {
// do nothing
Expand Down Expand Up @@ -100,6 +104,16 @@ class MediaBaseViewer extends BaseViewer {
}).catch(this.handleAssetError);
}

/**
* Click handler for media container
*
* @private
* @return {void}
*/
containerClickHandler() {
this.mediaContainerEl.classList.remove(CLASS_ELEM_KEYBOARD_FOCUS);
}

/**
* Handler for meta data load for the media element.
*
Expand All @@ -120,6 +134,7 @@ class MediaBaseViewer extends BaseViewer {

// Make media element visible after resize
this.showMedia();
this.mediaContainerEl.focus();
}

/**
Expand Down Expand Up @@ -178,6 +193,9 @@ class MediaBaseViewer extends BaseViewer {
*/
handleVolume() {
const volume = cache.has(MEDIA_VOLUME_CACHE_KEY) ? cache.get(MEDIA_VOLUME_CACHE_KEY) : DEFAULT_VOLUME;
if (volume !== 0) {
this.oldVolume = volume;
}

if (this.mediaEl.volume !== volume) {
this.debouncedEmit('volume', volume);
Expand All @@ -203,7 +221,6 @@ class MediaBaseViewer extends BaseViewer {
*/
loadUI() {
this.mediaControls = new MediaControls(this.mediaContainerEl, this.mediaEl);
this.mediaControls.setDuration(this.mediaEl.duration);

// Add event listeners for the media controls
this.addEventListenersForMediaControls();
Expand Down Expand Up @@ -476,25 +493,38 @@ class MediaBaseViewer extends BaseViewer {
* @return {boolean} consumed or not
*/
onKeydown(key) {
// Return false when media controls are not ready or are focused
if (!this.mediaControls || this.mediaControls.isFocused()) {
// Return false when media controls are not ready
if (!this.mediaControls) {
return false;
}

const k = key.toLowerCase();
switch (k) {
case 'tab':
case 'shift+tab':
this.mediaContainerEl.classList.add(CLASS_ELEM_KEYBOARD_FOCUS);
this.mediaControls.show();
return false; // So that tab can proceed to do its default behavior of going to the next element
case 'space':
case 'k':
this.togglePlay();
break;
case 'arrowleft':
this.quickSeek(-5);
if (this.mediaControls.isVolumeScrubberFocused()) {
this.decreaseVolume();
} else {
this.quickSeek(-5);
}
break;
case 'j':
this.quickSeek(-10);
break;
case 'arrowright':
this.quickSeek(5);
if (this.mediaControls.isVolumeScrubberFocused()) {
this.increaseVolume();
} else {
this.quickSeek(5);
}
break;
case 'l':
this.quickSeek(10);
Expand All @@ -504,10 +534,18 @@ class MediaBaseViewer extends BaseViewer {
this.setMediaTime(0);
break;
case 'arrowup':
this.increaseVolume();
if (this.mediaControls.isTimeScrubberFocused()) {
this.quickSeek(5);
} else {
this.increaseVolume();
}
break;
case 'arrowdown':
this.decreaseVolume();
if (this.mediaControls.isTimeScrubberFocused()) {
this.quickSeek(-5);
} else {
this.decreaseVolume();
}
break;
case 'shift+>':
this.mediaControls.increaseSpeed();
Expand Down
69 changes: 31 additions & 38 deletions src/lib/viewers/media/MediaControls.html
Original file line number Diff line number Diff line change
@@ -1,66 +1,59 @@
<div class="bp-media-controls-wrapper">
<div class="bp-media-time-scrubber-container"></div>
<div class="bp-media-time-scrubber-container" tabindex="0"></div>
<div class="bp-media-controls-container">
<button class="bp-media-controls-btn bp-media-playpause-icon">
<svg class="bp-media-play-icon" fill="#fff" height="24" viewBox="0 0 24 24" width="24" aria-labelledby="title" focusable="false">
<title>Play</title>
<svg class="bp-media-play-icon" fill="#fff" height="24" viewBox="0 0 24 24" width="24" focusable="false">
<path d="M8 5v14l11-7z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg class="bp-media-pause-icon" fill="#fff" height="24" viewBox="0 0 24 24" width="24" aria-labelledby="title" focusable="false">
<title>Pause</title>
<svg class="bp-media-pause-icon" fill="#fff" height="24" viewBox="0 0 24 24" width="24" focusable="false">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
</button>
<div class="bp-media-controls-volume-wrapper">
<div class="bp-media-controls-volume-control">
<button class="bp-media-controls-btn bp-media-volume-icon bp-media-volume-icon-is-high">
<svg viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="#FFF" d="M0 14v6h4l5 5V9l-5 5H0z" />
<path fill="none" d="M-3 5h24v24H-3V5z" />
</svg>
<svg class="bp-media-volume-icon-mute" viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="none" d="M-3 5h24v24H-3V5z" />
<path fill="#fff" d="M15.5 20.2l-2-2-2 2-1.2-1.2 2-2-2-2 1.2-1.2 2 2 2-2 1.2 1.2-2 2 2 2" />
</svg>
<svg class="bp-media-volume-icon-medium" viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="#FFF" d="M13.5 17c0-1.8-1-3.3-2.5-4v8c1.5-.7 2.5-2.2 2.5-4z" />
<path fill="none" d="M-3 5h24v24H-3V5z" />
</svg>
<svg class="bp-media-volume-icon-high" viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="#FFF" d="M13.5 17c0-1.8-1-3.3-2.5-4v8c1.5-.7 2.5-2.2 2.5-4zM11 8.2v2.1c2.9.9 5 3.5 5 6.7s-2.1 5.9-5 6.7v2.1c4-.9 7-4.5 7-8.8s-3-7.9-7-8.8z" />
<path fill="none" d="M-3 5h24v24H-3V5z" />
</svg>
</button>
</div>
<div class="bp-media-volume-scrubber-container"></div>
<button class="bp-media-controls-btn bp-media-volume-icon bp-media-volume-icon-is-high">
<svg viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="#FFF" d="M0 14v6h4l5 5V9l-5 5H0z" />
<path fill="none" d="M-3 5h24v24H-3V5z" />
</svg>
<svg class="bp-media-volume-icon-mute" viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="none" d="M-3 5h24v24H-3V5z" />
<path fill="#fff" d="M15.5 20.2l-2-2-2 2-1.2-1.2 2-2-2-2 1.2-1.2 2 2 2-2 1.2 1.2-2 2 2 2" />
</svg>
<svg class="bp-media-volume-icon-medium" viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="#FFF" d="M13.5 17c0-1.8-1-3.3-2.5-4v8c1.5-.7 2.5-2.2 2.5-4z" />
<path fill="none" d="M-3 5h24v24H-3V5z" />
</svg>
<svg class="bp-media-volume-icon-high" viewBox="-3 5 24 24" width="24" height="24" focusable="false">
<path fill="#FFF" d="M13.5 17c0-1.8-1-3.3-2.5-4v8c1.5-.7 2.5-2.2 2.5-4zM11 8.2v2.1c2.9.9 5 3.5 5 6.7s-2.1 5.9-5 6.7v2.1c4-.9 7-4.5 7-8.8s-3-7.9-7-8.8z" />
<path fill="none" d="M-3 5h24v24H-3V5z" />
</svg>
</button>
<div class="bp-media-volume-scrubber-container-wrapper">
<div class="bp-media-volume-scrubber-container" tabindex="0"></div>
</div>
<label class="bp-media-controls-label bp-media-controls-timecode">00:00</label>
<span class="bp-media-controls-label">&nbsp;&#47;&nbsp;</span>
<label class="bp-media-controls-label bp-media-controls-duration">00:00</label>
<button class="bp-media-controls-btn bp-media-gear-icon" aria-haspopup="true">
<svg width="24" height="24" viewBox="0 0 24 24" focusable="false">
<path fill="#fff" d="M19.4 13c0-.3.1-.6.1-1s0-.7-.1-1l2.1-1.6c.2-.1.2-.4.1-.6l-2-3.5c-.1-.2-.4-.3-.6-.2l-2.5 1c-.5-.4-1.1-.7-1.7-1l-.4-2.7c.1-.2-.2-.4-.4-.4h-4c-.2 0-.5.2-.5.4l-.4 2.7c-.6.2-1.1.6-1.7 1l-2.5-1c-.2-.1-.4 0-.6.2l-2 3.5c-.1.1 0 .4.2.6L4.6 11c0 .3-.1.6-.1 1s0 .7.1 1l-2.1 1.6c-.2.1-.2.4-.1.6l2 3.5c.1.2.3.3.6.2l2.5-1c.5.4 1.1.7 1.7 1l.4 2.6c0 .2.2.4.5.4h4c.2 0 .5-.2.5-.4l.4-2.6c.6-.2 1.2-.6 1.7-1l2.5 1c.2.1.5 0 .6-.2l2-3.5c.1-.2.1-.5-.1-.6L19.4 13zM12 15.5c-1.9 0-3.5-1.6-3.5-3.5s1.6-3.5 3.5-3.5 3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5z"/>
</svg>
<span class="bp-media-controls-hd">HD</span>
</button>
<button class="bp-media-controls-btn bp-media-fullscreen-icon">
<svg class="bp-enter-fullscreen-icon" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="title" focusable="false">
<title>Enter fullscreen</title>
<svg class="bp-enter-fullscreen-icon" width="24" height="24" viewBox="0 0 24 24" focusable="false">
<g fill="none" fill-rule="evenodd">
<path d="M15 3l2.3 2.3-2.89 2.87 1.42 1.42L18.7 6.7 21 9V3h-6zM3 3v6l2.3-2.3 2.87 2.89 1.42-1.42L6.7 5.3 9 3H3zm11.41 12.83l2.89 2.87L15 21h6v-6l-2.3 2.3-2.87-2.89-1.42 1.42zM5.3 17.3L3 15v6h6l-2.3-2.3 2.89-2.87-1.42-1.42L5.3 17.3z" fill="#fff" />
<path d="M0 0h24v24H0z" />
</g>
</svg>
<svg class="bp-exit-fullscreen-icon" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="title" focusable="false">
<title>Exit fullscreen</title>
<svg class="bp-exit-fullscreen-icon" width="24" height="24" viewBox="0 0 24 24" focusable="false">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" />
<path d="M19.58 3l-2.87 2.89-2.3-2.3v6h6l-2.3-2.3L21 4.42 19.58 3zM4.42 3L3 4.42l2.89 2.87-2.3 2.3h6v-6l-2.3 2.3L4.42 3zm9.99 11.41v6l2.3-2.3L19.58 21 21 19.58l-2.89-2.87 2.3-2.3h-6zm-10.82 0l2.3 2.3L3 19.58 4.42 21l2.87-2.89 2.3 2.3v-6h-6z" fill="#fff" />
</g>
</svg>
</button>
<button class="bp-media-controls-btn bp-media-gear-icon">
<svg version="1.1" width="24" height="24" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve" aria-labelledby="title" focusable="false">
<title>Settings</title>
<path fill="#fff" d="M19.4,13c0-0.3,0.1-0.6,0.1-1s0-0.7-0.1-1l2.1-1.6c0.2-0.1,0.2-0.4,0.1-0.6l-2-3.5c-0.1-0.2-0.4-0.3-0.6-0.2l-2.5,1 c-0.5-0.4-1.1-0.7-1.7-1l-0.4-2.7C14.5,2.2,14.2,2,14,2h-4C9.8,2,9.5,2.2,9.5,2.4L9.1,5.1C8.5,5.3,8,5.7,7.4,6.1l-2.5-1 C4.7,5,4.5,5.1,4.3,5.3l-2,3.5C2.2,8.9,2.3,9.2,2.5,9.4L4.6,11c0,0.3-0.1,0.6-0.1,1s0,0.7,0.1,1l-2.1,1.6c-0.2,0.1-0.2,0.4-0.1,0.6 l2,3.5C4.5,18.9,4.7,19,5,18.9l2.5-1c0.5,0.4,1.1,0.7,1.7,1l0.4,2.6c0,0.2,0.2,0.4,0.5,0.4h4c0.2,0,0.5-0.2,0.5-0.4l0.4-2.6 c0.6-0.2,1.2-0.6,1.7-1l2.5,1c0.2,0.1,0.5,0,0.6-0.2l2-3.5c0.1-0.2,0.1-0.5-0.1-0.6L19.4,13z M12,15.5c-1.9,0-3.5-1.6-3.5-3.5 s1.6-3.5,3.5-3.5s3.5,1.6,3.5,3.5S13.9,15.5,12,15.5z"></path>
</svg>
<span class="bp-media-controls-hd">HD</span>
</button>
</div>
</div>
Loading

0 comments on commit 687dba4

Please sign in to comment.