diff --git a/.changeset/smart-carrots-exist.md b/.changeset/smart-carrots-exist.md new file mode 100644 index 00000000000..839897728d5 --- /dev/null +++ b/.changeset/smart-carrots-exist.md @@ -0,0 +1,5 @@ +--- +'polaris.shopify.com': minor +--- + +Major refactor of site search diff --git a/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.module.scss b/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.module.scss index 71a10cf0a0c..a22c5234fba 100644 --- a/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.module.scss +++ b/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.module.scss @@ -31,7 +31,7 @@ } @media (hover: hover) { - &.isHighlighted a { + &[data-is-current-result="true"] a { background: var(--search-highlight-color); } } diff --git a/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.tsx b/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.tsx index 4d1cc512cf5..b5bd9bc16b2 100644 --- a/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.tsx +++ b/polaris.shopify.com/src/components/ComponentGrid/ComponentGrid.tsx @@ -1,14 +1,10 @@ import Image from "../Image"; import Link from "next/link"; -import { SearchResultItem } from "../../types"; -import { - className, - getReadableStatusValue, - slugify, -} from "../../utils/various"; +import { getReadableStatusValue, slugify } from "../../utils/various"; import { Status } from "../../types"; import styles from "./ComponentGrid.module.scss"; import StatusBadge from "../StatusBadge"; +import { useGlobalSearchResult } from "../GlobalSearch/GlobalSearch"; interface ComponentGridProps { children: React.ReactNode; @@ -18,7 +14,7 @@ function ComponentGrid({ children }: ComponentGridProps) { return ; } -interface ComponentGridItemProps extends SearchResultItem { +interface ComponentGridItemProps { name: string; description: string; url: string; @@ -29,20 +25,14 @@ function ComponentGridItem({ name, description, url, - searchResultData, status, }: ComponentGridItemProps) { + const searchAttributes = useGlobalSearchResult(); + return ( -
  • +
  • - +
    +
    +
    + {title &&

    {title}

    } +
      {children}
    +
    +
    +
    + ); +} + +interface FoundationsGridItemProps { + title: string; + excerpt: string; + url: string; + icon: JSX.Element; + category: string; +} + +function FoundationsGridItem({ + title, + excerpt, + url, + icon, + category, +}: FoundationsGridItemProps) { + const searchAttributes = useGlobalSearchResult(); + + return ( +
  • + + +
    {icon}
    +

    {title}

    +

    {stripMarkdownLinks(excerpt)}

    +
    + +
  • + ); +} + +FoundationsGrid.Item = FoundationsGridItem; + +export default FoundationsGrid; diff --git a/polaris.shopify.com/src/components/FoundationsGrid/index.ts b/polaris.shopify.com/src/components/FoundationsGrid/index.ts new file mode 100644 index 00000000000..26650d4ca88 --- /dev/null +++ b/polaris.shopify.com/src/components/FoundationsGrid/index.ts @@ -0,0 +1,3 @@ +import FoundationsGrid from "./FoundationsGrid"; + +export default FoundationsGrid; diff --git a/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.module.scss b/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.module.scss index 8b0c9abfc9a..ecdf94d2d4d 100644 --- a/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.module.scss +++ b/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.module.scss @@ -1,125 +1,5 @@ -@import "../../styles/fonts.scss"; -@import "../../styles/variables.scss"; -@import "../../styles/mixins.scss"; - -.FoundationsIndexPage { - h2 { - @include heading-2; - margin-bottom: 1.5rem; - } -} - .Categories { display: flex; flex-direction: column; gap: 3rem; } - -.Category { - ul { - display: flex; - flex-wrap: wrap; - display: grid; - gap: 0.75rem; - // https://evanminto.com/blog/intrinsically-responsive-css-grid-minmax-min/ - grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); - - @media screen and (max-width: $breakpointDesktopLarge) { - // Foundations section currently has 5 articles. This avoids - // a 4 + 1 situation which looks horrible. - grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr)); - } - } - - a { - height: 100%; - position: relative; - padding: 1.25rem 1.25rem 1rem; - display: block; - color: inherit; - border-radius: var(--border-radius-800); - box-shadow: var(--card-shadow); - - &:hover { - box-shadow: var(--card-shadow-hover); - } - - &:focus-visible { - box-shadow: var(--focus-outline); - } - - &:before { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 45px; - margin: auto; - border-radius: var(--border-radius-800) var(--border-radius-800) 0 0; - opacity: 0.25; - } - - .Icon { - position: relative; - margin-bottom: 1rem; - display: flex; - align-items: center; - justify-content: center; - height: 50px; - width: 50px; - border-radius: var(--border-radius-800); - - svg { - filter: brightness(-100%); - opacity: 0.66; - } - - @include dark-mode { - svg { - filter: brightness(1000%); - } - } - } - - h4 { - @include heading-4; - color: var(--text-strong); - overflow: hidden; - width: 100%; - text-overflow: ellipsis; - white-space: pre; - } - - p { - margin: 0; - font-size: var(--font-size-100); - display: -webkit-box; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; - overflow: hidden; - } - } -} - -.Categories { - .Category { - &:nth-child(1) a:before, - &:nth-child(1) .Icon { - background: var(--decorative-1); - } - &:nth-child(2) a:before, - &:nth-child(2) .Icon { - background: var(--decorative-2); - } - &:nth-child(3) a:before, - &:nth-child(3) .Icon { - background: var(--decorative-3); - } - &:nth-child(4) a:before, - &:nth-child(4) .Icon { - background: var(--decorative-4); - } - } -} diff --git a/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.tsx b/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.tsx index af1976656f1..ceb4d95f143 100644 --- a/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.tsx +++ b/polaris.shopify.com/src/components/FoundationsIndexPage/FoundationsIndexPage.tsx @@ -1,8 +1,8 @@ import { foundationsNavItems } from "../../data/navItems"; import styles from "./FoundationsIndexPage.module.scss"; -import Link from "next/link"; import Layout from "../Layout"; import PageMeta from "../PageMeta"; +import FoundationsGrid from "../FoundationsGrid"; interface Props {} @@ -21,30 +21,23 @@ function FoundationsIndexPage({}: Props) { >
    {foundationsNavItems.map((category) => { - const url = category.children && category.children[0].url; - if (!url) return null; + if (!category.children) return null; return ( -
    -
    -

    {category.title}

    - -
    -
    + + {category.children.map((child) => { + if (!child.url) return null; + return ( + + ); + })} + ); })}
    diff --git a/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.module.scss b/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.module.scss index 7cf03101d72..c630c042e0b 100644 --- a/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.module.scss +++ b/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.module.scss @@ -160,41 +160,6 @@ } } -.FoundationsResults { - display: flex; - flex-direction: column; - gap: 8px; -} - -.FoundationsResult { - position: relative; - padding: 1rem; - box-shadow: var(--card-shadow); - border-radius: var(--border-radius-600); - - @media (hover: hover) { - &.isHighlighted { - background: var(--search-highlight-color); - } - } - - a { - display: block; - color: inherit; - } - - h4 { - font-weight: var(--font-weight-500); - font-size: var(--font-size-300); - color: var(--text-strong); - } - - p { - margin-top: 0.1rem; - font-size: var(--font-size-100); - } -} - @keyframes search-results-enter { from { transform: scale(0.98); @@ -207,7 +172,7 @@ } } -.ModalBackdrop { +.PreventBackgroundInteractions { position: fixed; top: 0; right: 0; diff --git a/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.tsx b/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.tsx index 26b405c1fdf..e8068392094 100644 --- a/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.tsx +++ b/polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.tsx @@ -1,9 +1,8 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, createContext, useContext } from "react"; import { search } from "../../utils/search"; import { GroupedSearchResults, SearchResultCategory, - SearchResultItem, SearchResults, } from "../../types"; import styles from "./GlobalSearch.module.scss"; @@ -11,24 +10,49 @@ import { useRouter } from "next/router"; import IconGrid from "../IconGrid"; import ComponentGrid from "../ComponentGrid"; import TokenList from "../TokenList"; -import Link from "next/link"; -import { className, slugify, stripMarkdownLinks } from "../../utils/various"; import { Dialog } from "@headlessui/react"; import { KeyboardEventHandler } from "react"; - -interface Props {} +import FoundationsGrid from "../FoundationsGrid"; +import { foundationsNavItems } from "../../data/navItems"; + +const CATEGORY_NAMES: { [key in SearchResultCategory]: string } = { + components: "Components", + foundations: "Foundations", + tokens: "Tokens", + icons: "Icons", +}; + +const foundationsIcons: { [title: string]: JSX.Element } = {}; +Object.entries(foundationsNavItems).forEach(([, value]) => { + value.children?.forEach((child) => { + foundationsIcons[child.title] = child.icon; + }); +}); + +const SearchContext = createContext({ id: "", currentItemId: "" }); + +export function useGlobalSearchResult() { + const searchContext = useContext(SearchContext); + if (!searchContext.id) return null; + const { id, currentItemId } = searchContext; + + return { + id, + "data-is-global-search-result": true, + "data-is-current-result": currentItemId === id, + tabIndex: -1, + }; +} function scrollToTop() { const overflowEl = document.querySelector(`.${styles.ResultsInner}`); - if (overflowEl) { - overflowEl.scrollTo({ top: 0, behavior: "smooth" }); - } + overflowEl?.scrollTo({ top: 0, behavior: "smooth" }); } function scrollIntoView() { const overflowEl = document.querySelector(`.${styles.ResultsInner}`); const highlightedEl = document.querySelector( - '#search-results [data-is-active-descendant="true"]' + '#search-results [data-is-current-result="true"]' ); if (overflowEl && highlightedEl) { @@ -49,22 +73,18 @@ function scrollIntoView() { } } -function GlobalSearch({}: Props) { - const [searchResults, setSearchResults] = useState(); +function GlobalSearch() { + const [searchResults, setSearchResults] = useState([]); const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const [activeDescendant, setActiveDescendant] = useState(0); + const [currentResultIndex, setCurrentResultIndex] = useState(0); const router = useRouter(); let resultsInRenderedOrder: SearchResults = []; - if (searchResults) { - Object.values(searchResults) - .sort((a, b) => a.maxScore - b.maxScore) - .forEach((group) => { - resultsInRenderedOrder = [...resultsInRenderedOrder, ...group.results]; - }); - } + searchResults.forEach((group) => { + resultsInRenderedOrder = [...resultsInRenderedOrder, ...group.results]; + }); const searchResultsCount = resultsInRenderedOrder.length; @@ -83,17 +103,16 @@ function GlobalSearch({}: Props) { }, []); useEffect(() => { + setCurrentResultIndex(0); setSearchResults(search(searchTerm.trim())); - setActiveDescendant(0); scrollToTop(); }, [searchTerm]); - useEffect(() => scrollIntoView(), [activeDescendant]); + useEffect(() => scrollIntoView(), [currentResultIndex]); useEffect(() => { - const handler = () => { - setIsOpen(false); - }; + const handler = () => setIsOpen(false); + router.events.on("beforeHistoryChange", handler); router.events.on("hashChangeComplete", handler); @@ -114,51 +133,30 @@ function GlobalSearch({}: Props) { ) => { switch (evt.code) { case "ArrowDown": - if (activeDescendant < searchResultsCount - 1) { - setActiveDescendant(activeDescendant + 1); + if (currentResultIndex < searchResultsCount - 1) { + setCurrentResultIndex(currentResultIndex + 1); evt.preventDefault(); } break; case "ArrowUp": - if (activeDescendant > 0) { - setActiveDescendant(activeDescendant - 1); + if (currentResultIndex > 0) { + setCurrentResultIndex(currentResultIndex - 1); evt.preventDefault(); } break; case "Enter": - setIsOpen(false); - const url = resultsInRenderedOrder[activeDescendant].url; - router.push(url); + if (resultsInRenderedOrder.length > 0) { + setIsOpen(false); + const url = resultsInRenderedOrder[currentResultIndex].url; + router.push(url); + } break; } }; - const getItemId = (resultIndex: number): string => { - return `result${slugify(resultsInRenderedOrder[resultIndex].url)}`; - }; - - const getItemProps = ({ - resultIndex, - }: { - resultIndex: number; - }): SearchResultItem => { - const isHighlighted = resultIndex === activeDescendant; - return { - searchResultData: { - isHighlighted, - itemAttributes: { - id: getItemId(resultIndex), - "data-is-active-descendant": isHighlighted, - }, - tabIndex: -1, - url: resultsInRenderedOrder[resultIndex].url, - }, - }; - }; - - let resultIndex = -1; + const currentItemId = resultsInRenderedOrder[currentResultIndex]?.id || ""; return ( <> @@ -172,182 +170,181 @@ function GlobalSearch({}: Props) { setIsOpen(false)}> -
    +
    - <> - - {isOpen && ( -
    -
    - -
    - - setSearchTerm(evt.target.value)} - role="combobox" - aria-controls="search-results" - aria-expanded={searchResultsCount > 0} - aria-activedescendant={ - searchResultsCount > 0 - ? getItemId(activeDescendant) - : undefined - } - onKeyDown={handleKeyboardNavigation} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - spellCheck={false} - placeholder="Search" - /> - + + {isOpen && ( +
    +
    +
    - )} -
    - {searchResults && - Object.entries(searchResults) - .sort((a, b) => a[1].maxScore - b[1].maxScore) - .map(([category]) => { - const typedCategory = category as SearchResultCategory; - - switch (typedCategory) { - case "Foundations": - const results = searchResults[typedCategory].results; - if (results.length === 0) return null; - return ( - -
    - {results.map((result) => { - resultIndex++; - const { searchResultData } = getItemProps({ - resultIndex, - }); - return ( -
  • - - -

    {result.meta.title}

    -

    - {stripMarkdownLinks( - result.meta.excerpt - )} -

    -
    - -
  • - ); - })} -
    -
    - ); - - case "Components": { - const results = searchResults[typedCategory].results; - if (results.length === 0) return null; - return ( - - - {results.map((result) => { - resultIndex++; - return ( - - ); - })} - - - ); - } - - case "Tokens": { - const results = searchResults[typedCategory].results; - if (results.length === 0) return null; - return ( - - - {results.map((result) => { - resultIndex++; - return ( - - ); - })} - - - ); - } - - case "Icons": { - const results = searchResults[typedCategory].results; - if (results.length === 0) return null; - - return ( - - - {results.map((result) => { - resultIndex++; - return ( - - ); - })} - - - ); - } - } - })} + setSearchTerm(evt.target.value)} + role="combobox" + aria-controls="search-results" + aria-expanded={searchResultsCount > 0} + aria-activedescendant={currentItemId} + onKeyDown={handleKeyboardNavigation} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck={false} + placeholder="Search" + /> +
    - - + )} +
    + {searchResults && ( + + )} +
    +
    ); } +function SearchResults({ + searchResults, + currentItemId, +}: { + searchResults: GroupedSearchResults; + currentItemId: string; +}) { + return ( + <> + {searchResults.map(({ category, results }) => { + if (results.length === 0) return null; + switch (category) { + case "foundations": + return ( + + + {results.map(({ id, url, meta }) => { + if (!meta.foundations) return null; + const { title, excerpt, category } = meta.foundations; + const icon = foundationsIcons[title]; + return ( + + + + ); + })} + + + ); + + case "components": { + return ( + + + {results.map(({ id, url, meta }) => { + if (!meta.components) return null; + const { name, description, status } = meta.components; + return ( + + + + ); + })} + + + ); + } + + case "tokens": { + return ( + + + {results.map(({ id, meta }) => { + if (!meta.tokens) return null; + const { token, category } = meta.tokens; + return ( + + + + ); + })} + + + ); + } + + case "icons": { + return ( + + + {results.map(({ id, meta }) => { + if (!meta.icons) return null; + const { icon } = meta.icons; + return ( + + + + ); + })} + + + ); + } + + default: + return []; + } + })} + + ); +} + function SearchIcon() { return ( @@ -360,15 +357,15 @@ function SearchIcon() { } function ResultsGroup({ - title, + category, children, }: { - title: string; + category: SearchResultCategory; children: React.ReactNode; }) { return (
    -

    {title}

    +

    {CATEGORY_NAMES[category]}

    {children}
    ); diff --git a/polaris.shopify.com/src/components/IconGrid/IconGrid.module.scss b/polaris.shopify.com/src/components/IconGrid/IconGrid.module.scss index 1e092c3c254..f54114e5555 100644 --- a/polaris.shopify.com/src/components/IconGrid/IconGrid.module.scss +++ b/polaris.shopify.com/src/components/IconGrid/IconGrid.module.scss @@ -2,12 +2,24 @@ .IconGrid { border-radius: var(--border-radius-600); - border: 1px solid var(--border-color-light); - overflow: hidden; + position: relative; + + &:before { + content: ""; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-shadow: inset 0 0 0 1px var(--border-color-light), + 0 0 0 5px var(--surface); + pointer-events: none; + border-radius: var(--border-radius-600); + } } .IconGridInner { - margin: 0 -1px -1px 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); } @@ -21,10 +33,6 @@ border-right: 1px solid var(--border-color-light); gap: 1.125rem; - &:focus-visible { - box-shadow: var(--focus-outline-inside); - } - p { flex: 1; white-space: nowrap; @@ -32,9 +40,18 @@ text-overflow: ellipsis; } + &:focus-visible, &.isSelected, &:hover { background: var(--search-highlight-color); + box-shadow: none; + } + + @media (hover: hover) { + &[data-is-current-result="true"] { + background: var(--search-highlight-color); + box-shadow: none; + } } } diff --git a/polaris.shopify.com/src/components/IconGrid/IconGrid.tsx b/polaris.shopify.com/src/components/IconGrid/IconGrid.tsx index 7656a40ed60..0feb972e010 100644 --- a/polaris.shopify.com/src/components/IconGrid/IconGrid.tsx +++ b/polaris.shopify.com/src/components/IconGrid/IconGrid.tsx @@ -1,6 +1,6 @@ import Image from "../Image"; +import { useGlobalSearchResult } from "../GlobalSearch/GlobalSearch"; import { className } from "../../utils/various"; -import { SearchResultItem } from "../../types"; import styles from "./IconGrid.module.scss"; import { Icon } from "@shopify/polaris-icons/metadata"; import Link from "next/link"; @@ -21,19 +21,16 @@ function IconGrid({ title, children }: IconGridProps) { ); } -interface IconGridItemProps extends SearchResultItem { +interface IconGridItemProps { icon: Icon; query?: string; activeIcon?: string; } -function IconGridItem({ - icon, - activeIcon, - query, - searchResultData, -}: IconGridItemProps) { +function IconGridItem({ icon, activeIcon, query }: IconGridItemProps) { const { id, name, description } = icon; + const searchAttributes = useGlobalSearchResult(); + return (
  • {({ columns }) => ( {columns.preview && ( - {searchResultData?.url && ( - + {isClickableSearchResult && ( + View token @@ -159,7 +160,7 @@ function TokenListItem({ >