Skip to content

Commit

Permalink
Fixing aria-markup and keyboard accessibility/focus management for qu…
Browse files Browse the repository at this point in the history
…ality plugin.

* Adds aria-controls and aria-expanded status.
* Rewrites EventListeners to handle showing/hiding the flyout with keyboard and mouse events.
  • Loading branch information
digitas-git committed Oct 7, 2021
1 parent a99bc07 commit b127f48
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 60 deletions.
16 changes: 8 additions & 8 deletions demo/quality.html
Expand Up @@ -9,7 +9,7 @@
<link rel="icon" href="favicon.ico" type="image/x-icon">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/4.2.6/mediaelementplayer.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.1/mediaelementplayer.css">
<link rel="stylesheet" href="../dist/quality/quality.css">
<link rel="stylesheet" href="demo.css">
</head>
Expand All @@ -22,15 +22,15 @@ <h1>Quality Plugin</h1>
<p>This plugin allows the generation of a menu with different video/audio qualities, depending of the elements set in the
&lt;source&gt; tags, such as <i>title</i> and <i>data-quality</i></p>

<p><strong>NOTE:</strong> This is an improved version of Source Chooser plugin, so it is encouarged the use of this plugin instead of Source Chooser</p>
<p><strong>NOTE:</strong> This is an improved version of Source Chooser plugin, so it is encouraged to use this plugin instead of Source Chooser</p>

<h2>Video Player</h2>

<div class="media-wrapper">
<video id="player1" width="750" height="421" controls preload="none" poster="http://mediaelementjs.com/images/big_buck_bunny.jpg">
<source type="application/x-mpegURL" src="https://video-dev.github.io/streams/x36xhzz/x36xhzz.m3u8" data-quality="HD">
<source type="video/mp4" src="https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4" data-quality="SD">
<source type="video/mp4" src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4" data-quality="LD">
<source type="video/mp4" src="http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_2160p_60fps_normal.mp4" data-quality="HD">
<source type="video/mp4" src="http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4" data-quality="SD">
<source type="video/mp4" src="http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_native_60fps_normal.mp4" data-quality="LD">
</video>
</div>

Expand All @@ -55,13 +55,13 @@ <h2>API</h2>
<td>qualityText</td>
<td>string</td>
<td>null</td>
<td>Title for Quality button for WARIA purposes</td>
<td>Title for Quality button for WCAG ARIA purposes</td>
</tr>
</tbody>
</table>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/4.2.6/mediaelement-and-player.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.1/mediaelement-and-player.min.js"></script>
<script src="../dist/quality/quality.js"></script>
<script>
var mediaElements = document.querySelectorAll('video, audio');
Expand All @@ -73,4 +73,4 @@ <h2>API</h2>
}
</script>
</body>
</html>
</html>
14 changes: 10 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "mediaelement-plugins",
"version": "2.6.0",
"version": "2.6.1",
"repository": {
"type": "git",
"url": "https://github.com/mediaelement/mediaelement-plugins.git"
Expand Down Expand Up @@ -41,7 +41,7 @@
},
"dependencies": {
"global": "^4.3.1",
"mediaelement": "^4.0.7"
"mediaelement": "^5.0.1"
},
"browserify": {
"extensions": [
Expand Down
122 changes: 76 additions & 46 deletions src/quality/quality.js
Expand Up @@ -34,8 +34,8 @@ Object.assign(mejs.MepDefaults, {
autoHLS: false,
/**
* @type Function
*/
qualityChangeCallback: null
*/
qualityChangeCallback: null
});

Object.assign(MediaElementPlayer.prototype, {
Expand Down Expand Up @@ -148,16 +148,14 @@ Object.assign(MediaElementPlayer.prototype, {
currentQuality = defaultValue;

// Get initial quality

player.qualitiesButton = document.createElement('div');
player.qualitiesButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}qualities-button`;
player.qualitiesButton.innerHTML = `<button type="button" aria-controls="${t.id}" title="${qualityTitle}" ` +
`aria-label="${qualityTitle}" tabindex="0">${defaultValue}</button>` +
const generateId = Math.floor(Math.random() * 100);
player.qualitiesContainer = document.createElement('div');
player.qualitiesContainer.className = `${t.options.classPrefix}button ${t.options.classPrefix}qualities-button`;
player.qualitiesContainer.innerHTML = `<button type="button" title="${qualityTitle}" aria-label="${qualityTitle}" aria-controls="qualitieslist-${generateId}">${defaultValue}</button>` +
`<div class="${t.options.classPrefix}qualities-selector ${t.options.classPrefix}offscreen">` +
`<ul class="${t.options.classPrefix}qualities-selector-list"></ul>` +
`</div>`;
`<ul class="${t.options.classPrefix}qualities-selector-list" id="qualitieslist-${generateId}" tabindex="-1"></ul></div>`;

t.addControlElement(player.qualitiesButton, 'qualities');
t.addControlElement(player.qualitiesContainer, 'qualities');

qualityMap.forEach(function (value, key) {
if (key !== 'map_keys_1') {
Expand All @@ -166,42 +164,77 @@ Object.assign(MediaElementPlayer.prototype, {
quality = key,
inputId = `${t.id}-qualities-${quality}`
;
player.qualitiesButton.querySelector('ul').innerHTML += `<li class="${t.options.classPrefix}qualities-selector-list-item">` +
`<input class="${t.options.classPrefix}qualities-selector-input" type="radio" name="${t.id}_qualities"` +
`disabled="disabled" value="${quality}" id="${inputId}" ` +
`${(quality === defaultValue ? ' checked="checked"' : '')}/>` +
`<label for="${inputId}" class="${t.options.classPrefix}qualities-selector-label` +
`${(quality === defaultValue ? ` ${t.options.classPrefix}qualities-selected` : '')}">` +
`${src.title || quality}</label>` +
`</li>`;
player.qualitiesContainer.querySelector('ul').innerHTML += `<li class="${t.options.classPrefix}qualities-selector-list-item">` +
`<input class="${t.options.classPrefix}qualities-selector-input" type="radio" name="${t.id}_qualities" disabled="disabled"` +
`value="${quality}" id="${inputId}" ${(quality === defaultValue ? ' checked="checked"' : '')} />` +
`<label for="${inputId}" class="${t.options.classPrefix}qualities-selector-label ${(quality === defaultValue ? ` ${t.options.classPrefix}qualities-selected` : '')}">` +
`${src.title || quality} </label></li>`;
}
});

let isOffScreen = true;
const
inEvents = ['mouseenter', 'focusin'],
outEvents = ['mouseleave', 'focusout'],
qualityContainer = player.qualitiesContainer,
qualityButton = player.qualitiesContainer.querySelector(`button`),
qualitiesSelector = player.qualitiesContainer.querySelector(`.${t.options.classPrefix}qualities-selector`),
qualitiesList = player.qualitiesContainer.querySelector(`.${t.options.classPrefix}qualities-selector-list`),
// Enable inputs after they have been appended to controls to avoid tab and up/down arrow focus issues
radios = player.qualitiesButton.querySelectorAll('input[type="radio"]'),
labels = player.qualitiesButton.querySelectorAll(`.${t.options.classPrefix}qualities-selector-label`),
selector = player.qualitiesButton.querySelector(`.${t.options.classPrefix}qualities-selector`)
radios = player.qualitiesContainer.querySelectorAll('input[type="radio"]'),
labels = player.qualitiesContainer.querySelectorAll(`.${t.options.classPrefix}qualities-selector-label`)
;

// hover or keyboard focus
for (let i = 0, total = inEvents.length; i < total; i++) {
player.qualitiesButton.addEventListener(inEvents[i], () => {
mejs.Utils.removeClass(selector, `${t.options.classPrefix}offscreen`);
selector.style.height = `${selector.querySelector('ul').offsetHeight}px`;
selector.style.top = `${(-1 * parseFloat(selector.offsetHeight))}px`;
});
function hideSelector() {
setTimeout(() => {
mejs.Utils.addClass(qualitiesSelector, `${t.options.classPrefix}offscreen`);
}, 50);
qualityButton.removeAttribute('aria-expanded');
qualitiesList.style.display = `none`;
qualityButton.focus();
isOffScreen = true;
}

for (let i = 0, total = outEvents.length; i < total; i++) {
player.qualitiesButton.addEventListener(outEvents[i], () => {
setTimeout(() => {
mejs.Utils.addClass(selector, `${t.options.classPrefix}offscreen`);
}, 50);
});
function showSelector() {
mejs.Utils.removeClass(qualitiesSelector, `${t.options.classPrefix}offscreen`);
qualitiesList.style.display = `block`;
qualitiesSelector.style.height = `${qualitiesSelector.querySelector('ul').offsetHeight}px`;
qualitiesSelector.style.top = `${(-1 * parseFloat(qualitiesSelector.offsetHeight))}px`;
qualityButton.setAttribute('aria-expanded', 'true');
qualitiesList.focus();
isOffScreen = false;
}

qualityButton.addEventListener('click', () => {
if (isOffScreen === true) {
showSelector();
} else {
hideSelector();
}
});

qualitiesList.addEventListener('focusout', (event) =>{
if (!qualityContainer.contains(event.relatedTarget)) {
hideSelector();
}
});

qualityButton.addEventListener('mouseenter', () =>{
showSelector();
});

qualityContainer.addEventListener('mouseleave', () =>{
hideSelector();
});

// Close with Escape key.
// Allow up/down arrow to change the selected radio without changing the volume.
qualityContainer.addEventListener('keydown', (event) => {
if(event.key === "Escape"){
hideSelector();
}

event.stopPropagation();
});

for (let i = 0, total = radios.length; i < total; i++) {
const radio = radios[i];
radio.disabled = false;
Expand Down Expand Up @@ -238,6 +271,7 @@ Object.assign(MediaElementPlayer.prototype, {
}
});
}

for (let i = 0, total = labels.length; i < total; i++) {
labels[i].addEventListener('click', function () {
const
Expand All @@ -248,10 +282,6 @@ Object.assign(MediaElementPlayer.prototype, {
});
}

//Allow up/down arrow to change the selected radio without changing the volume.
selector.addEventListener('keydown', (e) => {
e.stopPropagation();
});
},

/**
Expand All @@ -262,8 +292,8 @@ Object.assign(MediaElementPlayer.prototype, {
*/
cleanquality (player) {
if (player) {
if (player.qualitiesButton) {
player.qualitiesButton.remove();
if (player.qualitiesContainer) {
player.qualitiesContainer.remove();
}
}
},
Expand Down Expand Up @@ -358,7 +388,7 @@ Object.assign(MediaElementPlayer.prototype, {
* @param {MediaElement} media
*/
switchDashQuality (player, media) {
const radios = player.qualitiesButton.querySelectorAll('input[type="radio"]');
const radios = player.qualitiesContainer.querySelectorAll('input[type="radio"]');
for (let index = 0; index < radios.length; index++) {
if (radios[index].checked) {
if (index === 0 ) {
Expand All @@ -377,7 +407,7 @@ Object.assign(MediaElementPlayer.prototype, {
* @param {MediaElement} media
*/
switchHLSQuality (player, media) {
const radios = player.qualitiesButton.querySelectorAll('input[type="radio"]');
const radios = player.qualitiesContainer.querySelectorAll('input[type="radio"]');
for (let index = 0; index < radios.length; index++) {
if (radios[index].checked) {
if (index === 0 ) {
Expand All @@ -402,7 +432,7 @@ Object.assign(MediaElementPlayer.prototype, {
;
currentQuality = newQuality;

const selected = player.qualitiesButton.querySelectorAll(`.${t.options.classPrefix}qualities-selected`);
const selected = player.qualitiesContainer.querySelectorAll(`.${t.options.classPrefix}qualities-selected`);
for (let i = 0, total = selected.length; i < total; i++) {
mejs.Utils.removeClass(selected[i], `${t.options.classPrefix}qualities-selected`);
}
Expand All @@ -413,7 +443,7 @@ Object.assign(MediaElementPlayer.prototype, {
mejs.Utils.addClass(siblings[j], `${t.options.classPrefix}qualities-selected`);
}

player.qualitiesButton.querySelector('button').innerHTML = newQuality;
player.qualitiesContainer.querySelector('button').innerHTML = newQuality;
},

/**
Expand Down

0 comments on commit b127f48

Please sign in to comment.