1
0
mirror of https://github.com/videojs/video.js.git synced 2024-11-28 08:58:46 +02:00

@OwenEdwards added ClickableComponent. Fixed keyboard operation of buttons. closes #3032

This commit is contained in:
Owen Edwards 2016-01-25 18:30:12 -05:00 committed by Gary Katsevman
parent 81caccd154
commit 16f0179c07
9 changed files with 231 additions and 83 deletions

View File

@ -2,7 +2,7 @@ CHANGELOG
========= =========
## HEAD (Unreleased) ## HEAD (Unreleased)
_(none)_ * @OwenEdwards added ClickableComponent. Fixed keyboard operation of buttons ([view](https://github.com/videojs/video.js/pull/3032))
-------------------- --------------------

View File

@ -1,10 +1,11 @@
/** /**
* @file button.js * @file button.js
*/ */
import ClickableComponent from './clickable-component.js';
import Component from './component'; import Component from './component';
import * as Dom from './utils/dom.js';
import * as Events from './utils/events.js'; import * as Events from './utils/events.js';
import * as Fn from './utils/fn.js'; import * as Fn from './utils/fn.js';
import log from './utils/log.js';
import document from 'global/document'; import document from 'global/document';
import assign from 'object.assign'; import assign from 'object.assign';
@ -13,122 +14,77 @@ import assign from 'object.assign';
* *
* @param {Object} player Main Player * @param {Object} player Main Player
* @param {Object=} options Object of option names and values * @param {Object=} options Object of option names and values
* @extends Component * @extends ClickableComponent
* @class Button * @class Button
*/ */
class Button extends Component { class Button extends ClickableComponent {
constructor(player, options) { constructor(player, options) {
super(player, options); super(player, options);
this.emitTapEvents();
this.on('tap', this.handleClick);
this.on('click', this.handleClick);
this.on('focus', this.handleFocus);
this.on('blur', this.handleBlur);
} }
/** /**
* Create the component's DOM element * Create the component's DOM element
* *
* @param {String=} type Element's node type. e.g. 'div' * @param {String=} type Element's node type. e.g. 'div'
* @param {Object=} props An object of element attributes that should be set on the element Tag name * @param {Object=} props An object of properties that should be set on the element
* @param {Object=} attributes An object of attributes that should be set on the element
* @return {Element} * @return {Element}
* @method createEl * @method createEl
*/ */
createEl(tag='button', props={}, attributes={}) { createEl(tag='button', props={}, attributes={}) {
props = assign({ props = assign({
className: this.buildCSSClass(), className: this.buildCSSClass()
tabIndex: 0
}, props); }, props);
// Add standard Aria info if (tag !== 'button') {
log.warn(`Creating a Button with an HTML element of ${tag} is deprecated; use ClickableComponent instead.`);
}
// Add attributes for button element
attributes = assign({ attributes = assign({
role: 'button',
type: 'button', // Necessary since the default button type is "submit" type: 'button', // Necessary since the default button type is "submit"
'aria-live': 'polite' // let the screen reader user know that the text of the button may change 'aria-live': 'polite' // let the screen reader user know that the text of the button may change
}, attributes); }, attributes);
let el = super.createEl(tag, props, attributes); let el = Component.prototype.createEl.call(this, tag, props, attributes);
this.controlTextEl_ = Dom.createEl('span', { this.createControlTextEl(el);
className: 'vjs-control-text'
});
el.appendChild(this.controlTextEl_);
this.controlText(this.controlText_);
return el; return el;
} }
/** /**
* Controls text - both request and localize * Adds a child component inside this button
* *
* @param {String} text Text for button * @param {String|Component} child The class name or instance of a child to add
* @return {String} * @param {Object=} options Options, including options to be passed to children of the child.
* @method controlText * @return {Component} The child component (created by this process if a string was used)
* @deprecated
* @method addChild
*/ */
controlText(text) { addChild(child, options={}) {
if (!text) return this.controlText_ || 'Need Text'; let className = this.constructor.name;
log.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
this.controlText_ = text; // Avoid the error message generated by ClickableComponent's addChild method
this.controlTextEl_.innerHTML = this.localize(this.controlText_); return Component.prototype.addChild.call(this, child, options);
return this;
} }
/** /**
* Allows sub components to stack CSS class names * Handle KeyPress (document level) - Extend with specific functionality for button
*
* @return {String}
* @method buildCSSClass
*/
buildCSSClass() {
return `vjs-control vjs-button ${super.buildCSSClass()}`;
}
/**
* Handle Click - Override with specific functionality for button
*
* @method handleClick
*/
handleClick() {}
/**
* Handle Focus - Add keyboard functionality to element
*
* @method handleFocus
*/
handleFocus() {
Events.on(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
/**
* Handle KeyPress (document level) - Trigger click when keys are pressed
* *
* @method handleKeyPress * @method handleKeyPress
*/ */
handleKeyPress(event) { handleKeyPress(event) {
// Check for space bar (32) or enter (13) keys // Ignore Space (32) or Enter (13) key operation, which is handled by the browser for a button.
if (event.which === 32 || event.which === 13) { if (event.which === 32 || event.which === 13) {
event.preventDefault(); } else {
this.handleClick(event); super.handleKeyPress(event); // Pass keypress handling up for unsupported keys
} }
} }
/**
* Handle Blur - Remove keyboard triggers
*
* @method handleBlur
*/
handleBlur() {
Events.off(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
} }
Component.registerComponent('Button', Button); Component.registerComponent('Button', Button);
export default Button; export default Button;

View File

@ -0,0 +1,174 @@
/**
* @file button.js
*/
import Component from './component';
import * as Dom from './utils/dom.js';
import * as Events from './utils/events.js';
import * as Fn from './utils/fn.js';
import log from './utils/log.js';
import document from 'global/document';
import assign from 'object.assign';
/**
* Clickable Component which is clickable or keyboard actionable, but is not a native HTML button
*
* @param {Object} player Main Player
* @param {Object=} options Object of option names and values
* @extends Component
* @class ClickableComponent
*/
class ClickableComponent extends Component {
constructor(player, options) {
super(player, options);
this.emitTapEvents();
this.on('tap', this.handleClick);
this.on('click', this.handleClick);
this.on('focus', this.handleFocus);
this.on('blur', this.handleBlur);
}
/**
* Create the component's DOM element
*
* @param {String=} type Element's node type. e.g. 'div'
* @param {Object=} props An object of properties that should be set on the element
* @param {Object=} attributes An object of attributes that should be set on the element
* @return {Element}
* @method createEl
*/
createEl(tag='div', props={}, attributes={}) {
props = assign({
className: this.buildCSSClass(),
tabIndex: 0
}, props);
if (tag === 'button') {
log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
}
// Add ARIA attributes for clickable element which is not a native HTML button
attributes = assign({
role: 'button',
'aria-live': 'polite' // let the screen reader user know that the text of the element may change
}, attributes);
let el = super.createEl(tag, props, attributes);
this.createControlTextEl(el);
return el;
}
/**
* create control text
*
* @param {Element} el Parent element for the control text
* @return {Element}
* @method controlText
*/
createControlTextEl(el) {
this.controlTextEl_ = Dom.createEl('span', {
className: 'vjs-control-text'
});
if (el) {
el.appendChild(this.controlTextEl_);
}
this.controlText(this.controlText_);
return this.controlTextEl_;
}
/**
* Controls text - both request and localize
*
* @param {String} text Text for element
* @return {String}
* @method controlText
*/
controlText(text) {
if (!text) return this.controlText_ || 'Need Text';
this.controlText_ = text;
this.controlTextEl_.innerHTML = this.localize(this.controlText_);
return this;
}
/**
* Allows sub components to stack CSS class names
*
* @return {String}
* @method buildCSSClass
*/
buildCSSClass() {
return `vjs-control vjs-button ${super.buildCSSClass()}`;
}
/**
* Adds a child component inside this clickable-component
*
* @param {String|Component} child The class name or instance of a child to add
* @param {Object=} options Options, including options to be passed to children of the child.
* @return {Component} The child component (created by this process if a string was used)
* @method addChild
*/
addChild(child, options={}) {
// TODO: Fix adding an actionable child to a ClickableComponent; currently
// it will cause issues with assistive technology (e.g. screen readers)
// which support ARIA, since an element with role="button" cannot have
// actionable child elements.
//let className = this.constructor.name;
//log.warn(`Adding a child to a ClickableComponent (${className}) can cause issues with assistive technology which supports ARIA, since an element with role="button" cannot have actionable child elements.`);
return super.addChild(child, options);
}
/**
* Handle Click - Override with specific functionality for component
*
* @method handleClick
*/
handleClick() {}
/**
* Handle Focus - Add keyboard functionality to element
*
* @method handleFocus
*/
handleFocus() {
Events.on(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
/**
* Handle KeyPress (document level) - Trigger click when Space or Enter key is pressed
*
* @method handleKeyPress
*/
handleKeyPress(event) {
// Support Space (32) or Enter (13) key operation to fire a click event
if (event.which === 32 || event.which === 13) {
event.preventDefault();
this.handleClick(event);
} else if (super.handleKeyPress) {
super.handleKeyPress(event); // Pass keypress handling up for unsupported keys
}
}
/**
* Handle Blur - Remove keyboard triggers
*
* @method handleBlur
*/
handleBlur() {
Events.off(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
}
Component.registerComponent('ClickableComponent', ClickableComponent);
export default ClickableComponent;

View File

@ -1,7 +1,6 @@
/** /**
* @file volume-menu-button.js * @file volume-menu-button.js
*/ */
import Button from '../button.js';
import * as Fn from '../utils/fn.js'; import * as Fn from '../utils/fn.js';
import Component from '../component.js'; import Component from '../component.js';
import Menu from '../menu/menu.js'; import Menu from '../menu/menu.js';

View File

@ -1,7 +1,7 @@
/** /**
* @file menu-button.js * @file menu-button.js
*/ */
import Button from '../button.js'; import ClickableComponent from '../clickable-component.js';
import Component from '../component.js'; import Component from '../component.js';
import Menu from './menu.js'; import Menu from './menu.js';
import * as Dom from '../utils/dom.js'; import * as Dom from '../utils/dom.js';
@ -16,7 +16,7 @@ import toTitleCase from '../utils/to-title-case.js';
* @extends Button * @extends Button
* @class MenuButton * @class MenuButton
*/ */
class MenuButton extends Button { class MenuButton extends ClickableComponent {
constructor(player, options={}){ constructor(player, options={}){
super(player, options); super(player, options);
@ -25,7 +25,6 @@ class MenuButton extends Button {
this.on('keydown', this.handleKeyPress); this.on('keydown', this.handleKeyPress);
this.el_.setAttribute('aria-haspopup', true); this.el_.setAttribute('aria-haspopup', true);
this.el_.setAttribute('role', 'button');
} }
/** /**

View File

@ -1,7 +1,7 @@
/** /**
* @file menu-item.js * @file menu-item.js
*/ */
import Button from '../button.js'; import ClickableComponent from '../clickable-component.js';
import Component from '../component.js'; import Component from '../component.js';
import assign from 'object.assign'; import assign from 'object.assign';
@ -13,7 +13,7 @@ import assign from 'object.assign';
* @extends Button * @extends Button
* @class MenuItem * @class MenuItem
*/ */
class MenuItem extends Button { class MenuItem extends ClickableComponent {
constructor(player, options) { constructor(player, options) {
super(player, options); super(player, options);

View File

@ -1,7 +1,7 @@
/** /**
* @file poster-image.js * @file poster-image.js
*/ */
import Button from './button.js'; import ClickableComponent from './clickable-component.js';
import Component from './component.js'; import Component from './component.js';
import * as Fn from './utils/fn.js'; import * as Fn from './utils/fn.js';
import * as Dom from './utils/dom.js'; import * as Dom from './utils/dom.js';
@ -15,7 +15,7 @@ import * as browser from './utils/browser.js';
* @extends Button * @extends Button
* @class PosterImage * @class PosterImage
*/ */
class PosterImage extends Button { class PosterImage extends ClickableComponent {
constructor(player, options){ constructor(player, options){
super(player, options); super(player, options);

View File

@ -4,7 +4,7 @@ import TestHelpers from './test-helpers.js';
q.module('Button'); q.module('Button');
test('should localize its text', function(){ test('should localize its text', function(){
expect(1); expect(2);
var player, testButton, el; var player, testButton, el;
@ -21,5 +21,6 @@ test('should localize its text', function(){
testButton.controlText_ = 'Play'; testButton.controlText_ = 'Play';
el = testButton.createEl(); el = testButton.createEl();
ok(el.nodeName.toLowerCase().match('button'));
ok(el.innerHTML.match('Juego')); ok(el.innerHTML.match('Juego'));
}); });

View File

@ -0,0 +1,19 @@
import ClickableComponent from '../../src/js/clickable-component.js';
import TestHelpers from './test-helpers.js';
q.module('ClickableComponent');
test('should create a div with role="button"', function(){
expect(2);
var player, testClickableComponent, el;
player = TestHelpers.makePlayer({
});
testClickableComponent = new ClickableComponent(player);
el = testClickableComponent.createEl();
equal(el.nodeName.toLowerCase(), 'div', 'the name of the element is "div"');
equal(el.getAttribute('role').toLowerCase(), 'button', 'the role of the element is "button"');
});