Skip to content

Commit

Permalink
Merge pull request #758 from ooyala/player-1016
Browse files Browse the repository at this point in the history
[PLAYER-1016] Basic accessibility support
  • Loading branch information
pilievOoyala authored Jun 14, 2017
2 parents fcbde88 + ddeafb4 commit 20acaf4
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 71 deletions.
13 changes: 11 additions & 2 deletions js/components/accessibilityControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ AccessibilityControls.prototype = {
var currentTime;
var newPlayheadTime;
var newVolume;
var targetTagName;

if (e.keyCode === CONSTANTS.KEYCODES.SPACE_KEY){
if (e.target && typeof e.target.tagName === "string") {
targetTagName = e.target.tagName.toLowerCase();
}

// We override the default behavior when the target element is a button (pressing
// the spacebar on a button should activate it).
// Note that this is not a comprehensive fix for all clickable elements, this is
// mostly meant to enable keyboard navigation on control bar elements.
if (e.keyCode === CONSTANTS.KEYCODES.SPACE_KEY && targetTagName !== "button") {
e.preventDefault();
this.controller.togglePlayPause();
}
Expand Down Expand Up @@ -76,4 +85,4 @@ AccessibilityControls.prototype = {
}
};

module.exports = AccessibilityControls;
module.exports = AccessibilityControls;
125 changes: 87 additions & 38 deletions js/components/controlBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ var ControlBar = React.createClass({
}
},

/**
* Some browsers give focus to buttons after click, which leaves
* them highlighted. This overrides the browser's default behavior.
*
* @param {event} evt The mouse up event object
*/
blurOnMouseUp: function(evt) {
if (evt.currentTarget) {
evt.currentTarget.blur();
}
},

handlePlayClick: function() {
this.props.controller.togglePlayPause();
},
Expand Down Expand Up @@ -160,25 +172,31 @@ var ControlBar = React.createClass({
}
},

//TODO(dustin) revisit this, doesn't feel like the "react" way to do this.
highlight: function(evt) {
var color = this.props.skinConfig.controlBar.iconStyle.active.color ? this.props.skinConfig.controlBar.iconStyle.active.color : this.props.skinConfig.general.accentColor;
var opacity = this.props.skinConfig.controlBar.iconStyle.active.opacity;
Utils.highlight(evt.target, opacity, color);
handlePlayPauseFocus: function() {
this.props.controller.state.playPauseButtonFocused = true;
},

removeHighlight: function(evt) {
var color = this.props.skinConfig.controlBar.iconStyle.inactive.color;
var opacity = this.props.skinConfig.controlBar.iconStyle.inactive.opacity;
Utils.removeHighlight(evt.target, opacity, color);
handlePlayPauseBlur: function() {
this.props.controller.state.playPauseButtonFocused = false;
},

volumeHighlight:function() {
this.highlight({target: ReactDOM.findDOMNode(this.refs.volumeIcon)});
//TODO(dustin) revisit this, doesn't feel like the "react" way to do this.
highlight: function(evt) {
var iconElement = Utils.getEventIconElement(evt);
if (iconElement) {
var color = this.props.skinConfig.controlBar.iconStyle.active.color ? this.props.skinConfig.controlBar.iconStyle.active.color : this.props.skinConfig.general.accentColor;
var opacity = this.props.skinConfig.controlBar.iconStyle.active.opacity;
Utils.highlight(iconElement, opacity, color);
}
},

volumeRemoveHighlight:function() {
this.removeHighlight({target: ReactDOM.findDOMNode(this.refs.volumeIcon)});
removeHighlight: function(evt) {
var iconElement = Utils.getEventIconElement(evt);
if (iconElement) {
var color = this.props.skinConfig.controlBar.iconStyle.inactive.color;
var opacity = this.props.skinConfig.controlBar.iconStyle.inactive.opacity;
Utils.removeHighlight(iconElement, opacity, color);
}
},

changeVolumeSlider: function(event) {
Expand All @@ -191,23 +209,34 @@ var ControlBar = React.createClass({

populateControlBar: function() {
var dynamicStyles = this.setupItemStyle();
var playIcon = "";
var playIcon, playPauseAriaLabel;
if (this.props.playerState == CONSTANTS.STATE.PLAYING) {
playIcon = "pause";
playPauseAriaLabel = CONSTANTS.ARIA_LABELS.PAUSE;
} else if (this.props.playerState == CONSTANTS.STATE.END) {
playIcon = "replay";
playPauseAriaLabel = CONSTANTS.ARIA_LABELS.REPLAY;
} else {
playIcon = "play";
playPauseAriaLabel = CONSTANTS.ARIA_LABELS.PLAY;
}

var volumeIcon = (this.props.controller.state.volumeState.muted ? "volumeOff" : "volume");
var volumeIcon, volumeAriaLabel;
if (this.props.controller.state.volumeState.muted) {
volumeIcon = "volumeOff";
volumeAriaLabel = CONSTANTS.ARIA_LABELS.UNMUTE;
} else {
volumeIcon = "volume";
volumeAriaLabel = CONSTANTS.ARIA_LABELS.MUTE;
}

var fullscreenIcon = "";
var fullscreenIcon, fullscreenAriaLabel;
if (this.props.controller.state.fullscreen) {
fullscreenIcon = "compress"
}
else {
fullscreenIcon = "compress";
fullscreenAriaLabel = CONSTANTS.ARIA_LABELS.EXIT_FULLSCREEN;
} else {
fullscreenIcon = "expand";
fullscreenAriaLabel = CONSTANTS.ARIA_LABELS.FULLSCREEN;
}

var totalTime = 0;
Expand All @@ -230,7 +259,8 @@ var ControlBar = React.createClass({

volumeBars.push(<a data-volume={(i+1)/10} className={volumeClass} key={i}
style={barStyle}
onClick={this.handleVolumeClick}></a>);
onClick={this.handleVolumeClick}
aria-hidden="true"></a>);
}

var volumeSlider = <div className="oo-volume-slider"><Slider value={parseFloat(this.props.controller.state.volumeState.volume)}
Expand Down Expand Up @@ -286,11 +316,19 @@ var ControlBar = React.createClass({
selectedStyle["color"] = this.props.skinConfig.general.accentColor ? this.props.skinConfig.general.accentColor : null;

var controlItemTemplates = {
"playPause": <a className="oo-play-pause oo-control-bar-item" onClick={this.handlePlayClick} key="playPause">
<Icon {...this.props} icon={playIcon}
style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>,
"playPause": <button className="oo-play-pause oo-control-bar-item"
onClick={this.handlePlayClick}
onMouseUp={this.blurOnMouseUp}
onMouseOver={this.highlight}
onMouseOut={this.removeHighlight}
onFocus={this.handlePlayPauseFocus}
onBlur={this.handlePlayPauseBlur}
key="playPause"
tabIndex="0"
aria-label={playPauseAriaLabel}
autoFocus={this.props.controller.state.playPauseButtonFocused}>
<Icon {...this.props} icon={playIcon} style={dynamicStyles.iconCharacter} />
</button>,

"live": <a className={liveClass}
ref="LiveButton"
Expand All @@ -300,10 +338,16 @@ var ControlBar = React.createClass({
</a>,

"volume": <div className="oo-volume oo-control-bar-item" key="volume">
<Icon {...this.props} icon={volumeIcon} ref="volumeIcon"
style={this.props.skinConfig.controlBar.iconStyle.inactive}
<button className="oo-mute-unmute oo-control-bar-item"
onClick={this.handleVolumeIconClick}
onMouseOver={this.volumeHighlight} onMouseOut={this.volumeRemoveHighlight}/>
onMouseUp={this.blurOnMouseUp}
onMouseOver={this.highlight}
onMouseOut={this.removeHighlight}
tabIndex="0"
aria-label={volumeAriaLabel}>
<Icon {...this.props} icon={volumeIcon} ref="volumeIcon"
style={this.props.skinConfig.controlBar.iconStyle.inactive} />
</button>
{volumeControls}
</div>,

Expand All @@ -314,48 +358,53 @@ var ControlBar = React.createClass({
"flexibleSpace": <div className="oo-flexible-space oo-control-bar-flex-space" key="flexibleSpace"></div>,

"moreOptions": <a className="oo-more-options oo-control-bar-item"
onClick={this.handleMoreOptionsClick} key="moreOptions">
onClick={this.handleMoreOptionsClick} key="moreOptions" aria-hidden="true">
<Icon {...this.props} icon="ellipsis" style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>,

"quality": (
<div className="oo-popover-button-container" key="quality">
{videoQualityPopover}
<a className={qualityClass} onClick={this.handleQualityClick} style={selectedStyle}>
<a className={qualityClass} onClick={this.handleQualityClick} style={selectedStyle} aria-hidden="true">
<Icon {...this.props} icon="quality" style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>
</div>
),

"discovery": <a className="oo-discovery oo-control-bar-item"
onClick={this.handleDiscoveryClick} key="discovery">
onClick={this.handleDiscoveryClick} key="discovery" aria-hidden="true">
<Icon {...this.props} icon="discovery" style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>,

"closedCaption": (
<div className="oo-popover-button-container" key="closedCaption">
{closedCaptionPopover}
<a className={captionClass} onClick={this.handleClosedCaptionClick} style={selectedStyle}>
<a className={captionClass} onClick={this.handleClosedCaptionClick} style={selectedStyle} aria-hidden="true">
<Icon {...this.props} icon="cc" style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>
</div>
),

"share": <a className="oo-share oo-control-bar-item"
onClick={this.handleShareClick} key="share">
onClick={this.handleShareClick} key="share" aria-hidden="true">
<Icon {...this.props} icon="share" style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>,

"fullscreen": <a className="oo-fullscreen oo-control-bar-item"
onClick={this.handleFullscreenClick} key="fullscreen">
<Icon {...this.props} icon={fullscreenIcon} style={dynamicStyles.iconCharacter}
onMouseOver={this.highlight} onMouseOut={this.removeHighlight}/>
</a>,
"fullscreen": <button className="oo-fullscreen oo-control-bar-item"
onClick={this.handleFullscreenClick}
onMouseUp={this.blurOnMouseUp}
onMouseOver={this.highlight}
onMouseOut={this.removeHighlight}
key="fullscreen"
tabIndex="0"
aria-label={fullscreenAriaLabel}>
<Icon {...this.props} icon={fullscreenIcon} style={dynamicStyles.iconCharacter} />
</button>,

"logo": <Logo key="logo" imageUrl={this.props.skinConfig.controlBar.logo.imageResource.url}
clickUrl={this.props.skinConfig.controlBar.logo.clickUrl}
Expand Down
22 changes: 14 additions & 8 deletions js/components/moreOptionsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ var MoreOptionsPanel = React.createClass({
},

highlight: function (evt) {
var color = this.props.skinConfig.moreOptionsScreen.iconStyle.active.color;
var opacity = this.props.skinConfig.moreOptionsScreen.iconStyle.active.opacity;
Utils.highlight(evt.target, opacity, color);
var iconElement = Utils.getEventIconElement(evt);
if (iconElement) {
var color = this.props.skinConfig.moreOptionsScreen.iconStyle.active.color;
var opacity = this.props.skinConfig.moreOptionsScreen.iconStyle.active.opacity;
Utils.highlight(iconElement, opacity, color);
}
},

removeHighlight: function (evt) {
var color = this.props.skinConfig.moreOptionsScreen.iconStyle.inactive.color;
var opacity = this.props.skinConfig.moreOptionsScreen.iconStyle.inactive.opacity;
Utils.removeHighlight(evt.target, opacity, color);
var iconElement = Utils.getEventIconElement(evt);
if (iconElement) {
var color = this.props.skinConfig.moreOptionsScreen.iconStyle.inactive.color;
var opacity = this.props.skinConfig.moreOptionsScreen.iconStyle.inactive.opacity;
Utils.removeHighlight(iconElement, opacity, color);
}
},

buildMoreOptionsButtonList: function () {
Expand Down Expand Up @@ -80,7 +86,7 @@ var MoreOptionsPanel = React.createClass({

var items = this.props.controller.state.moreOptionsItems;
var moreOptionsItems = [];

for (var i = 0; i < items.length; i++) {
moreOptionsItems.push(optionsItemsTemplates[items[i].name]);
}
Expand Down Expand Up @@ -124,4 +130,4 @@ MoreOptionsPanel.defaultProps = {
}
};

module.exports = MoreOptionsPanel;
module.exports = MoreOptionsPanel;
58 changes: 52 additions & 6 deletions js/components/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,53 @@ var Utils = {
return isFinite(Number(property)) ? Number(property) : 0;
},


/**
* Determines whether an element contains a class or not.
* TODO:
* classList.contains is much better for this purpose, but our current version
* of React Test Utils generates events with a null classList, which results in
* broken unit tests.
*
* @param {DOMElement} element The DOM element which we want to check
* @param {String} className The name of the class we want to match
* @return {Boolean} True if the element contains the given class, false otherwise
*/
elementHasClass: function(element, className) {
if (!element) {
return false;
}
return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
},

/**
* Returns the icon element associated with an event (usually mouseover or mouseout),
* which can be either the event's target element itself or a child of the target element.
* The icon is matched with a class name.
* This is needed in order to circumvent a Firefox issue that prevents mouse events from
* being triggered in elements that are children of buttons (such as icons).
*
* @param {String} domEvent The event whose icon element we want to extract
* @param {String} iconClass The class that will be used to match the icon element
* @return {Object} The element that has been identified as the icon, or null if none was found
*/
getEventIconElement: function(domEvent, iconClass) {
var iconElement = null;
var classToMatch = iconClass || 'oo-icon';
var currentTarget = domEvent ? domEvent.currentTarget : null;

if (currentTarget) {
// Check to see if the target itself is the icon, otherwise get
// the first icon child
if (this.elementHasClass(currentTarget, classToMatch)) {
iconElement = currentTarget;
} else {
iconElement = currentTarget.querySelector('.' + classToMatch);
}
}
return iconElement;
},

/**
* Highlight the given element for hover effects
*
Expand All @@ -314,9 +361,10 @@ var Utils = {
highlight: function(target, opacity, color) {
target.style.opacity = opacity;
target.style.color = color;
target.style.WebkitFilter = "drop-shadow(0px 0px 3px rgba(255,255,255,0.8))";
target.style.filter = "drop-shadow(0px 0px 3px rgba(255,255,255,0.8))";
target.style.msFilter = "progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=0, Color='#fff')";
// HEADSUP
// This is currently the same style as the one used in _mixins.scss.
// We should change both styles whenever we update this.
target.style.textShadow = "0px 0px 3px rgba(255, 255, 255, 0.5), 0px 0px 6px rgba(255, 255, 255, 0.5), 0px 0px 9px rgba(255, 255, 255, 0.5)";
},

/**
Expand All @@ -329,9 +377,7 @@ var Utils = {
removeHighlight: function(target, opacity, color) {
target.style.opacity = opacity;
target.style.color = color;
target.style.WebkitFilter = "";
target.style.filter = "";
target.style.msFilter = "";
target.style.textShadow = "";
},

/**
Expand Down
12 changes: 12 additions & 0 deletions js/constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ module.exports = {
SHADOW: "Shadow"
},

ARIA_LABELS: {
VIDEO_PLAYER: "Video Player",
START_PLAYBACK: "Start Playback",
PLAY: "Play",
PAUSE: "Pause",
REPLAY: "Replay",
MUTE: "Mute",
UNMUTE: "Unmute",
FULLSCREEN: "Fullscreen",
EXIT_FULLSCREEN: "Exit Fullscreen",
},

KEYCODES: {
SPACE_KEY: 32,
LEFT_ARROW_KEY: 37,
Expand Down
Loading

0 comments on commit 20acaf4

Please sign in to comment.