1
0
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:
Owen Edwards 2016-01-25 19:05:10 -05:00 committed by Gary Katsevman
parent 16f0179c07
commit e05931dd19
9 changed files with 158 additions and 44 deletions

View File

@ -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))
-------------------- --------------------

View File

@ -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
}); });
} }
} }

View File

@ -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();
} }
} }

View File

@ -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);
} }

View File

@ -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
})); }));
} }

View File

@ -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;

View File

@ -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
} }
} }

View File

@ -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);

View File

@ -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);