diff --git a/packages/styleguide/package.json b/packages/styleguide/package.json
index cf2e1556db3..ae47abf66e7 100644
--- a/packages/styleguide/package.json
+++ b/packages/styleguide/package.json
@@ -15,12 +15,14 @@
"homepage": "/",
"dependencies": {
"@mdx-js/react": "~1.5.0",
+ "accessible-autocomplete": "~2.0.1",
"array-includes": "~3.0.3",
"chroma-js": "~2.0.2",
"classnames": "~2.2.5",
"common-tags": "~1.8.0",
"core-js": "~2.6.0",
"firacode": "~1.205.0",
+ "fuzzyjs": "~4.0.3",
"object.entries": "~1.1.0",
"polished": "~2.3.1",
"pretty": "~2.0.0",
@@ -33,7 +35,7 @@
"react-frame-component": "~4.0.0",
"react-ga": "~2.6.0",
"react-responsive": "~6.0.1",
- "react-router-dom": "~5.0.0",
+ "react-router-dom": "~5.1.2",
"react-select": "~3.0.8",
"styled-components": "~4.1.2",
"svg4everybody": "~2.1.9",
diff --git a/packages/styleguide/src/components/App/App.js b/packages/styleguide/src/components/App/App.js
index 960746781e1..9325c0449f7 100644
--- a/packages/styleguide/src/components/App/App.js
+++ b/packages/styleguide/src/components/App/App.js
@@ -14,6 +14,7 @@ import Sidebar from './../Sidebar';
import Navigation from './../Navigation';
import NavigationBar from './../NavigationBar';
import Sitemap from './../Sitemap';
+import Search from './../Search';
class App extends Component {
static displayName = 'App';
@@ -118,14 +119,15 @@ class App extends Component {
}>
+
+
-
this.handleNavLinkClick()}
diff --git a/packages/styleguide/src/components/Navigation/index.js b/packages/styleguide/src/components/Navigation/index.js
index 2b3fa7032a0..c177b728057 100644
--- a/packages/styleguide/src/components/Navigation/index.js
+++ b/packages/styleguide/src/components/Navigation/index.js
@@ -1,152 +1,119 @@
-import React from 'react';
-import { array, bool, func, string } from 'prop-types';
+import React, { useState, useEffect } from 'react';
+import { array, bool, func } from 'prop-types';
import cx from 'classnames';
import styled from 'styled-components';
-import { withRouter } from 'react-router-dom';
+import { withRouter, useLocation } from 'react-router-dom';
import { rem } from './../../style/utils';
-
import Category from './Category';
import NavLink from './NavLink';
const CLASS_ROOT = 'sg-nav';
-class Navigation extends React.Component {
- static displayName = 'Navigation';
-
- static propTypes = {
- isMain: bool,
- routes: array,
- pathname: string,
- onNavLinkClick: func,
+const Navigation = ({
+ className,
+ routes = [],
+ isMain,
+ onNavLinkClick,
+ ...other
+}) => {
+ const classes = cx(CLASS_ROOT, className);
+ let location = useLocation();
+
+ const [activeCategories, setActiveCategories] = useState({});
+
+ /**
+ * Set category active state
+ */
+ const setCategoryActiveState = (id, value = true) => {
+ setActiveCategories({
+ ...activeCategories,
+ [id]: value,
+ });
};
- constructor(props) {
- super(props);
- this.state = {
- activeLinks: [],
- };
- this.handleClick = this.handleClick.bind(this);
- }
-
- componentWillMount() {
- const path = this.props.pathname;
-
- let pathArray = [];
- pathArray = path.split('/');
- pathArray = pathArray.filter(e => String(e).trim());
-
- const activeLinks = this.copyActiveLinks(pathArray.length - 1);
+ /**
+ * This method sets every category state (from pathname)
+ * from first parent to current category to active
+ */
+ const setActiveCategoriesFromPathname = () => {
+ let url = '';
+ const locPath = location.pathname;
+ const categories = locPath
+ // remove first '/' and last item of the path
+ .substring(1, locPath.lastIndexOf('/'))
+ .split('/')
+ .reduce((acc, curr) => {
+ url += '/' + curr;
+ return { ...acc, [url]: true };
+ }, {});
+
+ // set all new active categories
+ setActiveCategories({ ...activeCategories, ...categories });
+ };
- pathArray.forEach((element, i) => {
- activeLinks[i].push(`/${element}`);
- });
+ useEffect(
+ () => {
+ setActiveCategoriesFromPathname();
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [location.pathname]
+ );
+
+ const getNavList = (nodes = [], path = '') => (
+
+ {nodes.map(node => {
+ let item = null;
+ let nestedList = null;
+
+ const url = path + node.path;
+ if (node.nodes) {
+ item = (
+ {
+ setCategoryActiveState(url, !activeCategories[url]);
+ }}
+ isActive={activeCategories[url]}
+ >
+ {node.title}
+
+ );
- this.setState({
- activeLinks,
- });
- }
-
- handleClick(activeLink, depthLevel) {
- const activeLinks = this.copyActiveLinks(depthLevel);
-
- if (activeLinks[depthLevel].indexOf(activeLink) === -1) {
- activeLinks[depthLevel].push(activeLink);
- } else {
- activeLinks[depthLevel] = this.removeActive(
- activeLinks[depthLevel],
- activeLink
- );
- }
-
- this.setState({
- activeLinks,
- });
- }
-
- // eslint-disable-next-line class-methods-use-this
- removeActive(arr, element) {
- return arr.filter(e => e !== element);
- }
-
- copyActiveLinks(depthLevel) {
- const activeLinks = this.state.activeLinks.slice(0);
-
- for (let i = 0; i <= depthLevel; i += 1) {
- activeLinks[i] = activeLinks[i] || [];
- }
-
- return activeLinks;
- }
-
- isActive(element, depthLevel) {
- const activeLinks = this.copyActiveLinks(depthLevel);
-
- return Array.isArray(activeLinks) || activeLinks.length
- ? activeLinks[depthLevel].includes(element)
- : false;
- }
-
- render() {
- const {
- className,
- routes = [],
- isMain,
- onNavLinkClick,
- ...other
- } = this.props;
-
- const classes = cx(CLASS_ROOT, className);
-
- const getNavList = (nodes = [], path = '', depthLevel = 0) => (
-
- {nodes.map(node => {
- let item = null;
- let nestedList = null;
-
- if (node.nodes) {
- item = (
- this.handleClick(node.path, depthLevel)}
- isActive={this.isActive(node.path, depthLevel)}
- >
- {node.title}
-
- );
-
- const depthLevelUpdated = depthLevel + 1;
- nestedList = getNavList(
- node.nodes,
- path + node.path,
- depthLevelUpdated
- );
- } else {
- item = (
-
- {node.title}
-
- );
- }
-
- return (
-
- {item}
- {nestedList}
-
+ nestedList = getNavList(node.nodes, url);
+ } else {
+ item = (
+
+ {node.title}
+
);
- })}
-
- );
-
- // div has to wrapp Nav because of nice layout
- return (
-
- {getNavList(routes)}
-
- );
- }
-}
+ }
+
+ return (
+
+ {item}
+ {nestedList}
+
+ );
+ })}
+
+ );
+
+ // div has to wrapp Nav because of nice layout
+ return (
+
+ {getNavList(routes)}
+
+ );
+};
+
+Navigation.displayName = 'Navigation';
+
+Navigation.propTypes = {
+ isMain: bool,
+ routes: array,
+ onNavLinkClick: func,
+};
const StyledNav = styled.nav`
width: ${props => rem(props.theme.sizes.menuWidth)};
@@ -174,6 +141,6 @@ const ListItem = styled.li`
export default withRouter(
({ location, match, history, staticContext, ...other }) => (
-
+
)
);
diff --git a/packages/styleguide/src/components/NavigationBar/index.js b/packages/styleguide/src/components/NavigationBar/index.js
index 81a0e9219f6..a36481c095a 100644
--- a/packages/styleguide/src/components/NavigationBar/index.js
+++ b/packages/styleguide/src/components/NavigationBar/index.js
@@ -8,7 +8,7 @@ const propTypes = {
onButtonClick: func,
};
-const NavigationBar = ({ className, isActive,onButtonClick, ...other }) => {
+const NavigationBar = ({ className, isActive, onButtonClick, ...other }) => {
const classes = cx({ 'is-active': isActive }, 'navigation-bar', className);
return (
diff --git a/packages/styleguide/src/components/Search/Search.js b/packages/styleguide/src/components/Search/Search.js
new file mode 100644
index 00000000000..57fd9891ab5
--- /dev/null
+++ b/packages/styleguide/src/components/Search/Search.js
@@ -0,0 +1,187 @@
+import React from 'react';
+import { array, string } from 'prop-types';
+import { withRouter, useHistory } from 'react-router-dom';
+import styled from 'styled-components';
+/** https://www.npmjs.com/package/fuzzyjs */
+import { match, surround } from 'fuzzyjs';
+import Autocomplete from 'accessible-autocomplete/react';
+
+import { rem } from './../../style/utils';
+
+const propTypes = {
+ list: array.isRequired,
+ placeholder: string,
+};
+
+const defaultProps = {
+ placeholder: 'Search',
+};
+
+const Search = ({ list, placeholder }) => {
+ let history = useHistory();
+
+ const formatMatches = (matches, parentPath, parentTitle) =>
+ matches.map(match => ({
+ title: match.original.title,
+ path: parentPath + match.original.path,
+ string: match.string,
+ isHidden: match.original.hasNodes,
+ }));
+
+ const parseFullPathList = (items, parentPath = '') => {
+ return items.reduce((acc, curr) => {
+ const path = parentPath + curr.path;
+
+ return [
+ ...acc,
+ { title: curr.title, path: path, hasNodes: curr.nodes !== undefined },
+ ...(curr.nodes ? parseFullPathList(curr.nodes, path) : []),
+ ];
+ }, []);
+ };
+
+ const surroundMatch = (string, result) =>
+ surround(string, {
+ result,
+ prefix: '',
+ suffix: '',
+ });
+
+ const getMatch = (value, string) =>
+ match(value, string, {
+ withScore: true,
+ withRanges: true,
+ });
+
+ const search = (value, list, parentPath = '', parentTitle = '') => {
+ if (value === '') {
+ return [];
+ }
+
+ const fullPathsList = parseFullPathList(list);
+ // search the first level children for match
+ // const parentMatches = fuzzy.filter(value, fullPathsList, options);
+ const parentMatches = fullPathsList
+ .reduce((acc, curr) => {
+ const result = {
+ // search in title
+ title: getMatch(value, curr.title),
+ // search in path
+ path: getMatch(value, curr.path),
+ };
+
+ return result.path.match
+ ? [
+ ...acc,
+ {
+ original: curr,
+ result: result,
+ score: {
+ title: result.title.score,
+ path: result.path.score,
+ // get the highest score from the two
+ full: Math.max(
+ result.title.score !== 0 ? result.title.score : -1000,
+ result.path.score !== 0 ? result.path.score : -1000
+ ),
+ },
+ string: {
+ title: surroundMatch(curr.title, result.title),
+ path: surroundMatch(curr.path, result.path),
+ },
+ },
+ ]
+ : acc;
+ }, [])
+ // sort from high to low
+ .sort((a, b) => b.score.full - a.score.full);
+ // format the matched items for later use
+ return formatMatches(parentMatches, parentPath, parentTitle);
+ };
+
+ return (
+
+ {
+ const filteredResults = search(value, list);
+ syncResults(filteredResults.filter(result => !result.isHidden));
+ }}
+ templates={{
+ inputValue: () => '',
+ suggestion: result =>
+ result &&
+ `${result.string.title}
+ ${result.string.path}`,
+ }}
+ onConfirm={confirmed => {
+ if (!confirmed) return false;
+ const path = confirmed.path;
+ history.push(path);
+ }}
+ />
+
+ );
+};
+
+const StyledAutocompleteWrapper = styled.div`
+ width: ${props => rem(props.theme.sizes.menuWidth)};
+ box-sizing: content-box;
+ font-family: ${props => props.theme.fontFamily};
+ font-size: ${props => rem(props.theme.fontSizes.base)};
+ line-height: ${props => props.theme.lineHeights.base};
+
+ .autocomplete__input {
+ width: 100%;
+ line-height: 20px;
+ border: 1px solid black;
+ padding: 5px 10px;
+ }
+
+ .autocomplete__option {
+ padding: ${props => rem(props.theme.spaces.tiny)}
+ ${props => rem(props.theme.spaces.small)};
+ line-height: 1.2em;
+ }
+
+ .autocomplete__option--focused {
+ background: #eee;
+ }
+
+ .autocomplete__menu {
+ &--hidden {
+ display: none;
+ }
+ &--visible {
+ position: absolute;
+ z-index: 10000;
+ white-space: nowrap;
+ background: white;
+ border: 1px solid black;
+ padding: 0;
+ width: 94%;
+ max-height: calc(8.25 * 52px);
+ overflow: auto;
+ left: 50%;
+ transform: translate(-50%, -17px);
+ }
+ }
+
+ ul {
+ padding: 5px;
+ li {
+ list-style: none;
+ }
+ }
+`;
+
+Search.displayName = 'Search';
+Search.propTypes = propTypes;
+Search.defaultProps = defaultProps;
+
+export default withRouter(Search);
diff --git a/packages/styleguide/src/components/Search/index.js b/packages/styleguide/src/components/Search/index.js
new file mode 100644
index 00000000000..85bb434b226
--- /dev/null
+++ b/packages/styleguide/src/components/Search/index.js
@@ -0,0 +1 @@
+export { default } from './Search';