1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-10 11:10:19 +02:00

fix: make sure hotkeys are not triggered outside the player or in form fields within the player (#5969)

This commit is contained in:
Pat O'Neill 2019-05-30 11:16:51 -04:00 committed by Gary Katsevman
parent 5a7fe48b07
commit 79eadac252
17 changed files with 673 additions and 274 deletions

View File

@ -47,8 +47,7 @@ class BigPlayButton extends Button {
// exit early if clicked via the mouse
if (this.mouseused_ && event.clientX && event.clientY) {
silencePromise(playPromise);
// call handleFocus manually to get hotkeys working
this.player_.handleFocus({});
this.player_.tech(true).focus();
return;
}
@ -56,7 +55,7 @@ class BigPlayButton extends Button {
const playToggle = cb && cb.getChild('playToggle');
if (!playToggle) {
this.player_.focus();
this.player_.tech(true).focus();
return;
}
@ -69,10 +68,10 @@ class BigPlayButton extends Button {
}
}
handleKeyPress(event) {
handleKeyDown(event) {
this.mouseused_ = false;
super.handleKeyPress(event);
super.handleKeyDown(event);
}
handleMouseDown(event) {

View File

@ -104,13 +104,20 @@ class Button extends ClickableComponent {
*
* @listens keydown
*/
handleKeyPress(event) {
// Ignore Space or Enter key operation, which is handled by the browser for a button.
if (!(keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter'))) {
handleKeyDown(event) {
// Pass keypress handling up for unsupported keys
super.handleKeyPress(event);
// Ignore Space or Enter key operation, which is handled by the browser for
// a button - though not for its super class, ClickableComponent. Also,
// prevent the event from propagating through the DOM and triggering Player
// hotkeys. We do not preventDefault here because we _want_ the browser to
// handle it.
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
event.stopPropagation();
return;
}
// Pass keypress handling up for unsupported keys
super.handleKeyDown(event);
}
}

View File

@ -3,16 +3,13 @@
*/
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 './utils/obj';
import keycode from 'keycode';
/**
* Clickable Component which is clickable or keyboard actionable,
* but is not a native HTML button.
* Component which is clickable or keyboard actionable, but is not a
* native HTML button.
*
* @extends Component
*/
@ -36,7 +33,7 @@ class ClickableComponent extends Component {
}
/**
* Create the `Component`s DOM element.
* Create the `ClickableComponent`s DOM element.
*
* @param {string} [tag=div]
* The element's node type.
@ -83,7 +80,7 @@ class ClickableComponent extends Component {
}
/**
* Create a control text element on this `Component`
* Create a control text element on this `ClickableComponent`
*
* @param {Element} [el]
* Parent element for the control text.
@ -109,7 +106,7 @@ class ClickableComponent extends Component {
}
/**
* Get or set the localize text to use for the controls on the `Component`.
* Get or set the localize text to use for the controls on the `ClickableComponent`.
*
* @param {string} [text]
* Control text for element.
@ -146,7 +143,7 @@ class ClickableComponent extends Component {
}
/**
* Enable this `Component`s element.
* Enable this `ClickableComponent`
*/
enable() {
if (!this.enabled_) {
@ -157,13 +154,12 @@ class ClickableComponent extends Component {
this.el_.setAttribute('tabIndex', this.tabIndex_);
}
this.on(['tap', 'click'], this.handleClick);
this.on('focus', this.handleFocus);
this.on('blur', this.handleBlur);
this.on('keydown', this.handleKeyDown);
}
}
/**
* Disable this `Component`s element.
* Disable this `ClickableComponent`
*/
disable() {
this.enabled_ = false;
@ -173,27 +169,15 @@ class ClickableComponent extends Component {
this.el_.removeAttribute('tabIndex');
}
this.off(['tap', 'click'], this.handleClick);
this.off('focus', this.handleFocus);
this.off('blur', this.handleBlur);
this.off('keydown', this.handleKeyDown);
}
/**
* This gets called when a `ClickableComponent` gets:
* - Clicked (via the `click` event, listening starts in the constructor)
* - Tapped (via the `tap` event, listening starts in the constructor)
* - The following things happen in order:
* 1. {@link ClickableComponent#handleFocus} is called via a `focus` event on the
* `ClickableComponent`.
* 2. {@link ClickableComponent#handleFocus} adds a listener for `keydown` on using
* {@link ClickableComponent#handleKeyPress}.
* 3. `ClickableComponent` has not had a `blur` event (`blur` means that focus was lost). The user presses
* the space or enter key.
* 4. {@link ClickableComponent#handleKeyPress} calls this function with the `keydown`
* event as a parameter.
* Event handler that is called when a `ClickableComponent` receives a
* `click` or `tap` event.
*
* @param {EventTarget~Event} event
* The `keydown`, `tap`, or `click` event that caused this function to be
* called.
* The `tap` or `click` event that caused this function to be called.
*
* @listens tap
* @listens click
@ -202,52 +186,31 @@ class ClickableComponent extends Component {
handleClick(event) {}
/**
* This gets called when a `ClickableComponent` gains focus via a `focus` event.
* Turns on listening for `keydown` events. When they happen it
* calls `this.handleKeyPress`.
* Event handler that is called when a `ClickableComponent` receives a
* `keydown` event.
*
* @param {EventTarget~Event} event
* The `focus` event that caused this function to be called.
*
* @listens focus
*/
handleFocus(event) {
Events.on(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
/**
* Called when this ClickableComponent has focus and a key gets pressed down. By
* default it will call `this.handleClick` when the key is space or enter.
* By default, if the key is Space or Enter, it will trigger a `click` event.
*
* @param {EventTarget~Event} event
* The `keydown` event that caused this function to be called.
*
* @listens keydown
*/
handleKeyPress(event) {
// Support Space or Enter key operation to fire a click event
handleKeyDown(event) {
// Support Space or Enter key operation to fire a click event. Also,
// prevent the event from propagating through the DOM and triggering
// Player hotkeys.
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
event.preventDefault();
event.stopPropagation();
this.trigger('click');
} else {
// Pass keypress handling up for unsupported keys
super.handleKeyPress(event);
super.handleKeyDown(event);
}
}
/**
* Called when a `ClickableComponent` loses focus. Turns off the listener for
* `keydown` events. Which Stops `this.handleKeyPress` from getting called.
*
* @param {EventTarget~Event} event
* The `blur` event that caused this function to be called.
*
* @listens blur
*/
handleBlur(event) {
Events.off(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
}
Component.registerComponent('ClickableComponent', ClickableComponent);

View File

@ -36,25 +36,10 @@ class CloseButton extends Button {
return `vjs-close-button ${super.buildCSSClass()}`;
}
/**
* This gets called when a `CloseButton` has focus and `keydown` is triggered via a key
* press.
*
* @param {EventTarget~Event} event
* The event that caused this function to get called.
*
* @listens keydown
*/
handleKeyPress(event) {
// Override the default `Button` behavior, and don't pass the keypress event
// up to the player because this button is part of a `ModalDialog`, which
// doesn't pass keypresses to the player either.
}
/**
* This gets called when a `CloseButton` gets clicked. See
* {@link ClickableComponent#handleClick} for more information on when this will be
* triggered
* {@link ClickableComponent#handleClick} for more information on when
* this will be triggered
*
* @param {EventTarget~Event} event
* The `keydown`, `tap`, or `click` event that caused this function to be

View File

@ -1078,18 +1078,35 @@ class Component {
}
/**
* When this Component receives a keydown event which it does not process,
* When this Component receives a `keydown` event which it does not process,
* it passes the event to the Player for handling.
*
* @param {EventTarget~Event} event
* The `keydown` event that caused this function to be called.
*/
handleKeyPress(event) {
handleKeyDown(event) {
if (this.player_) {
this.player_.handleKeyPress(event);
// We only stop propagation here because we want unhandled events to fall
// back to the browser.
event.stopPropagation();
this.player_.handleKeyDown(event);
}
}
/**
* Many components used to have a `handleKeyPress` method, which was poorly
* named because it listened to a `keydown` event. This method name now
* delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
* will not see their method calls stop working.
*
* @param {EventTarget~Event} event
* The event that caused this function to be called.
*/
handleKeyPress(event) {
this.handleKeyDown(event);
}
/**
* Emit a 'tap' events when touch event support gets detected. This gets used to
* support toggling the controls through a tap on the video. They get enabled

View File

@ -407,30 +407,36 @@ class SeekBar extends Slider {
*
* @listens keydown
*/
handleKeyPress(event) {
handleKeyDown(event) {
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
event.preventDefault();
event.stopPropagation();
this.handleAction(event);
} else if (keycode.isEventKey(event, 'Home')) {
event.preventDefault();
event.stopPropagation();
this.player_.currentTime(0);
} else if (keycode.isEventKey(event, 'End')) {
event.preventDefault();
event.stopPropagation();
this.player_.currentTime(this.player_.duration());
} else if (/^[0-9]$/.test(keycode(event))) {
event.preventDefault();
event.stopPropagation();
const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;
this.player_.currentTime(this.player_.duration() * gotoFraction);
} else if (keycode.isEventKey(event, 'PgDn')) {
event.preventDefault();
event.stopPropagation();
this.player_.currentTime(this.player_.currentTime() - (STEP_SECONDS * PAGE_KEY_MULTIPLIER));
} else if (keycode.isEventKey(event, 'PgUp')) {
event.preventDefault();
event.stopPropagation();
this.player_.currentTime(this.player_.currentTime() + (STEP_SECONDS * PAGE_KEY_MULTIPLIER));
} else {
// Pass keypress handling up for unsupported keys
super.handleKeyPress(event);
// Pass keydown handling up for unsupported keys
super.handleKeyDown(event);
}
}
}

View File

@ -5,11 +5,8 @@ import Button from '../button.js';
import Component from '../component.js';
import Menu from './menu.js';
import * as Dom from '../utils/dom.js';
import * as Fn from '../utils/fn.js';
import * as Events from '../utils/events.js';
import toTitleCase from '../utils/to-title-case.js';
import { IS_IOS } from '../utils/browser.js';
import document from 'global/document';
import keycode from 'keycode';
/**
@ -50,12 +47,12 @@ class MenuButton extends Component {
this.on(this.menuButton_, 'tap', this.handleClick);
this.on(this.menuButton_, 'click', this.handleClick);
this.on(this.menuButton_, 'focus', this.handleFocus);
this.on(this.menuButton_, 'blur', this.handleBlur);
this.on(this.menuButton_, 'keydown', this.handleKeyDown);
this.on(this.menuButton_, 'mouseenter', () => {
this.menu.show();
});
this.on('keydown', this.handleSubmenuKeyPress);
this.on('keydown', this.handleSubmenuKeyDown);
}
/**
@ -246,48 +243,23 @@ class MenuButton extends Component {
this.menuButton_.blur();
}
/**
* This gets called when a `MenuButton` gains focus via a `focus` event.
* Turns on listening for `keydown` events. When they happen it
* calls `this.handleKeyPress`.
*
* @param {EventTarget~Event} event
* The `focus` event that caused this function to be called.
*
* @listens focus
*/
handleFocus() {
Events.on(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
/**
* Called when a `MenuButton` loses focus. Turns off the listener for
* `keydown` events. Which Stops `this.handleKeyPress` from getting called.
*
* @param {EventTarget~Event} event
* The `blur` event that caused this function to be called.
*
* @listens blur
*/
handleBlur() {
Events.off(document, 'keydown', Fn.bind(this, this.handleKeyPress));
}
/**
* Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
* {@link ClickableComponent#handleKeyPress} for instances where this is called.
* {@link ClickableComponent#handleKeyDown} for instances where this is called.
*
* @param {EventTarget~Event} event
* The `keydown` event that caused this function to be called.
*
* @listens keydown
*/
handleKeyPress(event) {
handleKeyDown(event) {
// Escape or Tab unpress the 'button'
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
if (this.buttonPressed_) {
this.unpressButton();
}
// Don't preventDefault for Tab key - we still want to lose focus
if (!keycode.isEventKey(event, 'Tab')) {
event.preventDefault();
@ -300,14 +272,21 @@ class MenuButton extends Component {
event.preventDefault();
this.pressButton();
}
} else {
// NOTE: This is a special case where we don't pass unhandled
// keypress events up to the Component handler, because it is
// just entending the keypress handling of the actual `Button`
// in the `MenuButton` which already passes unused keys up.
}
}
/**
* This method name now delegates to `handleSubmenuKeyDown`. This means
* anyone calling `handleSubmenuKeyPress` will not see their method calls
* stop working.
*
* @param {EventTarget~Event} event
* The event that caused this function to be called.
*/
handleSubmenuKeyPress(event) {
this.handleSubmenuKeyDown(event);
}
/**
* Handle a `keydown` event on a sub-menu. The listener for this is added in
* the constructor.
@ -317,7 +296,7 @@ class MenuButton extends Component {
*
* @listens keydown
*/
handleSubmenuKeyPress(event) {
handleSubmenuKeyDown(event) {
// Escape or Tab unpress the 'button'
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
if (this.buttonPressed_) {
@ -331,8 +310,8 @@ class MenuButton extends Component {
}
} else {
// NOTE: This is a special case where we don't pass unhandled
// keypress events up to the Component handler, because it is
// just entending the keypress handling of the `MenuItem`
// keydown events up to the Component handler, because it is
// just entending the keydown handling of the `MenuItem`
// in the `Menu` which already passes unused keys up.
}
}

View File

@ -72,17 +72,17 @@ class MenuItem extends ClickableComponent {
/**
* Ignore keys which are used by the menu, but pass any other ones up. See
* {@link ClickableComponent#handleKeyPress} for instances where this is called.
* {@link ClickableComponent#handleKeyDown} for instances where this is called.
*
* @param {EventTarget~Event} event
* The `keydown` event that caused this function to be called.
*
* @listens keydown
*/
handleKeyPress(event) {
handleKeyDown(event) {
if (!MenuKeys.some((key) => keycode.isEventKey(event, key))) {
// Pass keypress handling up for unused keys
super.handleKeyPress(event);
// Pass keydown handling up for unused keys
super.handleKeyDown(event);
}
}

View File

@ -35,7 +35,7 @@ class Menu extends Component {
this.focusedChild_ = -1;
this.on('keydown', this.handleKeyPress);
this.on('keydown', this.handleKeyDown);
// All the menu item instances share the same blur handler provided by the menu container.
this.boundHandleBlur_ = Fn.bind(this, this.handleBlur);
@ -211,22 +211,19 @@ class Menu extends Component {
*
* @listens keydown
*/
handleKeyPress(event) {
handleKeyDown(event) {
// Left and Down Arrows
if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
event.preventDefault();
event.stopPropagation();
this.stepForward();
// Up and Right Arrows
} else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
event.preventDefault();
event.stopPropagation();
this.stepBack();
} else {
// NOTE: This is a special case where we don't pass unhandled
// keypress events up to the Component handler, because this
// is just adding a keypress handler on top of the MenuItem's
// existing keypress handler, which already handles passing keypress
// events up.
}
}

View File

@ -2,7 +2,6 @@
* @file modal-dialog.js
*/
import * as Dom from './utils/dom';
import * as Fn from './utils/fn';
import Component from './component';
import window from 'global/window';
import document from 'global/document';
@ -119,21 +118,6 @@ class ModalDialog extends Component {
return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
}
/**
* Handles `keydown` events on the document, looking for ESC, which closes
* the modal.
*
* @param {EventTarget~Event} event
* The keypress that triggered this event.
*
* @listens keydown
*/
handleKeyPress(event) {
if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
this.close();
}
}
/**
* Returns the label string for this modal. Primarily used for accessibility.
*
@ -195,9 +179,7 @@ class ModalDialog extends Component {
player.pause();
}
if (this.closeable()) {
this.on(this.el_.ownerDocument, 'keydown', Fn.bind(this, this.handleKeyPress));
}
this.on('keydown', this.handleKeyDown);
// Hide controls and note if they were enabled.
this.hadControls_ = player.controls();
@ -260,9 +242,7 @@ class ModalDialog extends Component {
player.play();
}
if (this.closeable()) {
this.off(this.el_.ownerDocument, 'keydown', Fn.bind(this, this.handleKeyPress));
}
this.off('keydown', this.handleKeyDown);
if (this.hadControls_) {
player.controls(true);
@ -444,8 +424,6 @@ class ModalDialog extends Component {
this.previouslyActiveEl_ = activeEl;
this.focus();
this.on(document, 'keydown', this.handleKeyDown);
}
}
@ -459,8 +437,6 @@ class ModalDialog extends Component {
this.previouslyActiveEl_.focus();
this.previouslyActiveEl_ = null;
}
this.off(document, 'keydown', this.handleKeyDown);
}
/**
@ -469,6 +445,16 @@ class ModalDialog extends Component {
* @listens keydown
*/
handleKeyDown(event) {
// Do not allow keydowns to reach out of the modal dialog.
event.stopPropagation();
if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
event.preventDefault();
this.close();
return;
}
// exit early if it isn't a tab key
if (!keycode.isEventKey(event, 'Tab')) {
return;

View File

@ -354,7 +354,6 @@ class Player extends Component {
// Create bound methods for document listeners.
this.boundDocumentFullscreenChange_ = Fn.bind(this, this.documentFullscreenChange_);
this.boundFullWindowOnEscKey_ = Fn.bind(this, this.fullWindowOnEscKey);
this.boundHandleKeyPress_ = Fn.bind(this, this.handleKeyPress);
// create logger
this.log = createLogger(this.id_);
@ -532,9 +531,8 @@ class Player extends Component {
this.reportUserActivity();
this.one('play', this.listenForUserActivity_);
this.on('focus', this.handleFocus);
this.on('blur', this.handleBlur);
this.on('stageclick', this.handleStageClick_);
this.on('keydown', this.handleKeyDown);
this.breakpoints(this.options_.breakpoints);
this.responsive(this.options_.responsive);
@ -563,7 +561,6 @@ class Player extends Component {
// Make sure all player-specific document listeners are unbound. This is
Events.off(document, FullscreenApi.fullscreenchange, this.boundDocumentFullscreenChange_);
Events.off(document, 'keydown', this.boundFullWindowOnEscKey_);
Events.off(document, 'keydown', this.boundHandleKeyPress_);
if (this.styleEl_ && this.styleEl_.parentNode) {
this.styleEl_.parentNode.removeChild(this.styleEl_);
@ -2804,35 +2801,6 @@ class Player extends Component {
this.trigger('exitFullWindow');
}
/**
* This gets called when a `Player` gains focus via a `focus` event.
* Turns on listening for `keydown` events. When they happen it
* calls `this.handleKeyPress`.
*
* @param {EventTarget~Event} event
* The `focus` event that caused this function to be called.
*
* @listens focus
*/
handleFocus(event) {
// call off first to make sure we don't keep adding keydown handlers
Events.off(document, 'keydown', this.boundHandleKeyPress_);
Events.on(document, 'keydown', this.boundHandleKeyPress_);
}
/**
* Called when a `Player` loses focus. Turns off the listener for
* `keydown` events. Which Stops `this.handleKeyPress` from getting called.
*
* @param {EventTarget~Event} event
* The `blur` event that caused this function to be called.
*
* @listens blur
*/
handleBlur(event) {
Events.off(document, 'keydown', this.boundHandleKeyPress_);
}
/**
* Called when this Player has focus and a key gets pressed down, or when
* any Component of this player receives a key press that it doesn't handle.
@ -2844,19 +2812,49 @@ class Player extends Component {
*
* @listens keydown
*/
handleKeyPress(event) {
handleKeyDown(event) {
const {userActions} = this.options_;
if (this.options_.userActions && this.options_.userActions.hotkeys && (this.options_.userActions.hotkeys !== false)) {
// Bail out if hotkeys are not configured.
if (!userActions || !userActions.hotkeys) {
return;
}
if (typeof this.options_.userActions.hotkeys === 'function') {
// Function that determines whether or not to exclude an element from
// hotkeys handling.
const excludeElement = (el) => {
const tagName = el.tagName.toLowerCase();
this.options_.userActions.hotkeys.call(this, event);
// These tags will be excluded entirely.
const excludedTags = ['textarea'];
} else {
this.handleHotkeys(event);
// Inputs matching these types will still trigger hotkey handling as
// they are not text inputs.
const allowedInputTypes = [
'button',
'checkbox',
'hidden',
'radio',
'reset',
'submit'
];
if (tagName === 'input') {
return allowedInputTypes.indexOf(el.type) === -1;
}
return excludedTags.indexOf(tagName) !== -1;
};
// Bail out if the user is focused on an interactive form element.
if (excludeElement(this.el_.ownerDocument.activeElement)) {
return;
}
if (typeof userActions.hotkeys === 'function') {
userActions.hotkeys.call(this, event);
} else {
this.handleHotkeys(event);
}
}
@ -2882,8 +2880,8 @@ class Player extends Component {
} = hotkeys;
if (fullscreenKey.call(this, event)) {
event.preventDefault();
event.stopPropagation();
const FSToggle = Component.getComponent('FullscreenToggle');
@ -2892,16 +2890,16 @@ class Player extends Component {
}
} else if (muteKey.call(this, event)) {
event.preventDefault();
event.stopPropagation();
const MuteToggle = Component.getComponent('MuteToggle');
MuteToggle.prototype.handleClick.call(this);
} else if (playPauseKey.call(this, event)) {
event.preventDefault();
event.stopPropagation();
const PlayToggle = Component.getComponent('PlayToggle');

View File

@ -112,14 +112,13 @@ class PosterImage extends ClickableComponent {
return;
}
this.player_.tech(true).focus();
if (this.player_.paused()) {
silencePromise(this.player_.play());
} else {
this.player_.pause();
}
// call handleFocus manually to get hotkeys working
this.player_.handleFocus({});
}
}

View File

@ -56,8 +56,7 @@ class Slider extends Component {
this.on('mousedown', this.handleMouseDown);
this.on('touchstart', this.handleMouseDown);
this.on('focus', this.handleFocus);
this.on('blur', this.handleBlur);
this.on('keydown', this.handleKeyDown);
this.on('click', this.handleClick);
this.on(this.player_, 'controlsvisible', this.update);
@ -83,8 +82,7 @@ class Slider extends Component {
this.off('mousedown', this.handleMouseDown);
this.off('touchstart', this.handleMouseDown);
this.off('focus', this.handleFocus);
this.off('blur', this.handleBlur);
this.off('keydown', this.handleKeyDown);
this.off('click', this.handleClick);
this.off(this.player_, 'controlsvisible', this.update);
this.off(doc, 'mousemove', this.handleMouseMove);
@ -293,18 +291,6 @@ class Slider extends Component {
return position.x;
}
/**
* Handle a `focus` event on this `Slider`.
*
* @param {EventTarget~Event} event
* The `focus` event that caused this function to run.
*
* @listens focus
*/
handleFocus() {
this.on(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress);
}
/**
* Handle a `keydown` event on the `Slider`. Watches for left, rigth, up, and down
* arrow keys. This function will only be called when the slider has focus. See
@ -315,36 +301,26 @@ class Slider extends Component {
*
* @listens keydown
*/
handleKeyPress(event) {
handleKeyDown(event) {
// Left and Down Arrows
if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
event.preventDefault();
event.stopPropagation();
this.stepBack();
// Up and Right Arrows
} else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
event.preventDefault();
event.stopPropagation();
this.stepForward();
} else {
// Pass keypress handling up for unsupported keys
super.handleKeyPress(event);
// Pass keydown handling up for unsupported keys
super.handleKeyDown(event);
}
}
/**
* Handle a `blur` event on this `Slider`.
*
* @param {EventTarget~Event} event
* The `blur` event that caused this function to run.
*
* @listens blur
*/
handleBlur() {
this.off(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress);
}
/**
* Listener for click events on slider, used to prevent clicks
* from bubbling up to parent elements like button menus.
@ -353,7 +329,7 @@ class Slider extends Component {
* Event that caused this object to run
*/
handleClick(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
}

View File

@ -2,7 +2,6 @@
* @file text-track-settings.js
*/
import window from 'global/window';
import document from 'global/document';
import Component from '../component';
import ModalDialog from '../modal-dialog';
import {createEl} from '../utils/dom';
@ -590,7 +589,6 @@ class TextTrackSettings extends ModalDialog {
*/
conditionalBlur_() {
this.previouslyActiveEl_ = null;
this.off(document, 'keydown', this.handleKeyDown);
const cb = this.player_.controlBar;
const subsCapsBtn = cb && cb.subsCapsButton;

View File

@ -146,35 +146,25 @@ QUnit.test('should remove old event listeners when the menu item adds to the new
// `Menu`.`children` will be called when triggering blur event on the menu item.
const menuChildrenSpy = sinon.spy(watchedMenu, 'children');
// The number of blur listeners is two because `ClickableComponent`
// adds the blur event listener during the construction and
assert.strictEqual(eventData.handlers.blur.length, 1, 'the number of blur listeners is one');
// The number of click listeners is two because `ClickableComponent`
// adds the click event listener during the construction and
// `MenuItem` inherits from `ClickableComponent`.
assert.strictEqual(eventData.handlers.blur.length, 2, 'the number of blur listeners is two');
// Same reason mentioned above.
assert.strictEqual(eventData.handlers.click.length, 2, 'the number of click listeners is two');
const blurListenerAddedByMenu = eventData.handlers.blur[1];
const clickListenerAddedByMenu = eventData.handlers.click[1];
assert.strictEqual(
typeof blurListenerAddedByMenu.calledOnce,
'undefined',
'previous blur listener wrapped in the spy should be removed'
);
assert.strictEqual(
typeof clickListenerAddedByMenu.calledOnce,
'undefined',
'previous click listener wrapped in the spy should be removed'
);
const blurListenerSpy = eventData.handlers.blur[1] = sinon.spy(blurListenerAddedByMenu);
const clickListenerSpy = eventData.handlers.click[1] = sinon.spy(clickListenerAddedByMenu);
TestHelpers.triggerDomEvent(menuItem.el(), 'blur');
assert.ok(blurListenerSpy.calledOnce, 'blur event listener should be called');
assert.strictEqual(blurListenerSpy.getCall(0).args[0].target, menuItem.el(), 'event target should be the `menuItem`');
assert.ok(menuChildrenSpy.calledOnce, '`watchedMenu`.`children` has been called');
TestHelpers.triggerDomEvent(menuItem.el(), 'click');

View File

@ -5,7 +5,11 @@ import ModalDialog from '../../src/js/modal-dialog';
import * as Dom from '../../src/js/utils/dom';
import TestHelpers from './test-helpers';
const ESC = 27;
const getMockEscapeEvent = () => ({
which: 27,
preventDefault() {},
stopPropagation() {}
});
QUnit.module('ModalDialog', {
@ -55,6 +59,8 @@ const tabTestHelper = function(assert, player) {
shiftKey: shift,
preventDefault() {
prevented = true;
},
stopPropagation() {
}
});
@ -208,12 +214,12 @@ QUnit.test('pressing ESC triggers close(), but only when the modal is opened', f
const spy = sinon.spy();
this.modal.on('modalclose', spy);
this.modal.handleKeyPress({which: ESC});
this.modal.handleKeyDown(getMockEscapeEvent());
assert.expect(2);
assert.strictEqual(spy.callCount, 0, 'ESC did not close the closed modal');
this.modal.open();
this.modal.handleKeyPress({which: ESC});
this.modal.handleKeyDown(getMockEscapeEvent());
assert.strictEqual(spy.callCount, 1, 'ESC closed the now-opened modal');
});
@ -392,7 +398,7 @@ QUnit.test('closeable()', function(assert) {
assert.notOk(this.modal.getChild('closeButton'), 'the close button is no longer a child of the modal');
assert.notOk(initialCloseButton.el(), 'the initial close button was disposed');
this.modal.handleKeyPress({which: ESC});
this.modal.handleKeyDown(getMockEscapeEvent());
assert.ok(this.modal.opened(), 'the modal was not closed by the ESC key');
this.modal.close();
@ -406,7 +412,7 @@ QUnit.test('closeable()', function(assert) {
assert.notOk(this.modal.opened(), 'the modal was closed by the new close button');
this.modal.open();
this.modal.handleKeyPress({which: ESC});
this.modal.handleKeyDown(getMockEscapeEvent());
assert.notOk(this.modal.opened(), 'the modal was closed by the ESC key');
});
@ -494,7 +500,7 @@ QUnit.test('"uncloseable" option', function(assert) {
assert.notOk(modal.getChild('closeButton'), 'the close button is not present');
modal.open();
modal.handleKeyPress({which: ESC});
modal.handleKeyDown(getMockEscapeEvent());
assert.strictEqual(spy.callCount, 0, 'ESC did not close the modal');
modal.dispose();
});

View File

@ -0,0 +1,493 @@
/* eslint-env qunit */
import document from 'global/document';
import keycode from 'keycode';
import sinon from 'sinon';
import TestHelpers from './test-helpers';
import FullscreenApi from '../../src/js/fullscreen-api.js';
QUnit.module('Player: User Actions: Double Click', {
beforeEach() {
this.clock = sinon.useFakeTimers();
this.player = TestHelpers.makePlayer({controls: true});
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
QUnit.test('by default, double-click opens fullscreen', function(assert) {
let fullscreen = false;
this.player.isFullscreen = () => fullscreen;
this.player.requestFullscreen = sinon.spy();
this.player.exitFullscreen = sinon.spy();
this.player.handleTechDoubleClick_({target: this.player.tech_.el_});
assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen once');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
fullscreen = true;
this.player.handleTechDoubleClick_({target: this.player.tech_.el_});
assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen once');
assert.strictEqual(this.player.exitFullscreen.callCount, 1, 'has exited fullscreen');
});
QUnit.test('when controls are disabled, double-click does nothing', function(assert) {
let fullscreen = false;
this.player.controls(false);
this.player.isFullscreen = () => fullscreen;
this.player.requestFullscreen = sinon.spy();
this.player.exitFullscreen = sinon.spy();
this.player.handleTechDoubleClick_({target: this.player.tech_.el_});
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
fullscreen = true;
this.player.handleTechDoubleClick_({target: this.player.tech_.el_});
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
});
QUnit.test('when userActions.doubleClick is false, double-click does nothing', function(assert) {
let fullscreen = false;
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
doubleClick: false
}
});
this.player.isFullscreen = () => fullscreen;
this.player.requestFullscreen = sinon.spy();
this.player.exitFullscreen = sinon.spy();
this.player.handleTechDoubleClick_({target: this.player.tech_.el_});
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
fullscreen = true;
this.player.handleTechDoubleClick_({target: this.player.tech_.el_});
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
});
QUnit.test('when userActions.doubleClick is a function, that function is called instead of going fullscreen', function(assert) {
let fullscreen = false;
const doubleClickSpy = sinon.spy();
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
doubleClick: doubleClickSpy
}
});
this.player.isFullscreen = () => fullscreen;
this.player.requestFullscreen = sinon.spy();
this.player.exitFullscreen = sinon.spy();
let event = {target: this.player.tech_.el_};
this.player.handleTechDoubleClick_(event);
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
assert.strictEqual(doubleClickSpy.callCount, 1, 'has called the doubleClick handler');
assert.strictEqual(doubleClickSpy.getCall(0).args[0], event, 'has passed the event to the handler');
fullscreen = true;
event = {target: this.player.tech_.el_};
this.player.handleTechDoubleClick_(event);
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
assert.strictEqual(doubleClickSpy.callCount, 2, 'has called the doubleClick handler');
assert.strictEqual(doubleClickSpy.getCall(1).args[0], event, 'has passed the event to the handler');
});
QUnit.module('Player: User Actions: Hotkeys', {
beforeEach() {
this.clock = sinon.useFakeTimers();
this.player = TestHelpers.makePlayer();
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
const mockKeyDownEvent = (key) => {
return {
preventDefault() {},
stopPropagation() {},
type: 'keydown',
which: keycode.codes[key]
};
};
const defaultKeyTests = {
fullscreen(player, assert, positive) {
let fullscreen;
if (document[FullscreenApi.fullscreenEnabled] === false) {
assert.ok(true, 'skipped fullscreen test because not supported');
assert.ok(true, 'skipped fullscreen test because not supported');
assert.ok(true, 'skipped fullscreen test because not supported');
assert.ok(true, 'skipped fullscreen test because not supported');
return;
}
player.isFullscreen = () => fullscreen;
player.requestFullscreen = sinon.spy();
player.exitFullscreen = sinon.spy();
fullscreen = false;
player.handleKeyDown(mockKeyDownEvent('f'));
if (positive) {
assert.strictEqual(player.requestFullscreen.callCount, 1, 'has gone fullscreen');
assert.strictEqual(player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
} else {
assert.strictEqual(player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
}
fullscreen = true;
player.handleKeyDown(mockKeyDownEvent('f'));
if (positive) {
assert.strictEqual(player.requestFullscreen.callCount, 1, 'has gone fullscreen');
assert.strictEqual(player.exitFullscreen.callCount, 1, 'has exited fullscreen');
} else {
assert.strictEqual(player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
}
},
mute(player, assert, positive) {
let muted = false;
player.muted = sinon.spy((val) => {
if (val !== undefined) {
muted = val;
}
return muted;
});
player.handleKeyDown(mockKeyDownEvent('m'));
if (positive) {
assert.strictEqual(player.muted.callCount, 2, 'muted was called twice (get and set)');
assert.strictEqual(player.muted.lastCall.args[0], true, 'most recent call was to mute');
} else {
assert.strictEqual(player.muted.callCount, 0, 'muted was not called');
}
player.handleKeyDown(mockKeyDownEvent('m'));
if (positive) {
assert.strictEqual(player.muted.callCount, 4, 'muted was called twice (get and set)');
assert.strictEqual(player.muted.lastCall.args[0], false, 'most recent call was to unmute');
} else {
assert.strictEqual(player.muted.callCount, 0, 'muted was not called');
}
},
playPause(player, assert, positive) {
let paused;
player.paused = () => paused;
player.pause = sinon.spy();
player.play = sinon.spy();
paused = true;
player.handleKeyDown(mockKeyDownEvent('k'));
if (positive) {
assert.strictEqual(player.pause.callCount, 0, 'has not paused');
assert.strictEqual(player.play.callCount, 1, 'has played');
} else {
assert.strictEqual(player.pause.callCount, 0, 'has not paused');
assert.strictEqual(player.play.callCount, 0, 'has not played');
}
paused = false;
player.handleKeyDown(mockKeyDownEvent('k'));
if (positive) {
assert.strictEqual(player.pause.callCount, 1, 'has paused');
assert.strictEqual(player.play.callCount, 1, 'has played');
} else {
assert.strictEqual(player.pause.callCount, 0, 'has not paused');
assert.strictEqual(player.play.callCount, 0, 'has not played');
}
paused = true;
player.handleKeyDown(mockKeyDownEvent('space'));
if (positive) {
assert.strictEqual(player.pause.callCount, 1, 'has paused');
assert.strictEqual(player.play.callCount, 2, 'has played twice');
} else {
assert.strictEqual(player.pause.callCount, 0, 'has not paused');
assert.strictEqual(player.play.callCount, 0, 'has not played');
}
paused = false;
player.handleKeyDown(mockKeyDownEvent('space'));
if (positive) {
assert.strictEqual(player.pause.callCount, 2, 'has paused twice');
assert.strictEqual(player.play.callCount, 2, 'has played twice');
} else {
assert.strictEqual(player.pause.callCount, 0, 'has not paused');
assert.strictEqual(player.play.callCount, 0, 'has not played');
}
}
};
QUnit.test('by default, hotkeys are disabled', function(assert) {
assert.expect(14);
defaultKeyTests.fullscreen(this.player, assert, false);
defaultKeyTests.mute(this.player, assert, false);
defaultKeyTests.playPause(this.player, assert, false);
});
QUnit.test('when userActions.hotkeys is true, hotkeys are enabled', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: true
}
});
assert.expect(16);
defaultKeyTests.fullscreen(this.player, assert, true);
defaultKeyTests.mute(this.player, assert, true);
defaultKeyTests.playPause(this.player, assert, true);
});
QUnit.test('when userActions.hotkeys is an object, hotkeys are enabled', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: {}
}
});
assert.expect(16);
defaultKeyTests.fullscreen(this.player, assert, true);
defaultKeyTests.mute(this.player, assert, true);
defaultKeyTests.playPause(this.player, assert, true);
});
QUnit.test('when userActions.hotkeys.fullscreenKey can be a function', function(assert) {
if (document[FullscreenApi.fullscreenEnabled] === false) {
assert.expect(0);
return;
}
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: {
fullscreenKey: sinon.spy((e) => keycode.isEventKey(e, 'x'))
}
}
});
let fullscreen;
this.player.isFullscreen = () => fullscreen;
this.player.requestFullscreen = sinon.spy();
this.player.exitFullscreen = sinon.spy();
fullscreen = false;
this.player.handleKeyDown(mockKeyDownEvent('f'));
assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
this.player.handleKeyDown(mockKeyDownEvent('x'));
assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
fullscreen = true;
this.player.handleKeyDown(mockKeyDownEvent('x'));
assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen');
assert.strictEqual(this.player.exitFullscreen.callCount, 1, 'has exited fullscreen');
});
QUnit.test('when userActions.hotkeys.muteKey can be a function', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: {
muteKey: sinon.spy((e) => keycode.isEventKey(e, 'x'))
}
}
});
let muted = false;
this.player.muted = sinon.spy((val) => {
if (val !== undefined) {
muted = val;
}
return muted;
});
this.player.handleKeyDown(mockKeyDownEvent('m'));
assert.strictEqual(this.player.muted.callCount, 0, 'muted was not called');
this.player.handleKeyDown(mockKeyDownEvent('x'));
assert.strictEqual(this.player.muted.callCount, 2, 'muted was called twice (get and set)');
assert.strictEqual(this.player.muted.lastCall.args[0], true, 'most recent call was to mute');
this.player.handleKeyDown(mockKeyDownEvent('x'));
assert.strictEqual(this.player.muted.callCount, 4, 'muted was called twice (get and set)');
assert.strictEqual(this.player.muted.lastCall.args[0], false, 'most recent call was to unmute');
});
QUnit.test('when userActions.hotkeys.playPauseKey can be a function', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: {
playPauseKey: sinon.spy((e) => keycode.isEventKey(e, 'x'))
}
}
});
let paused;
this.player.paused = () => paused;
this.player.pause = sinon.spy();
this.player.play = sinon.spy();
paused = true;
this.player.handleKeyDown(mockKeyDownEvent('k'));
this.player.handleKeyDown(mockKeyDownEvent('space'));
assert.strictEqual(this.player.pause.callCount, 0, 'has not paused');
assert.strictEqual(this.player.play.callCount, 0, 'has not played');
this.player.handleKeyDown(mockKeyDownEvent('x'));
assert.strictEqual(this.player.pause.callCount, 0, 'has not paused');
assert.strictEqual(this.player.play.callCount, 1, 'has played');
paused = false;
this.player.handleKeyDown(mockKeyDownEvent('x'));
assert.strictEqual(this.player.pause.callCount, 1, 'has paused');
assert.strictEqual(this.player.play.callCount, 1, 'has played');
});
QUnit.test('hotkeys are ignored when focus is in a textarea', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: true
}
});
const textarea = document.createElement('textarea');
this.player.el_.appendChild(textarea);
textarea.focus();
assert.expect(14);
defaultKeyTests.fullscreen(this.player, assert, false);
defaultKeyTests.mute(this.player, assert, false);
defaultKeyTests.playPause(this.player, assert, false);
});
QUnit.test('hotkeys are ignored when focus is in a text input', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: true
}
});
const input = document.createElement('input');
input.type = 'text';
this.player.el_.appendChild(input);
input.focus();
assert.expect(14);
defaultKeyTests.fullscreen(this.player, assert, false);
defaultKeyTests.mute(this.player, assert, false);
defaultKeyTests.playPause(this.player, assert, false);
});
QUnit.test('hotkeys are NOT ignored when focus is on a button element', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: true
}
});
const button = document.createElement('button');
this.player.el_.appendChild(button);
button.focus();
assert.expect(16);
defaultKeyTests.fullscreen(this.player, assert, true);
defaultKeyTests.mute(this.player, assert, true);
defaultKeyTests.playPause(this.player, assert, true);
});
QUnit.test('hotkeys are NOT ignored when focus is on a button input', function(assert) {
this.player.dispose();
this.player = TestHelpers.makePlayer({
controls: true,
userActions: {
hotkeys: true
}
});
const input = document.createElement('input');
input.type = 'button';
this.player.el_.appendChild(input);
input.focus();
assert.expect(16);
defaultKeyTests.fullscreen(this.player, assert, true);
defaultKeyTests.mute(this.player, assert, true);
defaultKeyTests.playPause(this.player, assert, true);
});