Page the Font Awesome search results instead of accumulating them (task 9ca0bc36)

"Show more" now advances a page index and renders exactly one 40-icon
window (with Previous to go back), so repeated clicks never mount more
than one page. Adds a test that pages through all matches for "-" and
asserts every page stays bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 18:06:24 -04:00
co-authored by Claude Opus 5.5
parent 532bda5c74
commit 5f52def422
3 changed files with 55 additions and 16 deletions
+2
View File
@@ -642,4 +642,6 @@
.defs-show-more {
align-self: flex-start;
display: flex;
gap: 8px;
}
+33 -3
View File
@@ -1609,7 +1609,7 @@ describe("DefinitionsTab Font Awesome search", () => {
expect(within(s).queryByRole("radiogroup", { name: "Font Awesome search results" })).toBeNull();
});
it("a query swaps the quick picks for bounded, labelled results with Show more", async () => {
it("a query swaps the quick picks for one bounded, labelled page of results", async () => {
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "New" }));
const s = sheet();
@@ -1619,10 +1619,17 @@ describe("DefinitionsTab Font Awesome search", () => {
expect(within(s).queryByRole("radiogroup", { name: "Font Awesome" })).toBeNull();
expect(resultKeys(s)).toHaveLength(40);
expect(within(results(s)).getAllByRole("radio")[0].textContent).toBe(searchFaIcons("a")[0].label);
expect(within(s).getByText(`Showing 40 of ${total}`)).toBeTruthy();
expect(within(s).getByText(`Showing 1–40 of ${total}`)).toBeTruthy();
expect(within(s).queryByRole("button", { name: "Previous" })).toBeNull();
// "Show more" replaces the page rather than appending to it.
const firstPage = resultKeys(s);
fireEvent.click(within(s).getByRole("button", { name: "Show more" }));
expect(resultKeys(s)).toHaveLength(80);
expect(resultKeys(s)).toEqual(searchFaIcons("a").slice(40, 80).map((icon) => `fa:${icon.name}`));
expect(resultKeys(s).some((key) => firstPage.includes(key))).toBe(false);
expect(within(s).getByText(`Showing 41–80 of ${total}`)).toBeTruthy();
fireEvent.click(within(s).getByRole("button", { name: "Previous" }));
expect(resultKeys(s)).toEqual(firstPage);
// A new query starts again from the first page.
fireEvent.change(searchInput(s), { target: { value: "dog" } });
@@ -1634,6 +1641,29 @@ describe("DefinitionsTab Font Awesome search", () => {
expect(within(s).getByRole("radiogroup", { name: "Font Awesome" })).toBeTruthy();
});
it("paging through every match never mounts more than one page", async () => {
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "New" }));
const s = sheet();
fireEvent.change(searchInput(s), { target: { value: "-" } });
const all = searchFaIcons("-").map((icon) => `fa:${icon.name}`);
expect(all.length).toBeGreaterThan(900);
const seen: string[] = [];
for (;;) {
expect(results(s).children.length).toBeGreaterThan(0);
expect(results(s).children.length).toBeLessThanOrEqual(40);
seen.push(...resultKeys(s));
const more = within(s).queryByRole("button", { name: "Show more" });
if (!more) break;
fireEvent.click(more);
}
// Every match stays reachable, one page at a time, in order.
expect(seen).toEqual(all);
const lastStart = Math.floor((all.length - 1) / 40) * 40 + 1;
expect(within(s).getByText(`Showing ${lastStart}–${all.length} of ${all.length}`)).toBeTruthy();
});
it("selecting a search result POSTs fa:<name> and renders on its row", async () => {
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "New" }));
+20 -13
View File
@@ -92,7 +92,7 @@ const REPEATS_BY_RULE: Record<DefinitionRule["rule_type"], Repeats> = {
/** How many upcoming dates the edit sheet previews. */
const PREVIEW_COUNT = 5;
/** Font Awesome search results shown at once, and added per "Show more". */
/** Font Awesome search results per page; only one page is ever mounted. */
const FA_RESULTS_PAGE = 40;
/** Quiet period after the last rule edit before the preview is requested. */
@@ -498,7 +498,9 @@ function EditSheet({
: target.assignees.filter((a) => !activeChildren.some((child) => child.id === a.id));
const [pickerOpen, setPickerOpen] = useState(false);
const faResults = useMemo(() => searchFaIcons(draft.faText), [draft.faText]);
const [faShown, setFaShown] = useState(FA_RESULTS_PAGE);
const [faPage, setFaPage] = useState(0);
const faStart = faPage * FA_RESULTS_PAGE;
const faEnd = Math.min(faStart + FA_RESULTS_PAGE, faResults.length);
const noIcon = draft.icon === null && !draft.emoji && !draft.faText.trim();
const [saving, setSaving] = useState(false);
const [confirming, setConfirming] = useState(false);
@@ -676,7 +678,7 @@ function EditSheet({
placeholder="Search or type an icon name, e.g. dog"
value={draft.faText}
onChange={(e) => {
setFaShown(FA_RESULTS_PAGE);
setFaPage(0);
update({ faText: e.target.value, icon: faTypedIconKey(e.target.value), emoji: "" });
}}
/>
@@ -693,7 +695,7 @@ function EditSheet({
role="radiogroup"
aria-label="Font Awesome search results"
>
{faResults.slice(0, faShown).map((icon) => {
{faResults.slice(faStart, faEnd).map((icon) => {
const { name, label } = icon;
const on = faIconName(draft.icon) === name;
return (
@@ -716,16 +718,21 @@ function EditSheet({
})}
</div>
<p className="defs-hint">
Showing {Math.min(faShown, faResults.length)} of {faResults.length}
Showing {faStart + 1}–{faEnd} of {faResults.length}
</p>
{faResults.length > faShown ? (
<button
type="button"
className="defs-retry defs-show-more"
onClick={() => setFaShown((shown) => shown + FA_RESULTS_PAGE)}
>
Show more
</button>
{faResults.length > FA_RESULTS_PAGE ? (
<div className="defs-show-more">
{faPage > 0 ? (
<button type="button" className="defs-retry" onClick={() => setFaPage((page) => page - 1)}>
Previous
</button>
) : null}
{faEnd < faResults.length ? (
<button type="button" className="defs-retry" onClick={() => setFaPage((page) => page + 1)}>
Show more
</button>
) : null}
</div>
) : null}
</>
) : null