fix(player): YouTube's fullscreen button stays clickable on a short player
The click layer over the embed left the bottom 22 % of the stage free. YouTube's new embed keeps its fullscreen button 80-128 px above the iframe's bottom, so on a stage under ~580 px (a small window, a browser zoomed to 150 %) the layer lay over it and a click paused instead. The cutouts now have px floors (56 top, 140 bottom) and are measured against the iframe's own box.
This commit is contained in:
@@ -444,6 +444,33 @@ describe("PlayerModal — the side arrows", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// YouTube's embed keeps its fullscreen button at a fixed 80–128 px above the iframe's bottom, its gear
|
||||
// and volume along the top. A cutout given only as a share of the height lay over that button on a
|
||||
// short player (a small window, a browser zoomed to 150 %): the click paused instead.
|
||||
describe("PlayerModal — the click layer over the video", () => {
|
||||
const layer = () => {
|
||||
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
||||
const A = vid("A", 0);
|
||||
renderModal(
|
||||
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
||||
);
|
||||
return screen.getByTestId("yt-click-layer");
|
||||
};
|
||||
|
||||
it("leaves YouTube's own controls a px floor at the top and the bottom, whatever the height", () => {
|
||||
const el = layer();
|
||||
expect(el.style.top).toBe("max(12%, 56px)");
|
||||
expect(el.style.bottom).toBe("max(22%, 140px)");
|
||||
});
|
||||
|
||||
it("measures those edges against the iframe's box, not the stage under our docked bar", () => {
|
||||
const el = layer();
|
||||
expect(el.parentElement!.classList.contains("player-stage")).toBe(false);
|
||||
expect(el.parentElement!.closest(".player-stage")).toBeTruthy();
|
||||
expect(el.parentElement!.className).toMatch(/(^|\s)(relative|absolute)(\s|$)/);
|
||||
});
|
||||
});
|
||||
|
||||
// At 133% zoom (a 685px window) a full-width card ran 20px past the bottom and cut its channel
|
||||
// row. The card's width is capped from the window height less what sits under the video, measured.
|
||||
describe("PlayerModal — fits the window's height", () => {
|
||||
|
||||
@@ -106,6 +106,13 @@ const WIDE_PLAYER_PX = 1280;
|
||||
const SIDE_PANEL = "20rem";
|
||||
// The narrowest the stacked video gets to make room for the chrome under it (see the card's maxWidth).
|
||||
const MIN_VIDEO = "36rem";
|
||||
// YouTube's embed draws its controls at fixed CSS px from the iframe's edges, whatever its size: the
|
||||
// volume, captions and gear along the top, the fullscreen button above the seek bar 80–128 px up from
|
||||
// the bottom. The click layer's cutouts never shrink below those, else on a short player (a small window,
|
||||
// a browser zoomed to 150 % or more) the layer lies over that button and a click on it pauses instead.
|
||||
// Px, not rem: the text-size slider does not grow YouTube's controls.
|
||||
const YT_TOP_CONTROLS_PX = 56;
|
||||
const YT_BOTTOM_CONTROLS_PX = 140;
|
||||
|
||||
// The meta line's items each lead with a "·" that parts them from what stands before: the channel name
|
||||
// on its line. In the wide panel the meta has a line of its own, where the first "·" would lead nothing.
|
||||
@@ -689,35 +696,44 @@ export default function PlayerModal({
|
||||
which would drop any class on mountRef itself — the iframe then sizes to this wrapper. */}
|
||||
<div
|
||||
className={
|
||||
maximized || isFullscreen ? "absolute inset-x-0 top-0 bottom-14" : "w-full h-full"
|
||||
maximized || isFullscreen
|
||||
? "absolute inset-x-0 top-0 bottom-14"
|
||||
: "relative w-full h-full"
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={mountRef}
|
||||
className={`w-full h-full ${playerError != null ? "invisible" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
{/* Interaction layer over the CENTRE of the video: catches click (play/pause) + wheel
|
||||
volume (the wheel event bubbles from here to the modal's listener — over the bare
|
||||
cross-origin iframe it would be swallowed) and stops the iframe stealing keyboard focus.
|
||||
It leaves the top AND bottom edges uncovered so YouTube's edge controls stay reachable.
|
||||
It leaves the top AND bottom edges uncovered so YouTube's edge controls stay reachable,
|
||||
measured against the iframe it sits in (in the big views that excludes our docked bar)
|
||||
and never thinner than those controls (see YT_*_CONTROLS_PX).
|
||||
Armed in the big views too (that is what keeps wheel volume working after F, the reported
|
||||
gap) — it only yields to `nativeControls`, i.e. once the user opens YouTube's own
|
||||
settings menu (focus moves into the iframe), so that menu is navigable end to end.
|
||||
Hidden on error so the "Open on YouTube" CTA is clickable. */}
|
||||
{playerError == null && (
|
||||
<div
|
||||
data-testid="yt-click-layer"
|
||||
onClick={() => {
|
||||
focusModal();
|
||||
togglePlay();
|
||||
}}
|
||||
title={t("player.shortcutsHint")}
|
||||
aria-label={t("player.shortcutsHint")}
|
||||
className={`absolute inset-x-0 top-[12%] bottom-[22%] z-base cursor-pointer ${
|
||||
style={{
|
||||
top: `max(12%, ${YT_TOP_CONTROLS_PX}px)`,
|
||||
bottom: `max(22%, ${YT_BOTTOM_CONTROLS_PX}px)`,
|
||||
}}
|
||||
className={`absolute inset-x-0 z-base cursor-pointer ${
|
||||
nativeControls ? "pointer-events-none" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Our own control bar for the big views (maximise / our fullscreen), where the card's
|
||||
controls are off-screen. It's IN the stage (so it survives true fullscreen, where a
|
||||
<body> portal would not) and DOCKED in its own strip: the video area above is inset by
|
||||
|
||||
Reference in New Issue
Block a user