diff --git a/packages/main/__karma_snapshots__/OverflowToolbar.md b/packages/main/__karma_snapshots__/OverflowToolbar.md new file mode 100644 index 00000000000..549d557b5b8 --- /dev/null +++ b/packages/main/__karma_snapshots__/OverflowToolbar.md @@ -0,0 +1,378 @@ +# `OverflowToolbar` + +#### `Toolbar with Align:Start and Design:PageFooter` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Middle and Design:PageFooter` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:End and Design:PageFooter` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:SpaceBetween and Design:PageFooter` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Start and Design:ContainerBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Middle and Design:ContainerBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:End and Design:ContainerBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:SpaceBetween and Design:ContainerBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Start and Design:ContentBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Middle and Design:ContentBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:End and Design:ContentBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:SpaceBetween and Design:ContentBar` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Start and Design:ContentBarTransparent` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:Middle and Design:ContentBarTransparent` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:End and Design:ContentBarTransparent` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Toolbar with Align:SpaceBetween and Design:ContentBarTransparent` + +``` + + + + + +
+ Test +
+
+
+
+
+
+``` + +#### `Overflow Toolbar with Spacer` + +``` + + + + + +
+ + Test + + + + Test 2 + +
+
+
+
+
+
+``` + +#### `Overflow Toolbar collapsed Elements` + +``` + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+
+``` + diff --git a/packages/main/src/components/OverflowToolbar/OverflowToolbar.jss.ts b/packages/main/src/components/OverflowToolbar/OverflowToolbar.jss.ts new file mode 100644 index 00000000000..a8b6c368b7d --- /dev/null +++ b/packages/main/src/components/OverflowToolbar/OverflowToolbar.jss.ts @@ -0,0 +1,59 @@ +import { JSSTheme } from '../../interfaces/JSSTheme'; +import { ContentDensity } from '../../lib/ContentDensity'; + +const styles = ({ contentDensity, parameters }: JSSTheme) => ({ + toolbarRoot: { + display: 'flex', + whiteSpace: 'nowrap', + overflow: 'hidden', + alignItems: 'center', + boxSizing: 'border-box', + background: 'transparent', + height: ContentDensity.Compact === contentDensity ? '2rem' : '3rem', + '&:focus': { + outline: 0 + }, + '& :first-child': { + marginLeft: '0.25rem' + }, + '& > *': { + marginRight: '0.5rem !important' // we need the !important here because otherwise e.g. the SearchField has no border + } + }, + toolbarAlignStart: { + justifyContent: 'flex-start' + }, + + toolbarAlignEnd: { + justifyContent: 'flex-end' + }, + + toolbarAlignMiddle: { + justifyContent: 'center' + }, + + toolbarAlignSpaceBetween: { + justifyContent: 'space-between' + }, + pageFooter: { + background: parameters.sapUiPageFooterBackground, + borderTop: `1px solid ${parameters.sapUiPageFooterBorderColor}` + }, + + containerBar: { + background: parameters.sapUiGroupContentBackground + // borderBottom: null + }, + + contentBar: { + background: parameters.sapUiListHeaderBackground, + borderBottom: `1px solid ${parameters.sapUiListHeaderBorderColor}` + }, + + contentBarTransparent: { + background: parameters.sapUiToolbarBackground, + borderBottom: `1px solid ${parameters.sapUiGroupTitleBorderColor}` + } +}); + +export default styles; diff --git a/packages/main/src/components/OverflowToolbar/OverflowToolbar.karma.tsx b/packages/main/src/components/OverflowToolbar/OverflowToolbar.karma.tsx new file mode 100644 index 00000000000..3aca6d3585a --- /dev/null +++ b/packages/main/src/components/OverflowToolbar/OverflowToolbar.karma.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { OverflowToolbar } from '../../lib/OverflowToolbar'; +import { mountThemedComponent } from '@shared/tests/utils'; +import { expect, use } from 'chai'; +import { ToolbarAlignment } from '../../lib/ToolbarAlignment'; +import { ToolbarDesign } from '../../lib/ToolbarDesign'; +import { Button } from '../../lib/Button'; +import { Label } from '../../lib/Label'; +import { Switch } from '../../lib/Switch'; +import { matchSnapshot } from 'chai-karma-snapshot'; + +use(matchSnapshot); + +const alignDesignFactory = () => { + Object.values(ToolbarDesign).forEach((design) => { + Object.values(ToolbarAlignment).forEach((align) => { + it(`Toolbar with Align:${align} and Design:${design}`, () => { + // console.log(align + design); + expect( + mountThemedComponent( + + Test + + ).debug() + ).to.matchSnapshot(); + }); + }); + }); +}; + +describe('OverflowToolbar', () => { + alignDesignFactory(); + + it(`Overflow Toolbar with Spacer`, () => { + const wrapper = mountThemedComponent( + + Test + + Test 2 + + ); + expect(wrapper.render().children().length).equal(3); + expect(wrapper.debug()).to.matchSnapshot(); + }); + + it(`Overflow Toolbar collapsed Elements`, () => { + const wrapper = mountThemedComponent( + + + + + + + + + + ); + expect(wrapper.update().children().length).equal(1); + expect(wrapper.debug()).to.matchSnapshot(); + }); +}); diff --git a/packages/main/src/components/OverflowToolbar/demo.stories.tsx b/packages/main/src/components/OverflowToolbar/demo.stories.tsx new file mode 100644 index 00000000000..8b678151d24 --- /dev/null +++ b/packages/main/src/components/OverflowToolbar/demo.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { select } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; +import { OverflowToolbar } from '../../lib/OverflowToolbar'; +import { ToolbarAlignment } from '../../lib/ToolbarAlignment'; +import { ToolbarDesign } from '../../lib/ToolbarDesign'; +import { Button } from '../../lib/Button'; +import { Label } from '../../lib/Label'; +import { Switch } from '../../lib/Switch'; + +const renderOverflowToolbar = () => ( + + + + + + + + + +); + +storiesOf('Components | Overflow Toolbar', module).add('Overflow Toolbar', renderOverflowToolbar); diff --git a/packages/main/src/components/OverflowToolbar/index.tsx b/packages/main/src/components/OverflowToolbar/index.tsx new file mode 100644 index 00000000000..06aa3c927fa --- /dev/null +++ b/packages/main/src/components/OverflowToolbar/index.tsx @@ -0,0 +1,198 @@ +import React, { Children, Component, CSSProperties, ReactNode, ReactNodeArray, RefObject } from 'react'; +import styles from './OverflowToolbar.jss'; +import { StyleClassHelper, withStyles } from '@ui5/webcomponents-react-base'; +import { ToolbarAlignment } from '../../lib/ToolbarAlignment'; +import { ToolbarDesign } from '../../lib/ToolbarDesign'; +import { CommonProps } from '../../interfaces/CommonProps'; +import { ClassProps } from '../../interfaces/ClassProps'; +import { Popover } from '../../lib/Popover'; +import { Button } from '../../lib/Button'; +import { PlacementType } from '../../lib/PlacementType'; +import boot from '@ui5/webcomponents-base/src/boot'; + +export interface ToolbarPropTypes extends CommonProps { + width?: CSSProperties['width']; + + children: ReactNode | ReactNodeArray; + + align?: ToolbarAlignment; + + toolbarDesign?: ToolbarDesign; + + overflow?: boolean; +} + +interface ToolbarInternalProps extends ToolbarPropTypes, ClassProps {} + +@withStyles(styles) +export class OverflowToolbar extends Component { + static defaultProps = { + width: 'auto', + align: ToolbarAlignment.Start, + toolbarDesign: ToolbarDesign.ContentBar, + overflow: true + }; + state = { + children: this.props.children, + toolbarWidth: 0, + popoverElements: [], + popoverOpen: true, + renderToggle: false, + previousWidth: [] + }; + private toolbarRef: RefObject = React.createRef(); + + componentDidMount(): void { + boot().then(() => { + if (this.props.overflow) { + if (this.state.toolbarWidth === 0 && this.toolbarRef.current !== null) { + this.setState({ toolbarWidth: this.toolbarRef.current.scrollWidth }); + } + window.addEventListener('resize', () => { + requestAnimationFrame(() => { + this.handleResize(); + }); + }); + } + }); + } + + componentDidUpdate(prevProps: Readonly, prevState, snapshot?: any): void { + boot().then(() => { + if (this.props.overflow && this.toolbarRef.current !== null) { + if (this.toolbarRef.current.clientWidth < this.toolbarRef.current.scrollWidth) { + this.handleResize(); + } + if (this.props.width !== prevProps.width) { + this.handleResize(); + } + } + }); + } + + componentWillUnmount(): void { + window.removeEventListener('resize', this.handleResize); + } + + private handleResize = () => { + const toolbarRef = this.toolbarRef.current; + let newChildren = this.state.children; + let { popoverElements, renderToggle, previousWidth, toolbarWidth, children } = this.state; + if (toolbarWidth > toolbarRef.clientWidth) { + if (toolbarRef.clientWidth < toolbarRef.scrollWidth) { + newChildren = Children.toArray(children).slice(0, -1); + popoverElements = [Children.toArray(children).slice(-1)[0]].concat(popoverElements); + renderToggle = true; + previousWidth = [toolbarRef.scrollWidth + 10].concat(previousWidth); + } + this.setState({ + toolbarWidth: toolbarRef.scrollWidth, + children: newChildren, + renderToggle, + popoverElements, + previousWidth + }); + } + if (toolbarWidth < toolbarRef.clientWidth) { + this.addToolbarItem(newChildren, popoverElements, previousWidth, renderToggle); + } + }; + + private addToolbarItem(newChildren, popoverElements, previousWidth, renderToggle) { + const toolbarRef = this.toolbarRef.current; + let { children } = this.state; + if (Children.count(this.props.children) !== Children.count(children)) { + if (toolbarRef.clientWidth === toolbarRef.scrollWidth && toolbarRef.clientWidth >= previousWidth[0]) { + const currentChildrenLength = Children.count(children); + newChildren = Children.toArray(children).concat([ + Children.only(Children.toArray(this.props.children)[currentChildrenLength]) + ]); + popoverElements.shift(); + previousWidth.shift(); + } + } + this.setState({ + toolbarWidth: toolbarRef.clientWidth, + children: newChildren, + popoverElements, + previousWidth + }); + if (Children.count(this.props.children) !== Children.count(children)) { + if (toolbarRef.clientWidth === toolbarRef.scrollWidth && toolbarRef.clientWidth >= previousWidth[0]) { + this.addToolbarItem(newChildren, popoverElements, previousWidth, renderToggle); + } + } else { + this.setState({ renderToggle: false }); + } + } + + render() { + const { width, align, toolbarDesign, classes, className, style, tooltip } = this.props; + const rootClasses = StyleClassHelper.of(classes.toolbarRoot); + const overflowClasses = StyleClassHelper.of(classes.overflowRoot); + + switch (align) { + case ToolbarAlignment.Start: + rootClasses.put(classes.toolbarAlignStart); + break; + case ToolbarAlignment.End: + rootClasses.put(classes.toolbarAlignEnd); + break; + case ToolbarAlignment.Middle: + rootClasses.put(classes.toolbarAlignMiddle); + break; + case ToolbarAlignment.SpaceBetween: + rootClasses.put(classes.toolbarAlignSpaceBetween); + break; + default: + rootClasses.put(classes.toolbarAlignStart); + } + + switch (toolbarDesign) { + case ToolbarDesign.ContentBar: + rootClasses.put(classes.contentBar); + overflowClasses.put(classes.contentBar); + break; + case ToolbarDesign.PageFooter: + rootClasses.put(classes.pageFooter); + overflowClasses.put(classes.pageFooter); + break; + case ToolbarDesign.ContainerBar: + rootClasses.put(classes.containerBar); + overflowClasses.put(classes.containerBar); + break; + case ToolbarDesign.ContentBarTransparent: + rootClasses.put(classes.contentBarTransparent); + overflowClasses.put(classes.contentBarTransparent); + break; + default: + rootClasses.put(classes.containerBar); + overflowClasses.put(classes.containerBar); + } + + if (className) { + rootClasses.put(className); + } + + const inlineStyle = { width }; + if (style) { + Object.assign(inlineStyle, style); + } + return ( +
+ {this.state.children} + {this.state.renderToggle && ( + } + noHeader={true} + placementType={PlacementType.Bottom} + > +
{this.state.popoverElements}
+
+ )} +
+ ); + } +} diff --git a/packages/main/src/enums/ToolbarAlignment.ts b/packages/main/src/enums/ToolbarAlignment.ts new file mode 100644 index 00000000000..1a4b387b4d9 --- /dev/null +++ b/packages/main/src/enums/ToolbarAlignment.ts @@ -0,0 +1,6 @@ +export enum ToolbarAlignment { + Start = 'Start', + Middle = 'Middle', + End = 'End', + SpaceBetween = 'SpaceBetween' +} diff --git a/packages/main/src/enums/ToolbarDesign.ts b/packages/main/src/enums/ToolbarDesign.ts new file mode 100644 index 00000000000..9cf35547648 --- /dev/null +++ b/packages/main/src/enums/ToolbarDesign.ts @@ -0,0 +1,6 @@ +export enum ToolbarDesign { + PageFooter = 'PageFooter', + ContainerBar = 'ContainerBar', + ContentBar = 'ContentBar', + ContentBarTransparent = 'ContentBarTransparent' +} diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index ac8392617cb..cadbb411898 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -66,6 +66,7 @@ import { ObjectPageSection } from './lib/ObjectPageSection'; import { ObjectPageSubSection } from './lib/ObjectPageSubSection'; import { ObjectStatus } from './lib/ObjectStatus'; import { Option } from './lib/Option'; +import { OverflowToolbar } from './lib/OverflowToolbar'; import { Page } from './lib/Page'; import { PageBackgroundDesign } from './lib/PageBackgroundDesign'; import { Panel } from './lib/Panel'; @@ -107,6 +108,8 @@ import { TitleLevel } from './lib/TitleLevel'; import { ToggleButton } from './lib/ToggleButton'; import { Token } from './lib/Token'; import { Tokenizer } from './lib/Tokenizer'; +import { ToolbarAlignment } from './lib/ToolbarAlignment'; +import { ToolbarDesign } from './lib/ToolbarDesign'; import { ValueState } from './lib/ValueState'; import { VariantManagement } from './lib/VariantManagement'; import { VerticalAlign } from './lib/VerticalAlign'; @@ -177,6 +180,7 @@ export { ObjectPageSubSection, ObjectStatus, Option, + OverflowToolbar, Page, PageBackgroundDesign, Panel, @@ -218,6 +222,8 @@ export { ToggleButton, Token, Tokenizer, + ToolbarAlignment, + ToolbarDesign, ValueState, VariantManagement, VerticalAlign, diff --git a/packages/main/src/lib/OverflowToolbar.ts b/packages/main/src/lib/OverflowToolbar.ts new file mode 100644 index 00000000000..a5815cfe64b --- /dev/null +++ b/packages/main/src/lib/OverflowToolbar.ts @@ -0,0 +1,3 @@ +import { OverflowToolbar } from '../components/OverflowToolbar'; + +export { OverflowToolbar }; diff --git a/packages/main/src/lib/ToolbarAlignment.ts b/packages/main/src/lib/ToolbarAlignment.ts new file mode 100644 index 00000000000..1169eec73c2 --- /dev/null +++ b/packages/main/src/lib/ToolbarAlignment.ts @@ -0,0 +1,3 @@ +import { ToolbarAlignment } from '../enums/ToolbarAlignment'; + +export { ToolbarAlignment }; diff --git a/packages/main/src/lib/ToolbarDesign.ts b/packages/main/src/lib/ToolbarDesign.ts new file mode 100644 index 00000000000..222af42fa76 --- /dev/null +++ b/packages/main/src/lib/ToolbarDesign.ts @@ -0,0 +1,3 @@ +import { ToolbarDesign } from '../enums/ToolbarDesign'; + +export { ToolbarDesign };