-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Adds a transient button component (#8629)
## Description Adds a `TransientButton` component for the types of button that are shown on top of the video briefly during playback and reappear when there is user activity. e.g. Unmute buttons, skip intro. It aims is to be a generic button type to be extended. Some basic styles are provided but kept light to not complicate customisation. It's important to insert a transient button before the control bar for the tab order to make sense. _Optionally_ takes focus when shown. ## Specific Changes proposed Adds `TransientButton` component. ## Requirements Checklist - [x] Feature implemented / Bug fixed - [ ] If necessary, more likely in a feature request than a bug fix - [x] Change has been verified in an actual browser (Chrome, Firefox, IE) - [x] Unit Tests updated or fixed - [ ] Docs/guides updated - [x] Example: https://deploy-preview-8629--videojs-preview.netlify.app/sandbox/transient-button.html - [x] Has no DOM changes which impact accessiblilty or trigger warnings (e.g. Chrome issues tab) - [x] Has no changes to JSDoc which cause `npm run docs:api` to error - [ ] Reviewed by Two Core Contributors
- Loading branch information
1 parent
f701102
commit 1afe504
Showing
7 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<title>Video.js Sandbox</title> | ||
<link href="../dist/video-js.css" rel="stylesheet" type="text/css" /> | ||
<script src="../dist/video.js"></script> | ||
<style> | ||
article { | ||
max-width: 800px; | ||
margin: 0 auto; | ||
} | ||
.vjs-transient-button.unmute-button span::before { | ||
content: "\f104"; | ||
font-family: "VideoJS"; | ||
vertical-align: middle; | ||
padding-right: 0.3em; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<article> | ||
<h1>Transient button demo</h1> | ||
<video-js | ||
id="vid1" | ||
class="vjs-fluid" | ||
controls | ||
muted | ||
preload="auto" | ||
poster="https://vjs.zencdn.net/v/oceans.png" | ||
> | ||
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4" /> | ||
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm" /> | ||
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg" /> | ||
<track | ||
kind="captions" | ||
src="../docs/examples/shared/example-captions.vtt" | ||
srclang="en" | ||
label="English" | ||
/> | ||
</video-js> | ||
</article> | ||
<p>An unmute transient button will show after playback starts if muted.</p> | ||
<p> | ||
Transient buttons to skip into / credits / recap display at times defined | ||
in a metadata track. | ||
</p> | ||
<script> | ||
const player = videojs("#vid1"); | ||
|
||
player.ready(function () { | ||
// Adds an unmute button that umutes and goes away when clicked | ||
player.one("playing", function () { | ||
if (this.muted()) { | ||
const unmuteButton = player.addChild( | ||
"TransientButton", | ||
{ | ||
controlText: "Unmute", | ||
position: ["top", "left"], | ||
className: "unmute-button", | ||
clickHandler: function () { | ||
this.player().muted(false); | ||
this.dispose(); | ||
}, | ||
}, | ||
player.children().indexOf(player.getChild("ControlBar")) | ||
); | ||
unmuteButton.show(); | ||
} | ||
}); | ||
|
||
// A track that defines skippable parts | ||
const track = player.addRemoteTextTrack({ | ||
src: | ||
"data:text/vtt;base64," + | ||
btoa(`WEBVTT | ||
|
||
00:01.000 --> 00:10.000 | ||
Recap | ||
|
||
00:15.000 --> 00:20.000 | ||
Intro | ||
|
||
00:40.000 --> 00:47.000 | ||
Credits | ||
|
||
`), | ||
kind: "metadata", | ||
label: "skip_sections", | ||
}).track; | ||
|
||
let skipButtons = []; | ||
|
||
track.addEventListener("cuechange", function () { | ||
const cue = track.activeCues[0]; | ||
if (cue) { | ||
const skipButton = player.addChild( | ||
"TransientButton", | ||
{ | ||
controlText: `Skip ${cue.text}`, | ||
position: ["bottom", "right"], | ||
clickHandler: () => { | ||
player.currentTime(cue.endTime); | ||
}, | ||
takeFocus: true, | ||
}, | ||
player.children().indexOf(player.getChild("ControlBar")) | ||
); | ||
skipButtons.push(skipButton); | ||
skipButton.show(); | ||
} else { | ||
while (skipButtons.length > 0) { | ||
skipButtons.shift().dispose(); | ||
} | ||
} | ||
}); | ||
track.mode = "hidden"; | ||
}); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
.video-js .vjs-transient-button { | ||
position: absolute; | ||
height: 3em; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
background-color: rgba(50, 50, 50, 0.5); | ||
cursor: pointer; | ||
opacity: 1; | ||
transition: opacity 1s; | ||
} | ||
|
||
.video-js:not(.vjs-has-started) .vjs-transient-button { | ||
display: none; | ||
} | ||
|
||
.video-js.not-hover .vjs-transient-button:not(.force-display), | ||
.video-js.vjs-user-inactive .vjs-transient-button:not(.force-display) { | ||
opacity: 0; | ||
} | ||
|
||
.video-js .vjs-transient-button span { | ||
padding: 0 0.5em; | ||
} | ||
|
||
.video-js .vjs-transient-button.vjs-left { | ||
left: 1em; | ||
} | ||
|
||
.video-js .vjs-transient-button.vjs-right { | ||
right: 1em; | ||
} | ||
|
||
.video-js .vjs-transient-button.vjs-top { | ||
top: 1em; | ||
} | ||
|
||
.video-js .vjs-transient-button.vjs-near-top { | ||
top: 4em; | ||
} | ||
|
||
.video-js .vjs-transient-button.vjs-bottom { | ||
bottom: 4em; | ||
} | ||
|
||
.video-js .vjs-transient-button:hover { | ||
background-color: rgba(50, 50, 50, 0.9); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import Button from './button.js'; | ||
import Component from './component.js'; | ||
import {merge} from './utils/obj'; | ||
import * as Dom from './utils/dom.js'; | ||
|
||
/** @import Player from './player' */ | ||
|
||
/** | ||
* @typedef {object} TransientButtonOptions | ||
* @property {string} [controlText] Control text, usually visible for these buttons | ||
* @property {number} [initialDisplay=4000] Time in ms that button should initially remain visible | ||
* @property {Array<'top'|'neartop'|'bottom'|'left'|'right'>} [position] Array of position strings to add basic styles for positioning | ||
* @property {string} [className] Class(es) to add | ||
* @property {boolean} [takeFocus=false] Whether element sohuld take focus when shown | ||
* @property {Function} [clickHandler] Function called on button activation | ||
*/ | ||
|
||
/** @type {TransientButtonOptions} */ | ||
const defaults = { | ||
initialDisplay: 4000, | ||
position: [], | ||
takeFocus: false | ||
}; | ||
|
||
/** | ||
* A floating transient button. | ||
* It's recommended to insert these buttons _before_ the control bar with the this argument to `addChild` | ||
* for a logical tab order. | ||
* | ||
* @example | ||
* ``` | ||
* player.addChild( | ||
* 'TransientButton', | ||
* options, | ||
* player.children().indexOf(player.getChild("ControlBar")) | ||
* ) | ||
* ``` | ||
* | ||
* @extends Button | ||
*/ | ||
class TransientButton extends Button { | ||
/** | ||
* TransientButton constructor | ||
* | ||
* @param {Player} player The button's player | ||
* @param {TransientButtonOptions} options Options for the transient button | ||
*/ | ||
constructor(player, options) { | ||
options = merge(defaults, options); | ||
super(player, options); | ||
this.controlText(options.controlText); | ||
this.hide(); | ||
|
||
// When shown, the float button will be visible even if the user is inactive. | ||
// Clear this if there is any interaction. | ||
player.on(['useractive', 'userinactive'], (e) => { | ||
this.removeClass('force-display'); | ||
}); | ||
} | ||
|
||
/** | ||
* Return CSS class including position classes | ||
* | ||
* @return {string} CSS class list | ||
*/ | ||
buildCSSClass() { | ||
return `vjs-transient-button focus-visible ${this.options_.position.map((c) => `vjs-${c}`).join(' ')}`; | ||
} | ||
|
||
/** | ||
* Create the button element | ||
* | ||
* @return {HTMLButtonElement} The button element | ||
*/ | ||
createEl() { | ||
/** @type HTMLButtonElement */ | ||
const el = Dom.createEl( | ||
'button', {}, { | ||
type: 'button', | ||
class: this.buildCSSClass() | ||
}, | ||
Dom.createEl('span') | ||
); | ||
|
||
this.controlTextEl_ = el.querySelector('span'); | ||
|
||
return el; | ||
} | ||
|
||
/** | ||
* Show the button. The button will remain visible for the `initialDisplay` time, default 4s, | ||
* and when there is user activity. | ||
*/ | ||
show() { | ||
super.show(); | ||
this.addClass('force-display'); | ||
if (this.options_.takeFocus) { | ||
this.el().focus({ preventScroll: true}); | ||
} | ||
|
||
this.forceDisplayTimeout = this.player_.setTimeout(() => { | ||
this.removeClass('force-display'); | ||
}, this.options_.initialDisplay); | ||
} | ||
|
||
/** | ||
* Hide the display, even if during the `initialDisplay` time. | ||
*/ | ||
hide() { | ||
this.removeClass('force-display'); | ||
super.hide(); | ||
} | ||
|
||
/** | ||
* Dispose the component | ||
*/ | ||
dispose() { | ||
this.player_.clearTimeout(this.forceDisplayTimeout); | ||
super.dispose(); | ||
} | ||
} | ||
|
||
Component.registerComponent('TransientButton', TransientButton); | ||
export default TransientButton; |
Oops, something went wrong.