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.
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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<number, Video[]>) =>
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1174,7 +1174,9 @@ function PlaylistDetail() {
|
||||
cover plus a fourth block under it, for roughly twice this height. */}
|
||||
<div className="px-4 py-2 max-w-5xl mx-auto">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-20 h-11.25 rounded-lg bg-card overflow-hidden shrink-0">
|
||||
{/* 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. */}
|
||||
<div className="hidden w-20 h-11.25 rounded-lg bg-card overflow-hidden shrink-0 sm:block">
|
||||
{items[0]?.thumbnail_url && (
|
||||
<RemoteImage
|
||||
src={items[0].thumbnail_url}
|
||||
@@ -1217,16 +1219,17 @@ function PlaylistDetail() {
|
||||
className="inline-flex items-center gap-1.5 text-2xs text-accent mr-1"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
|
||||
{t("playlists.unsynced")}
|
||||
<span className="sr-only xl:not-sr-only">{t("playlists.unsynced")}</span>
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => shown.length && openPlayer(shown[0]!.id)}
|
||||
disabled={!shown.length}
|
||||
label={t("playlists.playAll")}
|
||||
icon={<Play className="fill-current" />}
|
||||
>
|
||||
{t("playlists.playAll")}
|
||||
<span className="hidden sm:inline">{t("playlists.playAll")}</span>
|
||||
</Button>
|
||||
{/* 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. */}
|
||||
|
||||
@@ -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 (
|
||||
<div ref={ref} className="flex items-center gap-1 mt-1 flex-wrap xl:gap-2">
|
||||
<span role="status" className="text-xs whitespace-nowrap">
|
||||
<span className="font-medium">{t("playlists.selectedCount", { count })}</span>
|
||||
<span className="sr-only font-medium sm:not-sr-only">
|
||||
{t("playlists.selectedCount", { count })}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
title={t("playlists.selectedCount", { count })}
|
||||
className="font-medium sm:hidden"
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
{hidden > 0 && (
|
||||
<span className="text-muted">
|
||||
<span aria-hidden="true" className="hidden xl:inline">
|
||||
@@ -117,7 +127,7 @@ export function SelectionBand({
|
||||
<span className="hidden xl:inline">{t("playlists.clearView")}</span>
|
||||
</Button>
|
||||
)}
|
||||
<div className="ml-auto">{trailing}</div>
|
||||
<div className="ml-auto hidden sm:block">{trailing}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user