fix: tag counts, chip focus, upload naming, share links and unsized clips hold after round 2
- A tag counts the followed channels the Subscriptions tab shows: hidden ones are left out, and a subscribe, unsubscribe, hide or tag toggle refreshes the count. The Tag manager's row and its hover list count the same channels as the chip; /api/tags sends both counts under their names. - A chip's count keeps the chip's colour after a dot (a dimmed count fell under 4.5:1). A pick in the open "+N" fold closes it; a chip that leaves the list hands the focus to the one in its place. The fold is a dialog of toggle buttons, not an empty menu. - Upload: a folder name left by a press is made once the press is over, if the dialog is still open, instead of guessing a dismissal from Modal's markup. While the folder is made, its place holds the focus and a live region says so. - Share: an expired link has no Copy and a warn badge; the link made on open goes into the cached list, so a reopen makes no second one; the New link form survives the link made on open. - Watch: a repeated Enter sends one unlock while one is on its way. See all buttons are named with their rail. - An unsized clip of a rotated file gets the upright size its crop cut.
This commit is contained in:
@@ -9,7 +9,13 @@ import { act, render, screen, fireEvent, cleanup } from "@testing-library/react"
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||
return { ...actual, useTranslation: () => ({ t: (k: string) => k }) };
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (k: string, o?: { action?: string; title?: string }) =>
|
||||
o?.title ? `${o.action}: ${o.title}` : k,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import { Carousel } from "./Carousel";
|
||||
@@ -54,7 +60,10 @@ describe("Carousel", () => {
|
||||
);
|
||||
expect(screen.getByText("card-a")).toBeTruthy();
|
||||
expect(screen.getByRole("heading", { name: "Recently added" })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.seeAll" }));
|
||||
// Named with its rail, the visible words first: a page of rails has a See all on each.
|
||||
const seeAll = screen.getByRole("button", { name: "common.seeAll: Recently added" });
|
||||
expect(seeAll.textContent).toBe("common.seeAll");
|
||||
fireEvent.click(seeAll);
|
||||
expect(onSeeAll).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
@@ -109,7 +109,21 @@ export function Carousel({
|
||||
<div className="mb-3 flex items-baseline gap-2">
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
{onSeeAll && (
|
||||
<Button variant="link-muted" onClick={onSeeAll} className="ml-auto gap-0.5 text-xs">
|
||||
<Button
|
||||
variant="link-muted"
|
||||
onClick={onSeeAll}
|
||||
// Named with its rail: a page of rails has a See all on each, and a screen reader's
|
||||
// list of buttons (or a voice command) must tell them apart. The visible words lead.
|
||||
aria-label={
|
||||
label
|
||||
? t("common.seeAllOf", {
|
||||
action: seeAllLabel ?? t("common.seeAll"),
|
||||
title: label,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
className="ml-auto gap-0.5 text-xs"
|
||||
>
|
||||
{seeAllLabel ?? t("common.seeAll")}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
@@ -231,7 +231,6 @@ export default function Modal({
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
data-dismiss=""
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</IconButton>
|
||||
|
||||
Reference in New Issue
Block a user