diff --git a/src/components/FeatherIcon.js b/src/components/FeatherIcon.js
index dbf50892a..39e4d8199 100644
--- a/src/components/FeatherIcon.js
+++ b/src/components/FeatherIcon.js
@@ -83,6 +83,12 @@ const ICONS = {
github: (
),
+ search: (
+ <>
+
+
+ >
+ ),
'upload-cloud': (
<>
diff --git a/src/components/Navigation.js b/src/components/Navigation.js
index 56553b5cd..bf5f31a12 100644
--- a/src/components/Navigation.js
+++ b/src/components/Navigation.js
@@ -1,127 +1,55 @@
-import React, { Fragment, useState, useContext } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
-import { Link } from 'gatsby';
import cx from 'classnames';
-import { BreadcrumbContext } from './BreadcrumbContext';
-import FeatherIcon from './FeatherIcon';
-import NewRelicIcon from './NewRelicIcon';
+import NavigationItems from './NavigationItems';
import pages from '../data/sidenav.json';
-
+import matchSearchString from '../utils/matchSearchString';
import styles from './Navigation.module.scss';
-// recursively create navigation
-const renderNav = (pages, depthLevel = 0) => {
- // TODO: Refactor this function into a component
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const crumbs = useContext(BreadcrumbContext).flatMap((x) => x.displayName);
- const isHomePage = crumbs.length === 0 && depthLevel === 0;
- const iconLibrary = {
- 'Collect data': 'collectData',
- 'Build apps': 'buildApps',
- 'Automate workflows': 'automation',
- 'Explore docs': 'developerDocs',
- };
-
- const groupedPages = pages.reduce((groups, page) => {
- const { group = '' } = page;
-
- return {
- ...groups,
- [group]: [...(groups[group] || []), page],
- };
- }, {});
-
- return Object.entries(groupedPages).map(([group, pages]) => (
-
- {group && (
- {group}
- )}
- {pages.map((page) => {
- const [isExpanded, setIsExpanded] = useState(
- isHomePage || crumbs.includes(page.displayName)
- );
- const isCurrentPage = crumbs[crumbs.length - 1] === page.displayName;
- const headerIcon = depthLevel === 0 && (
-
- );
-
- return (
-
- {page.url ? (
-
-
- {headerIcon}
- {page.displayName}
-
- {isCurrentPage && (
-
- )}
-
- ) : (
-
- )}
- {page.children && (
-
- {renderNav(page.children, depthLevel + 1)}
-
- )}
-
- );
- })}
-
- ));
+const filterPageNames = (pages, searchTerm, parent = []) => {
+ return [
+ ...new Set(
+ pages.flatMap((page) => {
+ if (page.children) {
+ return filterPageNames(page.children, searchTerm, [
+ ...parent,
+ page.displayName,
+ ]);
+ } else if (matchSearchString(page.displayName, searchTerm)) {
+ return [...parent, page.displayName];
+ } else if (parent.some((el) => matchSearchString(el, searchTerm))) {
+ return [...parent];
+ }
+ })
+ ),
+ ];
};
-const Navigation = ({ className }) => {
+const Navigation = ({ className, searchTerm }) => {
+ const searchTermSanitized = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const filteredPageNames =
+ searchTerm !== '' ? filterPageNames(pages, searchTermSanitized) : undefined;
+
return (
);
};
Navigation.propTypes = {
className: PropTypes.string,
+ searchTerm: PropTypes.string,
};
export default Navigation;
diff --git a/src/components/Navigation.module.scss b/src/components/Navigation.module.scss
index 408650aaa..57428ada2 100644
--- a/src/components/Navigation.module.scss
+++ b/src/components/Navigation.module.scss
@@ -2,87 +2,7 @@
font-size: 0.875rem;
}
-.listNav,
-.nestedNav {
- list-style: none;
-}
-
-.nestedNav {
- padding-left: 1rem;
- display: none;
-
- &.isExpanded {
- display: block;
- }
-
- [data-depth] > & {
- padding-left: calc(0.5rem + 1em);
- }
-}
-
.listNav {
+ list-style: none;
padding-left: 0;
}
-
-.navLink {
- color: var(--color-neutrals-900);
- margin-bottom: 1rem;
- display: flex;
- align-items: center;
- justify-content: space-between;
- text-decoration: none;
- transition: 0.1s;
-
- &:not(.isCurrentPage):hover {
- color: var(--color-neutrals-600);
- }
-
- [data-depth='0'] > & {
- font-weight: bold;
- }
-}
-
-.headerIcon {
- margin-right: 0.5rem;
-}
-
-button.navLink {
- color: var(--color-neutrals-900);
- background: inherit;
- border: none;
- padding: 0;
- font-size: inherit;
- font-weight: inherit;
-
- &:focus {
- outline: none;
- }
-}
-
-.currentPageIndicator {
- stroke-width: 4;
-}
-
-.nestedChevron {
- margin-right: 0.5rem;
- stroke-width: 4;
- transition: 0.2s;
- &.isExpanded {
- transform: rotate(90deg);
- }
-}
-
-.groupName {
- color: var(--color-neutrals-600);
- font-weight: bold;
- font-size: 0.75rem;
- text-transform: uppercase;
-
- &:not(:first-child) {
- margin-top: 2rem;
- }
-}
-
-.isCurrentPage {
- font-weight: bold;
-}
diff --git a/src/components/NavigationItems.js b/src/components/NavigationItems.js
new file mode 100644
index 000000000..ae5cb1323
--- /dev/null
+++ b/src/components/NavigationItems.js
@@ -0,0 +1,171 @@
+import React, { Fragment, useState, useContext } from 'react';
+import PropTypes from 'prop-types';
+import FeatherIcon from './FeatherIcon';
+import NewRelicIcon from './NewRelicIcon';
+import { Link } from 'gatsby';
+import cx from 'classnames';
+import { BreadcrumbContext } from './BreadcrumbContext';
+import styles from './NavigationItems.module.scss';
+import { link } from '../types';
+
+const getHighlightedText = (text, highlight) => {
+ const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
+ return (
+
+ {parts.map((part) =>
+ part.toLowerCase() === highlight.toLowerCase() ? {part} : part
+ )}
+
+ );
+};
+
+const NavigationItems = ({
+ pages,
+ filteredPageNames,
+ searchTerm,
+ depthLevel = 0,
+}) => {
+ const groupedPages = pages.reduce((groups, page) => {
+ const { group = '' } = page;
+
+ return {
+ ...groups,
+ [group]: [...(groups[group] || []), page],
+ };
+ }, {});
+
+ return Object.entries(groupedPages).map(([group, pages]) => {
+ const showGroup =
+ (group && !filteredPageNames) ||
+ (group &&
+ filteredPageNames &&
+ pages.some((el) => filteredPageNames.includes(el.displayName)));
+ return (
+
+ {showGroup && (
+ {group}
+ )}
+ {pages.map((page, index) => (
+
+ ))}
+
+ );
+ });
+};
+
+const NavItem = ({ page, depthLevel, searchTerm, filteredPageNames }) => {
+ const crumbs = useContext(BreadcrumbContext).flatMap((x) => x.displayName);
+ const isHomePage = crumbs.length === 0 && depthLevel === 0;
+
+ const [isExpanded, setIsExpanded] = useState(
+ isHomePage || crumbs.includes(page.displayName)
+ );
+
+ const iconLibrary = {
+ 'Collect data': 'collectData',
+ 'Build apps': 'buildApps',
+ 'Automate workflows': 'automation',
+ 'Explore docs': 'developerDocs',
+ };
+ const isCurrentPage = crumbs[crumbs.length - 1] === page.displayName;
+ const headerIcon = depthLevel === 0 && (
+
+ );
+ const display = filteredPageNames
+ ? getHighlightedText(page.displayName, searchTerm)
+ : page.displayName;
+
+ if (filteredPageNames && !filteredPageNames.includes(page.displayName))
+ return null;
+
+ return (
+
+ {page.url ? (
+
+
+ {headerIcon}
+ {display}
+
+ {isCurrentPage && (
+
+ )}
+
+ ) : (
+
+ )}
+ {page.children && (
+
+ )}
+
+ );
+};
+
+NavigationItems.propTypes = {
+ pages: PropTypes.array.isRequired,
+ filteredPageNames: PropTypes.array,
+ searchTerm: PropTypes.string,
+ depthLevel: PropTypes.number,
+};
+
+NavItem.propTypes = {
+ page: link,
+ filteredPageNames: PropTypes.array,
+ searchTerm: PropTypes.string,
+ depthLevel: PropTypes.number.isRequired,
+};
+
+export default NavigationItems;
diff --git a/src/components/NavigationItems.module.scss b/src/components/NavigationItems.module.scss
new file mode 100644
index 000000000..516dfb096
--- /dev/null
+++ b/src/components/NavigationItems.module.scss
@@ -0,0 +1,92 @@
+.nestedNav {
+ list-style: none;
+}
+
+.nestedNav {
+ padding-left: 1rem;
+ display: none;
+
+ &.isExpanded {
+ display: block;
+ }
+
+ [data-depth] > & {
+ padding-left: calc(0.5rem + 1em);
+ }
+}
+
+.navLink {
+ color: var(--color-neutrals-900);
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ text-decoration: none;
+ transition: 0.1s;
+
+ &:not(.isCurrentPage):hover {
+ color: var(--color-neutrals-600);
+ }
+
+ [data-depth='0'] > & {
+ font-weight: 600;
+ padding: 1em 0;
+ }
+}
+
+.headerIcon {
+ margin-right: 0.5rem;
+}
+
+button.navLink {
+ color: var(--color-neutrals-900);
+ background: inherit;
+ border: none;
+ padding: 0;
+ font-size: inherit;
+ font-weight: inherit;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.currentPageIndicator {
+ stroke-width: 4;
+}
+
+.nestedChevron {
+ margin-right: 0.5rem;
+ stroke-width: 4;
+ transition: 0.2s;
+ &.isExpanded {
+ transform: rotate(90deg);
+ }
+}
+
+.groupName {
+ color: var(--color-neutrals-600);
+ font-weight: bold;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+
+ &:not(:first-child) {
+ margin-top: 2rem;
+ }
+}
+
+.isCurrentPage {
+ font-weight: bold;
+}
+
+.filterOn {
+ ul {
+ display: block;
+ }
+ .nestedChevron {
+ transform: rotate(90deg);
+ }
+ .groupName {
+ margin-top: 0.5rem;
+ }
+}
diff --git a/src/components/SearchInput.js b/src/components/SearchInput.js
new file mode 100644
index 000000000..bc6dbb4d9
--- /dev/null
+++ b/src/components/SearchInput.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import FeatherIcon from './FeatherIcon';
+import styles from './SearchInput.module.scss';
+
+const SearchInput = ({ className, ...props }) => (
+
+
+
+
+);
+
+SearchInput.propTypes = {
+ className: PropTypes.string,
+};
+
+export default SearchInput;
diff --git a/src/components/SearchInput.module.scss b/src/components/SearchInput.module.scss
new file mode 100644
index 000000000..bf48fcaef
--- /dev/null
+++ b/src/components/SearchInput.module.scss
@@ -0,0 +1,19 @@
+.container {
+ display: inline-flex;
+ align-items: center;
+ width: 100%;
+ position: relative;
+}
+
+.icon {
+ position: absolute;
+ right: 0.5rem;
+ stroke: var(--color-neutrals-700);
+}
+
+.input {
+ width: 100%;
+ font-size: 0.875rem;
+ padding: 0.5rem;
+ padding-right: calc(1rem + 1em);
+}
diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js
index b7c5205dd..524b08a21 100644
--- a/src/components/Sidebar.js
+++ b/src/components/Sidebar.js
@@ -1,19 +1,30 @@
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'gatsby';
import cx from 'classnames';
import Logo from './Logo';
import Navigation from './Navigation';
+import SearchInput from './SearchInput';
import styles from './Sidebar.module.scss';
-const Sidebar = ({ className }) => (
-
-);
+const Sidebar = ({ className }) => {
+ const [searchTerm, setSearchTerm] = useState('');
+
+ return (
+
+ );
+};
Sidebar.propTypes = {
className: PropTypes.string,
diff --git a/src/components/Sidebar.module.scss b/src/components/Sidebar.module.scss
index f007215b5..c5a7a93e9 100644
--- a/src/components/Sidebar.module.scss
+++ b/src/components/Sidebar.module.scss
@@ -19,3 +19,7 @@
.nav {
margin-top: 1rem;
}
+
+.searchInput {
+ margin: 1rem 0;
+}
diff --git a/src/utils/matchSearchString.js b/src/utils/matchSearchString.js
new file mode 100644
index 000000000..bd3063610
--- /dev/null
+++ b/src/utils/matchSearchString.js
@@ -0,0 +1,8 @@
+const matchSearchString = (str, searchTerm) => {
+ return new RegExp(
+ searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
+ 'i'
+ ).test(str);
+};
+
+export default matchSearchString;