diff --git a/src/components/adslot-ui/VerticalNavigation/index.jsx b/src/components/adslot-ui/VerticalNavigation/index.jsx
new file mode 100644
index 000000000..51d505e66
--- /dev/null
+++ b/src/components/adslot-ui/VerticalNavigation/index.jsx
@@ -0,0 +1,123 @@
+import _ from 'lodash';
+import classnames from 'classnames';
+import React from 'react';
+import PropTypes from 'prop-types';
+import './styles.scss';
+
+const MenuItem = ({ children }) => children;
+
+class VerticalNavigation extends React.Component {
+ static propTypes = {
+ isCollapsed: PropTypes.bool,
+ onClick: PropTypes.func,
+ dts: PropTypes.string,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ isCollapsed: false,
+ };
+
+ constructor(props) {
+ super(props);
+ this.menuList = this.renderMenu(props);
+ this.contentList = this.renderContent(props);
+ }
+
+ shouldComponentUpdate(nextProps) {
+ this.menuList = this.renderMenu(nextProps);
+
+ // only render collapse/expand
+ if (nextProps.isCollapsed !== this.props.isCollapsed) {
+ return true;
+ }
+
+ this.contentList = this.renderContent(nextProps);
+ return true;
+ }
+
+ getActiveTabIndex = children => {
+ const activeIndex = _.findIndex(children, 'props.isActive');
+ return activeIndex === -1 ? 0 : activeIndex;
+ };
+
+ renderContent = ({ children }) => {
+ const activeTabIndex = this.getActiveTabIndex(children);
+ const contentList = React.Children.map(children, (child, index) => {
+ if (!child.props.content) {
+ // eslint-disable-next-line no-console
+ console.warn('Navigation does not render MenuItem that have no content prop.');
+ return null;
+ }
+
+ const contentClassnames = classnames([
+ 'aui--vertical-navigation-component__content-item',
+ { 'aui--vertical-navigation-component__content-item-is-active': index === activeTabIndex },
+ ]);
+
+ return (
+
+ {child}
+
+ );
+ });
+
+ return _.compact(contentList);
+ };
+
+ renderMenu = ({ children, isCollapsed }) => {
+ const menuList = [];
+ const activeTabIndex = this.getActiveTabIndex(children);
+
+ React.Children.forEach(children, (child, index) => {
+ if (!child.props.content) {
+ // eslint-disable-next-line no-console
+ console.warn('Navigation does not render MenuItem that have no content prop.');
+ return;
+ }
+
+ const classNames = classnames([
+ 'aui--vertical-navigation-component__menu-item',
+ { 'aui--vertical-navigation-component__menu-item-is-active': index === activeTabIndex },
+ ]);
+ menuList.push(
+
+ {child.props.content({ isCollapsed })}
+
+ );
+ });
+
+ return menuList;
+ };
+
+ render() {
+ const { className, dts, isCollapsed } = this.props;
+ const componentClasses = classnames('aui--vertical-navigation-component', className);
+
+ const menuClasses = classnames([
+ 'aui--vertical-navigation-component__menu',
+ 'aui--vertical-navigation-component__menu-is-animated',
+ {
+ 'aui--vertical-navigation-component__menu-is-collapsed': isCollapsed,
+ },
+ ]);
+
+ return (
+
+ );
+ }
+}
+
+VerticalNavigation.MenuItem = MenuItem;
+
+export default VerticalNavigation;
diff --git a/src/components/adslot-ui/VerticalNavigation/index.spec.jsx b/src/components/adslot-ui/VerticalNavigation/index.spec.jsx
new file mode 100644
index 000000000..2e5b84d55
--- /dev/null
+++ b/src/components/adslot-ui/VerticalNavigation/index.spec.jsx
@@ -0,0 +1,111 @@
+import VerticalNav from 'adslot-ui/VerticalNavigation';
+import { mount } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+
+describe('VerticalNavComponent', () => {
+ const makeProps = override => ({
+ isCollapsed: false,
+ onClick: sinon.spy(),
+ dts: 'test-dts',
+ className: 'custom-class',
+ ...override,
+ });
+ const makeMenuItemProps = override => ({
+ isActive: false,
+ dts: 'menu-item-dts',
+ content: sinon.spy(),
+ onClick: sinon.spy(),
+ ...override,
+ });
+
+ let sandbox;
+
+ before(() => {
+ sandbox = sinon.createSandbox();
+ });
+
+ afterEach(() => sandbox.restore());
+
+ it('should render with props', () => {
+ const menuLabel1 = () => Tab 1
;
+ const menuLabel2 = () => Tab 2
;
+
+ const wrapper = mount(
+
+ Content 1
+ Content 1
+
+ );
+
+ expect(wrapper.find('.aui--vertical-navigation-component.custom-class')).to.have.length(1);
+
+ const menuItems = wrapper.find('.aui--vertical-navigation-component__menu-item');
+ expect(menuItems).to.have.length(3); // 1 collapse, 2 menu items
+ expect(menuItems.at(0).find('.aui--vertical-navigation-component__menu-item-collapse')).to.have.length(1);
+ expect(menuItems.at(1).text()).to.equal('Tab 1');
+ expect(menuItems.at(2).text()).to.equal('Tab 2');
+ });
+
+ it('should dispaly warnings if child element does not have `content` prop', () => {
+ sandbox.stub(console, 'warn');
+ const wrapper = mount(
+
+ Some random element
+
+ );
+
+ // only renders collapse item
+ expect(wrapper.find('.aui--vertical-navigation-component__menu-item')).to.have.length(1);
+ /* eslint-disable no-console */
+ expect(console.warn.calledTwice).to.equal(true);
+ expect(console.warn.args[0]).to.eql(['Navigation does not render MenuItem that have no content prop.']);
+ /* eslint-enable no-console */
+ });
+
+ it('should only update menu if props.isCollapsed is changed', () => {
+ const menuLabel1 = () => Tab 1
;
+ const menuLabel2 = () => Tab 2
;
+ const wrapper = mount(
+
+
+ Content 1
+
+ Content 1
+
+ );
+
+ const renderMenuSpy = sandbox.spy(wrapper.instance(), 'renderMenu');
+ const renderContentSpy = sandbox.spy(wrapper.instance(), 'renderContent');
+ wrapper.setProps({ isCollapsed: true });
+
+ expect(renderMenuSpy.called).to.equal(true);
+ expect(renderContentSpy.called).to.equal(false);
+ });
+
+ it('should render both menu and content if active tab changes', () => {
+ const menuLabel1 = () => Tab 1
;
+ const menuLabel2 = () => Tab 2
;
+ const wrapper = mount(
+
+
+ Content 1
+
+ Content 1
+
+ );
+ const renderMenuSpy = sandbox.spy(wrapper.instance(), 'renderMenu');
+ const renderContentSpy = sandbox.spy(wrapper.instance(), 'renderContent');
+ wrapper.setProps({
+ children: [
+ Content 1,
+
+ Content 1
+ ,
+ ],
+ });
+
+ expect(renderMenuSpy.called).to.equal(true);
+ expect(renderContentSpy.called).to.equal(true);
+ });
+});
diff --git a/src/components/adslot-ui/VerticalNavigation/styles.scss b/src/components/adslot-ui/VerticalNavigation/styles.scss
new file mode 100644
index 000000000..011bf734b
--- /dev/null
+++ b/src/components/adslot-ui/VerticalNavigation/styles.scss
@@ -0,0 +1,67 @@
+@import '~styles/variable';
+@import '~styles/mixin';
+
+$menu-min-width: 60px;
+$menu-max-width: 260px;
+
+.aui--vertical-navigation-component {
+ display: flex;
+ flex-direction: row;
+
+ .aui--vertical-navigation-component__menu {
+ width: $menu-max-width;
+ font-size: $font-size-subheader;
+ font-family: $font-family-sans-serif;
+ font-weight: $font-weight-bold;
+
+ &.aui--vertical-navigation-component__menu-is-collapsed {
+ width: $menu-min-width;
+ }
+
+ &.aui--vertical-navigation-component__menu-is-animated {
+ transition: width 1s ease-in-out;
+ }
+
+ .aui--vertical-navigation-component__menu-item {
+ height: 60px;
+ padding: 10px;
+ margin-right: 6px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ white-space: nowrap;
+ overflow: hidden;
+
+ .aui--vertical-navigation-component__menu-item-collapse {
+ display: flex;
+ width: 40px;
+ height: 40px;
+ flex-direction: column;
+ justify-content: center;
+ padding: 8px;
+
+ &:hover {
+ border-radius: 50%;
+ background-color: $color-gray-lighter;
+ }
+ }
+
+ .aui--vertical-navigation-component__menu-item-collapse-icon {
+ @include svg-icon('~styles/icons/burger.svg', 24px, 24px);
+ }
+ }
+ }
+
+ .aui--vertical-navigation-component__content {
+ flex-grow: 1;
+
+ .aui--vertical-navigation-component__content-item {
+ display: none;
+
+ &.aui--vertical-navigation-component__content-item-is-active {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/src/components/adslot-ui/index.js b/src/components/adslot-ui/index.js
index 3baa2bdb0..d9a1d381a 100644
--- a/src/components/adslot-ui/index.js
+++ b/src/components/adslot-ui/index.js
@@ -31,11 +31,13 @@ import HoverDropdownMenu from 'adslot-ui/HoverDropdownMenu';
import fastStatelessWrapper from 'adslot-ui/fastStatelessWrapper';
import InformationBox from 'adslot-ui/InformationBox';
import Nav from 'adslot-ui/Navigation';
+import VerticalNav from 'adslot-ui/VerticalNavigation';
import OverlayLoader from 'adslot-ui/OverlayLoader';
import ActionPanel from 'adslot-ui/ActionPanel';
export {
Accordion,
+ ActionPanel,
AlertInput,
ButtonGroup,
Carousel,
@@ -69,5 +71,5 @@ export {
InformationBox,
HoverDropdownMenu,
OverlayLoader,
- ActionPanel,
+ VerticalNav,
};
diff --git a/src/index.js b/src/index.js
index 7e1a37c01..a548295f4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -44,6 +44,7 @@ import {
import {
Accordion,
+ ActionPanel,
AlertInput,
ButtonGroup,
Carousel,
@@ -77,11 +78,12 @@ import {
InformationBox,
HoverDropdownMenu,
OverlayLoader,
- ActionPanel,
+ VerticalNav,
} from 'adslot-ui';
export {
Accordion,
+ ActionPanel,
Alert,
AlertInput,
Avatar,
@@ -146,5 +148,5 @@ export {
InformationBox,
HoverDropdownMenu,
OverlayLoader,
- ActionPanel,
+ VerticalNav,
};
diff --git a/src/styles/icons/burger.svg b/src/styles/icons/burger.svg
new file mode 100644
index 000000000..41ed01c17
--- /dev/null
+++ b/src/styles/icons/burger.svg
@@ -0,0 +1,39 @@
+
+
+
diff --git a/src/styles/icons/details.svg b/src/styles/icons/details.svg
new file mode 100644
index 000000000..f5daca238
--- /dev/null
+++ b/src/styles/icons/details.svg
@@ -0,0 +1,43 @@
+
+
+
diff --git a/src/styles/mixin.scss b/src/styles/mixin.scss
index fead5517b..ea8d42436 100644
--- a/src/styles/mixin.scss
+++ b/src/styles/mixin.scss
@@ -3,3 +3,11 @@
text-overflow: ellipsis;
white-space: nowrap;
}
+
+@mixin svg-icon($uri, $width: $icon-size, $height: $icon-size) {
+ width: $width;
+ height: $height;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-image: url($uri);
+}
diff --git a/www/components/Layout/index.jsx b/www/components/Layout/index.jsx
index 78e892f90..db4a5be19 100644
--- a/www/components/Layout/index.jsx
+++ b/www/components/Layout/index.jsx
@@ -58,6 +58,7 @@ import InformationBoxExample from '../../examples/InformationBoxExample';
import SplitPaneExample from '../../examples/SplitPaneExample';
import HoverDropdownMenuExample from '../../examples/HoverDropdownMenuExample';
import NavigationExample from '../../examples/NavigationExample';
+import VerticalNavigationExample from '../../examples/VerticalNavigationExample';
import OverlayLoaderExample from '../../examples/OverlayLoaderExample';
import SearchExample from '../../examples/SearchExample';
import ActionPanelExample from '../../examples/ActionPanelExample';
@@ -93,7 +94,7 @@ const componentsBySection = {
'typography-and-text-layout': ['text-ellipsis'],
'stats-and-data': ['count-badge', 'statistic', 'totals', 'slicey'],
'icons-and-graphics': ['svg-symbol', 'svg-symbol-circle'],
- navigation: ['breadcrumb', 'tab', 'hover-dropdown-menu', 'navigation-tabs'],
+ navigation: ['breadcrumb', 'tab', 'hover-dropdown-menu', 'navigation-tabs', 'vertical-navigation-tabs'],
'feedback-and-states': ['alert', 'empty', 'spinner', 'overlay-loader', 'pretty-diff', 'status-pill'],
dialogue: ['popover', 'help-icon-popover', 'avatar'],
modals: ['confirm-modal'],
@@ -210,6 +211,7 @@ class PageLayout extends React.Component {
+
diff --git a/www/examples/VerticalNavigationExample.jsx b/www/examples/VerticalNavigationExample.jsx
new file mode 100644
index 000000000..d9a88ff94
--- /dev/null
+++ b/www/examples/VerticalNavigationExample.jsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import Example from '../components/Example';
+import { VerticalNav } from '../../src';
+
+const MenuContent = ({ isCollapsed, isActive, label, icon }) => (
+
+);
+
+class VerticalNavigationExample extends React.PureComponent {
+ state = {
+ isCollapsed: false,
+ activeTab: 'Tab 1',
+ };
+
+ handleCollapse = () => this.setState(prevState => ({ isCollapsed: !prevState.isCollapsed }));
+
+ handleMenuClick = tabName => () => this.setState({ activeTab: tabName });
+
+ renderMenu1 = ({ isCollapsed }) => (
+ : ,
+ }}
+ />
+ );
+
+ render() {
+ return (
+
+
+ this is a content
+
+ (
+ ,
+ }}
+ />
+ )}
+ >
+ this is another content
+
+
+ );
+ }
+}
+
+const exampleProps = {
+ componentName: 'Vertical Navigation Tabs',
+ exampleCodeSnippet: `
+
+
+ this is a content
+
+ (
+ ,
+ }}
+ />
+ )}
+ >
+ this is another content
+
+
+ `,
+ propTypeSectionArray: [
+ {
+ propTypes: [
+ {
+ propType: 'isCollapsed',
+ type: 'bool',
+ defaultValue: false
,
+ },
+ {
+ propType: 'dts',
+ type: 'string',
+ note: 'data-test-selector; used for testing purposes',
+ },
+ {
+ propType: 'onClick',
+ type: 'func',
+ note: (
+
+ event handler for clicking on the collapse/expand button
+
const onClick = () => ...
+
+ ),
+ },
+ {
+ propType: 'className',
+ type: 'string',
+ },
+ ],
+ },
+ ],
+};
+
+export default () => (
+
+
+
+);
diff --git a/www/examples/styles.scss b/www/examples/styles.scss
index e4c39ff0e..f8956d0d5 100644
--- a/www/examples/styles.scss
+++ b/www/examples/styles.scss
@@ -1,4 +1,5 @@
@import '~styles/variable';
+@import '~styles/mixin';
@mixin accent($id, $color-accent) {
.tag-component-accent {
@@ -185,6 +186,53 @@
}
}
}
+
+ &.vertical-navigation-tabs-example {
+ .aui--vertical-navigation-component__menu-item {
+ &.aui--vertical-navigation-component__menu-item-is-active {
+ .menu-icon {
+ border-radius: 50%;
+ background-color: $color-gray-lighter;
+ }
+ }
+ }
+
+ .menu-content {
+ display: flex;
+ flex-direction: row;
+
+ &:hover {
+ .menu-icon {
+ border-radius: 50%;
+ background-color: $color-gray-lighter;
+ }
+ }
+
+ .menu-icon {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 8px;
+
+ .product-details-icon {
+ @include svg-icon('~styles/icons/details.svg', 24px, 24px);
+ }
+
+ .burger-icon {
+ @include svg-icon('~styles/icons/burger.svg', 24px, 24px);
+ }
+ }
+
+ .menu-label {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 8px;
+ }
+ }
+ }
}
.full-width {