Skip to content

Commit

Permalink
feat: Add a mouse volume tooltip (#6824)
Browse files Browse the repository at this point in the history
  • Loading branch information
gjanblaszczyk authored Mar 23, 2021
1 parent 239c9a1 commit b2edfd2
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/css/components/_volume.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
&:before {
position: absolute;
font-size: 0.9em; // Doing this to match the handle on play progress.
z-index: 1;
}
}

Expand All @@ -148,6 +149,7 @@
&:before {
top: -0.5em;
left: -0.3em;
z-index: 1;
}
}
.vjs-slider-horizontal .vjs-volume-level {
Expand Down Expand Up @@ -184,3 +186,83 @@
.video-js .vjs-volume-horizontal .vjs-menu {
left: -2em;
}

// .vjs-volume-tooltip
//
// These elements are displayed above the volume bar.
//
// By default, they are hidden and only shown when hovering over the volume
// control.
.video-js .vjs-volume-tooltip {
@include background-color-with-alpha(#fff, 0.8);
@include border-radius(0.3em);
color: #000;
float: right;
font-family: $text-font-family;
font-size: 1em;
padding: 6px 8px 8px 8px;
pointer-events: none;
position: absolute;
top: -3.4em;
visibility: hidden;
z-index: 1;
}

.video-js .vjs-volume-control:hover .vjs-volume-tooltip,
.video-js .vjs-volume-control:hover .vjs-progress-holder:focus .vjs-volume-tooltip {
display: block;
font-size: 1em;
visibility: visible;
}

.video-js .vjs-volume-vertical:hover .vjs-volume-tooltip,
.video-js .vjs-volume-vertical:hover .vjs-progress-holder:focus .vjs-volume-tooltip {
left: 1em;
top: -12px;
}

.video-js .vjs-volume-control.disabled:hover .vjs-volume-tooltip {
font-size: 1em;
}

// .vjs-mouse-display / MouseVolumeLevelDisplay
//
// This element tracks the mouse position along the volume control and
// includes a tooltip, which displays the volume level.
.video-js .vjs-volume-control .vjs-mouse-display {
display: none;
position: absolute;
width: 100%;
height: 1px;
background-color: #000;
z-index: 1;
}

.video-js .vjs-volume-horizontal .vjs-mouse-display {
width: 1px;
height: 100%;
}

.vjs-no-flex .vjs-volume-control .vjs-mouse-display {
z-index: 0;
}

.video-js .vjs-volume-control:hover .vjs-mouse-display {
display: block;
}

.video-js.vjs-user-inactive .vjs-volume-control .vjs-mouse-display {
visibility: hidden;
opacity: 0;
$trans: visibility 1.0s, opacity 1.0s;
@include transition($trans);
}

.video-js.vjs-user-inactive.vjs-no-flex .vjs-volume-control .vjs-mouse-display {
display: none;
}

.vjs-mouse-display .vjs-volume-tooltip {
color: #fff;
@include background-color-with-alpha(#000, 0.8);
}
87 changes: 87 additions & 0 deletions src/js/control-bar/volume-control/mouse-volume-level-display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @file mouse-volume-level-display.js
*/
import Component from '../../component.js';
import * as Fn from '../../utils/fn.js';

import './volume-level-tooltip';

/**
* The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
* {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
* indicating the volume level which is represented by a given point in the
* {@link VolumeBar}.
*
* @extends Component
*/
class MouseVolumeLevelDisplay extends Component {

/**
* Creates an instance of this class.
*
* @param {Player} player
* The {@link Player} that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*/
constructor(player, options) {
super(player, options);
this.update = Fn.throttle(Fn.bind(this, this.update), Fn.UPDATE_REFRESH_INTERVAL);
}

/**
* Create the DOM element for this class.
*
* @return {Element}
* The element that was created.
*/
createEl() {
return super.createEl('div', {
className: 'vjs-mouse-display'
});
}

/**
* Enquires updates to its own DOM as well as the DOM of its
* {@link VolumeLevelTooltip} child.
*
* @param {Object} rangeBarRect
* The `ClientRect` for the {@link VolumeBar} element.
*
* @param {number} rangeBarPoint
* A number from 0 to 1, representing a horizontal/vertical reference point
* from the left edge of the {@link VolumeBar}
*
* @param {boolean} vertical
* Referees to the Volume control position
* in the control bar{@link VolumeControl}
*
*/
update(rangeBarRect, rangeBarPoint, vertical) {
const volume = 100 * rangeBarPoint;

this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
if (vertical) {
this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
} else {
this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
}
});
}
}

/**
* Default options for `MouseVolumeLevelDisplay`
*
* @type {Object}
* @private
*/
MouseVolumeLevelDisplay.prototype.options_ = {
children: [
'volumeLevelTooltip'
]
};

Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
export default MouseVolumeLevelDisplay;
25 changes: 25 additions & 0 deletions src/js/control-bar/volume-control/volume-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import Slider from '../../slider/slider.js';
import Component from '../../component.js';
import * as Dom from '../../utils/dom.js';
import clamp from '../../utils/clamp.js';
import {IS_IOS, IS_ANDROID} from '../../utils/browser.js';

// Required children
import './volume-level.js';
import './mouse-volume-level-display.js';

/**
* The bar that contains the volume level and can be clicked on to adjust the level
Expand Down Expand Up @@ -71,6 +74,23 @@ class VolumeBar extends Slider {
* @listens mousemove
*/
handleMouseMove(event) {
const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');

if (mouseVolumeLevelDisplay) {
const volumeBarEl = this.el();
const volumeBarRect = Dom.getBoundingClientRect(volumeBarEl);
const vertical = this.vertical();
let volumeBarPoint = Dom.getPointerPosition(volumeBarEl, event);

volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
// The default skin has a gap on either side of the `VolumeBar`. This means
// that it's possible to trigger this behavior outside the boundaries of
// the `VolumeBar`. This ensures we stay within it at all times.
volumeBarPoint = clamp(volumeBarPoint, 0, 1);

mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
}

if (!Dom.isSingleLeftClick(event)) {
return;
}
Expand Down Expand Up @@ -174,6 +194,11 @@ VolumeBar.prototype.options_ = {
barName: 'volumeLevel'
};

// MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
if (!IS_IOS && !IS_ANDROID) {
VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
}

/**
* Call the update event for this Slider when this event happens on the player.
*
Expand Down
1 change: 1 addition & 0 deletions src/js/control-bar/volume-control/volume-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class VolumeControl extends Component {

this.on('mousedown', this.handleMouseDown);
this.on('touchstart', this.handleMouseDown);
this.on('mousemove', this.handleMouseMove);

// while the slider is active (the mouse has been pressed down and
// is dragging) or in focus we do not want to hide the VolumeBar
Expand Down
133 changes: 133 additions & 0 deletions src/js/control-bar/volume-control/volume-level-tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @file volume-level-tooltip.js
*/
import Component from '../../component';
import * as Dom from '../../utils/dom.js';
import * as Fn from '../../utils/fn.js';

/**
* Volume level tooltips display a volume above or side by side the volume bar.
*
* @extends Component
*/
class VolumeLevelTooltip extends Component {

/**
* Creates an instance of this class.
*
* @param {Player} player
* The {@link Player} that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*/
constructor(player, options) {
super(player, options);
this.update = Fn.throttle(Fn.bind(this, this.update), Fn.UPDATE_REFRESH_INTERVAL);
}

/**
* Create the volume tooltip DOM element
*
* @return {Element}
* The element that was created.
*/
createEl() {
return super.createEl('div', {
className: 'vjs-volume-tooltip'
}, {
'aria-hidden': 'true'
});
}

/**
* Updates the position of the tooltip relative to the `VolumeBar` and
* its content text.
*
* @param {Object} rangeBarRect
* The `ClientRect` for the {@link VolumeBar} element.
*
* @param {number} rangeBarPoint
* A number from 0 to 1, representing a horizontal/vertical reference point
* from the left edge of the {@link VolumeBar}
*
* @param {boolean} vertical
* Referees to the Volume control position
* in the control bar{@link VolumeControl}
*
*/
update(rangeBarRect, rangeBarPoint, vertical, content) {
if (!vertical) {
const tooltipRect = Dom.getBoundingClientRect(this.el_);
const playerRect = Dom.getBoundingClientRect(this.player_.el());
const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;

if (!playerRect || !tooltipRect) {
return;
}

const spaceLeftOfPoint = (rangeBarRect.left - playerRect.left) + volumeBarPointPx;
const spaceRightOfPoint = (rangeBarRect.width - volumeBarPointPx) +
(playerRect.right - rangeBarRect.right);
let pullTooltipBy = tooltipRect.width / 2;

if (spaceLeftOfPoint < pullTooltipBy) {
pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
} else if (spaceRightOfPoint < pullTooltipBy) {
pullTooltipBy = spaceRightOfPoint;
}

if (pullTooltipBy < 0) {
pullTooltipBy = 0;
} else if (pullTooltipBy > tooltipRect.width) {
pullTooltipBy = tooltipRect.width;
}

this.el_.style.right = `-${pullTooltipBy}px`;
}
this.write(`${content}%`);
}

/**
* Write the volume to the tooltip DOM element.
*
* @param {string} content
* The formatted volume for the tooltip.
*/
write(content) {
Dom.textContent(this.el_, content);
}

/**
* Updates the position of the volume tooltip relative to the `VolumeBar`.
*
* @param {Object} rangeBarRect
* The `ClientRect` for the {@link VolumeBar} element.
*
* @param {number} rangeBarPoint
* A number from 0 to 1, representing a horizontal/vertical reference point
* from the left edge of the {@link VolumeBar}
*
* @param {boolean} vertical
* Referees to the Volume control position
* in the control bar{@link VolumeControl}
*
* @param {number} volume
* The volume level to update the tooltip to
*
* @param {Function} cb
* A function that will be called during the request animation frame
* for tooltips that need to do additional animations from the default
*/
updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
if (cb) {
cb();
}
});
}
}

Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
export default VolumeLevelTooltip;

0 comments on commit b2edfd2

Please sign in to comment.