diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 00000000000..3b4414c7ce8
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,19 @@
+# Settings in the [build] context are global and are applied to all contexts
+# unless otherwise overridden by more specific contexts.
+[build]
+ # Directory to change to before starting a build.
+ # This is where we will look for package.json/.nvmrc/etc.
+ base = "./"
+
+ # Directory (relative to root of your repo) that contains the deploy-ready
+ # HTML files and assets generated by the build. If a base directory has
+ # been specified, include it in the publish directory path.
+ publish = ".out/"
+
+ # Default build command.
+ command = "npm run build:storybook"
+
+# Deploy Preview context: all deploys generated from a pull/merge request will
+# inherit these settings.
+[context.deploy-preview]
+ SKIP_DOC_GENERATION=true
diff --git a/packages/main/src/components/SideNavigation/SideNavigation.jss.ts b/packages/main/src/components/SideNavigation/SideNavigation.jss.ts
new file mode 100644
index 00000000000..6c51aa1f82d
--- /dev/null
+++ b/packages/main/src/components/SideNavigation/SideNavigation.jss.ts
@@ -0,0 +1,35 @@
+import { JSSTheme } from '../../interfaces/JSSTheme';
+
+export const sideNavigationStyles = ({ parameters }: JSSTheme) => ({
+ sideNavigation: {
+ height: '100%',
+ borderRight: `0.0625rem solid ${parameters.sapUiGroupContentBorderColor}`,
+ backgroundColor: parameters.sapUiListBackground,
+ display: 'flex',
+ flexDirection: 'column',
+ transition: 'width 500ms'
+ },
+
+ expanded: {
+ width: '15rem'
+ },
+
+ condensed: {
+ width: '2.75rem',
+ '& $footerItemsSeparator': {
+ marginLeft: '0.5rem',
+ marginRight: '0.5rem'
+ }
+ },
+
+ collapsed: {
+ width: '15rem',
+ marginLeft: '-15.0625rem'
+ },
+
+ footerItemsSeparator: {
+ margin: '0.25rem 0.875rem',
+ height: '0.125rem',
+ backgroundColor: parameters.sapUiListBorderColor
+ }
+});
diff --git a/packages/main/src/components/SideNavigation/SideNavigation.test.tsx b/packages/main/src/components/SideNavigation/SideNavigation.test.tsx
new file mode 100644
index 00000000000..f15152da76b
--- /dev/null
+++ b/packages/main/src/components/SideNavigation/SideNavigation.test.tsx
@@ -0,0 +1,103 @@
+import { mountThemedComponent } from '@shared/tests/utils';
+import { SideNavigation } from '@ui5/webcomponents-react/lib/SideNavigation';
+import { SideNavigationListItem } from '@ui5/webcomponents-react/lib/SideNavigationListItem';
+import { SideNavigationOpenState } from '@ui5/webcomponents-react/lib/SideNavigationOpenState';
+import React from 'react';
+
+describe('SideNavigation', () => {
+ test('Expanded', () => {
+ const wrapper = mountThemedComponent(
+ ,
+
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+
+ test('Expanded without Icons', () => {
+ const wrapper = mountThemedComponent(
+ ,
+
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+
+ test('Condensed', () => {
+ const wrapper = mountThemedComponent(
+ ,
+
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+ test('Collapsed', () => {
+ const wrapper = mountThemedComponent(
+ ,
+
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+});
diff --git a/packages/main/src/components/SideNavigation/__snapshots__/SideNavigation.test.tsx.snap b/packages/main/src/components/SideNavigation/__snapshots__/SideNavigation.test.tsx.snap
new file mode 100644
index 00000000000..25b48358443
--- /dev/null
+++ b/packages/main/src/components/SideNavigation/__snapshots__/SideNavigation.test.tsx.snap
@@ -0,0 +1,686 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SideNavigation Collapsed 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sales
+
+
+ My Opportunities
+
+
+ My Leads
+
+
+ My CPQS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`SideNavigation Condensed 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sales
+
+
+ My Opportunities
+
+
+ My Leads
+
+
+ My CPQS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`SideNavigation Expanded 1`] = `
+
+
+
+
+
+ Overview
+
+
+
+
+
+ Calendar
+
+
+
+
+
+ Customers
+
+
+
+
+
+ Sales
+
+
+
+
+
+
+ My Opportunities
+
+
+
+
+
+ My Leads
+
+
+
+
+
+ My CPQS
+
+
+
+
+
+ Deliveries
+
+
+
+
+
+
+
+
+
+ Legal Information
+
+
+
+
+
+ Useful Links
+
+
+
+
+`;
+
+exports[`SideNavigation Expanded without Icons 1`] = `
+
+
+
+
+ Overview
+
+
+
+
+ Calendar
+
+
+
+
+ Customers
+
+
+
+
+ Sales
+
+
+
+
+
+ My Opportunities
+
+
+
+
+ My Leads
+
+
+
+
+ My CPQS
+
+
+
+
+ Deliveries
+
+
+
+
+
+
+
+
+ Legal Information
+
+
+
+
+ Useful Links
+
+
+
+
+`;
diff --git a/packages/main/src/components/SideNavigation/demo.stories.tsx b/packages/main/src/components/SideNavigation/demo.stories.tsx
new file mode 100644
index 00000000000..d4f27f66f14
--- /dev/null
+++ b/packages/main/src/components/SideNavigation/demo.stories.tsx
@@ -0,0 +1,39 @@
+import { action } from '@storybook/addon-actions';
+import { select, text, boolean } from '@storybook/addon-knobs';
+import { SideNavigation } from '@ui5/webcomponents-react/lib/SideNavigation';
+import { SideNavigationListItem } from '@ui5/webcomponents-react/lib/SideNavigationListItem';
+import { SideNavigationOpenState } from '@ui5/webcomponents-react/lib/SideNavigationOpenState';
+import React from 'react';
+
+export const defaultStory = () => (
+ ,
+
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+);
+
+defaultStory.story = {
+ name: 'Default'
+};
+
+export default {
+ title: 'Components | SideNavigation',
+ component: SideNavigation
+};
diff --git a/packages/main/src/components/SideNavigation/index.tsx b/packages/main/src/components/SideNavigation/index.tsx
new file mode 100644
index 00000000000..ef73543b33a
--- /dev/null
+++ b/packages/main/src/components/SideNavigation/index.tsx
@@ -0,0 +1,123 @@
+import { Event, StyleClassHelper } from '@ui5/webcomponents-react-base';
+import { List } from '@ui5/webcomponents-react/lib/List';
+import { SideNavigationOpenState } from '@ui5/webcomponents-react/lib/SideNavigationOpenState';
+import React, { Children, cloneElement, FC, forwardRef, ReactNode, Ref, useCallback, useEffect, useState } from 'react';
+import { createUseStyles } from 'react-jss';
+import { CommonProps } from '../../interfaces/CommonProps';
+import { JSSTheme } from '../../interfaces/JSSTheme';
+import { sideNavigationStyles } from './SideNavigation.jss';
+
+export interface SideNavigationProps extends CommonProps {
+ openState?: SideNavigationOpenState;
+ children?: ReactNode;
+ footerItems?: ReactNode[];
+ selectedId?: string | number;
+ onItemSelect?: (event: Event) => void;
+ noIcons?: boolean;
+}
+
+let lastFiredSelection = '';
+let lastParent = '';
+
+const useStyles = createUseStyles>(sideNavigationStyles, {
+ name: 'SideNavigation'
+});
+
+const SideNavigation: FC = forwardRef((props: SideNavigationProps, ref: Ref) => {
+ const { children, openState, footerItems, selectedId, onItemSelect, noIcons, style, className } = props;
+
+ const classes = useStyles();
+
+ const [internalSelectedId, setInternalSelectedId] = useState(selectedId);
+
+ useEffect(() => {
+ setInternalSelectedId(selectedId);
+ }, [selectedId, setInternalSelectedId]);
+
+ const sideNavigationClasses = StyleClassHelper.of(classes.sideNavigation);
+
+ switch (openState) {
+ case SideNavigationOpenState.Expanded: {
+ sideNavigationClasses.put(classes.expanded);
+ break;
+ }
+ case SideNavigationOpenState.Condensed: {
+ sideNavigationClasses.put(classes.condensed);
+ break;
+ }
+ case SideNavigationOpenState.Collapsed: {
+ sideNavigationClasses.put(classes.collapsed);
+ break;
+ }
+ }
+
+ if (className) {
+ sideNavigationClasses.put(className);
+ }
+
+ const onListItemSelected = useCallback(
+ (e) => {
+ const listItem = e.getParameter('item');
+
+ if (lastFiredSelection === listItem.dataset.id) {
+ return;
+ }
+
+ if (listItem.dataset.id === lastParent) {
+ lastParent = '';
+ return;
+ }
+
+ if (listItem.dataset.parentId) {
+ lastParent = listItem.dataset.parentId;
+ }
+
+ setInternalSelectedId(listItem.dataset.id);
+ onItemSelect(
+ Event.of(null, e, {
+ selectedItem: listItem
+ })
+ );
+ lastFiredSelection = listItem.dataset.id;
+ },
+ [onItemSelect, setInternalSelectedId]
+ );
+
+ return (
+
+
+ {Children.map(children, (child: any) =>
+ cloneElement(child, {
+ openState: openState,
+ selectedId: internalSelectedId,
+ noIcons
+ })
+ )}
+
+
+ {footerItems.length && }
+ {footerItems && (
+
+ {footerItems.map((item: any, index) =>
+ cloneElement(item, {
+ openState: openState,
+ key: index,
+ selectedId: internalSelectedId,
+ noIcons
+ })
+ )}
+
+ )}
+
+ );
+});
+
+SideNavigation.displayName = 'SideNavigation';
+
+SideNavigation.defaultProps = {
+ openState: SideNavigationOpenState.Expanded,
+ footerItems: [],
+ selectedId: null
+};
+
+export { SideNavigation };
diff --git a/packages/main/src/components/SideNavigationListItem/SideNavigationListItem.jss.ts b/packages/main/src/components/SideNavigationListItem/SideNavigationListItem.jss.ts
new file mode 100644
index 00000000000..09a8ee5d909
--- /dev/null
+++ b/packages/main/src/components/SideNavigationListItem/SideNavigationListItem.jss.ts
@@ -0,0 +1,64 @@
+import { JSSTheme } from '../../interfaces/JSSTheme';
+
+export const sideNavigationListItemStyles = ({ parameters }: JSSTheme) => ({
+ listItem: {
+ '&:active': {
+ '--sapUiBaseText': parameters.sapUiListActiveTextColor,
+ '& $icon, & $expandArrow': {
+ '--sapUiContentNonInteractiveIconColor': parameters.sapUiListActiveTextColor
+ }
+ }
+ },
+
+ noIcons: {
+ '& $text': {
+ paddingLeft: '1rem'
+ },
+ '&[data-is-child] $text': {
+ paddingLeft: '2rem'
+ },
+ boxSizing: 'border-box'
+ },
+
+ icon: {
+ '--sapUiContentNonInteractiveIconColor': parameters.sapUiContentIconColor,
+ fontSize: '1.125rem',
+ width: '2.75rem',
+ height: '2.75rem'
+ },
+
+ text: {},
+
+ expandArrow: {
+ '--sapUiContentNonInteractiveIconColor': parameters.sapUiContentIconColor,
+ fontSize: '0.875rem',
+ width: '2.5rem',
+ height: '2.5rem',
+ marginLeft: 'auto'
+ },
+
+ expanded: {
+ '--ui5-listitem-border-bottom': 'none'
+ },
+
+ compact: {
+ '& $icon': {
+ fontSize: '1rem',
+ height: '2rem'
+ },
+ '& $expandArrow': {
+ height: '2rem'
+ }
+ },
+
+ condensedExpandTriangle: {
+ width: '0',
+ height: '0',
+ borderStyle: 'solid',
+ borderWidth: '0 0 6px 6px',
+ borderColor: `transparent transparent ${parameters.sapUiContentIconColor} transparent`,
+ position: 'absolute',
+ right: '0.125rem',
+ bottom: '0.1875rem'
+ }
+});
diff --git a/packages/main/src/components/SideNavigationListItem/SideNavigationListItem.test.tsx b/packages/main/src/components/SideNavigationListItem/SideNavigationListItem.test.tsx
new file mode 100644
index 00000000000..c154a189861
--- /dev/null
+++ b/packages/main/src/components/SideNavigationListItem/SideNavigationListItem.test.tsx
@@ -0,0 +1,33 @@
+import { mountThemedComponent } from '@shared/tests/utils';
+import { SideNavigation } from '@ui5/webcomponents-react/lib/SideNavigation';
+import { SideNavigationListItem } from '@ui5/webcomponents-react/lib/SideNavigationListItem';
+import { SideNavigationOpenState } from '@ui5/webcomponents-react/lib/SideNavigationOpenState';
+import React from 'react';
+import { ContentDensity } from '../..';
+
+describe('SideNavigationListItem', () => {
+ test('Basic', () => {
+ const wrapper = mountThemedComponent();
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+
+ test('custom class name and style', () => {
+ const wrapper = mountThemedComponent(
+
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+
+ test('compact size', () => {
+ const wrapper = mountThemedComponent(, {
+ contentDensity: ContentDensity.Compact
+ });
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+});
diff --git a/packages/main/src/components/SideNavigationListItem/__snapshots__/SideNavigationListItem.test.tsx.snap b/packages/main/src/components/SideNavigationListItem/__snapshots__/SideNavigationListItem.test.tsx.snap
new file mode 100644
index 00000000000..1b7f6f19aba
--- /dev/null
+++ b/packages/main/src/components/SideNavigationListItem/__snapshots__/SideNavigationListItem.test.tsx.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SideNavigationListItem Basic 1`] = `
+
+
+
+`;
+
+exports[`SideNavigationListItem compact size 1`] = `
+
+
+
+`;
+
+exports[`SideNavigationListItem custom class name and style 1`] = `
+
+
+
+`;
diff --git a/packages/main/src/components/SideNavigationListItem/index.tsx b/packages/main/src/components/SideNavigationListItem/index.tsx
new file mode 100644
index 00000000000..34cc7e25cd5
--- /dev/null
+++ b/packages/main/src/components/SideNavigationListItem/index.tsx
@@ -0,0 +1,170 @@
+import { StyleClassHelper } from '@ui5/webcomponents-react-base';
+import { ContentDensity } from '@ui5/webcomponents-react/lib/ContentDensity';
+import { CustomListItem } from '@ui5/webcomponents-react/lib/CustomListItem';
+import { Icon } from '@ui5/webcomponents-react/lib/Icon';
+import { List } from '@ui5/webcomponents-react/lib/List';
+import { Popover } from '@ui5/webcomponents-react/lib/Popover';
+import { PopoverVerticalAlign } from '@ui5/webcomponents-react/lib/PopoverVerticalAlign';
+import { SideNavigationOpenState } from '@ui5/webcomponents-react/lib/SideNavigationOpenState';
+import { StandardListItem } from '@ui5/webcomponents-react/lib/StandardListItem';
+import { Text } from '@ui5/webcomponents-react/lib/Text';
+import React, {
+ Children,
+ cloneElement,
+ FC,
+ forwardRef,
+ ReactNode,
+ ReactNodeArray,
+ Ref,
+ useCallback,
+ useEffect,
+ useState
+} from 'react';
+import { createUseStyles, useTheme } from 'react-jss';
+import { CommonProps } from '../../interfaces/CommonProps';
+import { JSSTheme } from '../../interfaces/JSSTheme';
+import { sideNavigationListItemStyles } from './SideNavigationListItem.jss';
+
+export interface SideNavigationListItemProps extends CommonProps {
+ icon?: string;
+ text: string;
+ id: number | string;
+ children?: ReactNode | ReactNodeArray;
+}
+
+const useStyles = createUseStyles>(
+ sideNavigationListItemStyles,
+ {
+ name: 'SideNavigationListItem'
+ }
+);
+const SideNavigationListItem: FC = forwardRef(
+ (props: SideNavigationListItemProps, ref: Ref) => {
+ const { icon, text, id, children, tooltip, slot, className, style } = props;
+
+ const [isExpanded, setExpanded] = useState(false);
+
+ const handleToggleExpand = useCallback(() => {
+ setExpanded(!isExpanded);
+ }, [isExpanded, setExpanded]);
+
+ const classes = useStyles();
+ const theme = useTheme() as JSSTheme;
+
+ const listItemClasses = StyleClassHelper.of(classes.listItem);
+ if (theme.contentDensity === ContentDensity.Compact) {
+ listItemClasses.put(classes.compact);
+ }
+
+ if (className) {
+ listItemClasses.put(className);
+ }
+
+ if (isExpanded) {
+ listItemClasses.put(classes.expanded);
+ }
+
+ const noIcons = props['noIcons'];
+ if (noIcons) {
+ listItemClasses.put(classes.noIcons);
+ }
+
+ const childCount = Children.count(children);
+ const validChildren = Children.toArray(children).filter(Boolean);
+
+ const isOpenStateExpanded = props['openState'] === SideNavigationOpenState.Expanded;
+
+ useEffect(() => {
+ if (validChildren.length) {
+ const selectedElement = validChildren.find((child: any) => child.props.id === props['selectedId']);
+ if (selectedElement) {
+ setExpanded(isOpenStateExpanded);
+ }
+ }
+ }, [props['selectedId'], id, children, setExpanded, isOpenStateExpanded]);
+
+ const isSelfSelected = props['selectedId'] === id;
+ const hasSelectedChild =
+ !isOpenStateExpanded &&
+ childCount > 0 &&
+ !!validChildren.find((child: any) => child.props.id === props['selectedId']);
+
+ return (
+ <>
+ 0}
+ data-is-child={props['isChild']}
+ >
+ {(isOpenStateExpanded || childCount === 0) && icon && !noIcons && (
+
+ )}
+ {!isOpenStateExpanded && icon && childCount > 0 && !noIcons && (
+ }
+ >
+
+
+ {text}
+
+ {validChildren.map((child: any, index) => {
+ return (
+
+ {child.props.text}
+
+ );
+ })}
+
+
+ )}
+ {!isOpenStateExpanded && icon && childCount > 0 && }
+ {!icon && !noIcons && }
+ {isOpenStateExpanded && {text}}
+ {isOpenStateExpanded && childCount > 0 && (
+
+ )}
+
+ {isOpenStateExpanded &&
+ isExpanded &&
+ validChildren.map((child: any, index: number) => {
+ const style = child.props.style || {};
+ if (index !== childCount - 1) {
+ style['--ui5-listitem-border-bottom'] = 'none';
+ }
+
+ return cloneElement(child, {
+ icon: null,
+ style,
+ openState: props['openState'],
+ selectedId: props['selectedId'],
+ noIcons,
+ isChild: true
+ });
+ })}
+ >
+ );
+ }
+);
+
+SideNavigationListItem.displayName = 'SideNavigationListItem';
+
+SideNavigationListItem.defaultProps = {};
+
+export { SideNavigationListItem };
diff --git a/packages/main/src/enums/SideNavigationOpenState.ts b/packages/main/src/enums/SideNavigationOpenState.ts
new file mode 100644
index 00000000000..18ba6e703b0
--- /dev/null
+++ b/packages/main/src/enums/SideNavigationOpenState.ts
@@ -0,0 +1,5 @@
+export enum SideNavigationOpenState {
+ Expanded = 'Expanded',
+ Condensed = 'Condensed',
+ Collapsed = 'Collapsed'
+}
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index 2de90f59449..ac807763ad3 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -78,6 +78,9 @@ import { Select } from './lib/Select';
import { SemanticColor } from './lib/SemanticColor';
import { ShellBar } from './lib/ShellBar';
import { ShellBarItem } from './lib/ShellBarItem';
+import { SideNavigation } from './lib/SideNavigation';
+import { SideNavigationListItem } from './lib/SideNavigationListItem';
+import { SideNavigationOpenState } from './lib/SideNavigationOpenState';
import { Size } from './lib/Size';
import { Spinner } from './lib/Spinner';
import { StandardListItem } from './lib/StandardListItem';
@@ -182,6 +185,9 @@ export {
SemanticColor,
ShellBar,
ShellBarItem,
+ SideNavigation,
+ SideNavigationListItem,
+ SideNavigationOpenState,
Size,
Spinner,
StandardListItem,
diff --git a/packages/main/src/lib/SideNavigation.ts b/packages/main/src/lib/SideNavigation.ts
new file mode 100644
index 00000000000..922493895be
--- /dev/null
+++ b/packages/main/src/lib/SideNavigation.ts
@@ -0,0 +1,3 @@
+import { SideNavigation } from '../components/SideNavigation';
+
+export { SideNavigation };
diff --git a/packages/main/src/lib/SideNavigationListItem.ts b/packages/main/src/lib/SideNavigationListItem.ts
new file mode 100644
index 00000000000..d2f6c8916cb
--- /dev/null
+++ b/packages/main/src/lib/SideNavigationListItem.ts
@@ -0,0 +1,3 @@
+import { SideNavigationListItem } from '../components/SideNavigationListItem';
+
+export { SideNavigationListItem };
diff --git a/packages/main/src/lib/SideNavigationOpenState.ts b/packages/main/src/lib/SideNavigationOpenState.ts
new file mode 100644
index 00000000000..e9ffcc3d00c
--- /dev/null
+++ b/packages/main/src/lib/SideNavigationOpenState.ts
@@ -0,0 +1,3 @@
+import { SideNavigationOpenState } from '../enums/SideNavigationOpenState';
+
+export { SideNavigationOpenState };