From bd338244294e44ca916562e94d48a0ebed0ba18a Mon Sep 17 00:00:00 2001 From: Silver Date: Thu, 4 Jun 2026 17:31:33 -0400 Subject: [PATCH 1/7] feat: adding fuseJS to enable a new search component for the criteria and how-to test pages --- package-lock.json | 14 +++++++ package.json | 1 + .../content-display/content-display.tsx | 41 +++++++++++-------- .../custom-components/cards/cards.tsx | 5 ++- .../custom-components/search/search.scss | 19 +++++++++ .../custom-components/search/search.tsx | 36 ++++++++++++++++ src/hooks/useSearch.ts | 31 ++++++++++++++ 7 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 src/components/custom-components/search/search.scss create mode 100644 src/components/custom-components/search/search.tsx create mode 100644 src/hooks/useSearch.ts diff --git a/package-lock.json b/package-lock.json index e0784127..2cdf0331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "classnames": "^2.5.1", + "fuse.js": "^7.4.1", "graphql": "^16.9.0", "highlight.js": "^11.10.0", "lit": "^3.2.1", @@ -10544,6 +10545,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.1.tgz", + "integrity": "sha512-AY7lKAXK71hi3WgUvDy6oZL67UEHOOtvCAwVdOXHyJd6ZzftBy7QqxuXt4HxmmAhYjmp/YCuOELZtIvAdlZ+fw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 9b102643..5e7eb6d9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "classnames": "^2.5.1", + "fuse.js": "^7.4.1", "graphql": "^16.9.0", "highlight.js": "^11.10.0", "lit": "^3.2.1", diff --git a/src/components/content-display/content-display.tsx b/src/components/content-display/content-display.tsx index 5186bfa7..35475c50 100644 --- a/src/components/content-display/content-display.tsx +++ b/src/components/content-display/content-display.tsx @@ -7,6 +7,7 @@ import Button from 'components/custom-components/buttons/button/button'; import { useClipboard } from 'hooks/useClipboard'; import { useContentTabs } from 'hooks/useContentTabs'; import { useCriteriaManager } from 'hooks/useCriteriaManager'; +import { useSearch } from 'hooks/useSearch'; import React, { useEffect, useRef } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { Icons } from 'shared/Icons'; @@ -16,7 +17,7 @@ import Cards from '../custom-components/cards/cards'; import { SideNavItem } from '../navigation/nav.types'; import MarkdownContent from './markdown-content/markdown-content'; import { Criteria } from './markdown-content/markdown-content.types'; - +import SearchBar from '../custom-components/search/search'; import Breadcrumb from '../breadcrumb/breadcrumb'; import { useBreadcrumbs } from 'hooks/useBreadcrumbs'; import '../../styles/_code-blocks.scss'; @@ -35,7 +36,7 @@ const ContentDisplay: React.FC = ({ items, onToggleSideNav, }) => { - const [searchParams] = useSearchParams(); + const [pageParams] = useSearchParams(); const navigate = useNavigate(); const location = useLocation(); const tabsRef = useRef(null); @@ -52,10 +53,12 @@ const ContentDisplay: React.FC = ({ const breadcrumbs = useBreadcrumbs(documentation, items); + const { query, setQuery, results } = useSearch(currentItem?.children ?? []); + useEffect(() => { if (!tabs.length) return; - const tabFromURL = searchParams.get('tab'); + const tabFromURL = pageParams.get('tab'); if (tabFromURL) { const tabIndex = parseInt(tabFromURL, 10); if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex < tabs.length) { @@ -64,10 +67,10 @@ const ContentDisplay: React.FC = ({ } else { setActiveTab(0); } - }, [searchParams, setActiveTab, tabs.length]); + }, [pageParams, setActiveTab, tabs.length]); useEffect(() => { - const tabFromURL = searchParams.get('tab'); + const tabFromURL = pageParams.get('tab'); if (tabFromURL) { const tabIndex = parseInt(tabFromURL, 10); if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex < tabs.length) { @@ -76,7 +79,7 @@ const ContentDisplay: React.FC = ({ } else { setActiveTab(0); } - }, [searchParams, setActiveTab, tabs.length]); + }, [pageParams, setActiveTab, tabs.length]); // Track active tab changes useEffect(() => { @@ -106,6 +109,8 @@ const ContentDisplay: React.FC = ({ if (!currentItem) return
No content available
; const { label, generalNotes, children } = currentItem; + const cardItems = results ?? children; // content to render in overview cards + const cardsListId = `overviewCard-${label}`; let actionsButtonsVisible = Object.values(Criteria).some( (criteria) => criteria === tabs[activeTab]?.label @@ -135,16 +140,20 @@ const ContentDisplay: React.FC = ({ id="criteria-button"> - {isOverviewRoute && children && children.length > 0 && ( - ({ - title: child.label, - description: child.generalNotes || undefined, - link: `${location.pathname.replace(/\/overview$/, '')}/${ - child.name - }`, - }))} - /> + {isOverviewRoute && cardItems && cardItems.length > 0 && ( + <> + + ({ + title: child.label, + description: child.generalNotes || undefined, + link: `${location.pathname.replace(/\/overview$/, '')}/${ + child.name + }`, + }))} + /> + )} {!isOverviewRoute && tabs.length > 0 && ( diff --git a/src/components/custom-components/cards/cards.tsx b/src/components/custom-components/cards/cards.tsx index 127a937e..e7bf909d 100644 --- a/src/components/custom-components/cards/cards.tsx +++ b/src/components/custom-components/cards/cards.tsx @@ -11,11 +11,12 @@ interface Card { interface CardsProps { items: Card[]; + id?: string; } -const Cards: React.FC = ({ items }) => { +const Cards: React.FC = ({ items, id }) => { return ( -
    +
      {items.map((item) => (
    • diff --git a/src/components/custom-components/search/search.scss b/src/components/custom-components/search/search.scss new file mode 100644 index 00000000..ac7d4bcb --- /dev/null +++ b/src/components/custom-components/search/search.scss @@ -0,0 +1,19 @@ +.searchbar { + display: flex; + justify-content: space-between; + align-items: center; + + &__label { + margin-right: 1rem; + font: var(--magentaa11y-typeset-body-xl-strong); + } + + &__input { + height: 50px; + flex-grow: 1; + border-color: var(--color-text-link); + border-radius: 25px; + font: var(--magentaa11y-typeset-body-lg-normal); + padding: 0 1rem; + } +} \ No newline at end of file diff --git a/src/components/custom-components/search/search.tsx b/src/components/custom-components/search/search.tsx new file mode 100644 index 00000000..6402a8c1 --- /dev/null +++ b/src/components/custom-components/search/search.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import './search.scss'; + +interface SearchBarProps { + controlsId: string; + resultCount: number; + query: string; + onQueryChange: (query: string) => void; +} + +const SearchBar: React.FC = ({ controlsId, resultCount, query, onQueryChange }) => { + return ( +
      + + onQueryChange(e.target.value)} + aria-controls={controlsId} + aria-expanded="false" + aria-haspopup="false" + /> + + {query ? `${resultCount} result${resultCount !== 1 ? 's' : ''} found` : ''} + +
      + ); +}; + +export default SearchBar; diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts new file mode 100644 index 00000000..e88d9a42 --- /dev/null +++ b/src/hooks/useSearch.ts @@ -0,0 +1,31 @@ +import { useMemo, useState } from 'react'; +import Fuse from 'fuse.js'; + +interface ContentItem { + label: string; + name: string; + generalNotes?: string | null; + developerNotes?: string | null; +} + +export const useSearch = (items: ContentItem[]) => { + const [query, setQuery] = useState(''); + + const fuse = useMemo( + () => + new Fuse(items, { + keys: ['label', 'developerNotes', 'generalNotes'], + threshold: 0.2, + useTokenSearch: true, // can remove for fuzzier search, but it's kind of nice + includeScore: true, + }), + [items] + ); + + const matches = query ? fuse.search(query).map((r) => r.item) : null; + const results = matches && matches.length === 0 + ? [{ label: 'No results found', name: 'no-results', generalNotes: "No criteria matches this search."}] + : matches; + + return { query, setQuery, results }; +}; From d9307a78ef05ccde33c12fe07b66013323263ffe Mon Sep 17 00:00:00 2001 From: Silver Date: Thu, 4 Jun 2026 17:32:39 -0400 Subject: [PATCH 2/7] fix: one small thing --- src/components/custom-components/search/search.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/custom-components/search/search.tsx b/src/components/custom-components/search/search.tsx index 6402a8c1..e90764dc 100644 --- a/src/components/custom-components/search/search.tsx +++ b/src/components/custom-components/search/search.tsx @@ -11,14 +11,14 @@ interface SearchBarProps { const SearchBar: React.FC = ({ controlsId, resultCount, query, onQueryChange }) => { return (
      -