Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/PageHeader/ItemPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (titleAuthor) {
}
---

<div class="px-5 md:px-lg pt-5xl lg:pt-3xl pb-sm">
<div class="px-5 md:px-lg pt-[11.25rem] lg:pt-3xl pb-sm">
<div class="pb-xs flex flex-nowrap gap-2.5">
<a href={topic.url}>{topic.name}</a>
<span class="grid place-items-center"
Expand Down
82 changes: 71 additions & 11 deletions src/components/ReferenceDirectoryWithFilter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo, useState } from "preact/hooks";
import type { ReferenceDocContentItem } from "@/src/content/types";
import flask from "@src/content/ui/images/icons/flask.svg?raw";
import warning from "@src/content/ui/images/icons/warning.svg?raw";
Expand All @@ -10,17 +11,24 @@ type ReferenceDirectoryEntry = ReferenceDocContentItem & {
};
};

type ReferenceDirectorySubcategory = {
name: string;
entry?: ReferenceDirectoryEntry;
entries: ReferenceDirectoryEntry[];
};

type ReferenceDirectoryCategory = {
name: string;
subcats: ReferenceDirectorySubcategory[];
};

type ReferenceDirectoryProps = {
categoryData: {
name: string;
subcats: {
name: string;
entry?: ReferenceDirectoryEntry;
entries: ReferenceDirectoryEntry[];
}[];
}[];
categoryData: ReferenceDirectoryCategory[];
};

const normalizeSearchText = (value: string): string =>
value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().toLowerCase();

/**
* Convert Reference description to one-line description
* @param description String description
Expand Down Expand Up @@ -51,6 +59,34 @@ const getOneLineDescription = (description: string): string => {
export const ReferenceDirectoryWithFilter = ({
categoryData,
}: ReferenceDirectoryProps) => {
const [searchTerm, setSearchTerm] = useState("");

const filteredCategoryData = useMemo<ReferenceDirectoryCategory[]>(() => {
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 : (
<div class="content-grid">
Expand Down Expand Up @@ -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;
Expand All @@ -121,7 +157,15 @@ export const ReferenceDirectoryWithFilter = ({
};

const renderCategoryData = () => {
return categoryData.map((category) => (
if (filteredCategoryData.length === 0) {
return (
<p class="text-body-large mt-[var(--gutter-lg)]">
No matching references found.
</p>
);
}

return filteredCategoryData.map((category) => (
<section key={category.name}>
<h2
class={
Expand All @@ -145,6 +189,22 @@ export const ReferenceDirectoryWithFilter = ({

return (
<div class="-top-[75px] mx-5 min-h-[50vh] md:mx-lg">
<div class="mb-[var(--gutter-lg)] max-w-[32rem]">
<label class="sr-only" for="reference-directory-filter">
Search references
</label>
<input
id="reference-directory-filter"
type="search"
value={searchTerm}
onInput={(event) => {
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"
/>
</div>
{renderCategoryData()}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/SearchForm/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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
/>
Expand Down
1 change: 1 addition & 0 deletions src/layouts/ReferenceLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const pageJumpToState: JumpToState = {

<BaseLayout title={Astro.props.title} mainContentParentClass="mx-0" jumpToState={pageJumpToState}>
<ReferenceDirectoryWithFilter
client:load
categoryData={categoryData as any}
/>
</BaseLayout>
109 changes: 109 additions & 0 deletions test/components/ReferenceDirectoryWithFilter.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<ReferenceDirectoryWithFilter
categoryData={[
{
name: "p5.MediaElement",
subcats: [
{
name: "video",
entries: [
{
id: "1",
slug: "createVideo",
body: "",
collection: "reference",
data: {
module: "Shape",
submodule: "video",
file: "createVideo.mdx",
line: 1,
deprecated: "",
params: [],
path: "p5/createVideo",
title: "createVideo()",
description: "<p>Loads and plays a video element.</p>",
},
},
],
},
{
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: "<p>Controls the media volume for video playback.</p>",
},
},
],
},
],
},
{
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: "<p>No matching term here.</p>",
},
},
],
},
],
},
]}
/>,
);

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();
});
});