diff --git a/packages/base/src/hooks/useViewportRange.ts b/packages/base/src/hooks/useViewportRange.ts new file mode 100644 index 00000000000..21182e6fec3 --- /dev/null +++ b/packages/base/src/hooks/useViewportRange.ts @@ -0,0 +1,20 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Device } from '@ui5/webcomponents-react-base/lib/Device'; + +export const useViewportRange = (rangeSet) => { + const [currentRange, setCurrentRange] = useState(Device.media.getCurrentRange(rangeSet, window.innerWidth).name); + + const onWindowResize = useCallback( + ({ name: range }) => { + setCurrentRange(range); + }, + [currentRange, setCurrentRange] + ); + + useEffect(() => { + Device.media.attachHandler(onWindowResize, null, 'StdExt'); + return () => Device.resize.detachHandler(onWindowResize, null); + }, [onWindowResize]); + + return currentRange; +}; diff --git a/packages/base/src/lib/useViewportRange.ts b/packages/base/src/lib/useViewportRange.ts new file mode 100644 index 00000000000..2a7c4c73300 --- /dev/null +++ b/packages/base/src/lib/useViewportRange.ts @@ -0,0 +1,3 @@ +import { useViewportRange } from '../hooks/useViewportRange'; + +export { useViewportRange }; diff --git a/packages/main/src/components/AnalyticalTable/hooks/useCellStyling.ts b/packages/main/src/components/AnalyticalTable/hooks/useCellStyling.ts new file mode 100644 index 00000000000..0260fb830ab --- /dev/null +++ b/packages/main/src/components/AnalyticalTable/hooks/useCellStyling.ts @@ -0,0 +1,48 @@ +import { TextAlign } from '@ui5/webcomponents-react/lib/TextAlign'; +import { VerticalAlign } from '@ui5/webcomponents-react/lib/VerticalAlign'; +import { CSSProperties } from 'react'; + +export const useCellStyling = ({ rowHeight }, classes) => ({ column }) => { + const style: CSSProperties = {}; + + if (rowHeight) { + style.height = `${rowHeight}px`; + } + switch (column.hAlign) { + case TextAlign.Begin: + style.textAlign = 'start'; + break; + case TextAlign.Center: + style.textAlign = 'center'; + break; + case TextAlign.End: + style.textAlign = 'end'; + break; + case TextAlign.Left: + style.textAlign = 'left'; + break; + case TextAlign.Right: + style.textAlign = 'right'; + break; + } + switch (column.vAlign) { + case VerticalAlign.Bottom: + style.verticalAlign = 'bottom'; + break; + case VerticalAlign.Middle: + style.verticalAlign = 'middle'; + break; + case VerticalAlign.Top: + style.verticalAlign = 'top'; + break; + } + + let className = classes.tableCell; + if (column.className) { + className += ` ${column.className}`; + } + return { + className, + style + }; +}; diff --git a/packages/main/src/components/Form/CurrentViewportRangeContext.ts b/packages/main/src/components/Form/CurrentViewportRangeContext.ts new file mode 100644 index 00000000000..50dcdd5a8ac --- /dev/null +++ b/packages/main/src/components/Form/CurrentViewportRangeContext.ts @@ -0,0 +1,5 @@ +import React from 'react'; + +const CurrentRange = React.createContext(null); + +export { CurrentRange }; diff --git a/packages/main/src/components/Form/Form.jss.ts b/packages/main/src/components/Form/Form.jss.ts new file mode 100644 index 00000000000..daf85916178 --- /dev/null +++ b/packages/main/src/components/Form/Form.jss.ts @@ -0,0 +1,28 @@ +import { JSSTheme } from '../../interfaces/JSSTheme'; + +const styles = ({ parameters }: JSSTheme) => ({ + formTitle: { + borderBottom: `1px solid ${parameters.sapUiGroupTitleBorderColor}` + }, + formTitlePaddingBottom: { + paddingBottom: '2em' + }, + formPaddingBottom: { + paddingBottom: '1em' + }, + formGroupStyle: { + width: '100%', + paddingTop: '0.25em' + }, + formItemTopDiv: { + alignItems: 'center' + }, + formLabel: { + paddingRight: '0.5em' + }, + formElement: { + display: 'block' + } +}); + +export { styles }; diff --git a/packages/main/src/components/Form/Form.md b/packages/main/src/components/Form/Form.md new file mode 100644 index 00000000000..77d4c372a88 --- /dev/null +++ b/packages/main/src/components/Form/Form.md @@ -0,0 +1,3 @@ +```jsx +import { Form } from '@ui5/webcomponents-react/lib/Form'; +``` diff --git a/packages/main/src/components/Form/Form.stories.tsx b/packages/main/src/components/Form/Form.stories.tsx new file mode 100644 index 00000000000..3e49eafc12b --- /dev/null +++ b/packages/main/src/components/Form/Form.stories.tsx @@ -0,0 +1,55 @@ +import { Form } from './index'; +import React from 'react'; +import notes from './Form.md'; +import { FormItem } from './FormItem'; +import { FormGroup } from './FormGroup'; +import { CheckBox } from '@ui5/webcomponents-react/lib/CheckBox'; +import { Input } from '@ui5/webcomponents-react/lib/Input'; +import { InputType } from '@ui5/webcomponents-react/lib/InputType'; +import { Option } from '@ui5/webcomponents-react/lib/Option'; +import { Select } from '@ui5/webcomponents-react/lib/Select'; + +export const defaultStory = () => { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default { + title: 'Components | Form', + component: Form, + parameters: { notes } +}; diff --git a/packages/main/src/components/Form/Form.test.tsx b/packages/main/src/components/Form/Form.test.tsx new file mode 100644 index 00000000000..4e12db2553b --- /dev/null +++ b/packages/main/src/components/Form/Form.test.tsx @@ -0,0 +1,90 @@ +import { mountThemedComponent } from '@shared/tests/utils'; +import * as React from 'react'; +import { Form } from '@ui5/webcomponents-react/lib/Form'; +import { FormGroup } from '@ui5/webcomponents-react/lib/FormGroup'; +import { FormItem } from '@ui5/webcomponents-react/lib/FormItem'; +import { Input } from '../../webComponents/Input'; +import { InputType } from '../..'; + +const SIZE_S = 200; +const SIZE_M = 800; +const SIZE_L = 1200; +const SIZE_XL = 1600; +const component = ( +
+ + + + + + + + + + + + + + + + +
+); + +describe('Create a Form', () => { + test('size rate S; should create Label and Element with 100% width and display: block for top FormItem div', () => { + window = Object.assign(window, { innerWidth: SIZE_S }); + const wrapper = mountThemedComponent(component); + expect(wrapper.render()).toMatchSnapshot(); + }); + + test('size rate M; should create Label and Element with 16% and 83% width respectively and display: flex for top FormItem div', () => { + window = Object.assign(window, { innerWidth: SIZE_M }); + const wrapper = mountThemedComponent(component); + expect(wrapper.render()).toMatchSnapshot(); + }); + + test('size rate L; should create Label and Element with 33% and 66% width respectively and display: flex for top FormItem div', () => { + window = Object.assign(window, { innerWidth: SIZE_L }); + const wrapper = mountThemedComponent(component); + expect(wrapper.render()).toMatchSnapshot(); + }); + + test('size rate XL; should create Label and Element with 33% and 66% width respectively and display: flex for top FormItem div', () => { + window = Object.assign(window, { innerWidth: SIZE_XL }); + const wrapper = mountThemedComponent(component); + expect(wrapper.render()).toMatchSnapshot(); + }); + + test('should create a FormGroup and put ungrouped FormItems into it', () => { + const ungroupedChildren = ( +
+ + + + + + +
+ ); + const wrapper = mountThemedComponent(ungroupedChildren); + expect(wrapper.render()).toMatchSnapshot(); + }); + + test("should use a single FormGroup's title as a Form title if one is not set", () => { + const ungroupedChildren = ( +
+ + + + + + + + +
+ ); + const wrapper = mountThemedComponent(ungroupedChildren); + expect(wrapper.render()).toMatchSnapshot(); + }); +}); diff --git a/packages/main/src/components/Form/FormGroup/index.tsx b/packages/main/src/components/Form/FormGroup/index.tsx new file mode 100644 index 00000000000..12cd947407e --- /dev/null +++ b/packages/main/src/components/Form/FormGroup/index.tsx @@ -0,0 +1,54 @@ +import React, { Children, FC, forwardRef, ReactNode, ReactNodeArray, Ref } from 'react'; +import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; +import { styles } from '../Form.jss'; +import { FlexBoxAlignItems } from '@ui5/webcomponents-react/lib/FlexBoxAlignItems'; +import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection'; +import { FlexBoxJustifyContent } from '@ui5/webcomponents-react/lib/FlexBoxJustifyContent'; +import { Title } from '@ui5/webcomponents-react/lib/Title'; +import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; +import { createUseStyles } from 'react-jss'; +import { JSSTheme } from '../../../interfaces/JSSTheme'; + +export interface FormGroupProps { + title?: string; + children: ReactNode | ReactNodeArray; + type?: string; +} + +const useStyles = createUseStyles>(styles, { name: 'FormGroup' }); + +const FormGroup: FC = forwardRef((props: FormGroupProps, ref: Ref) => { + const { title, children } = props; + + const classes = useStyles(); + + return ( +
+ {title && ( + + {title} + + )} + + {Children.map(children, (child, index) => { + return ( +
+ {child} +
+ ); + })} +
+
+ ); +}); + +FormGroup.defaultProps = { + type: 'formGroup' +}; + +export { FormGroup }; diff --git a/packages/main/src/components/Form/FormItem/index.tsx b/packages/main/src/components/Form/FormItem/index.tsx new file mode 100644 index 00000000000..b12d98c0437 --- /dev/null +++ b/packages/main/src/components/Form/FormItem/index.tsx @@ -0,0 +1,86 @@ +import React, { FC, forwardRef, Ref, ReactNode, ReactNodeArray, useMemo, useContext } from 'react'; +import { Label } from '@ui5/webcomponents-react/lib/Label'; +import { styles } from '../Form.jss'; +import { createUseStyles } from 'react-jss'; +import { CurrentRange } from '../CurrentViewportRangeContext'; +import { JSSTheme } from '../../../interfaces/JSSTheme'; + +export interface FormItemProps { + labelText?: string; + children: ReactNode | ReactNodeArray; + type?: string; +} + +const calculateWidth = (rate) => { + return Math.floor((100 / 12) * rate) + '%'; +}; + +const useStyles = createUseStyles>(styles, { name: 'FormItem' }); + +const FormItem: FC = forwardRef((props: FormItemProps, ref: Ref) => { + const { labelText, children } = props; + + const currentRange = useContext(CurrentRange); + + const classes = useStyles(); + const topDivClass = classes.formItemTopDiv; + const labelClass = classes.formLabel; + const elementClass = classes.formElement; + + const memoizedStyles = useMemo(() => { + let labelWidth; + let labelTextAlign = 'flex-end'; + let display = 'flex'; + let elementWidth; + + switch (currentRange) { + case 'Phone': + labelWidth = '100%'; + elementWidth = '100%'; + display = 'block'; + labelTextAlign = 'flex-start'; + break; + case 'Tablet': + labelWidth = calculateWidth(2); + elementWidth = calculateWidth(10); + break; + case 'Desktop': + case 'LargeDesktop': + labelWidth = calculateWidth(4); + elementWidth = calculateWidth(8); + break; + } + + return { + topDivStyle: { + display: display + }, + labelStyle: { + width: labelWidth, + justifyContent: labelTextAlign + }, + elementStyle: { + width: elementWidth + } + }; + }, [children, currentRange]); + + return ( +
+
+ +
+ {children} +
+
+
+ ); +}); + +FormItem.defaultProps = { + type: 'formItem' +}; + +export { FormItem }; diff --git a/packages/main/src/components/Form/__snapshots__/Form.test.tsx.snap b/packages/main/src/components/Form/__snapshots__/Form.test.tsx.snap new file mode 100644 index 00000000000..20304834458 --- /dev/null +++ b/packages/main/src/components/Form/__snapshots__/Form.test.tsx.snap @@ -0,0 +1,837 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create a Form should create a FormGroup and put ungrouped FormItems into it 1`] = ` +Array [ + + Test form + , +
, +
+
+
+
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
, +] +`; + +exports[`Create a Form should use a single FormGroup's title as a Form title if one is not set 1`] = ` +Array [ + + To be Form title + , +
, +
+
+
+
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
, +] +`; + +exports[`Create a Form size rate L; should create Label and Element with 33% and 66% width respectively and display: flex for top FormItem div 1`] = ` +Array [ + + Test form + , +
, +
+
+
+ + Group 1 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
+
+ + Group 2 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
, +] +`; + +exports[`Create a Form size rate M; should create Label and Element with 16% and 83% width respectively and display: flex for top FormItem div 1`] = ` +Array [ + + Test form + , +
, +
+
+
+ + Group 1 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
+
+ + Group 2 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
, +] +`; + +exports[`Create a Form size rate S; should create Label and Element with 100% width and display: block for top FormItem div 1`] = ` +Array [ + + Test form + , +
, +
+
+
+ + Group 1 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
+
+ + Group 2 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
, +] +`; + +exports[`Create a Form size rate XL; should create Label and Element with 33% and 66% width respectively and display: flex for top FormItem div 1`] = ` +Array [ + + Test form + , +
, +
+
+
+ + Group 1 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
+
+ + Group 2 + +
+
+
+
+ + item 1 + +
+ +
+
+
+
+
+
+
+ + item 2 + +
+ +
+
+
+
+
+
+
+
, +] +`; diff --git a/packages/main/src/components/Form/index.tsx b/packages/main/src/components/Form/index.tsx new file mode 100644 index 00000000000..a4e04bc8fac --- /dev/null +++ b/packages/main/src/components/Form/index.tsx @@ -0,0 +1,86 @@ +import { Grid } from '@ui5/webcomponents-react/lib/Grid'; +import React, { FC, forwardRef, ReactElement, ReactNode, ReactNodeArray, Ref, useMemo } from 'react'; +import { CommonProps } from '../../interfaces/CommonProps'; +import { Title } from '@ui5/webcomponents-react/lib/Title'; +import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; +import { styles } from './Form.jss'; +import { createUseStyles } from 'react-jss'; +import { useViewportRange } from '@ui5/webcomponents-react-base/lib/useViewportRange'; +import { FormGroup } from './FormGroup'; +import { JSSTheme } from '../../interfaces/JSSTheme'; +import { CurrentRange } from './CurrentViewportRangeContext'; + +export interface FormPropTypes extends CommonProps { + /** + * Components that are placed into Form. + */ + children: ReactNode | ReactNodeArray; + /** + * Form title + */ + title?: string; +} + +const useStyles = createUseStyles>(styles, { name: 'Form' }); + +const Form: FC = forwardRef((props: FormPropTypes, ref: Ref) => { + const { title, children } = props; + + const classes = useStyles(); + const currentRange = useViewportRange('StdExt'); + + const [formGroups, updatedTitle] = useMemo(() => { + let formGroups: any; + let updatedTitle: string = title; + + // check if ungrouped FormItems exist amongst the Form's children and put them into an artificial FormGroup + if (Array.isArray(children)) { + const ungroupedItems = []; + formGroups = []; + children.forEach((child) => { + if ((child as ReactElement).props.type === 'formItem') { + ungroupedItems.push(child); + } else if ((child as ReactElement).props.type === 'formGroup') { + formGroups.push(child as ReactElement); + } + }); + + if (ungroupedItems.length > 0) { + formGroups.push(); + } + } else { + // check if a sole Form's group has a Title and take it as Form Title if one does not exist + const childProps = (children as ReactElement).props; + if ((!title || title.length === 0) && childProps.title && childProps.title.length > 0) { + updatedTitle = childProps.title; + formGroups = React.cloneElement(children as ReactElement, { title: null }); + } else { + formGroups = children; + } + } + + return [formGroups, updatedTitle]; + }, [children]); + + return ( + + {updatedTitle && ( + <> + + {updatedTitle} + +
+ + )} + + + ); +}); + +Form.defaultProps = { + children: [], + title: null +}; +Form.displayName = 'Form'; + +export { Form }; diff --git a/packages/main/src/components/Grid/Grid.stories.tsx b/packages/main/src/components/Grid/Grid.stories.tsx index db0aa5bdd63..7027bdbf5b4 100644 --- a/packages/main/src/components/Grid/Grid.stories.tsx +++ b/packages/main/src/components/Grid/Grid.stories.tsx @@ -1,10 +1,11 @@ import { Grid } from '@ui5/webcomponents-react/lib/Grid'; import React from 'react'; import notes from './Grid.md'; +import { action } from '@storybook/addon-actions'; export const defaultStory = () => { return ( - +
Div 1
Div 2
Div 3
diff --git a/packages/main/src/components/Grid/index.tsx b/packages/main/src/components/Grid/index.tsx index 3f1a0d6ed26..839a10f5847 100644 --- a/packages/main/src/components/Grid/index.tsx +++ b/packages/main/src/components/Grid/index.tsx @@ -1,4 +1,3 @@ -import { Device } from '@ui5/webcomponents-react-base/lib/Device'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import React, { Children, @@ -9,15 +8,12 @@ import React, { ReactNode, ReactNodeArray, Ref, - useCallback, - useEffect, - useMemo, - useState + useMemo } from 'react'; import { createUseStyles } from 'react-jss'; import { CommonProps } from '../../interfaces/CommonProps'; -import { JSSTheme } from '../../interfaces/JSSTheme'; import { styles } from './Grid.jss'; +import { useViewportRange } from '@ui5/webcomponents-react-base/lib/useViewportRange'; export enum GridPosition { Left = 'Left', @@ -109,22 +105,7 @@ const Grid: FC = forwardRef((props: GridPropTypes, ref: Ref { - const { name: range } = Device.media.getCurrentRange('StdExt', width); - setCurrentRange(range); - }, - [setCurrentRange] - ); - - useEffect(() => { - Device.resize.attachHandler(onWindowResize, null); - return () => { - Device.resize.detachHandler(onWindowResize, null); - }; - }, [onWindowResize]); + const currentRange = useViewportRange('StdExt'); const classes = useStyles(); const gridClasses = StyleClassHelper.of(classes.grid); diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 1b9a65a656f..071e4b339f7 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -37,6 +37,9 @@ import { FlexBoxAlignItems } from './lib/FlexBoxAlignItems'; import { FlexBoxDirection } from './lib/FlexBoxDirection'; import { FlexBoxJustifyContent } from './lib/FlexBoxJustifyContent'; import { FlexBoxWrap } from './lib/FlexBoxWrap'; +import { Form } from './lib/Form'; +import { FormGroup } from './lib/FormGroup'; +import { FormItem } from './lib/FormItem'; import { Grid } from './lib/Grid'; import { GridPosition } from './lib/GridPosition'; import { GroupHeaderListItem } from './lib/GroupHeaderListItem'; @@ -76,6 +79,8 @@ import { Popover } from './lib/Popover'; import { PopoverHorizontalAlign } from './lib/PopoverHorizontalAlign'; import { PopoverVerticalAlign } from './lib/PopoverVerticalAlign'; import { Priority } from './lib/Priority'; +import { ProductSwitch } from './lib/ProductSwitch'; +import { ProductSwitchItem } from './lib/ProductSwitchItem'; import { ProgressIndicator } from './lib/ProgressIndicator'; import { RadioButton } from './lib/RadioButton'; import { SegmentedButton } from './lib/SegmentedButton'; @@ -151,6 +156,9 @@ export { FlexBoxDirection, FlexBoxJustifyContent, FlexBoxWrap, + Form, + FormGroup, + FormItem, Grid, GridPosition, GroupHeaderListItem, @@ -190,6 +198,8 @@ export { PopoverHorizontalAlign, PopoverVerticalAlign, Priority, + ProductSwitch, + ProductSwitchItem, ProgressIndicator, RadioButton, SegmentedButton, diff --git a/packages/main/src/lib/Form.ts b/packages/main/src/lib/Form.ts new file mode 100644 index 00000000000..90750bc81ed --- /dev/null +++ b/packages/main/src/lib/Form.ts @@ -0,0 +1,3 @@ +import { Form } from '../components/Form'; + +export { Form }; diff --git a/packages/main/src/lib/FormGroup.ts b/packages/main/src/lib/FormGroup.ts new file mode 100644 index 00000000000..92d18d34599 --- /dev/null +++ b/packages/main/src/lib/FormGroup.ts @@ -0,0 +1,3 @@ +import { FormGroup } from '../components/Form/FormGroup'; + +export { FormGroup }; diff --git a/packages/main/src/lib/FormItem.ts b/packages/main/src/lib/FormItem.ts new file mode 100644 index 00000000000..3ffc791e4ad --- /dev/null +++ b/packages/main/src/lib/FormItem.ts @@ -0,0 +1,3 @@ +import { FormItem } from '../components/Form/FormItem'; + +export { FormItem };