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 (
- {props.children} + {props.children}
); } return ( - {props.children} + {props.children} ); }; Link.defaultProps = { + className: '', soft: false, strong: false, inverted: false, @@ -45,6 +41,7 @@ Link.defaultProps = { Link.propTypes = { children: PropTypes.node.isRequired, + className: PropTypes.string, href: PropTypes.string.isRequired, soft: PropTypes.bool, strong: PropTypes.bool, diff --git a/src/lib/components/Link/__snapshots__/Link.test.js.snap b/src/lib/components/Link/__snapshots__/Link.test.js.snap index 91bc522..107c9d1 100644 --- a/src/lib/components/Link/__snapshots__/Link.test.js.snap +++ b/src/lib/components/Link/__snapshots__/Link.test.js.snap @@ -30,7 +30,7 @@ exports[`Link component should accept modifiers correctly 1`] = ` className="p-top" > 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 ( +
+ { banner } +
+
    + { content } +
+
+
+ ); + } +} + +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 ( + + ); + } +} + +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 } + + + +
      + { children } +
    +
  • + ); + } +} + +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`] = ` +
    +