feat(ui): the six native selects become the house controls
Owner's pick from the S7 mockups (2026-09-19): a short fixed set is a SegmentedToggle — the live search's result count, the table's rows per page, a download profile's mode (on its own row, so the three worded choices fit a phone) and audio format — and a longer set or the user's own list is a FilterSelect: the profile's quality (seven heights) and the download dialog's profile. A lint rule refuses a native <select> from now on (the canonical "FE control" row). The px-frozen size rules also caught up with a fraction: `text-[9.5px]` had slipped past the integer-only pattern in the Glass tuner, which is rem now.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
|
||||
vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (k: string) => k }) }));
|
||||
|
||||
import Pager from "./Pager";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// Rows per page is a short fixed set, so it is the house's segmented control (debt burn 2026-09),
|
||||
// not a native <select>: one named group, the current size pressed, "All" as size 0.
|
||||
describe("Pager · rows per page", () => {
|
||||
it("offers each size and All as one named group, and reports the pick", () => {
|
||||
const onSizeChange = vi.fn();
|
||||
render(
|
||||
<Pager
|
||||
page={0}
|
||||
size={25}
|
||||
total={120}
|
||||
pageSizeOptions={[10, 25, 50, 100]}
|
||||
onPageChange={() => {}}
|
||||
onSizeChange={onSizeChange}
|
||||
sizeLabel="Rows per page"
|
||||
/>,
|
||||
);
|
||||
const group = screen.getByRole("group", { name: "Rows per page" });
|
||||
expect(group.querySelector('[aria-pressed="true"]')?.textContent).toBe("25");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "50" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "datatable.all" }));
|
||||
expect(onSizeChange.mock.calls).toEqual([[50], [0]]);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { hasPagerContent, pageCount } from "./pagerContent";
|
||||
import { FocusSafeButton } from "./FocusSafeButton";
|
||||
import SegmentedToggle from "./SegmentedToggle";
|
||||
|
||||
// The Prev / page-input / Next + "N per page" cluster, lifted out of DataTable so a non-table view
|
||||
// (the channel manager's card layout) can paginate the same way. Controlled: the caller owns page +
|
||||
@@ -93,24 +94,22 @@ export default function Pager({
|
||||
</div>
|
||||
)}
|
||||
{total > 0 && (
|
||||
<label className="flex items-center gap-2 text-muted">
|
||||
{sizeLabel}
|
||||
<select
|
||||
value={size}
|
||||
onChange={(e) => {
|
||||
onSizeChange(Number(e.target.value));
|
||||
<div className="flex items-center gap-2 text-muted">
|
||||
<span aria-hidden="true">{sizeLabel}</span>
|
||||
{/* A handful of fixed sizes plus "All": segmented, the house control for a short fixed set. */}
|
||||
<SegmentedToggle
|
||||
ariaLabel={sizeLabel}
|
||||
value={String(size)}
|
||||
onChange={(v) => {
|
||||
onSizeChange(Number(v));
|
||||
scrollListToTop();
|
||||
}}
|
||||
className="bg-surface border border-border rounded-md px-1.5 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
|
||||
>
|
||||
{pageSizeOptions.map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
<option value={0}>{t("datatable.all")}</option>
|
||||
</select>
|
||||
</label>
|
||||
options={[
|
||||
...pageSizeOptions.map((n) => ({ value: String(n), label: String(n) })),
|
||||
{ value: "0", label: t("datatable.all") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user