diff --git a/sandbox/global-hotkeys.html.example b/sandbox/global-hotkeys.html.example new file mode 100644 index 0000000000..62865ad887 --- /dev/null +++ b/sandbox/global-hotkeys.html.example @@ -0,0 +1,149 @@ + + + + + Video.js Sandbox + + + + +
+

Focus outside the player (e.g. click anywhere in the page) and press a hotkey (e.g. space bar). + The player hotkey should trigger and perform the action.

+

By setting globalHotkeys: true extends the normal hotkey behavior at the global level. Therefore + hotkeys: true also need to be set.

+

Note that if the focus is on an interactive element in the page, the global hotkey behavior would not trigger.

+
+ +
+

Player with default hotkeys

+ + + + + +

To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video

+
+ +

Player with 'x' button linked to play/pause

+ + + + + +

To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video

+
+ + +
+ +
+

Test Interactive Area

+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

This is a sample paragraph with some interactive content.

+
+ +
+ +
+ +
+ +
+
+ + diff --git a/src/js/player.js b/src/js/player.js index 0345f93e5a..b298c6c59e 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -374,6 +374,8 @@ class Player extends Component { this.boundUpdatePlayerHeightOnAudioOnlyMode_ = (e) => this.updatePlayerHeightOnAudioOnlyMode_(e); + this.boundGlobalKeydown_ = (e) => this.handleGlobalKeydown_(e); + // default isFullscreen_ to false this.isFullscreen_ = false; @@ -615,6 +617,10 @@ class Player extends Component { this.on('keydown', (e) => this.handleKeyDown(e)); this.on('languagechange', (e) => this.handleLanguagechange(e)); + if (this.isGlobalHotKeysEnabled()) { + Events.on(document.body, 'keydown', this.boundGlobalKeydown_); + } + this.breakpoints(this.options_.breakpoints); this.responsive(this.options_.responsive); @@ -650,6 +656,7 @@ class Player extends Component { // Make sure all player-specific document listeners are unbound. This is Events.off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); Events.off(document, 'keydown', this.boundFullWindowOnEscKey_); + Events.off(document.body, 'keydown', this.boundGlobalKeydown_); if (this.styleEl_ && this.styleEl_.parentNode) { this.styleEl_.parentNode.removeChild(this.styleEl_); @@ -2251,6 +2258,12 @@ class Player extends Component { this.trigger('textdata', data); } + handleGlobalKeydown_(event) { + if (event.target === document.body) { + this.handleKeyDown(event); + } + } + /** * Get object for cached values. * @@ -4574,6 +4587,10 @@ class Player extends Component { this.height(this.audioOnlyCache_.controlBarHeight); } + isGlobalHotKeysEnabled() { + return !!(this.options_ && this.options_.userActions && this.options_.userActions.globalHotkeys); + } + enableAudioOnlyUI_() { // Update styling immediately to show the control bar so we can get its height this.addClass('vjs-audio-only-mode'); diff --git a/test/unit/player-user-actions.test.js b/test/unit/player-user-actions.test.js index d639434edc..35a5614e2a 100644 --- a/test/unit/player-user-actions.test.js +++ b/test/unit/player-user-actions.test.js @@ -639,3 +639,58 @@ QUnit.test('hotkeys are NOT ignored when focus is on a button input', function(a defaultKeyTests.mute(this.player, assert, true); defaultKeyTests.playPause(this.player, assert, true); }); + +QUnit.module('Player: User Actions: Global Hotkeys', { + + beforeEach() { + this.clock = sinon.useFakeTimers(); + this.player = TestHelpers.makePlayer(); + }, + + afterEach() { + this.player.dispose(); + this.clock.restore(); + } +}); + +QUnit.test('when userActions.globalHotkeys is true, hotkeys are enabled at document.body level', function(assert) { + this.player.dispose(); + this.player = TestHelpers.makePlayer({ + controls: true, + userActions: { + globalHotkeys: true, + hotkeys: true + } + }); + + this.player.requestFullscreen = sinon.spy(); + + const event = new KeyboardEvent('keydown', { // eslint-disable-line no-undef + key: 'f' + }); + + document.body.dispatchEvent(event); + + assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen'); +}); + +QUnit.test('when userActions.globalHotkeys is NOT true, hotkeys are NOT enabled at document.body level', function(assert) { + this.player.dispose(); + this.player = TestHelpers.makePlayer({ + controls: true, + userActions: { + globalHotkeys: false, + hotkeys: true + } + }); + + this.player.requestFullscreen = sinon.spy(); + + const event = new KeyboardEvent('keydown', { // eslint-disable-line no-undef + key: 'f' + }); + + document.body.dispatchEvent(event); + + assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen'); +});