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 };