Skip to content

Commit b127f48

Browse files
committed
Fixing aria-markup and keyboard accessibility/focus management for quality plugin.
* Adds aria-controls and aria-expanded status. * Rewrites EventListeners to handle showing/hiding the flyout with keyboard and mouse events.
1 parent a99bc07 commit b127f48

4 files changed

Lines changed: 96 additions & 60 deletions

File tree

demo/quality.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<link rel="icon" href="favicon.ico" type="image/x-icon">
1010

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

25-
<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>
25+
<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>
2626

2727
<h2>Video Player</h2>
2828

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

@@ -55,13 +55,13 @@ <h2>API</h2>
5555
<td>qualityText</td>
5656
<td>string</td>
5757
<td>null</td>
58-
<td>Title for Quality button for WARIA purposes</td>
58+
<td>Title for Quality button for WCAG ARIA purposes</td>
5959
</tr>
6060
</tbody>
6161
</table>
6262
</div>
6363

64-
<script src="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/4.2.6/mediaelement-and-player.min.js"></script>
64+
<script src="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.1/mediaelement-and-player.min.js"></script>
6565
<script src="../dist/quality/quality.js"></script>
6666
<script>
6767
var mediaElements = document.querySelectorAll('video, audio');
@@ -73,4 +73,4 @@ <h2>API</h2>
7373
}
7474
</script>
7575
</body>
76-
</html>
76+
</html>

package-lock.json

Lines changed: 10 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mediaelement-plugins",
3-
"version": "2.6.0",
3+
"version": "2.6.1",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/mediaelement/mediaelement-plugins.git"
@@ -41,7 +41,7 @@
4141
},
4242
"dependencies": {
4343
"global": "^4.3.1",
44-
"mediaelement": "^4.0.7"
44+
"mediaelement": "^5.0.1"
4545
},
4646
"browserify": {
4747
"extensions": [

src/quality/quality.js

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Object.assign(mejs.MepDefaults, {
3434
autoHLS: false,
3535
/**
3636
* @type Function
37-
*/
38-
qualityChangeCallback: null
37+
*/
38+
qualityChangeCallback: null
3939
});
4040

4141
Object.assign(MediaElementPlayer.prototype, {
@@ -148,16 +148,14 @@ Object.assign(MediaElementPlayer.prototype, {
148148
currentQuality = defaultValue;
149149

150150
// Get initial quality
151-
152-
player.qualitiesButton = document.createElement('div');
153-
player.qualitiesButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}qualities-button`;
154-
player.qualitiesButton.innerHTML = `<button type="button" aria-controls="${t.id}" title="${qualityTitle}" ` +
155-
`aria-label="${qualityTitle}" tabindex="0">${defaultValue}</button>` +
151+
const generateId = Math.floor(Math.random() * 100);
152+
player.qualitiesContainer = document.createElement('div');
153+
player.qualitiesContainer.className = `${t.options.classPrefix}button ${t.options.classPrefix}qualities-button`;
154+
player.qualitiesContainer.innerHTML = `<button type="button" title="${qualityTitle}" aria-label="${qualityTitle}" aria-controls="qualitieslist-${generateId}">${defaultValue}</button>` +
156155
`<div class="${t.options.classPrefix}qualities-selector ${t.options.classPrefix}offscreen">` +
157-
`<ul class="${t.options.classPrefix}qualities-selector-list"></ul>` +
158-
`</div>`;
156+
`<ul class="${t.options.classPrefix}qualities-selector-list" id="qualitieslist-${generateId}" tabindex="-1"></ul></div>`;
159157

160-
t.addControlElement(player.qualitiesButton, 'qualities');
158+
t.addControlElement(player.qualitiesContainer, 'qualities');
161159

162160
qualityMap.forEach(function (value, key) {
163161
if (key !== 'map_keys_1') {
@@ -166,42 +164,77 @@ Object.assign(MediaElementPlayer.prototype, {
166164
quality = key,
167165
inputId = `${t.id}-qualities-${quality}`
168166
;
169-
player.qualitiesButton.querySelector('ul').innerHTML += `<li class="${t.options.classPrefix}qualities-selector-list-item">` +
170-
`<input class="${t.options.classPrefix}qualities-selector-input" type="radio" name="${t.id}_qualities"` +
171-
`disabled="disabled" value="${quality}" id="${inputId}" ` +
172-
`${(quality === defaultValue ? ' checked="checked"' : '')}/>` +
173-
`<label for="${inputId}" class="${t.options.classPrefix}qualities-selector-label` +
174-
`${(quality === defaultValue ? ` ${t.options.classPrefix}qualities-selected` : '')}">` +
175-
`${src.title || quality}</label>` +
176-
`</li>`;
167+
player.qualitiesContainer.querySelector('ul').innerHTML += `<li class="${t.options.classPrefix}qualities-selector-list-item">` +
168+
`<input class="${t.options.classPrefix}qualities-selector-input" type="radio" name="${t.id}_qualities" disabled="disabled"` +
169+
`value="${quality}" id="${inputId}" ${(quality === defaultValue ? ' checked="checked"' : '')} />` +
170+
`<label for="${inputId}" class="${t.options.classPrefix}qualities-selector-label ${(quality === defaultValue ? ` ${t.options.classPrefix}qualities-selected` : '')}">` +
171+
`${src.title || quality} </label></li>`;
177172
}
178173
});
174+
175+
let isOffScreen = true;
179176
const
180-
inEvents = ['mouseenter', 'focusin'],
181-
outEvents = ['mouseleave', 'focusout'],
177+
qualityContainer = player.qualitiesContainer,
178+
qualityButton = player.qualitiesContainer.querySelector(`button`),
179+
qualitiesSelector = player.qualitiesContainer.querySelector(`.${t.options.classPrefix}qualities-selector`),
180+
qualitiesList = player.qualitiesContainer.querySelector(`.${t.options.classPrefix}qualities-selector-list`),
182181
// Enable inputs after they have been appended to controls to avoid tab and up/down arrow focus issues
183-
radios = player.qualitiesButton.querySelectorAll('input[type="radio"]'),
184-
labels = player.qualitiesButton.querySelectorAll(`.${t.options.classPrefix}qualities-selector-label`),
185-
selector = player.qualitiesButton.querySelector(`.${t.options.classPrefix}qualities-selector`)
182+
radios = player.qualitiesContainer.querySelectorAll('input[type="radio"]'),
183+
labels = player.qualitiesContainer.querySelectorAll(`.${t.options.classPrefix}qualities-selector-label`)
186184
;
187185

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

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

206+
qualityButton.addEventListener('click', () => {
207+
if (isOffScreen === true) {
208+
showSelector();
209+
} else {
210+
hideSelector();
211+
}
212+
});
213+
214+
qualitiesList.addEventListener('focusout', (event) =>{
215+
if (!qualityContainer.contains(event.relatedTarget)) {
216+
hideSelector();
217+
}
218+
});
219+
220+
qualityButton.addEventListener('mouseenter', () =>{
221+
showSelector();
222+
});
223+
224+
qualityContainer.addEventListener('mouseleave', () =>{
225+
hideSelector();
226+
});
227+
228+
// Close with Escape key.
229+
// Allow up/down arrow to change the selected radio without changing the volume.
230+
qualityContainer.addEventListener('keydown', (event) => {
231+
if(event.key === "Escape"){
232+
hideSelector();
233+
}
234+
235+
event.stopPropagation();
236+
});
237+
205238
for (let i = 0, total = radios.length; i < total; i++) {
206239
const radio = radios[i];
207240
radio.disabled = false;
@@ -238,6 +271,7 @@ Object.assign(MediaElementPlayer.prototype, {
238271
}
239272
});
240273
}
274+
241275
for (let i = 0, total = labels.length; i < total; i++) {
242276
labels[i].addEventListener('click', function () {
243277
const
@@ -248,10 +282,6 @@ Object.assign(MediaElementPlayer.prototype, {
248282
});
249283
}
250284

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

257287
/**
@@ -262,8 +292,8 @@ Object.assign(MediaElementPlayer.prototype, {
262292
*/
263293
cleanquality (player) {
264294
if (player) {
265-
if (player.qualitiesButton) {
266-
player.qualitiesButton.remove();
295+
if (player.qualitiesContainer) {
296+
player.qualitiesContainer.remove();
267297
}
268298
}
269299
},
@@ -358,7 +388,7 @@ Object.assign(MediaElementPlayer.prototype, {
358388
* @param {MediaElement} media
359389
*/
360390
switchDashQuality (player, media) {
361-
const radios = player.qualitiesButton.querySelectorAll('input[type="radio"]');
391+
const radios = player.qualitiesContainer.querySelectorAll('input[type="radio"]');
362392
for (let index = 0; index < radios.length; index++) {
363393
if (radios[index].checked) {
364394
if (index === 0 ) {
@@ -377,7 +407,7 @@ Object.assign(MediaElementPlayer.prototype, {
377407
* @param {MediaElement} media
378408
*/
379409
switchHLSQuality (player, media) {
380-
const radios = player.qualitiesButton.querySelectorAll('input[type="radio"]');
410+
const radios = player.qualitiesContainer.querySelectorAll('input[type="radio"]');
381411
for (let index = 0; index < radios.length; index++) {
382412
if (radios[index].checked) {
383413
if (index === 0 ) {
@@ -402,7 +432,7 @@ Object.assign(MediaElementPlayer.prototype, {
402432
;
403433
currentQuality = newQuality;
404434

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

416-
player.qualitiesButton.querySelector('button').innerHTML = newQuality;
446+
player.qualitiesContainer.querySelector('button').innerHTML = newQuality;
417447
},
418448

419449
/**

0 commit comments

Comments
 (0)