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 && (