Bug 1549442 - Make sure to hide the Picture-in-Picture toggle when the mouse cursor leaves the window. r=JSON_voorhees

Originally, we were using the :hover pseudoclass to show the Picture-in-Picture toggle,
and using the DevTools InspectorUtils module to manually apply that :hover pseudoclass
in PictureInPictureToggleChild in the event that the <video> element wasn't able to get
the :hover element "naturally".

Removing this pseudoclass when the mouse leaves the browser window (without first
leaving the toggle) doesn't work, since the mouseout event (which we use to detect
the mouse leaving the window) fires _after_ :hover states would normally be cleared,
so it's too late to "unlock" it.

The solution ended up being replacing the :hover pseudoclass with a .hovering class
that PictureInPictureToggleChild continues to manage on its own.

Differential Revision: https://phabricator.services.mozilla.com/D47677

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mike Conley 2019-10-01 18:09:43 +00:00
Родитель 169212949e
Коммит 5794ef1442
2 изменённых файлов: 50 добавлений и 18 удалений

Просмотреть файл

@ -9,9 +9,6 @@ var EXPORTED_SYMBOLS = ["PictureInPictureChild", "PictureInPictureToggleChild"];
const { ActorChild } = ChromeUtils.import(
"resource://gre/modules/ActorChild.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
@ -24,8 +21,6 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/Services.jsm"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
const TOGGLE_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.enabled";
const TOGGLE_TESTING_PREF =
@ -150,6 +145,10 @@ class PictureInPictureToggleChild extends ActorChild {
}
break;
}
case "mouseout": {
this.onMouseOut(event);
break;
}
case "mousedown":
case "pointerup":
case "mouseup":
@ -281,6 +280,9 @@ class PictureInPictureToggleChild extends ActorChild {
capture: true,
});
this.content.windowRoot.addEventListener("click", this, { capture: true });
this.content.windowRoot.addEventListener("mouseout", this, {
capture: true,
});
}
removeMouseButtonListeners() {
@ -299,6 +301,9 @@ class PictureInPictureToggleChild extends ActorChild {
this.content.windowRoot.removeEventListener("click", this, {
capture: true,
});
this.content.windowRoot.removeEventListener("mouseout", this, {
capture: true,
});
}
/**
@ -463,6 +468,28 @@ class PictureInPictureToggleChild extends ActorChild {
}
}
/**
* Called on mouseout events to determine whether or not the mouse has
* exited the window.
*
* @param {Event} event The mouseout event.
*/
onMouseOut(event) {
if (!event.relatedTarget) {
// For mouseout events, if there's no relatedTarget (which normally
// maps to the element that the mouse entered into) then this means that
// we left the window.
let state = this.docState;
let video = state.weakOverVideo && state.weakOverVideo.get();
if (!video) {
return;
}
this.onMouseLeaveVideo(video);
}
}
/**
* Called for each mousemove event when we're tracking those events to
* determine if the cursor is hovering over a <video>.
@ -578,7 +605,7 @@ class PictureInPictureToggleChild extends ActorChild {
}
state.weakOverVideo = Cu.getWeakReference(video);
InspectorUtils.addPseudoClassLock(controlsOverlay, ":hover");
controlsOverlay.classList.add("hovering");
// Now that we're hovering the video, we'll check to see if we're
// hovering the toggle too.
@ -587,18 +614,14 @@ class PictureInPictureToggleChild extends ActorChild {
/**
* Checks if a mouse event is happening over a toggle element. If it is,
* sets the :hover pseudoclass on it. Otherwise, it clears the :hover
* pseudoclass.
* sets the hovering class on it. Otherwise, it clears the hovering
* class.
*
* @param {Element} toggle The Picture-in-Picture toggle to check.
* @param {MouseEvent} event A MouseEvent to test.
*/
checkHoverToggle(toggle, event) {
if (this.isMouseOverToggle(toggle, event)) {
InspectorUtils.addPseudoClassLock(toggle, ":hover");
} else {
InspectorUtils.removePseudoClassLock(toggle, ":hover");
}
toggle.classList.toggle("hovering", this.isMouseOverToggle(toggle, event));
}
/**
@ -614,14 +637,15 @@ class PictureInPictureToggleChild extends ActorChild {
if (shadowRoot) {
let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
InspectorUtils.removePseudoClassLock(controlsOverlay, ":hover");
InspectorUtils.removePseudoClassLock(toggle, ":hover");
controlsOverlay.classList.remove("hovering");
toggle.classList.remove("hovering");
}
state.weakOverVideo = null;
if (!this.toggleTesting) {
state.hideToggleDeferredTask.disarm();
state.mousemoveDeferredTask.disarm();
}
state.hideToggleDeferredTask = null;

Просмотреть файл

@ -473,15 +473,23 @@
margin-inline-start: var(--pip-toggle-padding);
}
.controlsOverlay:hover > .pictureInPictureToggleButton {
/**
* For the Picture-in-Picture toggle, we don't use the built-in :hover
* pseudoclass because there are many sites where the arrangement of the DOM
* or the CSS will conspire to prevent :hover from matching on the <video>
* itself. PictureInPictureToggleChild takes care of punching through those
* barriers, and manually sets a .hovering class.
*/
.controlsOverlay.hovering > .pictureInPictureToggleButton {
opacity: 0.8;
}
.controlsOverlay:hover[hidetoggle="true"] > .pictureInPictureToggleButton:not(:hover) {
.controlsOverlay[hidetoggle="true"].hovering > .pictureInPictureToggleButton:not(.hovering) {
opacity: 0;
}
.controlsOverlay:hover > .pictureInPictureToggleButton:hover {
.controlsOverlay.hovering > .pictureInPictureToggleButton.hovering {
opacity: 1;
transform: translateY(-50%) translateX(0);
}