fix(ui,i18n): E30 review round 1 — audit rows in the reader's language, one HU name per thing
- audit log: a job run named from its result data (nested phases too), a skipped run says so, a secret's reset reads as a reset, choices named as Configuration names them, interval / batch / counts localized; the inbox shares the same jobRunText - HU: remux is "Közvetlen közvetítés" (it shared "Közvetlen lejátszás" with direct play); backfill is behúzás, enrich is dúsítás, collection is kollekció, the allow-list is fehérlista; the last "jobnak" / "transzkódolás"; a test holds the options of one choice to distinct names - contract: every skipped / reason word a job returns is named in both languages - person page: the bio wait is timed from the open and a failing ask stops the polling; ClampedText (ui) shared with the collection page, whose short summary lost its dead More - a playlist names season 0 the Specials; the channel re-slot compares in the reader's alphabet; an invite's date is in the admin's language; the language-ordered lists are asked again once a language switch is saved (saveLanguage) - poster grids' column minimum in rem; lint refuses minmax(Npx,…)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A clamped text offers More only when it is actually cut (E30 S6 on the person page, shared with the
|
||||
// collection page in the E30 review, where a one-line summary carried a More that did nothing).
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { ClampedText } from "./ClampedText";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
const view = (text: string) => (
|
||||
<ClampedText text={text} lines={3} moreLabel="More" lessLabel="Less" />
|
||||
);
|
||||
|
||||
describe("ClampedText", () => {
|
||||
it("offers no toggle for a text that fits", () => {
|
||||
render(view("Short."));
|
||||
expect(screen.queryByText("More")).toBeNull(); // jsdom: nothing is cut
|
||||
});
|
||||
|
||||
it("offers More for a cut text, and Less once open", () => {
|
||||
const tall = vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(400);
|
||||
const box = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(60);
|
||||
try {
|
||||
render(view("A long text.\n\nOn and on."));
|
||||
const p = screen.getByText(/A long text/);
|
||||
expect(p.className).toContain("line-clamp-3");
|
||||
fireEvent.click(screen.getByText("More"));
|
||||
expect(screen.getByText("Less").getAttribute("aria-expanded")).toBe("true");
|
||||
expect(p.className).not.toContain("line-clamp-3");
|
||||
} finally {
|
||||
tall.mockRestore();
|
||||
box.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { Button } from "./Button";
|
||||
|
||||
// Static class names, so Tailwind sees them.
|
||||
const CLAMP = { 3: "line-clamp-3", 8: "line-clamp-8" } as const;
|
||||
|
||||
/** A long text (a biography, a summary) clamped to a few lines, with a More / Less toggle offered only
|
||||
* when the text is actually cut — measured, so a short text carries no dead button. Re-measured on a
|
||||
* resize: the window and the text-size slider both move where the cut falls. */
|
||||
export function ClampedText({
|
||||
text,
|
||||
lines,
|
||||
moreLabel,
|
||||
lessLabel,
|
||||
className,
|
||||
}: {
|
||||
text: string;
|
||||
lines: keyof typeof CLAMP;
|
||||
moreLabel: string;
|
||||
lessLabel: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [clipped, setClipped] = useState(false);
|
||||
const ref = useRef<HTMLParagraphElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || expanded) return;
|
||||
const measure = () => setClipped(el.scrollHeight > el.clientHeight + 1);
|
||||
measure();
|
||||
const ro = new ResizeObserver(measure);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [text, expanded]);
|
||||
return (
|
||||
<div className={className}>
|
||||
<p
|
||||
ref={ref}
|
||||
className={`measure whitespace-pre-line text-sm leading-relaxed text-muted ${expanded ? "" : CLAMP[lines]}`}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
{(clipped || expanded) && (
|
||||
<Button
|
||||
variant="link"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="mt-1 text-xs"
|
||||
>
|
||||
{expanded ? lessLabel : moreLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ domain-free.
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| overlays & anchoring | `AnchoredPopover` + `anchoredPopoverHooks` (the popover engine: arrows, typeahead, Escape, anchor-flip), `ActionMenu`, `ContextMenu` + `useContextMenu` (the SAME menu, opened by right-click / long-press / Shift+F10 / a row's `MoreButton` "…" via `buttonProps`; `anchor` for a panel a row opens; `PageMenu` for a page's background menu), `Modal` (+ `ModalFooter`; `initialFocus` by purpose, C4), `Overlay`, `SidePanel`, `Tooltip`, `Toaster` (+ `toastLevels`, `overlayOrder`; `lib/toastDock` docks one control under the stack, in every host), `CommandPalette`, `KeyboardHelp` |
|
||||
| actions & inputs | `Button`, `IconButton`, `Pressable` (`selectable` when its text is a title or a name), `Chip` + `chipStyles`, `DragHandle` + `OrderButtons` / `useOrderItems` (the non-drag reorder, WCAG 2.5.7) (one per button family, docs/DESIGN.md; a raw `<button>` or a bare `FocusSafeButton` outside the control primitives — eslint `BUTTON_PRIMITIVES` — is a lint error), `Input` + `fieldStyles`, `NativeControls`, `MenuCheck` (the drawn check of a menu row or a whole-row checkbox button), `SelectTrigger` (the field family; a raw `<input>` is a lint error), `useReturnFocus`, `surfaces` (the B5 glass roles; a bare glass class is a lint error), `Badge` (a static label), `LinkButton` / `IconLink` (a link wearing the button face) over `buttonStyles`, `Skeleton` (B8 first loads, through `LoadingState shape`), `NavRail` (B7), `form`, `DraftSaveBar`, `Combobox`, `FilterSelect`, `SegmentedToggle`, `SortControl`, `ColumnSortControl`, `PriorityStepper`, `UndoToolbar` |
|
||||
| data display | `DataTable`, `tableHeader` + `tableAlign`, `HiddenFilterCue` (names a filter whose column is off screen), `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup`, `RelativeTime` ("3d ago" with the exact minute on hover, docs/DESIGN.md "Copy") |
|
||||
| data display | `DataTable`, `tableHeader` + `tableAlign`, `HiddenFilterCue` (names a filter whose column is off screen), `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup`, `RelativeTime` ("3d ago" with the exact minute on hover, docs/DESIGN.md "Copy"), `ClampedText` (a long text clamped to a few lines; More / Less only when it is cut) |
|
||||
| page & panel structure | `PageShell` + `pageToolbarSlot` + `pageHeading`, `PageScroller`, `Headings` (`PageHeading`, `SectionLabel`), `SkipLink` + `landmarks`, `FilterFold` (B4), `PanelGroup`, `PanelGroups`, `Tabs`, `BackToTop` |
|
||||
| state & messaging | `QueryState`, `Banner`, `InlineNotice` |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user