mirror of
https://github.com/videojs/video.js.git
synced 2025-01-04 06:48:49 +02:00
@OwenEdwards Fixed menu keyboard access and ARIA labeling for screen readers. closes #3033
This commit is contained in:
parent
16f0179c07
commit
e05931dd19
@ -3,6 +3,7 @@ CHANGELOG
|
|||||||
|
|
||||||
## HEAD (Unreleased)
|
## HEAD (Unreleased)
|
||||||
* @OwenEdwards added ClickableComponent. Fixed keyboard operation of buttons ([view](https://github.com/videojs/video.js/pull/3032))
|
* @OwenEdwards added ClickableComponent. Fixed keyboard operation of buttons ([view](https://github.com/videojs/video.js/pull/3032))
|
||||||
|
* @OwenEdwards Fixed menu keyboard access and ARIA labeling for screen readers ([view](https://github.com/videojs/video.js/pull/3033))
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -38,6 +38,8 @@ class ControlBar extends Component {
|
|||||||
createEl() {
|
createEl() {
|
||||||
return super.createEl('div', {
|
return super.createEl('div', {
|
||||||
className: 'vjs-control-bar'
|
className: 'vjs-control-bar'
|
||||||
|
}, {
|
||||||
|
'role': 'group' // The control bar is a group, so it can contain menuitems
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,17 @@ import Component from '../../component.js';
|
|||||||
'kind': options['kind'],
|
'kind': options['kind'],
|
||||||
'player': player,
|
'player': player,
|
||||||
'label': options['kind'] + ' settings',
|
'label': options['kind'] + ' settings',
|
||||||
|
'selectable': false,
|
||||||
'default': false,
|
'default': false,
|
||||||
mode: 'disabled'
|
mode: 'disabled'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CaptionSettingsMenuItem has no concept of 'selected'
|
||||||
|
options['selectable'] = false;
|
||||||
|
|
||||||
super(player, options);
|
super(player, options);
|
||||||
this.addClass('vjs-texttrack-settings');
|
this.addClass('vjs-texttrack-settings');
|
||||||
|
this.controlText(', opens ' + options['kind'] + ' settings dialog');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +39,7 @@ import Component from '../../component.js';
|
|||||||
*/
|
*/
|
||||||
handleClick() {
|
handleClick() {
|
||||||
this.player().getChild('textTrackSettings').show();
|
this.player().getChild('textTrackSettings').show();
|
||||||
|
this.player().getChild('textTrackSettings').el_.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,9 @@ class OffTextTrackMenuItem extends TextTrackMenuItem {
|
|||||||
'mode': 'disabled'
|
'mode': 'disabled'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MenuItem is selectable
|
||||||
|
options['selectable'] = true;
|
||||||
|
|
||||||
super(player, options);
|
super(player, options);
|
||||||
this.selected(true);
|
this.selected(true);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,8 @@ class TextTrackButton extends MenuButton {
|
|||||||
// only add tracks that are of the appropriate kind and have a label
|
// only add tracks that are of the appropriate kind and have a label
|
||||||
if (track['kind'] === this.kind_) {
|
if (track['kind'] === this.kind_) {
|
||||||
items.push(new TextTrackMenuItem(this.player_, {
|
items.push(new TextTrackMenuItem(this.player_, {
|
||||||
|
// MenuItem is selectable
|
||||||
|
'selectable': true,
|
||||||
'track': track
|
'track': track
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ class TextTrackMenuItem extends MenuItem {
|
|||||||
// Modify options for parent MenuItem class's init.
|
// Modify options for parent MenuItem class's init.
|
||||||
options['label'] = track['label'] || track['language'] || 'Unknown';
|
options['label'] = track['label'] || track['language'] || 'Unknown';
|
||||||
options['selected'] = track['default'] || track['mode'] === 'showing';
|
options['selected'] = track['default'] || track['mode'] === 'showing';
|
||||||
|
|
||||||
super(player, options);
|
super(player, options);
|
||||||
|
|
||||||
this.track = track;
|
this.track = track;
|
||||||
|
@ -23,8 +23,9 @@ class MenuButton extends ClickableComponent {
|
|||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
this.on('keydown', this.handleKeyPress);
|
|
||||||
this.el_.setAttribute('aria-haspopup', true);
|
this.el_.setAttribute('aria-haspopup', true);
|
||||||
|
this.el_.setAttribute('role', 'menuitem');
|
||||||
|
this.on('keydown', this.handleSubmenuKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +50,7 @@ class MenuButton extends ClickableComponent {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.buttonPressed_ = false;
|
this.buttonPressed_ = false;
|
||||||
|
this.el_.setAttribute('aria-expanded', false);
|
||||||
|
|
||||||
if (this.items && this.items.length === 0) {
|
if (this.items && this.items.length === 0) {
|
||||||
this.hide();
|
this.hide();
|
||||||
@ -125,27 +127,6 @@ class MenuButton extends ClickableComponent {
|
|||||||
return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
|
return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus - Add keyboard functionality to element
|
|
||||||
* This function is not needed anymore. Instead, the
|
|
||||||
* keyboard functionality is handled by
|
|
||||||
* treating the button as triggering a submenu.
|
|
||||||
* When the button is pressed, the submenu
|
|
||||||
* appears. Pressing the button again makes
|
|
||||||
* the submenu disappear.
|
|
||||||
*
|
|
||||||
* @method handleFocus
|
|
||||||
*/
|
|
||||||
handleFocus() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can't turn off list display that we turned
|
|
||||||
* on with focus, because list would go away.
|
|
||||||
*
|
|
||||||
* @method handleBlur
|
|
||||||
*/
|
|
||||||
handleBlur() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When you click the button it adds focus, which
|
* When you click the button it adds focus, which
|
||||||
* will show the menu indefinitely.
|
* will show the menu indefinitely.
|
||||||
@ -170,25 +151,48 @@ class MenuButton extends ClickableComponent {
|
|||||||
/**
|
/**
|
||||||
* Handle key press on menu
|
* Handle key press on menu
|
||||||
*
|
*
|
||||||
* @param {Object} Key press event
|
* @param {Object} event Key press event
|
||||||
* @method handleKeyPress
|
* @method handleKeyPress
|
||||||
*/
|
*/
|
||||||
handleKeyPress(event) {
|
handleKeyPress(event) {
|
||||||
|
|
||||||
// Check for space bar (32) or enter (13) keys
|
// Escape (27) key or Tab (9) key unpress the 'button'
|
||||||
if (event.which === 32 || event.which === 13) {
|
if (event.which === 27 || event.which === 9) {
|
||||||
if (this.buttonPressed_){
|
if (this.buttonPressed_) {
|
||||||
this.unpressButton();
|
this.unpressButton();
|
||||||
} else {
|
}
|
||||||
|
// Don't preventDefault for Tab key - we still want to lose focus
|
||||||
|
if (event.which !== 9) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
// Up (38) key or Down (40) key press the 'button'
|
||||||
|
} else if (event.which === 38 || event.which === 40) {
|
||||||
|
if (!this.buttonPressed_) {
|
||||||
this.pressButton();
|
this.pressButton();
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
} else {
|
||||||
// Check for escape (27) key
|
super.handleKeyPress(event);
|
||||||
} else if (event.which === 27){
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle key press on submenu
|
||||||
|
*
|
||||||
|
* @param {Object} event Key press event
|
||||||
|
* @method handleSubmenuKeyPress
|
||||||
|
*/
|
||||||
|
handleSubmenuKeyPress(event) {
|
||||||
|
|
||||||
|
// Escape (27) key or Tab (9) key unpress the 'button'
|
||||||
|
if (event.which === 27 || event.which === 9){
|
||||||
if (this.buttonPressed_){
|
if (this.buttonPressed_){
|
||||||
this.unpressButton();
|
this.unpressButton();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
// Don't preventDefault for Tab key - we still want to lose focus
|
||||||
|
if (event.which !== 9) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,10 +204,8 @@ class MenuButton extends ClickableComponent {
|
|||||||
pressButton() {
|
pressButton() {
|
||||||
this.buttonPressed_ = true;
|
this.buttonPressed_ = true;
|
||||||
this.menu.lockShowing();
|
this.menu.lockShowing();
|
||||||
this.el_.setAttribute('aria-pressed', true);
|
this.el_.setAttribute('aria-expanded', true);
|
||||||
if (this.items && this.items.length > 0) {
|
this.menu.focus(); // set the focus into the submenu
|
||||||
this.items[0].el().focus(); // set the focus to the title of the submenu
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,7 +216,8 @@ class MenuButton extends ClickableComponent {
|
|||||||
unpressButton() {
|
unpressButton() {
|
||||||
this.buttonPressed_ = false;
|
this.buttonPressed_ = false;
|
||||||
this.menu.unlockShowing();
|
this.menu.unlockShowing();
|
||||||
this.el_.setAttribute('aria-pressed', false);
|
this.el_.setAttribute('aria-expanded', false);
|
||||||
|
this.el_.focus(); // Set focus back to this menu button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,18 @@ class MenuItem extends ClickableComponent {
|
|||||||
|
|
||||||
constructor(player, options) {
|
constructor(player, options) {
|
||||||
super(player, options);
|
super(player, options);
|
||||||
|
|
||||||
|
this.selectable = options['selectable'];
|
||||||
|
|
||||||
this.selected(options['selected']);
|
this.selected(options['selected']);
|
||||||
|
|
||||||
|
if (this.selectable) {
|
||||||
|
// TODO: May need to be either menuitemcheckbox or menuitemradio,
|
||||||
|
// and may need logical grouping of menu items.
|
||||||
|
this.el_.setAttribute('role', 'menuitemcheckbox');
|
||||||
|
} else {
|
||||||
|
this.el_.setAttribute('role', 'menuitem');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,7 +42,8 @@ class MenuItem extends ClickableComponent {
|
|||||||
createEl(type, props, attrs) {
|
createEl(type, props, attrs) {
|
||||||
return super.createEl('li', assign({
|
return super.createEl('li', assign({
|
||||||
className: 'vjs-menu-item',
|
className: 'vjs-menu-item',
|
||||||
innerHTML: this.localize(this.options_['label'])
|
innerHTML: this.localize(this.options_['label']),
|
||||||
|
tabIndex: -1
|
||||||
}, props), attrs);
|
}, props), attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,15 +63,22 @@ class MenuItem extends ClickableComponent {
|
|||||||
* @method selected
|
* @method selected
|
||||||
*/
|
*/
|
||||||
selected(selected) {
|
selected(selected) {
|
||||||
if (selected) {
|
if (this.selectable) {
|
||||||
this.addClass('vjs-selected');
|
if (selected) {
|
||||||
this.el_.setAttribute('aria-selected',true);
|
this.addClass('vjs-selected');
|
||||||
} else {
|
this.el_.setAttribute('aria-checked',true);
|
||||||
this.removeClass('vjs-selected');
|
// aria-checked isn't fully supported by browsers/screen readers,
|
||||||
this.el_.setAttribute('aria-selected',false);
|
// so indicate selected state to screen reader in the control text.
|
||||||
|
this.controlText(', selected');
|
||||||
|
} else {
|
||||||
|
this.removeClass('vjs-selected');
|
||||||
|
this.el_.setAttribute('aria-checked',false);
|
||||||
|
// Indicate un-selected state to screen reader
|
||||||
|
// Note that a space clears out the selected state text
|
||||||
|
this.controlText(' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.registerComponent('MenuItem', MenuItem);
|
Component.registerComponent('MenuItem', MenuItem);
|
||||||
|
@ -15,6 +15,14 @@ import * as Events from '../utils/events.js';
|
|||||||
*/
|
*/
|
||||||
class Menu extends Component {
|
class Menu extends Component {
|
||||||
|
|
||||||
|
constructor (player, options) {
|
||||||
|
super(player, options);
|
||||||
|
|
||||||
|
this.focusedChild_ = -1;
|
||||||
|
|
||||||
|
this.on('keydown', this.handleKeyPress);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a menu item to the menu
|
* Add a menu item to the menu
|
||||||
*
|
*
|
||||||
@ -25,6 +33,7 @@ class Menu extends Component {
|
|||||||
this.addChild(component);
|
this.addChild(component);
|
||||||
component.on('click', Fn.bind(this, function(){
|
component.on('click', Fn.bind(this, function(){
|
||||||
this.unlockShowing();
|
this.unlockShowing();
|
||||||
|
//TODO: Need to set keyboard focus back to the menuButton
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,10 +48,12 @@ class Menu extends Component {
|
|||||||
this.contentEl_ = Dom.createEl(contentElType, {
|
this.contentEl_ = Dom.createEl(contentElType, {
|
||||||
className: 'vjs-menu-content'
|
className: 'vjs-menu-content'
|
||||||
});
|
});
|
||||||
|
this.contentEl_.setAttribute('role', 'menu');
|
||||||
var el = super.createEl('div', {
|
var el = super.createEl('div', {
|
||||||
append: this.contentEl_,
|
append: this.contentEl_,
|
||||||
className: 'vjs-menu'
|
className: 'vjs-menu'
|
||||||
});
|
});
|
||||||
|
el.setAttribute('role', 'presentation');
|
||||||
el.appendChild(this.contentEl_);
|
el.appendChild(this.contentEl_);
|
||||||
|
|
||||||
// Prevent clicks from bubbling up. Needed for Menu Buttons,
|
// Prevent clicks from bubbling up. Needed for Menu Buttons,
|
||||||
@ -54,6 +65,72 @@ class Menu extends Component {
|
|||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle key press for menu
|
||||||
|
*
|
||||||
|
* @param {Object} event Event object
|
||||||
|
* @method handleKeyPress
|
||||||
|
*/
|
||||||
|
handleKeyPress (event) {
|
||||||
|
if (event.which === 37 || event.which === 40) { // Left and Down Arrows
|
||||||
|
event.preventDefault();
|
||||||
|
this.stepForward();
|
||||||
|
} else if (event.which === 38 || event.which === 39) { // Up and Right Arrows
|
||||||
|
event.preventDefault();
|
||||||
|
this.stepBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to next (lower) menu item for keyboard users
|
||||||
|
*
|
||||||
|
* @method stepForward
|
||||||
|
*/
|
||||||
|
stepForward () {
|
||||||
|
let stepChild = 0;
|
||||||
|
|
||||||
|
if (this.focusedChild_ !== undefined) {
|
||||||
|
stepChild = this.focusedChild_ + 1;
|
||||||
|
}
|
||||||
|
this.focus(stepChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to previous (higher) menu item for keyboard users
|
||||||
|
*
|
||||||
|
* @method stepBack
|
||||||
|
*/
|
||||||
|
stepBack () {
|
||||||
|
let stepChild = 0;
|
||||||
|
|
||||||
|
if (this.focusedChild_ !== undefined) {
|
||||||
|
stepChild = this.focusedChild_ - 1;
|
||||||
|
}
|
||||||
|
this.focus(stepChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set focus on a menu item in the menu
|
||||||
|
*
|
||||||
|
* @param {Object|String} item Index of child item set focus on
|
||||||
|
* @method focus
|
||||||
|
*/
|
||||||
|
focus (item = 0) {
|
||||||
|
let children = this.children();
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
if (item < 0) {
|
||||||
|
item = 0;
|
||||||
|
} else if (item >= children.length) {
|
||||||
|
item = children.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focusedChild_ = item;
|
||||||
|
|
||||||
|
children[item].el_.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.registerComponent('Menu', Menu);
|
Component.registerComponent('Menu', Menu);
|
||||||
|
Loading…
Reference in New Issue
Block a user