diff --git a/src/lib/components/Link/Link.js b/src/lib/components/Link/Link.js
index 585132a..6cd66b9 100644
--- a/src/lib/components/Link/Link.js
+++ b/src/lib/components/Link/Link.js
@@ -1,41 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
+import getClassName from '../../utils/getClassName';
const Link = (props) => {
- let classArray = [];
-
- if (props.soft) {
- classArray = [...classArray, 'p-link--soft'];
- }
-
- if (props.strong) {
- classArray = [...classArray, 'p-link--strong'];
- }
-
- if (props.inverted) {
- classArray = [...classArray, 'p-link--inverted'];
- }
-
- if (props.external) {
- classArray = [...classArray, 'p-link--external'];
- }
-
- const classString = classArray.join(' ');
-
- if (props.top) {
+ const {
+ soft, strong, inverted, external, top,
+ } = props;
+ const customClasses = props.className;
+
+ const className = getClassName({
+ 'p-link--soft': soft,
+ 'p-link--strong': strong,
+ 'p-link--inverted': inverted,
+ 'p-link--external': external,
+ 'p-top__link': top,
+ [`${customClasses}`]: customClasses,
+ });
+
+ if (top) {
return (
Back to top
diff --git a/src/lib/components/SideNav/SideNav.js b/src/lib/components/SideNav/SideNav.js
new file mode 100644
index 0000000..9605c80
--- /dev/null
+++ b/src/lib/components/SideNav/SideNav.js
@@ -0,0 +1,75 @@
+import React from 'react';
+
+import SideNavBanner from './SideNavBanner';
+
+class SideNav extends React.Component {
+ constructor() {
+ super();
+ this.handleMenuClick = this.handleMenuClick.bind(this);
+
+ this.state = { menuOpen: false };
+ }
+
+ handleMenuClick() {
+ const { menuOpen } = this.state;
+ this.setState({ menuOpen: !menuOpen });
+ }
+
+ render() {
+ const { children } = this.props;
+ const { menuOpen } = this.state;
+ const content = [];
+ let banner;
+
+ React.Children.forEach(children, (child) => {
+ if (child.type === SideNavBanner) {
+ banner = React.cloneElement(child, {
+ ...this.props,
+ onClick: this.handleMenuClick,
+ open: this.state.menuOpen,
+ });
+ } else {
+ content.push(child);
+ }
+ });
+
+ return (
+
+ );
+ }
+}
+
+SideNav.defaultProps = {
+ children: null,
+};
+
+SideNav.propTypes = {
+ children: (props, propName, componentName) => {
+ const prop = props[propName];
+ let error = null;
+ let count = 0;
+
+ React.Children.forEach(prop, (child) => {
+ if (child.type === SideNavBanner) {
+ count += 1;
+ }
+ });
+
+ if (count !== 1) {
+ error = new Error(`${componentName} should have exactly one child of type "SideNavBanner".`);
+ }
+
+ return error;
+ },
+};
+
+SideNav.displayName = 'SideNav';
+
+export default SideNav;
diff --git a/src/lib/components/SideNav/SideNav.stories.js b/src/lib/components/SideNav/SideNav.stories.js
new file mode 100644
index 0000000..1bd2dd6
--- /dev/null
+++ b/src/lib/components/SideNav/SideNav.stories.js
@@ -0,0 +1,184 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { number, text } from '@storybook/addon-knobs';
+import { withInfo } from '@storybook/addon-info';
+
+import Link from '../Link/Link';
+import List from '../List/List';
+import ListItem from '../List/ListItem';
+import SideNav from './SideNav';
+import SideNavBanner from './SideNavBanner';
+import SideNavGroup from './SideNavGroup';
+import SideNavLink from './SideNavLink';
+
+const options = {
+ range: true,
+ min: 3,
+ max: 12,
+ step: 1,
+};
+
+storiesOf('SideNav', module)
+ .add('Text Banner',
+ withInfo('The SideNav component allows for navigation from a collapsing menu on the side of the page. It requires a SideNavBanner which contains a title/logo, optional tagline, and a space for the small screen burger menu. SideNavLinks can be placed directly inside SideNav, or inside a SideNavGroup to keep them under section titles. This example will expand to fill the space available to it so it needs to be used in conjunction with the grid to set the layout.')(() => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Supplementary link 1
+
+
+ Supplementary link 2
+
+
+
+
),
+ ),
+ )
+
+ .add('Logo Banner',
+ withInfo('A logo object prop can also be passed to SideNavBanner, which will replace a simple text banner.')(() => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ vanillaframework.io
+
+
+
+
),
+ ),
+ );
diff --git a/src/lib/components/SideNav/SideNav.test.js b/src/lib/components/SideNav/SideNav.test.js
new file mode 100644
index 0000000..f06055d
--- /dev/null
+++ b/src/lib/components/SideNav/SideNav.test.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import ReactTestRenderer from 'react-test-renderer';
+
+import SideNav from './SideNav';
+import SideNavBanner from './SideNavBanner';
+import SideNavGroup from './SideNavGroup';
+import SideNavLink from './SideNavLink';
+
+describe('', () => {
+ it('renders with a text-based SideNavBanner correctly', () => {
+ const sidenav = ReactTestRenderer.create(
+
+
+ ,
+ );
+ const json = sidenav.toJSON();
+ expect(json).toMatchSnapshot();
+ });
+
+ it('renders with a logo-based SideNavBanner correctly', () => {
+ const sidenav = ReactTestRenderer.create(
+
+
+ ,
+ );
+ const json = sidenav.toJSON();
+ expect(json).toMatchSnapshot();
+ });
+
+ it('renders with a single SideNavLink correctly', () => {
+ const sidenav = ReactTestRenderer.create(
+
+
+
+ ,
+ );
+ const json = sidenav.toJSON();
+ expect(json).toMatchSnapshot();
+ });
+
+ it('renders with multiple SideNavLinks correctly', () => {
+ const sidenav = ReactTestRenderer.create(
+
+
+
+
+
+ ,
+ );
+ const json = sidenav.toJSON();
+ expect(json).toMatchSnapshot();
+ });
+
+ it('renders with a SideNavGroup correctly', () => {
+ const sidenav = ReactTestRenderer.create(
+
+
+
+
+
+ ,
+ );
+ const json = sidenav.toJSON();
+ expect(json).toMatchSnapshot();
+ });
+
+ it('renders with multiple SideNavGroups correctly', () => {
+ const sidenav = ReactTestRenderer.create(
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ const json = sidenav.toJSON();
+ expect(json).toMatchSnapshot();
+ });
+});
diff --git a/src/lib/components/SideNav/SideNavBanner.js b/src/lib/components/SideNav/SideNavBanner.js
new file mode 100644
index 0000000..7b7c4f5
--- /dev/null
+++ b/src/lib/components/SideNav/SideNavBanner.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+class SideNavBanner extends React.Component {
+ render() {
+ const {
+ href, logo, onClick, open, tagline, title,
+ } = this.props;
+
+ const Tag = href ? 'a' : 'div';
+
+ return (
+
+
+
+
+
+
+ {(logo.src ?
+ : title
+ )}
+
+
+ {tagline &&
{tagline}}
+
+
+
+ -
+ key === 'Enter' && onClick}
+ aria-hidden={open ? 'true' : 'false'}
+ role="button"
+ tabIndex={0}
+ />
+ key === 'Enter' && onClick}
+ aria-hidden={open ? 'false' : 'true'}
+ role="button"
+ tabIndex={0}
+ />
+
+
+
+
+
+
+ );
+ }
+}
+
+SideNavBanner.defaultProps = {
+ href: null,
+ logo: { src: null, alt: '' },
+ onClick: () => 1,
+ open: false,
+ tagline: '',
+ title: null,
+};
+
+SideNavBanner.propTypes = {
+ href: PropTypes.string,
+ logo: PropTypes.shape({ src: PropTypes.string, alt: PropTypes.string }),
+ onClick: PropTypes.func,
+ open: PropTypes.bool,
+ tagline: PropTypes.string,
+ title: PropTypes.string,
+};
+
+SideNavBanner.displayName = 'SideNavBanner';
+
+export default SideNavBanner;
diff --git a/src/lib/components/SideNav/SideNavGroup.js b/src/lib/components/SideNav/SideNavGroup.js
new file mode 100644
index 0000000..179bceb
--- /dev/null
+++ b/src/lib/components/SideNav/SideNavGroup.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import getClassName from '../../utils/getClassName';
+
+class SideNavGroup extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onClick = this.onClick.bind(this);
+
+ this.state = { selected: props.selected };
+ }
+
+ onClick(e) {
+ e.preventDefault();
+ const { selected } = this.state;
+ this.setState({ selected: !selected });
+ }
+
+ render() {
+ const { children, href, label } = this.props;
+ const { selected } = this.state;
+
+ const className = getClassName({
+ sidebar__link: true,
+ 'is-selected': selected,
+ });
+
+ return (
+
+
+ { label }
+
+
+
+
+
+ );
+ }
+}
+
+SideNavGroup.defaultProps = {
+ children: null,
+ href: '#',
+ selected: false,
+};
+
+SideNavGroup.propTypes = {
+ children: PropTypes.node,
+ href: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ selected: PropTypes.bool,
+};
+
+SideNavGroup.displayName = 'SideNavGroup';
+
+export default SideNavGroup;
diff --git a/src/lib/components/SideNav/SideNavLink.js b/src/lib/components/SideNav/SideNavLink.js
new file mode 100644
index 0000000..8db746a
--- /dev/null
+++ b/src/lib/components/SideNav/SideNavLink.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import getClassName from '../../utils/getClassName';
+
+const SideNavLink = (props) => {
+ const {
+ children, href, label, selected,
+ } = props;
+
+ const className = getClassName({
+ sidebar__link: true,
+ 'is-selected': selected,
+ });
+
+ return (
+
+
+ {label}
+ {children}
+
+
+ );
+};
+
+SideNavLink.defaultProps = {
+ children: null,
+ selected: false,
+};
+
+SideNavLink.propTypes = {
+ children: PropTypes.node,
+ label: PropTypes.string.isRequired,
+ href: PropTypes.string.isRequired,
+ selected: PropTypes.bool,
+};
+
+SideNavLink.displayName = 'SideNavLink';
+
+export default SideNavLink;
diff --git a/src/lib/components/SideNav/__snapshots__/SideNav.test.js.snap b/src/lib/components/SideNav/__snapshots__/SideNav.test.js.snap
new file mode 100644
index 0000000..5f79fa3
--- /dev/null
+++ b/src/lib/components/SideNav/__snapshots__/SideNav.test.js.snap
@@ -0,0 +1,569 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders with a SideNavGroup correctly 1`] = `
+
+`;
+
+exports[` renders with a logo-based SideNavBanner correctly 1`] = `
+
+`;
+
+exports[` renders with a single SideNavLink correctly 1`] = `
+
+`;
+
+exports[` renders with a text-based SideNavBanner correctly 1`] = `
+
+`;
+
+exports[` renders with multiple SideNavGroups correctly 1`] = `
+
+`;
+
+exports[` renders with multiple SideNavLinks correctly 1`] = `
+
+`;
diff --git a/src/lib/index.js b/src/lib/index.js
index d53d533..dbc7284 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -32,6 +32,10 @@ import NavigationLink from './components/Navigation/NavigationLink';
import Notification from './components/Notification/Notification';
import Pagination from './components/Pagination/Pagination';
import PaginationItem from './components/Pagination/PaginationItem';
+import SideNav from './components/SideNav/SideNav';
+import SideNavBanner from './components/SideNav/SideNavBanner';
+import SideNavGroup from './components/SideNav/SideNavGroup';
+import SideNavLink from './components/SideNav/SideNavLink';
import SteppedList from './components/SteppedList/SteppedList';
import SteppedListItem from './components/SteppedList/SteppedListItem';
import Strip from './components/Strip/Strip';
@@ -49,5 +53,7 @@ export {
CodeSnippet, DividerList, DividerListItem, Footer, FooterNav, FooterNavContainer, HeadingIcon,
Image, InlineImages, Link, List, ListItem, ListTree, ListTreeGroup, ListTreeItem, Matrix,
MatrixItem, MediaObject, Modal, MutedHeading, Navigation, NavigationBanner, NavigationLink,
- Notification, Pagination, PaginationItem, SteppedList, SteppedListItem, Strip, StripColumn,
- StripRow, Switch, Table, TableCell, TableRow, Tabs, TabsItem };
+ Notification, Pagination, PaginationItem, SideNav, SideNavBanner, SideNavGroup, SideNavLink,
+ SteppedList, SteppedListItem, Strip, StripColumn, StripRow, Switch, Table, TableCell, TableRow,
+ Tabs, TabsItem,
+};