diff --git a/src/components/PageHeader/ItemPage.astro b/src/components/PageHeader/ItemPage.astro index 6213d90494..96c8481678 100644 --- a/src/components/PageHeader/ItemPage.astro +++ b/src/components/PageHeader/ItemPage.astro @@ -19,7 +19,7 @@ if (titleAuthor) { } --- -
+
{topic.name} + value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().toLowerCase(); + /** * Convert Reference description to one-line description * @param description String description @@ -51,6 +59,34 @@ const getOneLineDescription = (description: string): string => { export const ReferenceDirectoryWithFilter = ({ categoryData, }: ReferenceDirectoryProps) => { + const [searchTerm, setSearchTerm] = useState(""); + + const filteredCategoryData = useMemo(() => { + const normalizedQuery = normalizeSearchText(searchTerm); + + if (!normalizedQuery) { + return categoryData; + } + + return categoryData + .map((category) => ({ + ...category, + subcats: category.subcats + .map((subcat) => ({ + ...subcat, + entries: subcat.entries.filter((entry) => { + const searchableText = `${entry.data.title} ${entry.data.description}`; + + return normalizeSearchText(searchableText).includes( + normalizedQuery, + ); + }), + })) + .filter((subcat) => subcat.entries.length > 0), + })) + .filter((category) => category.subcats.length > 0); + }, [categoryData, searchTerm]); + const renderEntries = (entries: ReferenceDirectoryEntry[]) => entries.length === 0 ? null : (
@@ -95,8 +131,8 @@ export const ReferenceDirectoryWithFilter = ({ }; const getSubcatHeading = ( - subcat: { name: string; entry?: any }, - category: { name: string }, + subcat: ReferenceDirectorySubcategory, + category: ReferenceDirectoryCategory, ) => { if (!subcatShouldHaveHeading(subcat, category)) { return null; @@ -121,7 +157,15 @@ export const ReferenceDirectoryWithFilter = ({ }; const renderCategoryData = () => { - return categoryData.map((category) => ( + if (filteredCategoryData.length === 0) { + return ( +

+ No matching references found. +

+ ); + } + + return filteredCategoryData.map((category) => (

+
+ + { + setSearchTerm(event.currentTarget.value); + }} + placeholder="Search references" + aria-label="Search references" + class="w-full rounded-[20px] border border-sidebar-type-color bg-body-color px-md py-3 text-body-large placeholder-sidebar-type-color focus:outline-0" + /> +
{renderCategoryData()}

); diff --git a/src/components/SearchForm/index.astro b/src/components/SearchForm/index.astro index 36107a1944..6c5629c5cb 100644 --- a/src/components/SearchForm/index.astro +++ b/src/components/SearchForm/index.astro @@ -20,7 +20,7 @@ const t = await getUiTranslator(currentLocale); type="search" placeholder={t("Search") as string} name="term" - class="border-accent-type-color border outline-offset-0 placeholder-accent-type-color bg-transparent pl-gutter-md py-[6px] md:py-[4px] lg:py-[8px] h-full w-full rounded-[20px] peer text-base" + class="border-accent-type-color border outline-offset-0 placeholder-accent-type-color bg-transparent pl-gutter-md py-[6px] md:py-[4px] lg:py-[8px] h-full w-full rounded-[20px] peer text-3xl" aria-label="Search through site content" required /> diff --git a/src/layouts/ReferenceLayout.astro b/src/layouts/ReferenceLayout.astro index 021d338f01..ca9e945aca 100644 --- a/src/layouts/ReferenceLayout.astro +++ b/src/layouts/ReferenceLayout.astro @@ -153,6 +153,7 @@ const pageJumpToState: JumpToState = { diff --git a/test/components/ReferenceDirectoryWithFilter.test.tsx b/test/components/ReferenceDirectoryWithFilter.test.tsx new file mode 100644 index 0000000000..ddbfc4c1eb --- /dev/null +++ b/test/components/ReferenceDirectoryWithFilter.test.tsx @@ -0,0 +1,109 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/preact"; +import { expect, it, suite } from "vitest"; +import { ReferenceDirectoryWithFilter } from "@components/ReferenceDirectoryWithFilter"; + +suite("ReferenceDirectoryWithFilter", () => { + it("filters by title and description, case-insensitively", () => { + render( + Loads and plays a video element.

", + }, + }, + ], + }, + { + name: "audio", + entries: [ + { + id: "2", + slug: "volume", + body: "", + collection: "reference", + data: { + module: "Shape", + submodule: "audio", + file: "volume.mdx", + line: 1, + deprecated: "", + params: [], + path: "p5/volume", + title: "volume()", + description: "

Controls the media volume for video playback.

", + }, + }, + ], + }, + ], + }, + { + name: "p5.Graphics", + subcats: [ + { + name: "misc", + entries: [ + { + id: "3", + slug: "alpha", + body: "", + collection: "reference", + data: { + module: "Shape", + submodule: "misc", + file: "alpha.mdx", + line: 1, + deprecated: "", + params: [], + path: "p5/alpha", + title: "alpha()", + description: "

No matching term here.

", + }, + }, + ], + }, + ], + }, + ]} + />, + ); + + const searchInput = screen.getByLabelText("Search references"); + + expect(screen.getByText("p5.MediaElement")).toBeTruthy(); + expect(screen.getByText("p5.Graphics")).toBeTruthy(); + expect(screen.getByText("video")).toBeTruthy(); + expect(screen.getByText("audio")).toBeTruthy(); + + fireEvent.input(searchInput, { target: { value: "VIDEO" } }); + + expect(screen.getByText("p5.MediaElement")).toBeTruthy(); + expect(screen.queryByText("p5.Graphics")).toBeNull(); + expect(screen.getByText("video")).toBeTruthy(); + expect(screen.getByText("audio")).toBeTruthy(); + expect(screen.getByText("createVideo()")).toBeTruthy(); + expect(screen.getByText("volume()")).toBeTruthy(); + expect(screen.queryByText("alpha()")).toBeNull(); + + cleanup(); + }); +}); \ No newline at end of file