Merge pull request #1041 from sampotts/a11y-improvements

A11y improvements
This commit is contained in:
Sam Potts 2018-06-17 01:34:11 +10:00 committed by GitHub
commit 3c9c1b4cdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 272 additions and 258 deletions

View File

@ -21,7 +21,7 @@
Again, more changes from @friday!
- Restore window reference in `utils.is.cue()`
- Restore window reference in `is.cue()`
- Fix InvalidStateError and IE11 issues
- Respect storage being disabled for storage getter

View File

@ -2,9 +2,9 @@
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
* `Array` of options (this builds the default controls based on your choices)
* `String` containing the desired HTML
* `Function` that will be executed and should return one of the above
- `Array` of options (this builds the default controls based on your choices)
- `String` containing the desired HTML
- `Function` that will be executed and should return one of the above
## Using default controls
@ -81,14 +81,14 @@ The classes and data attributes used in your template should match the `selector
You need to add several placeholders to your HTML template that are replaced when rendering:
* `{id}` - the dynamically generated ID for the player (for form controls)
* `{seektime}` - the seek time specified in options for fast forward and rewind
* `{title}` - the title of your media, if specified
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
### Limitations
* Currently the settings menus are not supported with custom controls HTML
* AirPlay and PiP buttons can be added but you will have to manage feature detection
- Currently the settings menus are not supported with custom controls HTML
- AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example
@ -105,7 +105,7 @@ const controls = `
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
<button type="button" class="plyr__control" aria-label="Play, {title}" data-plyr="play">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
@ -122,7 +122,7 @@ const controls = `
</div>
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<button type="button" class="plyr__control" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
@ -131,13 +131,13 @@ const controls = `
<div class="plyr__volume">
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
</div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
<button type="button" class="plyr__control" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
<button type="button" class="plyr__control" data-plyr="fullscreen">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

156
dist/plyr.js vendored
View File

@ -595,30 +595,6 @@ typeof navigator === "object" && (function (global, factory) {
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
// Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
function toggleState(element, input) {
// If multiple elements passed
if (is.array(element) || is.nodeList(element)) {
Array.from(element).forEach(function (target) {
return toggleState(target, input);
});
return;
}
// Bail if no target
if (!is.element(element)) {
return;
}
// Get state
var pressed = element.getAttribute('aria-pressed') === 'true';
var state = is.boolean(input) ? input : !pressed;
// Set the attribute on target
element.setAttribute('aria-pressed', state);
}
// ==========================================================================
var transitionEndEvent = function () {
@ -1271,11 +1247,12 @@ typeof navigator === "object" && (function (global, factory) {
}
// Render
return '' + (inverted ? '-' : '') + hours + format(mins) + ':' + format(secs);
return '' + (inverted && time > 0 ? '-' : '') + hours + format(mins) + ':' + format(secs);
}
// ==========================================================================
// TODO: Don't export a massive object - break down and create class
var controls = {
// Get icon URL
getIconUrl: function getIconUrl() {
@ -1289,8 +1266,7 @@ typeof navigator === "object" && (function (global, factory) {
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
// Find the UI controls
findElements: function findElements() {
try {
this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);
@ -1386,12 +1362,11 @@ typeof navigator === "object" && (function (global, factory) {
pip: 'PIP',
airplay: 'AirPlay'
};
var text = universals[type] || i18n.get(type, this.config);
var attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
});
return createElement('span', attributes, text);
},
@ -1493,9 +1468,6 @@ typeof navigator === "object" && (function (global, factory) {
// Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
// Add aria attributes
attributes['aria-pressed'] = false;
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@ -1517,19 +1489,26 @@ typeof navigator === "object" && (function (global, factory) {
this.elements.buttons[type] = button;
}
// Toggle classname when pressed property is set
var className = this.config.classNames.controlPressed;
Object.defineProperty(button, 'pressed', {
enumerable: true,
get: function get$$1() {
return hasClass(button, className);
},
set: function set$$1() {
var pressed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
toggleClass(button, className, pressed);
}
});
return button;
},
// Create an <input type='range'>
createRange: function createRange(type, attributes) {
// Seek label
var label = createElement('label', {
for: attributes.id,
id: attributes.id + '-label',
class: this.config.classNames.hidden
}, i18n.get(type, this.config));
// Seek input
var input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'range',
@ -1540,7 +1519,7 @@ typeof navigator === "object" && (function (global, factory) {
autocomplete: 'off',
// A11y fixes for https://github.com/sampotts/plyr/issues/905
role: 'slider',
'aria-labelledby': attributes.id + '-label',
'aria-label': i18n.get(type, this.config),
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0
@ -1551,10 +1530,7 @@ typeof navigator === "object" && (function (global, factory) {
// Set the fill for webkit now
controls.updateRangeFill.call(this, input);
return {
label: label,
input: input
};
return input;
},
@ -1576,7 +1552,6 @@ typeof navigator === "object" && (function (global, factory) {
played: 'played',
buffer: 'buffered'
}[type];
var suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
progress.innerText = '% ' + suffix.toLowerCase();
@ -1644,6 +1619,23 @@ typeof navigator === "object" && (function (global, factory) {
},
// Format a time for display
formatTime: function formatTime$$1() {
var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var inverted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
// Bail if the value isn't a number
if (!is.number(time)) {
return time;
}
// Always display hours if duration is over an hour
var forceHours = getHours(this.duration) > 0;
return formatTime(time, forceHours, inverted);
},
// Update the displayed time
updateTimeDisplay: function updateTimeDisplay() {
var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
@ -1655,11 +1647,8 @@ typeof navigator === "object" && (function (global, factory) {
return;
}
// Always display hours if duration is over an hour
var forceHours = getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.innerText = formatTime(time, forceHours, inverted);
target.innerText = controls.formatTime(time, inverted);
},
@ -1676,7 +1665,7 @@ typeof navigator === "object" && (function (global, factory) {
// Update mute state
if (is.element(this.elements.buttons.mute)) {
toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
}
},
@ -1762,8 +1751,20 @@ typeof navigator === "object" && (function (global, factory) {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// Set aria values for https://github.com/sampotts/plyr/issues/905
if (matches(range, this.config.selectors.inputs.seek)) {
range.setAttribute('aria-valuenow', this.currentTime);
var currentTime = controls.formatTime(this.currentTime);
var duration = controls.formatTime(this.duration);
var format$$1 = i18n.get('seekLabel', this.config);
range.setAttribute('aria-valuetext', format$$1.replace('{currentTime}', currentTime).replace('{duration}', duration));
} else if (matches(range, this.config.selectors.inputs.volume)) {
var percent = range.value * 100;
range.setAttribute('aria-valuenow', percent);
range.setAttribute('aria-valuetext', percent + '%');
} else {
range.setAttribute('aria-valuenow', range.value);
}
// WebKit only
if (!browser.isWebkit) {
@ -1849,11 +1850,16 @@ typeof navigator === "object" && (function (global, factory) {
// Show the duration on metadataloaded or durationchange events
durationUpdate: function durationUpdate() {
// Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
// Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
return;
}
// Update ARIA values
if (is.element(this.elements.inputs.seek)) {
this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
}
// If there's a spot to display duration
var hasDuration = is.element(this.elements.display.duration);
@ -2379,11 +2385,9 @@ typeof navigator === "object" && (function (global, factory) {
var progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// Seek range slider
var seek = controls.createRange.call(this, 'seek', {
progress.appendChild(controls.createRange.call(this, 'seek', {
id: 'plyr-seek-' + data.id
});
progress.appendChild(seek.label);
progress.appendChild(seek.input);
}));
// Buffer progress
progress.appendChild(controls.createProgress.call(this, 'buffer'));
@ -2433,11 +2437,9 @@ typeof navigator === "object" && (function (global, factory) {
};
// Create the volume range slider
var range = controls.createRange.call(this, 'volume', extend(attributes, {
volume.appendChild(controls.createRange.call(this, 'volume', extend(attributes, {
id: 'plyr-volume-' + data.id
}));
volume.appendChild(range.label);
volume.appendChild(range.input);
})));
this.elements.volume = volume;
@ -2702,7 +2704,6 @@ typeof navigator === "object" && (function (global, factory) {
Array.from(labels).forEach(function (label) {
toggleClass(label, _this8.config.classNames.hidden, false);
toggleClass(label, _this8.config.classNames.tooltip, true);
label.setAttribute('role', 'tooltip');
});
}
}
@ -2977,7 +2978,7 @@ typeof navigator === "object" && (function (global, factory) {
}
// Toggle state
toggleState(this.elements.buttons.captions, active);
this.elements.buttons.captions.pressed = active;
// Add class hook
toggleClass(this.elements.container, activeClass, active);
@ -3219,6 +3220,10 @@ typeof navigator === "object" && (function (global, factory) {
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
@ -3332,6 +3337,7 @@ typeof navigator === "object" && (function (global, factory) {
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@ -3346,6 +3352,7 @@ typeof navigator === "object" && (function (global, factory) {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
@ -3475,6 +3482,7 @@ typeof navigator === "object" && (function (global, factory) {
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
@ -3616,7 +3624,7 @@ typeof navigator === "object" && (function (global, factory) {
// Update toggle button
var button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
toggleState(button, this.active);
button.pressed = this.active;
}
// Trigger an event
@ -3986,9 +3994,6 @@ typeof navigator === "object" && (function (global, factory) {
// If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += ', ' + this.config.title;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
@ -4068,13 +4073,17 @@ typeof navigator === "object" && (function (global, factory) {
// Check playing state
checkPlaying: function checkPlaying(event) {
var _this3 = this;
// Class hooks
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
toggleState(this.elements.buttons.play, this.playing);
// Set state
Array.from(this.elements.buttons.play).forEach(function (target) {
target.pressed = _this3.playing;
});
// Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') {
@ -4088,7 +4097,7 @@ typeof navigator === "object" && (function (global, factory) {
// Check if media is loading
checkLoading: function checkLoading(event) {
var _this3 = this;
var _this4 = this;
this.loading = ['stalled', 'waiting'].includes(event.type);
@ -4098,10 +4107,10 @@ typeof navigator === "object" && (function (global, factory) {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(function () {
// Update progress bar loading class state
toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading);
// Update controls visibility
ui.toggleControls.call(_this3);
ui.toggleControls.call(_this4);
}, this.loading ? 250 : 0);
},
@ -7131,9 +7140,6 @@ typeof navigator === "object" && (function (global, factory) {
wrap(this.media, this.elements.container);
}
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Add style hook
ui.addStyleHook.call(this);

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5981,30 +5981,6 @@ typeof navigator === "object" && (function (global, factory) {
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
// Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
function toggleState(element, input) {
// If multiple elements passed
if (is$1.array(element) || is$1.nodeList(element)) {
Array.from(element).forEach(function (target) {
return toggleState(target, input);
});
return;
}
// Bail if no target
if (!is$1.element(element)) {
return;
}
// Get state
var pressed = element.getAttribute('aria-pressed') === 'true';
var state = is$1.boolean(input) ? input : !pressed;
// Set the attribute on target
element.setAttribute('aria-pressed', state);
}
// ==========================================================================
var transitionEndEvent = function () {
@ -6657,11 +6633,12 @@ typeof navigator === "object" && (function (global, factory) {
}
// Render
return '' + (inverted ? '-' : '') + hours + format(mins) + ':' + format(secs);
return '' + (inverted && time > 0 ? '-' : '') + hours + format(mins) + ':' + format(secs);
}
// ==========================================================================
// TODO: Don't export a massive object - break down and create class
var controls = {
// Get icon URL
getIconUrl: function getIconUrl() {
@ -6675,8 +6652,7 @@ typeof navigator === "object" && (function (global, factory) {
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
// Find the UI controls
findElements: function findElements() {
try {
this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);
@ -6772,12 +6748,11 @@ typeof navigator === "object" && (function (global, factory) {
pip: 'PIP',
airplay: 'AirPlay'
};
var text = universals[type] || i18n.get(type, this.config);
var attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
});
return createElement('span', attributes, text);
},
@ -6879,9 +6854,6 @@ typeof navigator === "object" && (function (global, factory) {
// Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
// Add aria attributes
attributes['aria-pressed'] = false;
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@ -6903,19 +6875,26 @@ typeof navigator === "object" && (function (global, factory) {
this.elements.buttons[type] = button;
}
// Toggle classname when pressed property is set
var className = this.config.classNames.controlPressed;
Object.defineProperty(button, 'pressed', {
enumerable: true,
get: function get() {
return hasClass(button, className);
},
set: function set() {
var pressed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
toggleClass(button, className, pressed);
}
});
return button;
},
// Create an <input type='range'>
createRange: function createRange(type, attributes) {
// Seek label
var label = createElement('label', {
for: attributes.id,
id: attributes.id + '-label',
class: this.config.classNames.hidden
}, i18n.get(type, this.config));
// Seek input
var input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'range',
@ -6926,7 +6905,7 @@ typeof navigator === "object" && (function (global, factory) {
autocomplete: 'off',
// A11y fixes for https://github.com/sampotts/plyr/issues/905
role: 'slider',
'aria-labelledby': attributes.id + '-label',
'aria-label': i18n.get(type, this.config),
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0
@ -6937,10 +6916,7 @@ typeof navigator === "object" && (function (global, factory) {
// Set the fill for webkit now
controls.updateRangeFill.call(this, input);
return {
label: label,
input: input
};
return input;
},
@ -6962,7 +6938,6 @@ typeof navigator === "object" && (function (global, factory) {
played: 'played',
buffer: 'buffered'
}[type];
var suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
progress.innerText = '% ' + suffix.toLowerCase();
@ -7030,6 +7005,23 @@ typeof navigator === "object" && (function (global, factory) {
},
// Format a time for display
formatTime: function formatTime$$1() {
var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var inverted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
// Bail if the value isn't a number
if (!is$1.number(time)) {
return time;
}
// Always display hours if duration is over an hour
var forceHours = getHours(this.duration) > 0;
return formatTime(time, forceHours, inverted);
},
// Update the displayed time
updateTimeDisplay: function updateTimeDisplay() {
var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
@ -7041,11 +7033,8 @@ typeof navigator === "object" && (function (global, factory) {
return;
}
// Always display hours if duration is over an hour
var forceHours = getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.innerText = formatTime(time, forceHours, inverted);
target.innerText = controls.formatTime(time, inverted);
},
@ -7062,7 +7051,7 @@ typeof navigator === "object" && (function (global, factory) {
// Update mute state
if (is$1.element(this.elements.buttons.mute)) {
toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
}
},
@ -7148,8 +7137,20 @@ typeof navigator === "object" && (function (global, factory) {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// Set aria values for https://github.com/sampotts/plyr/issues/905
if (matches(range, this.config.selectors.inputs.seek)) {
range.setAttribute('aria-valuenow', this.currentTime);
var currentTime = controls.formatTime(this.currentTime);
var duration = controls.formatTime(this.duration);
var format$$1 = i18n.get('seekLabel', this.config);
range.setAttribute('aria-valuetext', format$$1.replace('{currentTime}', currentTime).replace('{duration}', duration));
} else if (matches(range, this.config.selectors.inputs.volume)) {
var percent = range.value * 100;
range.setAttribute('aria-valuenow', percent);
range.setAttribute('aria-valuetext', percent + '%');
} else {
range.setAttribute('aria-valuenow', range.value);
}
// WebKit only
if (!browser.isWebkit) {
@ -7235,11 +7236,16 @@ typeof navigator === "object" && (function (global, factory) {
// Show the duration on metadataloaded or durationchange events
durationUpdate: function durationUpdate() {
// Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
// Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
return;
}
// Update ARIA values
if (is$1.element(this.elements.inputs.seek)) {
this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
}
// If there's a spot to display duration
var hasDuration = is$1.element(this.elements.display.duration);
@ -7765,11 +7771,9 @@ typeof navigator === "object" && (function (global, factory) {
var progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// Seek range slider
var seek = controls.createRange.call(this, 'seek', {
progress.appendChild(controls.createRange.call(this, 'seek', {
id: 'plyr-seek-' + data.id
});
progress.appendChild(seek.label);
progress.appendChild(seek.input);
}));
// Buffer progress
progress.appendChild(controls.createProgress.call(this, 'buffer'));
@ -7819,11 +7823,9 @@ typeof navigator === "object" && (function (global, factory) {
};
// Create the volume range slider
var range = controls.createRange.call(this, 'volume', extend(attributes, {
volume.appendChild(controls.createRange.call(this, 'volume', extend(attributes, {
id: 'plyr-volume-' + data.id
}));
volume.appendChild(range.label);
volume.appendChild(range.input);
})));
this.elements.volume = volume;
@ -8088,7 +8090,6 @@ typeof navigator === "object" && (function (global, factory) {
Array.from(labels).forEach(function (label) {
toggleClass(label, _this8.config.classNames.hidden, false);
toggleClass(label, _this8.config.classNames.tooltip, true);
label.setAttribute('role', 'tooltip');
});
}
}
@ -8363,7 +8364,7 @@ typeof navigator === "object" && (function (global, factory) {
}
// Toggle state
toggleState(this.elements.buttons.captions, active);
this.elements.buttons.captions.pressed = active;
// Add class hook
toggleClass(this.elements.container, activeClass, active);
@ -8605,6 +8606,10 @@ typeof navigator === "object" && (function (global, factory) {
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
@ -8718,6 +8723,7 @@ typeof navigator === "object" && (function (global, factory) {
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@ -8732,6 +8738,7 @@ typeof navigator === "object" && (function (global, factory) {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
@ -8861,6 +8868,7 @@ typeof navigator === "object" && (function (global, factory) {
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
@ -9002,7 +9010,7 @@ typeof navigator === "object" && (function (global, factory) {
// Update toggle button
var button = this.player.elements.buttons.fullscreen;
if (is$1.element(button)) {
toggleState(button, this.active);
button.pressed = this.active;
}
// Trigger an event
@ -9372,9 +9380,6 @@ typeof navigator === "object" && (function (global, factory) {
// If there's a media title set, use that for the label
if (is$1.string(this.config.title) && !is$1.empty(this.config.title)) {
label += ', ' + this.config.title;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
@ -9454,13 +9459,17 @@ typeof navigator === "object" && (function (global, factory) {
// Check playing state
checkPlaying: function checkPlaying(event) {
var _this3 = this;
// Class hooks
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
toggleState(this.elements.buttons.play, this.playing);
// Set state
Array.from(this.elements.buttons.play).forEach(function (target) {
target.pressed = _this3.playing;
});
// Only update controls on non timeupdate events
if (is$1.event(event) && event.type === 'timeupdate') {
@ -9474,7 +9483,7 @@ typeof navigator === "object" && (function (global, factory) {
// Check if media is loading
checkLoading: function checkLoading(event) {
var _this3 = this;
var _this4 = this;
this.loading = ['stalled', 'waiting'].includes(event.type);
@ -9484,10 +9493,10 @@ typeof navigator === "object" && (function (global, factory) {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(function () {
// Update progress bar loading class state
toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading);
// Update controls visibility
ui.toggleControls.call(_this3);
ui.toggleControls.call(_this4);
}, this.loading ? 250 : 0);
},
@ -12511,9 +12520,6 @@ typeof navigator === "object" && (function (global, factory) {
wrap$2(this.media, this.elements.container);
}
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Add style hook
ui.addStyleHook.call(this);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,6 @@ import {
insertAfter,
removeElement,
toggleClass,
toggleState,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
@ -193,7 +192,7 @@ const captions = {
}
// Toggle state
toggleState(this.elements.buttons.captions, active);
this.elements.buttons.captions.pressed = active;
// Add class hook
toggleClass(this.elements.container, activeClass, active);

View File

@ -18,6 +18,10 @@ const defaults = {
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
@ -153,6 +157,7 @@ const defaults = {
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@ -167,6 +172,7 @@ const defaults = {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
@ -334,6 +340,7 @@ const defaults = {
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',

116
src/js/controls.js vendored
View File

@ -20,7 +20,7 @@ import {
setAttributes,
toggleClass,
toggleHidden,
toggleState,
matches,
} from './utils/elements';
import { off, on } from './utils/events';
import is from './utils/is';
@ -29,6 +29,7 @@ import { extend } from './utils/objects';
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
import { formatTime, getHours } from './utils/time';
// TODO: Don't export a massive object - break down and create class
const controls = {
// Get icon URL
getIconUrl() {
@ -41,8 +42,7 @@ const controls = {
};
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
// Find the UI controls
findElements() {
try {
this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);
@ -139,12 +139,11 @@ const controls = {
pip: 'PIP',
airplay: 'AirPlay',
};
const text = universals[type] || i18n.get(type, this.config);
const attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
});
return createElement('span', attributes, text);
},
@ -250,9 +249,6 @@ const controls = {
// Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
// Add aria attributes
attributes['aria-pressed'] = false;
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@ -274,22 +270,23 @@ const controls = {
this.elements.buttons[type] = button;
}
// Toggle classname when pressed property is set
const className = this.config.classNames.controlPressed;
Object.defineProperty(button, 'pressed', {
enumerable: true,
get() {
return hasClass(button, className);
},
set(pressed = false) {
toggleClass(button, className, pressed);
},
});
return button;
},
// Create an <input type='range'>
createRange(type, attributes) {
// Seek label
const label = createElement(
'label',
{
for: attributes.id,
id: `${attributes.id}-label`,
class: this.config.classNames.hidden,
},
i18n.get(type, this.config),
);
// Seek input
const input = createElement(
'input',
@ -304,7 +301,7 @@ const controls = {
autocomplete: 'off',
// A11y fixes for https://github.com/sampotts/plyr/issues/905
role: 'slider',
'aria-labelledby': `${attributes.id}-label`,
'aria-label': i18n.get(type, this.config),
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0,
@ -318,10 +315,7 @@ const controls = {
// Set the fill for webkit now
controls.updateRangeFill.call(this, input);
return {
label,
input,
};
return input;
},
// Create a <progress>
@ -349,7 +343,6 @@ const controls = {
played: 'played',
buffer: 'buffered',
}[type];
const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
progress.innerText = `% ${suffix.toLowerCase()}`;
@ -412,6 +405,19 @@ const controls = {
list.appendChild(item);
},
// Format a time for display
formatTime(time = 0, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return time;
}
// Always display hours if duration is over an hour
const forceHours = getHours(this.duration) > 0;
return formatTime(time, forceHours, inverted);
},
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
@ -419,11 +425,8 @@ const controls = {
return;
}
// Always display hours if duration is over an hour
const forceHours = getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.innerText = formatTime(time, forceHours, inverted);
target.innerText = controls.formatTime(time, inverted);
},
// Update volume UI and storage
@ -439,7 +442,7 @@ const controls = {
// Update mute state
if (is.element(this.elements.buttons.mute)) {
toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
}
},
@ -518,8 +521,23 @@ const controls = {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// Set aria values for https://github.com/sampotts/plyr/issues/905
if (matches(range, this.config.selectors.inputs.seek)) {
range.setAttribute('aria-valuenow', this.currentTime);
const currentTime = controls.formatTime(this.currentTime);
const duration = controls.formatTime(this.duration);
const format = i18n.get('seekLabel', this.config);
range.setAttribute(
'aria-valuetext',
format.replace('{currentTime}', currentTime).replace('{duration}', duration),
);
} else if (matches(range, this.config.selectors.inputs.volume)) {
const percent = range.value * 100;
range.setAttribute('aria-valuenow', percent);
range.setAttribute('aria-valuetext', `${percent}%`);
} else {
range.setAttribute('aria-valuenow', range.value);
}
// WebKit only
if (!browser.isWebkit) {
@ -610,11 +628,16 @@ const controls = {
// Show the duration on metadataloaded or durationchange events
durationUpdate() {
// Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
// Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {
return;
}
// Update ARIA values
if (is.element(this.elements.inputs.seek)) {
this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
}
// If there's a spot to display duration
const hasDuration = is.element(this.elements.display.duration);
@ -1117,11 +1140,11 @@ const controls = {
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// Seek range slider
const seek = controls.createRange.call(this, 'seek', {
id: `plyr-seek-${data.id}`,
});
progress.appendChild(seek.label);
progress.appendChild(seek.input);
progress.appendChild(
controls.createRange.call(this, 'seek', {
id: `plyr-seek-${data.id}`,
}),
);
// Buffer progress
progress.appendChild(controls.createProgress.call(this, 'buffer'));
@ -1175,15 +1198,15 @@ const controls = {
};
// Create the volume range slider
const range = controls.createRange.call(
this,
'volume',
extend(attributes, {
id: `plyr-volume-${data.id}`,
}),
volume.appendChild(
controls.createRange.call(
this,
'volume',
extend(attributes, {
id: `plyr-volume-${data.id}`,
}),
),
);
volume.appendChild(range.label);
volume.appendChild(range.input);
this.elements.volume = volume;
@ -1448,7 +1471,6 @@ const controls = {
Array.from(labels).forEach(label => {
toggleClass(label, this.config.classNames.hidden, false);
toggleClass(label, this.config.classNames.tooltip, true);
label.setAttribute('role', 'tooltip');
});
}
},

View File

@ -4,7 +4,7 @@
// ==========================================================================
import browser from './utils/browser';
import { hasClass, toggleClass, toggleState, trapFocus } from './utils/elements';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
@ -16,7 +16,7 @@ function onChange() {
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
toggleState(button, this.active);
button.pressed = this.active;
}
// Trigger an event

View File

@ -263,9 +263,6 @@ class Plyr {
wrap(this.media, this.elements.container);
}
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Add style hook
ui.addStyleHook.call(this);

View File

@ -7,7 +7,7 @@ import controls from './controls';
import i18n from './i18n';
import support from './support';
import browser from './utils/browser';
import { getElement, toggleClass, toggleState } from './utils/elements';
import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import is from './utils/is';
import loadImage from './utils/loadImage';
@ -132,9 +132,6 @@ const ui = {
// If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
@ -216,8 +213,10 @@ const ui = {
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
toggleState(this.elements.buttons.play, this.playing);
// Set state
Array.from(this.elements.buttons.play).forEach(target => {
target.pressed = this.playing;
});
// Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') {

View File

@ -283,25 +283,3 @@ export function trapFocus(element = null, toggle = false) {
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
// Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
export function toggleState(element, input) {
// If multiple elements passed
if (is.array(element) || is.nodeList(element)) {
Array.from(element).forEach(target => toggleState(target, input));
return;
}
// Bail if no target
if (!is.element(element)) {
return;
}
// Get state
const pressed = element.getAttribute('aria-pressed') === 'true';
const state = is.boolean(input) ? input : !pressed;
// Set the attribute on target
element.setAttribute('aria-pressed', state);
}

View File

@ -32,5 +32,5 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {
}
// Render
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}

View File

@ -34,10 +34,10 @@
}
// Change icons on state change
.plyr__control[aria-pressed='false'] .icon--pressed,
.plyr__control[aria-pressed='true'] .icon--not-pressed,
.plyr__control[aria-pressed='false'] .label--pressed,
.plyr__control[aria-pressed='true'] .label--not-pressed {
.plyr__control:not(.plyr__control--pressed) .icon--pressed,
.plyr__control.plyr__control--pressed .icon--not-pressed,
.plyr__control:not(.plyr__control--pressed) .label--pressed,
.plyr__control.plyr__control--pressed .label--not-pressed {
display: none;
}