Skip to content

Commit

Permalink
feat(accessibility): tabs order and keyboard shortcuts (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Ziv committed Nov 26, 2017
1 parent 8181290 commit a1fa375
Show file tree
Hide file tree
Showing 24 changed files with 15,931 additions and 163 deletions.
15,428 changes: 15,425 additions & 3 deletions dist/playkit-ui.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/playkit-ui.js.map

Large diffs are not rendered by default.

59 changes: 49 additions & 10 deletions src/components/cvaa-overlay/cvaa-overlay.js
Expand Up @@ -11,6 +11,7 @@ import Overlay from '../overlay';
import DropDown from '../dropdown';
import Slider from '../slider';
import {default as Icon, IconType} from '../icon';
import {KeyMap} from "../../utils/key-map";

/**
* mapping state to props
Expand Down Expand Up @@ -141,33 +142,65 @@ class CVAAOverlay extends BaseComponent {
Advanced captions settings
</div>
<div>
<div className={style.sample} onClick={() => this.changeCaptionsStyle(this.captionsStyleDefault)}>Sample
<div tabIndex="0"
className={style.sample}
onClick={() => this.changeCaptionsStyle(this.captionsStyleDefault)}
onKeyDown={(e) => {
if (e.keyCode === KeyMap.ENTER) {
this.changeCaptionsStyle(this.captionsStyleDefault);
}
}}>Sample
{isEqual(this.props.player.textStyle, this.captionsStyleDefault) ?
<div className={style.activeTick}><Icon type={IconType.Check}/></div> : undefined}
</div>
<div className={[style.sample, style.blackBg].join(' ')}
onClick={() => this.changeCaptionsStyle(this.captionsStyleBlackBG)}>Sample
<div tabIndex="0"
className={[style.sample, style.blackBg].join(' ')}
onClick={() => this.changeCaptionsStyle(this.captionsStyleBlackBG)}
onKeyDown={(e) => {
if (e.keyCode === KeyMap.ENTER) {
this.changeCaptionsStyle(this.captionsStyleBlackBG);
}
}}>Sample
{isEqual(this.props.player.textStyle, this.captionsStyleBlackBG) ?
<div className={style.activeTick}><Icon type={IconType.Check}/></div> : undefined}
</div>
<div className={[style.sample, style.yellowText].join(' ')}
onClick={() => this.changeCaptionsStyle(this.captionsStyleYellow)}>Sample
<div tabIndex="0"
className={[style.sample, style.yellowText].join(' ')}
onClick={() => this.changeCaptionsStyle(this.captionsStyleYellow)}
onKeyDown={(e) => {
if (e.keyCode === KeyMap.ENTER) {
this.changeCaptionsStyle(this.captionsStyleYellow);
}
}}>Sample
{isEqual(this.props.player.textStyle, this.captionsStyleYellow) ?
<div className={style.activeTick}><Icon type={IconType.Check}/></div> : undefined}
</div>
</div>
{!this.isAdvancedStyleApplied() ?
(
<a className={style.buttonSaveCvaa} onClick={() => this.transitionToState(cvaaOverlayState.CustomCaptions)}>Set
custom caption</a>
<a tabIndex="0"
className={style.buttonSaveCvaa}
onClick={() => this.transitionToState(cvaaOverlayState.CustomCaptions)}
onKeyDown={(e) => {
if (e.keyCode === KeyMap.ENTER) {
this.transitionToState(cvaaOverlayState.CustomCaptions)
}
}}>Set custom caption</a>
) :
(
<div className={style.customCaptionsApplied}>
<div className={[style.sample, style.custom].join(' ')} style={this.state.customTextStyle.toCSS()}>
<div tabIndex="0"
className={[style.sample, style.custom].join(' ')}
style={this.state.customTextStyle.toCSS()}>
<span>Custom captions</span>
<div className={style.activeTick}><Icon type={IconType.Check}/></div>
</div>
<a onClick={() => this.transitionToState(cvaaOverlayState.CustomCaptions)}>Edit caption</a>
<a tabIndex="0" onClick={() => this.transitionToState(cvaaOverlayState.CustomCaptions)}
onKeyDown={(e) => {
if (e.keyCode === KeyMap.ENTER) {
this.transitionToState(cvaaOverlayState.CustomCaptions)
}
}}>Edit caption</a>
</div>
)
}
Expand Down Expand Up @@ -265,7 +298,13 @@ class CVAAOverlay extends BaseComponent {
onChange={backgroundOpacity => this.changeCustomStyle({backgroundOpacity: backgroundOpacity / 100})}/>
</div>
<div className={style.formGroupRow}>
<a onClick={() => this.changeCaptionsStyle(this.state.customTextStyle)}
<a tabIndex="0"
onClick={() => this.changeCaptionsStyle(this.state.customTextStyle)}
onKeyDown={(e) => {
if (e.keyCode === KeyMap.ENTER) {
this.changeCaptionsStyle(this.state.customTextStyle)
}
}}
className={[style.btn, style.btnBranded, style.btnBlock].join(' ')}>Apply</a>
</div>

Expand Down
30 changes: 26 additions & 4 deletions src/components/dropdown/dropdown.js
Expand Up @@ -4,6 +4,7 @@ import {h, Component} from 'preact';
import {connect} from 'preact-redux';
import Menu from '../menu';
import {default as Icon, IconType} from '../icon';
import {KeyMap} from "../../utils/key-map";

/**
* mapping state to props
Expand Down Expand Up @@ -58,6 +59,25 @@ class DropDown extends Component {
this.setState({dropMenuActive: false});
}

/**
* on key down handler - on enter open toggle drop down menu
*
* @param {KeyboardEvent} e - keyboard event
* @returns {void}
* @memberof DropDown
*/
onKeyDown(e: KeyboardEvent): void {
switch (e.keyCode) {
case KeyMap.ENTER:
this.setState({dropMenuActive: !this.state.dropMenuActive});
break;
case KeyMap.ESC:
this.onClose();
e.stopPropagation();
break;
}
}

/**
* listener function from Menu component to close the dropdown menu.
* set the internal state of dropMenuActive to false.
Expand Down Expand Up @@ -111,8 +131,11 @@ class DropDown extends Component {
return props.isMobile ? this.renderNativeSelect() :
(
<div className={this.state.dropMenuActive ? [style.dropdown, style.active].join(' ') : style.dropdown}>
<div className={style.dropdownButton}
onClick={() => this.setState({dropMenuActive: !this.state.dropMenuActive})}>
<div
tabIndex="0"
className={style.dropdownButton}
onClick={() => this.setState({dropMenuActive: !this.state.dropMenuActive})}
onKeyDown={e => this.onKeyDown(e)}>
<span>{this.getActiveOptionLabel()}</span>
<Icon type={IconType.ArrowDown}/>
</div>
Expand All @@ -121,8 +144,7 @@ class DropDown extends Component {
<Menu
options={props.options}
onSelect={(o) => this.onSelect(o)}
onClose={() => this.onClose()}
/>
onClose={() => this.onClose()}/>
}
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion src/components/fullscreen/fullscreen.js
Expand Up @@ -150,7 +150,8 @@ class FullscreenControl extends BaseComponent {
return (
<div className={[style.controlButtonContainer, style.controlFullscreen].join(' ')}>
<Localizer>
<button aria-label={<Text id='controls.fullscreen'/>}
<button tabIndex="0"
aria-label={<Text id='controls.fullscreen'/>}
className={this.props.fullscreen ? [style.controlButton, style.isFullscreen].join(' ') : style.controlButton}
onClick={() => this.toggleFullscreen()}>
<Icon type={IconType.Maximize}/>
Expand Down
192 changes: 130 additions & 62 deletions src/components/keyboard/keyboard.js
@@ -1,13 +1,31 @@
//@flow
import BaseComponent from '../base';
import {connect} from 'preact-redux';
import {actions} from '../../reducers/shell';
import {bindActions} from '../../utils/bind-actions';
import {KeyMap} from "../../utils/key-map";

/**
* KeyboardControl component
*
* @class KeyboardControl
* @extends {BaseComponent}
* mapping state to props
* @param {*} state - redux store state
* @returns {Object} - mapped state to this component
*/
const mapStateToProps = state => ({
playerNav: state.shell.playerNav
});

const SEEK_JUMP: number = 5;
const VOLUME_JUMP: number = 5;

@connect(mapStateToProps, bindActions(actions))
/**
* KeyboardControl component
*
* @class KeyboardControl
* @extends {BaseComponent}
*/
class KeyboardControl extends BaseComponent {
_activeTextTrack: ?Object = null;

/**
* Creates an instance of KeyboardControl.
Expand All @@ -16,74 +34,124 @@ class KeyboardControl extends BaseComponent {
*/
constructor(obj: Object) {
super({name: 'Keyboard', player: obj.player, config: obj.config});

let playerContainer: HTMLElement | null = document.getElementById(this.config.targetId);

const playerContainer: HTMLElement | null = document.getElementById(this.config.targetId);
if (!playerContainer) {
return;
}
playerContainer.onkeydown = (e) => {
let time, newVolume;
switch (e.which) {
case 32: // space
this.logger.debug("Keydown space");
this.player.paused ? this.player.play() : this.player.pause();
break;

case 38: // up
this.logger.debug("Keydown up");
newVolume = Math.round(this.player.volume * 100) + 5;
this.logger.debug(`Changing volume. ${this.player.volume} => ${newVolume}`);
if (this.player.muted) {
this.player.muted = false;
}
this.player.volume = newVolume / 100;
break;

case 40: // down
this.logger.debug("Keydown down");
newVolume = Math.round(this.player.volume * 100) - 5;
if (newVolume < 5) {
this.player.muted = true;
return;
}
this.logger.debug(`Changing volume. ${this.player.volume} => ${newVolume}`);
this.player.volume = newVolume / 100;
break;

case 37: // left
this.logger.debug("Keydown left");
time = (this.player.currentTime - 5) > 0 ? this.player.currentTime - 5 : 0;
this.player.currentTime = time;
break;

case 39: // right
this.logger.debug("Keydown right");
time = (this.player.currentTime + 5) > this.player.duration ? this.player.duration : this.player.currentTime + 5;
this.player.currentTime = time;
break;

default:
return;
playerContainer.onkeydown = (e: KeyboardEvent) => {
if (!this.props.playerNav && typeof this.keyboardHandlers[e.keyCode] === 'function') {
this.keyboardHandlers[e.keyCode](e.shiftKey);
}
};

this.disableKeyboardCommandsOnControls();
}

/**
* disable keyboard commands when control button is on focus to prevent
* double function execution.
*
* @returns {void}
* Handlers for keyboard commands
* @type { Object } - Maps key number to his handler
* @memberof KeyboardControl
*/
disableKeyboardCommandsOnControls(): void {
let controlButtonsElements = Array.from(document.getElementsByClassName('control-button'));
controlButtonsElements.forEach((element) => {
element.onkeydown = (e) => e.preventDefault();
});
}
keyboardHandlers: { [key: number]: Function } = {
[KeyMap.SPACE]: () => {
this.logger.debug("Keydown SPACE");
this.player.paused ? this.player.play() : this.player.pause();
},
[KeyMap.UP]: () => {
this.logger.debug("Keydown UP");
const newVolume = Math.round(this.player.volume * 100) + VOLUME_JUMP;
this.logger.debug(`Changing volume. ${this.player.volume} => ${newVolume}`);
if (this.player.muted) {
this.player.muted = false;
}
this.player.volume = newVolume / 100;
},
[KeyMap.DOWN]: () => {
this.logger.debug("Keydown DOWN");
const newVolume = Math.round(this.player.volume * 100) - VOLUME_JUMP;
if (newVolume < 5) {
this.player.muted = true;
return;
}
this.logger.debug(`Changing volume. ${this.player.volume} => ${newVolume}`);
this.player.volume = newVolume / 100;
},
[KeyMap.F]: () => {
this.logger.debug("Keydown F");
if (!this.player.isFullscreen()) {
this.logger.debug("Enter fullscreen");
this.player.enterFullscreen();
}
},
[KeyMap.ESC]: () => {
this.logger.debug("Keydown ESC");
if (this.player.isFullscreen()) {
this.logger.debug("Exit fullscreen");
this.player.exitFullscreen();
}
},
[KeyMap.LEFT]: () => {
this.logger.debug("Keydown LEFT");
const newTime = this.player.currentTime - SEEK_JUMP;
this.logger.debug(`Seek. ${this.player.currentTime} => ${(newTime > 0) ? newTime : 0}`);
this.player.currentTime = (newTime > 0) ? newTime : 0;
},
[KeyMap.RIGHT]: () => {
this.logger.debug("Keydown RIGHT");
const newTime = this.player.currentTime + SEEK_JUMP;
this.logger.debug(`Seek. ${this.player.currentTime} => ${(newTime > this.player.duration) ? this.player.duration : newTime}`);
this.player.currentTime = (newTime > this.player.duration) ? this.player.duration : newTime;
},
[KeyMap.HOME]: () => {
this.logger.debug("Keydown HOME");
this.logger.debug(`Seek. ${this.player.currentTime} => 0`);
this.player.currentTime = 0;
},
[KeyMap.END]: () => {
this.logger.debug("Keydown END");
this.logger.debug(`Seek. ${this.player.currentTime} => ${this.player.duration}`);
this.player.currentTime = this.player.duration;
},
[KeyMap.M]: () => {
this.logger.debug("Keydown M");
this.logger.debug(this.player.muted ? "Umnute" : "Mute");
this.player.muted = !this.player.muted;
},
[KeyMap.ADD]: (shiftKey) => {
this.logger.debug("Keydown ADD, shiftKey: " + shiftKey);
if (shiftKey) {
this.logger.debug(`Changing playback rate. ${this.player.playbackRate} => ${this.player.defaultPlaybackRate}`);
this.player.playbackRate = this.player.defaultPlaybackRate;
} else {
const playbackRate = this.player.playbackRate;
const index = this.player.playbackRates.indexOf(playbackRate);
if (index < this.player.playbackRates.length - 1) {
this.logger.debug(`Changing playback rate. ${playbackRate} => ${this.player.playbackRates[index + 1]}`);
this.player.playbackRate = this.player.playbackRates[index + 1];
}
}
},
[KeyMap.SUBTRACT]: () => {
this.logger.debug("Keydown SUBTRACT");
const playbackRate = this.player.playbackRate;
const index = this.player.playbackRates.indexOf(playbackRate);
if (index > 0) {
this.logger.debug(`Changing playback rate. ${playbackRate} => ${this.player.playbackRates[index - 1]}`);
this.player.playbackRate = this.player.playbackRates[index - 1];
}
},
[KeyMap.C]: () => {
this.logger.debug("Keydown C");
let activeTextTrack = this.player.getActiveTracks().text;
if (activeTextTrack.language === "off" && this._activeTextTrack) {
this.logger.debug(`Changing text track`, this._activeTextTrack);
this.player.selectTrack(this._activeTextTrack);
this._activeTextTrack = null;
} else if (activeTextTrack.language !== "off" && !this._activeTextTrack) {
this.logger.debug(`Hiding text track`);
this._activeTextTrack = activeTextTrack;
this.player.hideTextTrack();
}
}
};
}

export default KeyboardControl;

0 comments on commit a1fa375

Please sign in to comment.