fix(controller): preserve overlays in Firefox fullscreen

This commit is contained in:
joshpatra committed 2026-08-19 21:47:29 -04:00
1 parent f8cfbf05c8
commit 6d17f15bc0
3 files changed
+80 -7

No files matched your search

+20 -1
View File
@@ -3144,6 +3144,12 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
resizeObserver.observe(geometryMount); resizeObserver.observe(geometryMount);
} }
var mediaGeometryEvents = ["loadedmetadata", "play", "playing"];
mediaGeometryEvents.forEach(function(eventName) {
videoController.video.addEventListener(eventName, schedule, {
passive: true
});
});
win.addEventListener("resize", schedule, { passive: true }); win.addEventListener("resize", schedule, { passive: true });
doc.addEventListener("fullscreenchange", schedule, { passive: true }); doc.addEventListener("fullscreenchange", schedule, { passive: true });
geometryMount.addEventListener("scroll", schedule, { passive: true }); geometryMount.addEventListener("scroll", schedule, { passive: true });
@@ -3154,6 +3160,9 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
if (resizeObserver) resizeObserver.disconnect(); if (resizeObserver) resizeObserver.disconnect();
if (frameId !== null) win.cancelAnimationFrame(frameId); if (frameId !== null) win.cancelAnimationFrame(frameId);
if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer); if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer);
mediaGeometryEvents.forEach(function(eventName) {
videoController.video.removeEventListener(eventName, schedule);
});
win.removeEventListener("resize", schedule); win.removeEventListener("resize", schedule);
doc.removeEventListener("fullscreenchange", schedule); doc.removeEventListener("fullscreenchange", schedule);
geometryMount.removeEventListener("scroll", schedule); geometryMount.removeEventListener("scroll", schedule);
@@ -3329,8 +3338,15 @@ function syncControllerFullscreenMount(videoController) {
(fullscreenElement === video || (fullscreenElement === video ||
isComposedDescendant(video, fullscreenElement)) isComposedDescendant(video, fullscreenElement))
); );
var normalGeometryMount = getControllerGeometryMount(targetMount);
var normalMountIsAlreadyFullscreenVisible = Boolean(
fullscreenElement &&
fullscreenElement !== video &&
normalGeometryMount &&
isComposedDescendant(normalGeometryMount, fullscreenElement)
);
if (ownsFullscreen) { if (ownsFullscreen && !normalMountIsAlreadyFullscreenVisible) {
targetMount = getControllerMount(video, fullscreenElement); targetMount = getControllerMount(video, fullscreenElement);
} else if (!fullscreenElement && (!targetMount || !targetMount.isConnected)) { } else if (!fullscreenElement && (!targetMount || !targetMount.isConnected)) {
targetMount = getControllerMount(video); targetMount = getControllerMount(video);
@@ -3950,6 +3966,9 @@ function defineVideoController() {
const speed = this.video.playbackRate.toFixed(2); const speed = this.video.playbackRate.toFixed(2);
var wrapper = doc.createElement("div"); var wrapper = doc.createElement("div");
wrapper.classList.add("vsc-controller"); wrapper.classList.add("vsc-controller");
// Keep the host out of player layout while its shadow stylesheet loads.
wrapper.style.position = "absolute";
wrapper.style.pointerEvents = "none";
if (!hasUsableMediaSource(this.video)) if (!hasUsableMediaSource(this.video))
wrapper.classList.add("vsc-nosource"); wrapper.classList.add("vsc-nosource");
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden"); if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
-5
View File
@@ -1,9 +1,5 @@
:host { :host {
position: absolute !important; position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
pointer-events: none !important; pointer-events: none !important;
z-index: 2147483647 !important; z-index: 2147483647 !important;
white-space: normal; white-space: normal;
@@ -15,7 +11,6 @@
base absolute host rule. */ base absolute host rule. */
:host(.vsc-fullscreen-popover) { :host(.vsc-fullscreen-popover) {
position: fixed !important; position: fixed !important;
inset: auto !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
border: 0 !important; border: 0 !important;
+60 -1
View File
@@ -2,7 +2,8 @@ const {
createChromeMock, createChromeMock,
evaluateScript, evaluateScript,
flushAsyncWork, flushAsyncWork,
loadHtmlString loadHtmlString,
readWorkspaceFile
} = require("./helpers/extension-test-utils"); } = require("./helpers/extension-test-utils");
function bootInject(options) { function bootInject(options) {
@@ -280,6 +281,45 @@ describe("inject.js media/controller lifecycle regressions", () => {
expect(video.vsc.div.style.getPropertyValue("height")).toBe("226px"); expect(video.vsc.div.style.getPropertyValue("height")).toBe("226px");
}); });
it("does not let shadow host defaults override measured geometry", () => {
const shadowCss = readWorkspaceFile("extension/content/shadow.css");
const hostRule = shadowCss.match(/:host\s*\{([^}]*)\}/)[1];
const fullscreenRule = shadowCss.match(
/:host\(\.vsc-fullscreen-popover\)\s*\{([^}]*)\}/
)[1];
expect(hostRule).not.toMatch(/\b(?:top|left|width|height)\s*:/);
expect(fullscreenRule).not.toMatch(/\binset\s*:/);
});
it("repositions a Shorts controller when playback moves the video on-screen", async () => {
vi.useFakeTimers();
bootInject({ url: "https://www.youtube.com/shorts/example" });
await settleLifecycle();
const player = document.createElement("div");
player.className = "html5-video-player";
const video = document.createElement("video");
const playerRect = makeRect(40, 64, 351, 624);
let videoRect = makeRect(40, -560, 351, 624);
setRect(player, playerRect);
setBoxMetrics(player, playerRect.width, playerRect.height);
video.getBoundingClientRect = () => videoRect;
video.src = "blob:https://www.youtube.com/shorts";
player.appendChild(video);
document.body.appendChild(player);
window.ensureController(video, player);
expect(video.vsc.div.style.getPropertyValue("top")).toBe("-624px");
videoRect = playerRect;
video.dispatchEvent(new Event("playing"));
await vi.advanceTimersByTimeAsync(0);
expect(video.vsc.div.style.getPropertyValue("top")).toBe("0px");
});
it("targets the controller nearest the pointer unless change-all is selected", async () => { it("targets the controller nearest the pointer unless change-all is selected", async () => {
bootInject(); bootInject();
await settleLifecycle(); await settleLifecycle();
@@ -638,6 +678,25 @@ describe("inject.js media/controller lifecycle regressions", () => {
controller.controllerHostCleanup(); controller.controllerHostCleanup();
}); });
it("keeps a visible player-local host when Firefox fullscreens the page root", async () => {
bootInject();
await settleLifecycle();
const { mount, controller, wrapper } = createControlledVideo();
setRect(document.documentElement, makeRect(0, 0, 0, 0));
Object.defineProperty(document, "fullscreenElement", {
configurable: true,
value: document.documentElement
});
window.syncControllerFullscreenMount(controller);
expect(wrapper.parentElement).toBe(mount);
expect(wrapper.classList.contains("vsc-geometry-hidden")).toBe(false);
expect(document.documentElement.style.position).toBe("");
expect(document.documentElement.style.isolation).toBe("");
});
it("only promotes the directly-fullscreen video's controller", async () => { it("only promotes the directly-fullscreen video's controller", async () => {
bootInject(); bootInject();
await settleLifecycle(); await settleLifecycle();