From 083cd8317a74db83f92a37c036fb592c78b321df Mon Sep 17 00:00:00 2001 From: Sunny Zanchi Date: Wed, 16 Oct 2024 16:58:07 +0000 Subject: [PATCH] feat: search redesign (#1097) feat: redesigned search experience --- .../src/components/GlobalHeader.js | 184 ++++++--------- .../src/components/GlobalSearch.js | 214 ++++++++++++++++++ .../src/components/GlobalStyles/colors.js | 1 + .../src/components/GlobalStyles/themes.js | 8 + .../SearchDropdown/KeyboardLegend.js | 58 +++++ .../src/components/SearchDropdown/Results.js | 166 ++++++++++++++ .../SearchDropdown/SearchDropdown.js | 138 +++++++++++ .../src/components/SearchDropdown/Skeleton.js | 42 ++++ .../src/components/SearchDropdown/index.js | 18 ++ .../src/components/SearchInput.js | 54 ++++- .../src/components/SearchModal/useSearch.js | 14 +- .../src/icons/newrelic.js | 6 + .../src/icons/newrelic/arrow-down.js | 22 ++ .../src/icons/newrelic/arrow-go-to.js | 22 ++ .../src/icons/newrelic/arrow-up.js | 21 ++ .../src/utils/constants.js | 35 ++- 16 files changed, 875 insertions(+), 128 deletions(-) create mode 100644 packages/gatsby-theme-newrelic/src/components/GlobalSearch.js create mode 100644 packages/gatsby-theme-newrelic/src/components/SearchDropdown/KeyboardLegend.js create mode 100644 packages/gatsby-theme-newrelic/src/components/SearchDropdown/Results.js create mode 100644 packages/gatsby-theme-newrelic/src/components/SearchDropdown/SearchDropdown.js create mode 100644 packages/gatsby-theme-newrelic/src/components/SearchDropdown/Skeleton.js create mode 100644 packages/gatsby-theme-newrelic/src/components/SearchDropdown/index.js create mode 100644 packages/gatsby-theme-newrelic/src/icons/newrelic/arrow-down.js create mode 100644 packages/gatsby-theme-newrelic/src/icons/newrelic/arrow-go-to.js create mode 100644 packages/gatsby-theme-newrelic/src/icons/newrelic/arrow-up.js diff --git a/packages/gatsby-theme-newrelic/src/components/GlobalHeader.js b/packages/gatsby-theme-newrelic/src/components/GlobalHeader.js index 4e0252e80..dfbddbb2d 100644 --- a/packages/gatsby-theme-newrelic/src/components/GlobalHeader.js +++ b/packages/gatsby-theme-newrelic/src/components/GlobalHeader.js @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import useMedia from 'use-media'; import path from 'path'; import { css } from '@emotion/react'; -import { graphql, useStaticQuery, Link } from 'gatsby'; +import { graphql, useStaticQuery } from 'gatsby'; import { useLocation } from '@reach/router'; + import AnnouncementBanner from './AnnouncementBanner'; import DarkModeToggle from './DarkModeToggle'; import ExternalLink from './ExternalLink'; @@ -13,77 +14,24 @@ import Dropdown from './Dropdown'; import NewRelicLogo from './NewRelicLogo'; import Icon from './Icon'; import GlobalNavLink from './GlobalNavLink'; -import SearchInput from './SearchInput'; -import SearchModal from './SearchModal'; -import useQueryParams from '../hooks/useQueryParams'; +import GlobalSearch from './GlobalSearch'; + +import { HEADER_LINKS, NR_SITES } from '../utils/constants'; import useThemeTranslation from '../hooks/useThemeTranslation'; -import useHasMounted from '../hooks/useHasMounted'; import useInstrumentedHandler from '../hooks/useInstrumentedHandler'; -export const NR_SITES = { - DOCS: 'DOCS', - COMMUNITY: 'COMMUNITY', - LEARN: 'LEARN', -}; - -const HEADER_LINKS = new Map(); - -HEADER_LINKS.set(NR_SITES.DOCS, { - text: 'Docs', - href: 'https://docs.newrelic.com/', -}) - .set(NR_SITES.COMMUNITY, { - text: 'Community', - href: 'https://discuss.newrelic.com/', - }) - .set(NR_SITES.LEARN, { - text: 'Learn', - href: 'https://learn.newrelic.com/', - }); - -const createNavList = (listType, activeSite = null) => { - const navList = []; - HEADER_LINKS.forEach(({ text, href }) => { - switch (listType) { - case 'main': - navList.push( -
  • - - {text} - -
  • - ); - break; - case 'dropdown': - navList.push( - - {text} - - ); - break; - } - }); - return navList; -}; - // removes the site nav from the header in favor of the search bar // swaps out logo into collapsable nav const NAV_BREAKPOINT = '1070px'; const LOGO_TEXT_BREAKPOINT = '460px'; -const LAYOUT_BREAKPOINT = '1150px'; +const SEARCH_BREAKPOINT = '1355px'; +const SEARCH_BREAKPOINT_2 = '865px'; +const NAVLIST_BREAKPOINT = '1507px'; -const GlobalHeader = ({ className, activeSite, hideSearch = false }) => { - const hasMounted = useHasMounted(); +const GlobalHeader = ({ className, activeSite }) => { const location = useLocation(); - const { queryParams, setQueryParam, deleteQueryParam } = useQueryParams(); const { t } = useThemeTranslation(); + const [mobileSearchOpen, setMobileSearchOpen] = useState(false); const { allLocale: { nodes: locales }, @@ -123,12 +71,6 @@ const GlobalHeader = ({ className, activeSite, hideSearch = false }) => { return ( <> - { - deleteQueryParam('q'); - }} - isOpen={hasMounted && queryParams.has('q')} - />
    { top: 0; z-index: 80; height: var(--global-header-height); - @media screen and (max-width: ${LAYOUT_BREAKPOINT}) and (min-width: ${NAV_BREAKPOINT}) { + @media screen and (max-width: ${NAVLIST_BREAKPOINT}) { grid-template-columns: calc(150px + 1.5rem) minmax(0, 1fr); } @media screen and (max-width: ${mobileBreakpoint}) { @@ -215,7 +157,7 @@ const GlobalHeader = ({ className, activeSite, hideSearch = false }) => { /> - {createNavList('dropdown', activeSite)} + @@ -255,7 +197,7 @@ const GlobalHeader = ({ className, activeSite, hideSearch = false }) => { } `} > - {createNavList('main', activeSite)} + @@ -286,9 +228,14 @@ const GlobalHeader = ({ className, activeSite, hideSearch = false }) => { >
  • { @media screen and (max-width: ${NAV_BREAKPOINT}) { margin-left: 0; } + @media (max-width: ${SEARCH_BREAKPOINT}) { + --search-width: 13.3125rem; + } + @media (max-width: ${SEARCH_BREAKPOINT_2}) { + --search-width: 12rem; + } @media screen and (max-width: ${mobileBreakpoint}) { - display: none; + display: ${mobileSearchOpen ? 'block' : 'none'}; + position: static; } `} > - {!hideSearch && ( - <> - { - setQueryParam('q', ''); - }} - /> - - )} + setMobileSearchOpen(false)} />
  • { } `} > - setMobileSearchOpen(true)} css={css` - color: var(--system-text-primary-dark); - transition: all 0.2s ease-out; align-self: center; - padding-right: 1rem; + color: var(--system-text-primary-dark); display: none; + margin-right: 8px; + padding: 8px; + transition: all 0.2s ease-out; @media screen and (max-width: ${mobileBreakpoint}) { display: block; } - @media screen and (max-width: ${mobileBreakpoint}) { - padding-right: 0.25rem; - } `} > { display: block; `} name="fe-search" - size="1.5rem" + size="1.25rem" /> - + {locales.length > 1 && ( { GlobalHeader.propTypes = { className: PropTypes.string, activeSite: PropTypes.oneOf(Object.values(NR_SITES)), - hideSearch: PropTypes.bool, +}; + +const NavList = ({ listType, activeSite = null }) => { + const navList = []; + HEADER_LINKS.forEach(({ text, href }) => { + switch (listType) { + case 'main': + navList.push( +
  • + + {text} + +
  • + ); + break; + case 'dropdown': + navList.push( + + {text} + + ); + break; + } + }); + return navList; }; export default GlobalHeader; diff --git a/packages/gatsby-theme-newrelic/src/components/GlobalSearch.js b/packages/gatsby-theme-newrelic/src/components/GlobalSearch.js new file mode 100644 index 000000000..6302c56e7 --- /dev/null +++ b/packages/gatsby-theme-newrelic/src/components/GlobalSearch.js @@ -0,0 +1,214 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { navigate } from '@reach/router'; +import { css } from '@emotion/react'; +import { useThrottle } from 'react-use'; + +import useKeyPress from '../hooks/useKeyPress'; +import useThemeTranslation from '../hooks/useThemeTranslation'; +import { addPageAction } from '../utils/nrBrowserAgent'; + +import useSearch from './SearchModal/useSearch'; +import SearchInput from './SearchInput'; +import SearchDropdown, { DEFAULT_FILTER_TYPES } from './SearchDropdown'; + +const GlobalSearch = ({ onClose }) => { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const throttledQuery = useThrottle(query, 300); + const { fetchNextPage, results, status } = useSearch({ + searchTerm: throttledQuery, + filters: DEFAULT_FILTER_TYPES, + }); + // a const assignment here is causing the dev server to fail to build + let recentQueries = []; + try { + recentQueries = JSON.parse(localStorage.getItem(SAVED_SEARCH_KEY) ?? '[]'); + } catch (err) {} + // `null` when we're just in the searchbar and nothing is selected + // otherwise, `selected` is an integer. + const [selected, setSelected] = useState(null); + const possibleSelections = results.length + recentQueries.length; + + const moveUp = () => + setSelected((s) => { + if (s == null) return possibleSelections - 1; + const next = s - 1; + if (next < 0) { + return possibleSelections - 1; + } + return next; + }); + + const moveDown = () => + setSelected((s) => { + if (s == null) return 0; + const next = s + 1; + if (next > possibleSelections - 1) { + return 0; + } + return next; + }); + + useEffect(() => { + setSelected(null); + }, [query]); + + const searchRef = useRef(null); + const { t } = useThemeTranslation(); + + useKeyPress('/', (e) => { + // prevent quick search from opening in Firefox + e.preventDefault(); + // rAF prevents `/` from being typed in input after focusing + requestAnimationFrame(() => searchRef.current?.focus()); + }); + + const showSearchDropdown = query.length > 1 && open; + + return ( + <> + { + setQuery(''); + setOpen(false); + onClose(); + }} + onFocus={() => setOpen(true)} + onMove={(direction) => { + if (direction === 'prev') { + moveUp(); + } else { + moveDown(); + } + }} + onSubmit={(value) => { + if (value.length < 2) return; + if (selected != null) { + if (selected > recentQueries.length - 1) { + const position = selected - recentQueries.length; + const selectedResult = results[position]; + saveSearch(value); + addPageAction({ + eventName: 'swiftypeSearchResult', + category: 'GlobalSearch', + path: location.pathname, + resultCount: results.length, + position, + searchTerm: query, + searchType: 'globalSearch', + url: selectedResult.url, + }); + navigate(selectedResult.url); + } else { + const position = selected; + const recentQuery = recentQueries[position]; + addPageAction({ + eventName: 'savedQuerySearch', + category: 'GlobalSearch', + path: location.pathname, + searchTerm: recentQuery, + position, + searchType: 'globalSearch', + }); + navigate(`/search-results?query=${recentQuery}&page=1`); + } + } else { + saveSearch(value); + addPageAction({ + eventName: 'swiftypeSearchInput', + category: 'GlobalSearch', + path: location.pathname, + resultCount: results.length, + searchTerm: query, + searchType: 'globalSearch', + }); + navigate(`/search-results?query=${value}&page=1`); + } + }} + placeholder={t('searchInput.placeholder')} + ref={searchRef} + setValue={setQuery} + size={SearchInput.SIZE.MEDIUM} + value={query} + css={css` + --icon-size: 1.5rem; + width: var(--search-width); + + svg { + width: 1rem; + height: 1rem; + } + + input { + border: none; + height: 40px; + } + + @media (max-width: 760px) { + border: 0; + border-radius: 0; + position: absolute; + left: 0; + top: 0; + width: 100vw; + height: var(--global-header-height); + z-index: 99; + + & input { + border-radius: 0; + height: var(--global-header-height); + } + } + `} + /> + {showSearchDropdown && ( + setOpen(false)} + onRecentClick={(query, i) => { + addPageAction({ + eventName: 'savedQuerySearch', + category: 'GlobalSearch', + path: location.pathname, + searchTerm: query, + position: i, + searchType: 'globalSearch', + }); + }} + onResultClick={(result, i) => { + addPageAction({ + eventName: 'swiftypeSearchResult', + category: 'GlobalSearch', + path: location.pathname, + resultCount: results.length, + position: i, + searchTerm: query, + searchType: 'globalSearch', + url: result.url, + }); + saveSearch(query); + }} + query={query} + recentQueries={recentQueries} + results={results} + selected={selected} + status={status} + /> + )} + + ); +}; + +const SAVED_SEARCH_KEY = 'gatsby-theme-newrelic:saved-searches'; + +const saveSearch = (value) => { + const savedSearches = JSON.parse( + localStorage.getItem(SAVED_SEARCH_KEY) ?? '[]' + ); + savedSearches.push(value); + // only save the four most recent searches + const updated = savedSearches.slice(-4); + localStorage.setItem(SAVED_SEARCH_KEY, JSON.stringify(updated)); +}; + +export default GlobalSearch; diff --git a/packages/gatsby-theme-newrelic/src/components/GlobalStyles/colors.js b/packages/gatsby-theme-newrelic/src/components/GlobalStyles/colors.js index 8b59a905a..8b48a0a0d 100644 --- a/packages/gatsby-theme-newrelic/src/components/GlobalStyles/colors.js +++ b/packages/gatsby-theme-newrelic/src/components/GlobalStyles/colors.js @@ -77,5 +77,6 @@ export default css` --color-green: #b5bd68; --color-purple: #b294bb; + --search-dropdown-emphasis: #00ac69; --spooky-white: #f8f8ff; `; diff --git a/packages/gatsby-theme-newrelic/src/components/GlobalStyles/themes.js b/packages/gatsby-theme-newrelic/src/components/GlobalStyles/themes.js index ee5352d9e..e93ef27f6 100644 --- a/packages/gatsby-theme-newrelic/src/components/GlobalStyles/themes.js +++ b/packages/gatsby-theme-newrelic/src/components/GlobalStyles/themes.js @@ -34,6 +34,10 @@ export default css` --callout-tip-background-color: #d1f7d925; --callout-course-background-color: #00b3c310; + --search-dropdown-background: #fff; + --search-dropdown-border: #e7e9ea; + --search-dropdown-hover: rgba(0, 0, 0, 0.06); + input::placeholder { color: var(--primary-text-color); opacity: 80%; @@ -75,6 +79,10 @@ export default css` --callout-important-background-color: #14110020; --callout-tip-background-color: #02120020; + --search-dropdown-background: #1a2125; + --search-dropdown-border: #eaecec; + --search-dropdown-hover: rgba(255, 255, 255, 0.1); + input::placeholder { color: var(--primary-text-color); opacity: 80%; diff --git a/packages/gatsby-theme-newrelic/src/components/SearchDropdown/KeyboardLegend.js b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/KeyboardLegend.js new file mode 100644 index 000000000..33a085334 --- /dev/null +++ b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/KeyboardLegend.js @@ -0,0 +1,58 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +import Icon from '../Icon'; + +const KeyboardLegend = () => ( + +
    + + + + to select +
    +
    + + + + + + + to navigate +
    +
    + Escto close +
    +
    +); + +const LegendContainer = styled.div` + align-items: center; + border-top: 1px solid var(--search-dropdown-border); + color: var(--secondary-text-color); + display: flex; + gap: 1.5rem; + justify-content: center; + padding: 16px 0 0; + + & > div { + align-items: center; + display: flex; + } + + & kbd { + border: 1px solid currentColor; + border-radius: 4px; + display: inline-grid; + line-height: 1.1; + margin-right: 0.25rem; + padding: 2px 4px; + place-items: center; + } + + @media (max-width: 760px) { + display: none; + } +`; + +export default KeyboardLegend; diff --git a/packages/gatsby-theme-newrelic/src/components/SearchDropdown/Results.js b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/Results.js new file mode 100644 index 000000000..44508e2d3 --- /dev/null +++ b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/Results.js @@ -0,0 +1,166 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import Icon from '../Icon'; + +const Results = ({ onResultClick, onViewMore, results, selected }) => { + return ( + <> + + {results.map((result, i) => ( + onResultClick(result, i)} + > + +

    + {breadcrumbify( + result.url.replace( + /https:\/\/docs\.newrelic\.com(?:\/docs)?\//, + '' + ) + )} +

    +

    +

    + + + ))} + + + + View more{' '} + + + + ); +}; + +const List = styled.ul` + list-style: none; + margin: 0 calc(-1 * var(--outer-padding)); + max-height: 31.5rem; + overflow-y: scroll; + padding: 0; + + & em { + color: var(--search-dropdown-emphasis); + font-style: normal; + } +`; + +const Result = styled.li` + --top-padding: 0.25rem; + cursor: pointer; + margin: calc(-1 * var(--top-padding)) 0 0; + padding: var(--top-padding) var(--outer-padding) 0.5rem; + &.selected { + background: var(--search-dropdown-hover); + } + + &:not(:last-of-type) { + margin-bottom: 0.5rem; + } + + &:hover { + background: var(--search-dropdown-hover); + } + + & a { + color: currentColor; + text-decoration: none; + } +`; + +const ViewMore = styled.button` + background: transparent; + border: 0; + color: var(--secondary-text-color); + cursor: pointer; + font-size: 0.75rem; + margin: 0 0 0.5rem -0.25rem; + padding: 0.5rem 0.25rem; + + &:hover { + background: var(--search-dropdown-hover); + } +`; + +export const ResultType = PropTypes.shape({ + breadcrumb: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + blurb: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, +}); + +Results.propTypes = { + results: PropTypes.arrayOf(ResultType), +}; + +// we use the url segments for breadcrumbs, since we don't have real breadcrumbs. +// breadcrumbs that would wrap to two lines get their middle parts truncated away. +// we always want to keep the first and last URL segments. +// in the very rare case that the length of the first and last segments plus ' / ... / ' +// is greater than 80, we'll only show the last part, like '... / last-segment' +const breadcrumbify = (str) => { + // URLs should be all lowercase, so using lowercase 'o' as a reference, + // 72 about the upper limit on number of characters for us not wrap to two lines. + // in practice, many characters in the URL will + // be slimmer than an o, so we can use a higher limit. + const DESIRED_LENGTH = 80; + str = str.replace(/\/$/, ''); + + let parts = str.split('/'); + let result = parts.join(' / '); + + if (result.length <= DESIRED_LENGTH) return result; + + parts[parts.length - 2] = '...'; + result = parts.join(' / '); + + while (result.length > DESIRED_LENGTH) { + // keep the last item and the '...' in the second to last place + parts.splice(parts.length - 3, 1); + result = parts.join(' / '); + } + + return result; +}; + +export default Results; diff --git a/packages/gatsby-theme-newrelic/src/components/SearchDropdown/SearchDropdown.js b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/SearchDropdown.js new file mode 100644 index 000000000..050465817 --- /dev/null +++ b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/SearchDropdown.js @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import cx from 'classnames'; + +import KeyboardLegend from './KeyboardLegend'; +import Results, { ResultType } from './Results'; +import Skeleton from './Skeleton'; + +const SearchDropdown = ({ + fetchNextPage, + onClose, + onRecentClick, + onResultClick, + query, + recentQueries, + results, + selected, + status, + ...rest +}) => { + const loading = status === 'loading'; + const error = status === 'error'; + return ( + <> + + Recent search terms + {recentQueries.length > 0 && ( + + {recentQueries.map((query, i) => ( +

  • onRecentClick(query, i)} + > + {query} +
  • + ))} + + )} + + All searches + {error && } + {loading && !error && } + {!loading && !error && ( + + )} + + + + + ); +}; + +SearchDropdown.propTypes = { + fetchNextPage: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + onRecentClick: PropTypes.func.isRequired, + onResultClick: PropTypes.func.isRequired, + query: PropTypes.string, + recentQueries: PropTypes.arrayOf(PropTypes.string).isRequired, + results: PropTypes.arrayOf(ResultType), + selected: PropTypes.number, + status: PropTypes.oneOf(['idle', 'loading', 'error']).isRequired, +}; + +const Error = () => ( +
    + unable to load search results +
    +); + +const Container = styled.div` + --outer-padding: 16px; + + background: var(--search-dropdown-background); + border: 1px solid var(--search-dropdown-border); + border-radius: 4px; + left: 0; + padding: var(--outer-padding); + position: absolute; + top: 48px; + width: var(--search-dropdown-width); + z-index: 1; + + @media (max-width: 760px) { + top: var(--global-header-height); + width: 100vw; + } +`; + +const SectionHeading = styled.p` + font-weight: 500; + line-height: 1.125; + margin-bottom: 0.5rem; +`; + +const Overlay = styled.div` + background: rgba(0, 0, 0, 0.2); + height: calc(100vh - var(--global-header-height)); + left: 0; + position: fixed; + top: var(--global-header-height); + width: 100vw; +`; + +const RecentQueries = styled.ul` + display: flex; + gap: 0.5rem; + list-style: none; + margin: 0 0 1rem; + padding: 0; + + & li { + line-height: 1.125; + } + & li:hover, + & li.selected { + text-decoration: underline; + } + + & a { + color: currentColor; + text-decoration: none; + } +`; + +export default SearchDropdown; diff --git a/packages/gatsby-theme-newrelic/src/components/SearchDropdown/Skeleton.js b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/Skeleton.js new file mode 100644 index 000000000..2319fe69d --- /dev/null +++ b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/Skeleton.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +import { range } from '../../utils/array'; +import LoadingBox from '../Skeleton'; + +const Skeleton = () => + range(0, 5).map((n) => ( +
    + {/* breadcrumb */} + + {/* title */} + + {/* blurb */} + +
    + )); + +export default Skeleton; diff --git a/packages/gatsby-theme-newrelic/src/components/SearchDropdown/index.js b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/index.js new file mode 100644 index 000000000..263680a17 --- /dev/null +++ b/packages/gatsby-theme-newrelic/src/components/SearchDropdown/index.js @@ -0,0 +1,18 @@ +const defaultFilters = [ + { name: 'docs', isSelected: false }, + { name: 'developer', isSelected: false }, + { name: 'opensource', isSelected: false }, + { name: 'quickstarts', isSelected: false }, +]; + +const defaultSearchByFilters = [ + { name: 'title', isSelected: false }, + { name: 'body', isSelected: false }, +]; + +export const DEFAULT_FILTER_TYPES = [ + { type: 'source', defaultFilters: defaultFilters }, + { type: 'searchBy', defaultFilters: defaultSearchByFilters }, +]; + +export { default } from './SearchDropdown'; diff --git a/packages/gatsby-theme-newrelic/src/components/SearchInput.js b/packages/gatsby-theme-newrelic/src/components/SearchInput.js index 461390965..8497d6ad1 100644 --- a/packages/gatsby-theme-newrelic/src/components/SearchInput.js +++ b/packages/gatsby-theme-newrelic/src/components/SearchInput.js @@ -1,6 +1,8 @@ import React, { forwardRef, useState } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; +import { graphql, useStaticQuery } from 'gatsby'; + import Icon from './Icon'; import Link from './Link'; import composeHandlers from '../utils/composeHandlers'; @@ -41,6 +43,8 @@ const SearchInput = forwardRef( onClear, onFocus, onSubmit, + onMove, + setValue, size = 'medium', value, width, @@ -48,6 +52,19 @@ const SearchInput = forwardRef( }, ref ) => { + const { + site: { + layout: { mobileBreakpoint }, + }, + } = useStaticQuery(graphql` + query SearchInputQuery { + site { + layout { + mobileBreakpoint + } + } + } + `); const inputRef = useSyncedRef(ref); const [showHotKey, setShowHotkey] = useState(Boolean(focusWithHotKey)); @@ -64,6 +81,8 @@ const SearchInput = forwardRef( css={css` --horizontal-spacing: ${HORIZONTAL_SPACING[size]}; + border: 1px solid #eaecec; + border-radius: 4px; position: relative; width: ${width || '100%'}; ${size && styles.size[size].container} @@ -120,12 +139,19 @@ const SearchInput = forwardRef( value={value} {...props} type="text" + onInput={(e) => setValue(e.target.value)} onFocus={composeHandlers(onFocus, () => setShowHotkey(false))} onBlur={composeHandlers(onBlur, () => setShowHotkey(Boolean(focusWithHotKey)) )} onKeyDown={(e) => { switch (e.key) { + case 'ArrowUp': + onMove('prev'); + break; + case 'ArrowDown': + onMove('next'); + break; case 'Escape': onClear && onClear(); e.target.blur(); @@ -167,13 +193,35 @@ const SearchInput = forwardRef( } `} /> - {value && onClear && ( + + / + + {onClear && (