fix(player): the stacked layout's minimum width no longer clips the wide one
The 36rem floor guards the stacked layout's width/height feedback loop; the wide side-panel card has no such loop, and on a short wide window the floor made the video taller than the card, cutting off YouTube's control bar. The floor now applies to the stacked layout only; e2e covers a 1400x380 window.
This commit is contained in:
@@ -29,7 +29,8 @@ test.describe("player modal", () => {
|
|||||||
|
|
||||||
// The video's size, in the windows the owner meets: layout bugs no DOM test can see. Each viewport
|
// The video's size, in the windows the owner meets: layout bugs no DOM test can see. Each viewport
|
||||||
// is a real one — 1920×911 is a 1080p screen at 100% (the side-panel layout), 1097×520 the same
|
// is a real one — 1920×911 is a 1080p screen at 100% (the side-panel layout), 1097×520 the same
|
||||||
// screen at 175% or a small laptop at 100% (the stacked layout, on a short window).
|
// screen at 175% or a small laptop at 100% (the stacked layout, on a short window), 1400×380 a wide
|
||||||
|
// window made short by docked devtools (the side-panel layout, whose card does not scroll).
|
||||||
test.describe("player modal sizing", () => {
|
test.describe("player modal sizing", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.route(/youtube\.com/, (route) => route.abort());
|
await page.route(/youtube\.com/, (route) => route.abort());
|
||||||
@@ -41,6 +42,7 @@ test.describe("player modal sizing", () => {
|
|||||||
for (const [w, h] of [
|
for (const [w, h] of [
|
||||||
[1920, 911],
|
[1920, 911],
|
||||||
[1097, 520],
|
[1097, 520],
|
||||||
|
[1400, 380],
|
||||||
] as const) {
|
] as const) {
|
||||||
test(`the video keeps a real size at ${w}×${h}, and maximised fills the window`, async ({
|
test(`the video keeps a real size at ${w}×${h}, and maximised fills the window`, async ({
|
||||||
page,
|
page,
|
||||||
@@ -54,6 +56,16 @@ test.describe("player modal sizing", () => {
|
|||||||
const stage = await box(page, ".player-stage");
|
const stage = await box(page, ".player-stage");
|
||||||
expect(stage.width).toBeGreaterThanOrEqual(Math.min(w * 0.5, 560));
|
expect(stage.width).toBeGreaterThanOrEqual(Math.min(w * 0.5, 560));
|
||||||
expect(stage.height).toBeGreaterThan(stage.width * 0.5);
|
expect(stage.height).toBeGreaterThan(stage.width * 0.5);
|
||||||
|
// …and all of it is in sight: its bottom edge is the YouTube control bar, and the side-panel
|
||||||
|
// card clips what overflows it (it does not scroll), the stacked card scrolls to it.
|
||||||
|
const overflow = await page
|
||||||
|
.locator(".player-stage")
|
||||||
|
.first()
|
||||||
|
.evaluate((el) => {
|
||||||
|
const card = el.parentElement!;
|
||||||
|
return el.getBoundingClientRect().bottom - card.getBoundingClientRect().bottom;
|
||||||
|
});
|
||||||
|
expect(overflow).toBeLessThanOrEqual(0.5);
|
||||||
|
|
||||||
// Maximise (F): the stage is fixed to the viewport. In the side-panel layout its `self-start`
|
// Maximise (F): the stage is fixed to the viewport. In the side-panel layout its `self-start`
|
||||||
// once aligned the fixed box too, and it shrank to its content's height — 0 px, a thin line.
|
// once aligned the fixed box too, and it shrank to its content's height — 0 px, a thin line.
|
||||||
|
|||||||
@@ -512,7 +512,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|||||||
expect(screen.queryByRole("button", { name: "player.description" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "player.description" })).toBeNull();
|
||||||
// The row's width: the video at the window's height, plus the panel and the side strips.
|
// The row's width: the video at the window's height, plus the panel and the side strips.
|
||||||
expect(card.parentElement!.style.maxWidth).toBe(
|
expect(card.parentElement!.style.maxWidth).toBe(
|
||||||
"calc(max(36rem, (100dvh - 3rem - 0px) * 16 / 9) + 20rem + 7rem)",
|
"calc((100dvh - 3rem - 0px) * 16 / 9 + 20rem + 7rem)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function canScrollBy(el: HTMLElement | null, dir: -1 | 1): boolean {
|
|||||||
// a rem width, so it grows with the text size; the video takes what is left.
|
// a rem width, so it grows with the text size; the video takes what is left.
|
||||||
const WIDE_PLAYER_PX = 1280;
|
const WIDE_PLAYER_PX = 1280;
|
||||||
const SIDE_PANEL = "20rem";
|
const SIDE_PANEL = "20rem";
|
||||||
// The narrowest the video gets to make room for the chrome under it (see the card's maxWidth).
|
// The narrowest the stacked video gets to make room for the chrome under it (see the card's maxWidth).
|
||||||
const MIN_VIDEO = "36rem";
|
const MIN_VIDEO = "36rem";
|
||||||
|
|
||||||
// The meta line's items each lead with a "·" that parts them from what stands before: the channel name
|
// The meta line's items each lead with a "·" that parts them from what stands before: the channel name
|
||||||
@@ -614,7 +614,7 @@ export default function PlayerModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The card's width budget (its maxWidth, below): the video's, and the side strips' gutters.
|
// The card's width budget (its maxWidth, below): the video's, and the side strips' gutters.
|
||||||
const videoWidth = `max(${MIN_VIDEO}, (100dvh - 3rem - ${below}px) * 16 / 9)`;
|
const fitWidth = `(100dvh - 3rem - ${below}px) * 16 / 9`;
|
||||||
const gutters = hasQueue ? " + 7rem" : "";
|
const gutters = hasQueue ? " + 7rem" : "";
|
||||||
|
|
||||||
// Portaled to <body>, like every other full-screen overlay here. A fixed, full-viewport modal
|
// Portaled to <body>, like every other full-screen overlay here. A fixed, full-viewport modal
|
||||||
@@ -641,14 +641,16 @@ export default function PlayerModal({
|
|||||||
className={`relative flex w-full max-h-full ${hasQueue ? "sm:px-14" : ""}`}
|
className={`relative flex w-full max-h-full ${hasQueue ? "sm:px-14" : ""}`}
|
||||||
// The design width (63rem with the side strips, else 56rem), narrowed so the 16:9 video plus the
|
// The design width (63rem with the side strips, else 56rem), narrowed so the 16:9 video plus the
|
||||||
// chrome under it fits the window's height less the backdrop's padding (`p-6` = 3rem).
|
// chrome under it fits the window's height less the backdrop's padding (`p-6` = 3rem).
|
||||||
// Wide: no design cap — the video is as wide as the window's height allows, plus the side panel.
|
// Wide: no design cap — the video is as wide as the window's height allows, plus the side panel,
|
||||||
// Never narrower than MIN_VIDEO: the chrome under the video grows as the card narrows (its text
|
// whose content sits beside the video (`below` is ~0), so the fit is exact and the card, which
|
||||||
// wraps), so on a short window fitting it was a loop that ran the card down to 2 px (a laptop
|
// does not scroll there, always holds the whole video.
|
||||||
// at 100%, a 1080p screen at 175%). Past the floor the card scrolls instead.
|
// Stacked: never narrower than MIN_VIDEO. The chrome under the video grows as the card narrows
|
||||||
|
// (its text wraps), so on a short window fitting it was a loop that ran the card down to 2 px
|
||||||
|
// (a laptop at 100%, a 1080p screen at 175%). Past the floor the stacked card scrolls instead.
|
||||||
style={{
|
style={{
|
||||||
maxWidth: wide
|
maxWidth: wide
|
||||||
? `calc(${videoWidth} + ${SIDE_PANEL}${gutters})`
|
? `calc(${fitWidth} + ${SIDE_PANEL}${gutters})`
|
||||||
: `min(${hasQueue ? "63rem" : "56rem"}, calc(${videoWidth}${gutters}))`,
|
: `min(${hasQueue ? "63rem" : "56rem"}, calc(max(${MIN_VIDEO}, ${fitWidth})${gutters}))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasQueue && (
|
{hasQueue && (
|
||||||
|
|||||||
Reference in New Issue
Block a user