From 9b76d2f96cef5e95453be6bc7b93d388a379e338 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 1 Nov 2026 23:07:42 +0100 Subject: [PATCH] fix(playlists): the list's name keeps its room on a narrow window Below xl Unsynced is its dot; on a phone the cover is gone, Play all is its glyph, the band's count is its number and the undo pair stays out. The name was 0 px wide in Hungarian at 768 and on any phone, and a tick moved the list there. --- docs/DESIGN.md | 2 + .../modules/playlists/Playlists.dom.test.tsx | 46 ++++++++++++++++++- frontend/src/modules/playlists/Playlists.tsx | 9 ++-- .../src/modules/playlists/SelectionBand.tsx | 16 +++++-- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 693cd4e5..a08097fe 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -546,6 +546,7 @@ of a full-width panel and nothing on the right. Delete takes the selection when there is one, else the row under the keyboard, with no confirm. Below `xl` the band's buttons are glyphs named in their tooltips, and a filter's hidden count is "+N" with its sentence in the tooltip. The row the band replaces keeps its height, invisible, so a tick moves no row. + On a phone the count is its number (the words read out, and in the tooltip) and the undo pair stays out. - **Motion:** 150–300ms micro-interactions, transform/opacity only; `prefers-reduced-motion` is respected. [WCAG 2.3.3] @@ -848,3 +849,4 @@ of a full-width panel and nothing on the right. | 2026-11-01 | Selection band (owner D1 a, D3 a, D4 a) | The toolbar's second row while rows are selected, no floating bar: move (the last target as one click) and remove, no copy; both undo back to the places, a video the target held before stays there; a single row's move undoes too. | | 2026-11-01 | Grip on a phone (owner A) | Below a 24rem table a playlist row has no grip: the title keeps its width, and the row menu keeps the moves. | | 2026-11-01 | Selection band on a tablet (owner A) | Below `xl` the band is glyphs on one row, the names in tooltip and aria-label, as on the channel header; the replaced row stays in the same grid cell, invisible, so the first tick moves no row under the second tap. | +| 2026-11-01 | Playlist heading on a narrow window (owner A) | The list's name had no width left: below `xl` Unsynced is its dot (the word read out, the hint in the tooltip); on a phone the cover is gone and Play all is its glyph, named in the tooltip. | diff --git a/frontend/src/modules/playlists/Playlists.dom.test.tsx b/frontend/src/modules/playlists/Playlists.dom.test.tsx index 79d13f60..d2aabb4d 100644 --- a/frontend/src/modules/playlists/Playlists.dom.test.tsx +++ b/frontend/src/modules/playlists/Playlists.dom.test.tsx @@ -1134,6 +1134,43 @@ describe("Playlists — selecting rows", () => { }); }); +describe("Playlists — the list's name keeps its room on a narrow window", () => { + const head = (lists: Record) => + mount( + lists, + {}, + { + detail: async (id: number) => ({ + ...list(id, lists[id] ?? []), + yt_playlist_id: "PLx", + dirty: true, + }), + }, + ); + + it("says Unsynced by its dot below xl: the word still read out, the hint in the tooltip", async () => { + head({ 1: [vid("A", 0)] }); + const word = await screen.findByText("playlists.unsynced"); + expect(word.classList.contains("sr-only")).toBe(true); + expect(word.classList.contains("xl:not-sr-only")).toBe(true); + expect(word.parentElement!.getAttribute("title")).toBe("playlists.unsyncedHint"); + }); + + it("drops Play all's word and the cover on a phone, and keeps the button's name", async () => { + head({ 1: [vid("A", 0)] }); + const play = await screen.findByRole("button", { name: "playlists.playAll" }); + expect(play.getAttribute("title")).toBe("playlists.playAll"); + const word = [...play.querySelectorAll("span")].find( + (s) => s.textContent === "playlists.playAll", + )!; + expect(word.classList.contains("hidden")).toBe(true); + expect(word.classList.contains("sm:inline")).toBe(true); + const cover = document.querySelector('[class*="h-11.25"]')!; + expect(cover.classList.contains("hidden")).toBe(true); + expect(cover.classList.contains("sm:block")).toBe(true); + }); +}); + describe("Playlists — acting on the selected rows", () => { // A move remembers its target on the server, and an answer from nowhere would be the last toast. beforeEach(() => void vi.spyOn(api, "savePrefs").mockResolvedValue({} as never)); @@ -1500,7 +1537,14 @@ describe("Playlists — acting on the selected rows", () => { expect(short.getAttribute("aria-hidden")).toBe("true"); expect(short.getAttribute("title")).toBe("playlists.selectedHidden"); expect(short.classList.contains("xl:hidden")).toBe(true); - expect(onlyWide(screen.getByText("playlists.selectedCount"))).toBe(false); + const words = screen.getByText("playlists.selectedCount"); + expect(words.classList.contains("sr-only")).toBe(true); + expect(words.classList.contains("sm:not-sr-only")).toBe(true); + const number = screen.getByTitle("playlists.selectedCount"); + expect(number.getAttribute("aria-hidden")).toBe("true"); + expect(number.classList.contains("sm:hidden")).toBe(true); + const undo = screen.getByRole("button", { name: "common.undo" }); + expect(undo.closest("[class~='sm:block']")).toBeTruthy(); }); }); diff --git a/frontend/src/modules/playlists/Playlists.tsx b/frontend/src/modules/playlists/Playlists.tsx index 8e2fff67..cd75360f 100644 --- a/frontend/src/modules/playlists/Playlists.tsx +++ b/frontend/src/modules/playlists/Playlists.tsx @@ -1174,7 +1174,9 @@ function PlaylistDetail() { cover plus a fourth block under it, for roughly twice this height. */}
-
+ {/* Off on a phone, and the state's word and Play all's shrink: past them the list's name had + no width left at all. */} +
{items[0]?.thumbnail_url && ( - {t("playlists.unsynced")} + {t("playlists.unsynced")} )} {/* Rename, the pin, YouTube (open / push / revert) and Delete: the list's ONE menu, the same the rail row opens — the header keeps Play all. */} diff --git a/frontend/src/modules/playlists/SelectionBand.tsx b/frontend/src/modules/playlists/SelectionBand.tsx index e359b800..767afafc 100644 --- a/frontend/src/modules/playlists/SelectionBand.tsx +++ b/frontend/src/modules/playlists/SelectionBand.tsx @@ -7,7 +7,8 @@ import { Button } from "../../components/ui/Button"; * `count` is the rows shown, what an action covers; `hidden` the selected ones a filter hides, which * it leaves alone. With only hidden ones the actions stay in place, off, and the way out stays on. * Below `xl` the buttons are glyphs named in their tooltips: in words the row wraps there, and the - * first tick would push the list from under the second tap. */ + * first tick would push the list from under the second tap. On a phone the count is its number and + * the undo pair stays out, for the same one row. */ export function SelectionBand({ ref, count, @@ -40,7 +41,16 @@ export function SelectionBand({ return (
- {t("playlists.selectedCount", { count })} + + {t("playlists.selectedCount", { count })} + + {hidden > 0 && (
); }