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 { .defs-show-more {
align-self: flex-start; 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(); 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(); await renderReady();
fireEvent.click(screen.getByRole("button", { name: "New" })); fireEvent.click(screen.getByRole("button", { name: "New" }));
const s = sheet(); const s = sheet();
@@ -1619,10 +1619,17 @@ describe("DefinitionsTab Font Awesome search", () => {
expect(within(s).queryByRole("radiogroup", { name: "Font Awesome" })).toBeNull(); expect(within(s).queryByRole("radiogroup", { name: "Font Awesome" })).toBeNull();
expect(resultKeys(s)).toHaveLength(40); expect(resultKeys(s)).toHaveLength(40);
expect(within(results(s)).getAllByRole("radio")[0].textContent).toBe(searchFaIcons("a")[0].label); 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" })); 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. // A new query starts again from the first page.
fireEvent.change(searchInput(s), { target: { value: "dog" } }); 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(); 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 () => { it("selecting a search result POSTs fa:<name> and renders on its row", async () => {
await renderReady(); await renderReady();
fireEvent.click(screen.getByRole("button", { name: "New" })); 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. */ /** How many upcoming dates the edit sheet previews. */
const PREVIEW_COUNT = 5; 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; const FA_RESULTS_PAGE = 40;
/** Quiet period after the last rule edit before the preview is requested. */ /** 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)); : target.assignees.filter((a) => !activeChildren.some((child) => child.id === a.id));
const [pickerOpen, setPickerOpen] = useState(false); const [pickerOpen, setPickerOpen] = useState(false);
const faResults = useMemo(() => searchFaIcons(draft.faText), [draft.faText]); 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 noIcon = draft.icon === null && !draft.emoji && !draft.faText.trim();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [confirming, setConfirming] = useState(false); const [confirming, setConfirming] = useState(false);
@@ -676,7 +678,7 @@ function EditSheet({
placeholder="Search or type an icon name, e.g. dog" placeholder="Search or type an icon name, e.g. dog"
value={draft.faText} value={draft.faText}
onChange={(e) => { onChange={(e) => {
setFaShown(FA_RESULTS_PAGE); setFaPage(0);
update({ faText: e.target.value, icon: faTypedIconKey(e.target.value), emoji: "" }); update({ faText: e.target.value, icon: faTypedIconKey(e.target.value), emoji: "" });
}} }}
/> />
@@ -693,7 +695,7 @@ function EditSheet({
role="radiogroup" role="radiogroup"
aria-label="Font Awesome search results" aria-label="Font Awesome search results"
> >
{faResults.slice(0, faShown).map((icon) => { {faResults.slice(faStart, faEnd).map((icon) => {
const { name, label } = icon; const { name, label } = icon;
const on = faIconName(draft.icon) === name; const on = faIconName(draft.icon) === name;
return ( return (
@@ -716,16 +718,21 @@ function EditSheet({
})} })}
</div> </div>
<p className="defs-hint"> <p className="defs-hint">
Showing {Math.min(faShown, faResults.length)} of {faResults.length} Showing {faStart + 1}–{faEnd} of {faResults.length}
</p> </p>
{faResults.length > faShown ? ( {faResults.length > FA_RESULTS_PAGE ? (
<button <div className="defs-show-more">
type="button" {faPage > 0 ? (
className="defs-retry defs-show-more" <button type="button" className="defs-retry" onClick={() => setFaPage((page) => page - 1)}>
onClick={() => setFaShown((shown) => shown + FA_RESULTS_PAGE)} Previous
> </button>
Show more ) : null}
</button> {faEnd < faResults.length ? (
<button type="button" className="defs-retry" onClick={() => setFaPage((page) => page + 1)}>
Show more
</button>
) : null}
</div>
) : null} ) : null}
</> </>
) : null ) : null