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:
parent
81caccd154
commit
16f0179c07
@ -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))
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
102
src/js/button.js
102
src/js/button.js
@ -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;
|
||||||
|
174
src/js/clickable-component.js
Normal file
174
src/js/clickable-component.js
Normal 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;
|
@ -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';
|
||||||
|
@ -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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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'));
|
||||||
});
|
});
|
||||||
|
19
test/unit/clickable-component.test.js
Normal file
19
test/unit/clickable-component.test.js
Normal 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"');
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user