diff --git a/config/jest.config.js b/config/jest.config.js index 6d209225af5..2bcdc3366fb 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -19,7 +19,8 @@ module.exports = { '!packages/main/src/components/AnalyticalTable/types/*', // no table enums '!packages/base/src/styling/sap_fiori_3.ts', // no old theming parameters '!packages/base/src/styling/HSLColor.ts', // no deprecated HSL Util - '!packages/base/src/styling/font72.ts' // no deprecated font + '!packages/base/src/styling/font72.ts', // no deprecated font + '!packages/base/src/Scroller/*' // no scroll lib as it is not longer used ], setupFiles: ['jest-canvas-mock'], setupFilesAfterEnv: ['./config/jestsetup.ts'], diff --git a/config/jestsetup.ts b/config/jestsetup.ts index 76637b459c3..de97793de3e 100644 --- a/config/jestsetup.ts +++ b/config/jestsetup.ts @@ -4,6 +4,7 @@ import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { createSerializer } from 'enzyme-to-json'; import ResizeObserver from 'resize-observer-polyfill'; +import 'intersection-observer'; import '@ui5/webcomponents/dist/json-imports/i18n'; process.env.NODE_ENV = 'test'; diff --git a/package.json b/package.json index 8fd33b569aa..c4047e4600e 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "gzip-size": "^5.1.0", "husky": "^4.2.1", "identity-obj-proxy": "^3.0.0", + "intersection-observer": "^0.7.0", "jest": "^25.1.0", "jest-canvas-mock": "^2.2.0", "jest-environment-jsdom-sixteen": "^1.0.2", diff --git a/packages/base/package.json b/packages/base/package.json index 37368998558..221c48d1fc4 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -16,7 +16,8 @@ "**/modernizr.js", "core-js/**/*", "./src/polyfill/*.ts", - "./polyfill/*.js" + "./polyfill/*.js", + "intersection-observer" ], "scripts": { "clean": "rimraf cjs Device hooks lib polyfill styling types utils index.esm.js index.d.ts", @@ -26,7 +27,9 @@ }, "dependencies": { "core-js": "3.6.4", - "resize-observer-polyfill": "^1.5.1" + "intersection-observer": "^0.7.0", + "resize-observer-polyfill": "^1.5.1", + "smoothscroll-polyfill": "^0.4.4" }, "peerDependencies": { "react": "^16.8.0", diff --git a/packages/base/src/Scroller/Scroller.tsx b/packages/base/src/Scroller/Scroller.tsx index aced5a71dac..187ef4dbe82 100644 --- a/packages/base/src/Scroller/Scroller.tsx +++ b/packages/base/src/Scroller/Scroller.tsx @@ -1,6 +1,6 @@ import { IScroller } from '@ui5/webcomponents-react-base/interfaces/IScroller'; import React, { forwardRef, RefObject, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; - +import { deprecationNotice } from '@ui5/webcomponents-react-base/lib/Utils'; import { ScrollContentProvider } from './ScrollContextProvider'; import { scrollTo } from './ScrollHelper'; @@ -10,6 +10,11 @@ export interface Props { forceSelection?: boolean; } +deprecationNotice( + 'Scroller', + "'@ui5/webcomponents-react-base/lib/Scroller' is deprecated and will be removed in the next major release." +); + export const Scroller = forwardRef((props: Props, ref: RefObject) => { const { children, scrollContainer, forceSelection = true } = props; diff --git a/packages/base/src/polyfill/Edge.ts b/packages/base/src/polyfill/Edge.ts index 73a04c96f57..959ee689d10 100644 --- a/packages/base/src/polyfill/Edge.ts +++ b/packages/base/src/polyfill/Edge.ts @@ -1,4 +1,9 @@ import ResizeObserver from 'resize-observer-polyfill'; +import 'intersection-observer'; +import smoothscroll from 'smoothscroll-polyfill'; // @ts-ignore window.ResizeObserver = ResizeObserver; + +// required for scrollTo methods +smoothscroll.polyfill(); diff --git a/packages/base/src/polyfill/IE11.ts b/packages/base/src/polyfill/IE11.ts index d805cf33007..4eef4336c3b 100644 --- a/packages/base/src/polyfill/IE11.ts +++ b/packages/base/src/polyfill/IE11.ts @@ -3,6 +3,11 @@ import 'core-js/modules/es.object.assign'; import 'core-js/modules/es.object.values'; import 'core-js/modules/es.array.from'; import ResizeObserver from 'resize-observer-polyfill'; +import 'intersection-observer'; +import smoothscroll from 'smoothscroll-polyfill'; // @ts-ignore window.ResizeObserver = ResizeObserver; + +// required for scrollTo methods +smoothscroll.polyfill(); diff --git a/packages/main/src/components/AnalyticalTable/demo/generateData.ts b/packages/main/src/components/AnalyticalTable/demo/generateData.ts index faa220dcd97..644b5d416eb 100644 --- a/packages/main/src/components/AnalyticalTable/demo/generateData.ts +++ b/packages/main/src/components/AnalyticalTable/demo/generateData.ts @@ -52,8 +52,6 @@ const makeTreeEntry = (...lens) => { return makeDataLevel(); }; - - const makeEntry = () => ({ name: getRandomName(), longColumn: 'Really really long column content... donĀ“t crop please', diff --git a/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx b/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx index 8938d5dbd2f..366c910f8bd 100644 --- a/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx +++ b/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx @@ -1,6 +1,6 @@ import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { AvatarSize } from '@ui5/webcomponents-react/lib/AvatarSize'; -import React, { ReactElement, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState, ReactElement } from 'react'; import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; const styles = { @@ -51,7 +51,7 @@ export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => { className={classes.imageContainer} style={{ borderRadius: imageShapeCircle ? '50%' : 0, overflow: 'hidden' }} > - Company Logo + Object Page Image ); } else { @@ -65,10 +65,8 @@ export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => { } }, [image, imageShapeCircle]); - useLayoutEffect(() => { - requestAnimationFrame(() => { - setIsMounted(true); - }); + useEffect(() => { + setIsMounted(true); }, []); const containerClasses = StyleClassHelper.of(classes.base); diff --git a/packages/main/src/components/ObjectPage/ObjectPage.jss.ts b/packages/main/src/components/ObjectPage/ObjectPage.jss.ts index 79bec696f0b..d7fc840db03 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.jss.ts +++ b/packages/main/src/components/ObjectPage/ObjectPage.jss.ts @@ -5,76 +5,66 @@ const styles = { width: '100%', height: '100%', position: 'relative', - display: 'flex', - flexDirection: 'column', - isolation: 'isolate', whiteSpace: 'normal', fontFamily: ThemingParameters.sapFontFamily, - backgroundColor: ThemingParameters.sapBackgroundColor - }, - contentContainer: { + backgroundColor: ThemingParameters.sapBackgroundColor, overflowX: 'hidden', overflowY: 'auto', - position: 'relative', - flexGrow: 1 - }, - outerContentContainer: { - width: '100%', - height: '100%', - overflow: 'hidden' - }, - contentScrollContainer: { - position: 'relative' + '&::-webkit-scrollbar': { + backgroundColor: ThemingParameters.sapScrollBar_TrackColor, + width: ThemingParameters.sapScrollBar_Dimension + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: ThemingParameters.sapScrollBar_BorderColor, + '&:hover': { + backgroundColor: ThemingParameters.sapScrollBar_Hover_FaceColor + } + }, + '&::-webkit-scrollbar-corner': { + backgroundColor: ThemingParameters.sapScrollBar_TrackColor + }, + '& section[id="ObjectPageSection-1"] > div[role="heading"]': { + display: 'none' + } }, - anchorBar: { - paddingLeft: '2rem', - backgroundColor: ThemingParameters.sapObjectHeader_Background, - boxShadow: `inset 0 -0.0625rem ${ThemingParameters.sapObjectHeader_BorderColor}, inset 0 0.0625rem ${ThemingParameters.sapObjectHeader_BorderColor}`, - display: 'flex', - height: '2.75rem', - minHeight: '2.75rem', - position: 'relative' + iconTabBarMode: { + '& section[data-component-name="ObjectPageSection"] > div[role="heading"]': { + display: 'none' + } }, - sectionsContainer: { - '&:before': { - display: 'table', - content: '""' - }, - '& :first-child > div[role="heading"]': { + noHeader: { + '& $header': { display: 'none' }, - position: 'relative', - height: '100%', - // overflowX: 'hidden', - // overflowY: 'auto', - overflow: 'hidden', - backgroundColor: ThemingParameters.sapBackgroundColor, - '&:after': { - clear: 'both', - display: 'table', - content: '""' + '& $contentHeader': { + display: 'none' } }, - fillerDiv: { - backgroundColor: ThemingParameters.sapBackgroundColor + headerCollapsed: { + '& $contentHeader': { + display: 'none' + } }, // header header: { flexShrink: 0, - position: 'relative', backgroundColor: ThemingParameters.sapObjectHeader_Background, - '&$stickied': { - '& $image': { - opacity: '1', - height: '3rem', - width: '3rem', - margin: '0.25rem 1rem 0.25rem 0' - } - } + position: 'sticky', + top: 0, + zIndex: 2 }, contentHeader: { backgroundColor: ThemingParameters.sapObjectHeader_Background, - position: 'relative' + position: 'sticky', + paddingBottom: '0.25rem', + maxHeight: '500px', + overflow: 'hidden', + paddingLeft: '2rem' + }, + anchorBar: { + position: 'sticky', + zIndex: 2, + '--_ui5_tc_header_box_shadow': 'inset 0px -1px 0 0px rgba(0,0,0,0.15)' }, titleBar: { padding: '0.5rem 2rem', @@ -122,18 +112,8 @@ const styles = { padding: 0 } }, - stickied: {}, - headerContent: { - //paddingTop: '1.5rem', - paddingBottom: '0.25rem', - transition: 'max-height 0.5s', - maxHeight: '500px', - overflow: 'hidden', - paddingLeft: '2rem', - position: 'relative' - }, titleInHeaderContent: { - '& $headerContent': { + '& contentHeader': { paddingTop: 0, '& > *': { display: 'flex', @@ -148,17 +128,6 @@ const styles = { // paddingTop: 0 } }, - alwaysVisibleHeader: { - '& $headerContent': { - paddingLeft: 0 - }, - '& $contentHeader': { - marginTop: '0.5rem' - }, - '& $titleBar': { - paddingBottom: 0 - } - }, headerCustomContent: { display: 'inline-block', verticalAlign: 'top', @@ -196,13 +165,6 @@ const styles = { }, avatar: { marginRight: '1rem' - }, - toggleHeaderButton: { - position: 'absolute', - '--_ui5_button_compact_height': '1.25rem', - '--_ui5_button_base_height': '1.25rem', - top: `-0.625rem`, - left: 'calc(50% - 1rem)' } }; diff --git a/packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx b/packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx new file mode 100644 index 00000000000..b5c48aa9453 --- /dev/null +++ b/packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx @@ -0,0 +1,210 @@ +import { addCustomCSS } from '@ui5/webcomponents-base/dist/Theming'; +import '@ui5/webcomponents-icons/dist/icons/pushpin-off'; +import '@ui5/webcomponents-icons/dist/icons/slim-arrow-down'; +import '@ui5/webcomponents-icons/dist/icons/slim-arrow-up'; +import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; +import { Event } from '@ui5/webcomponents-react-base/lib/Event'; +import { Button } from '@ui5/webcomponents-react/lib/Button'; +import { List } from '@ui5/webcomponents-react/lib/List'; +import { PlacementType } from '@ui5/webcomponents-react/lib/PlacementType'; +import { Popover } from '@ui5/webcomponents-react/lib/Popover'; +import { TabContainer } from '@ui5/webcomponents-react/lib/TabContainer'; +import { ToggleButton } from '@ui5/webcomponents-react/lib/ToggleButton'; +import React, { CSSProperties, forwardRef, ReactElement, RefObject, useCallback, useRef, useState } from 'react'; +import { createUseStyles } from 'react-jss'; +import { Ui5PopoverDomRef } from '../../interfaces/Ui5PopoverDomRef'; +import { StandardListItem } from '../../webComponents/StandardListItem'; +import { ObjectPageAnchorButton } from './ObjectPageAnchorButton'; +import { safeGetChildrenArray } from './ObjectPageUtils'; + +addCustomCSS( + 'ui5-button', + ` +:host([data-ui5wcr-object-page-header-action]) .ui5-button-root { + padding: 0; +}` +); +addCustomCSS( + 'ui5-togglebutton', + ` +:host([data-ui5wcr-object-page-header-action]) .ui5-button-root { + padding: 0; +}` +); + +const anchorBarStyles = { + anchorBarActionButton: { + position: 'absolute', + '--_ui5_button_compact_height': '1.375rem', + '--_ui5_button_base_height': '1.375rem', + '--_ui5_button_base_min_compact_width': '1.375rem', + top: `-0.6875rem`, + marginLeft: `-0.6875rem`, + left: '50%', + '&:before, &:after': { + content: '""', + position: 'absolute', + width: '4rem', + top: '50%', + height: '0.0625rem' + }, + '&:before': { + right: '100%', + backgroundImage: `linear-gradient(to left, ${ThemingParameters.sapHighlightColor}, rgba(8,84,160,0))` + }, + '&:after': { + backgroundImage: `linear-gradient(to right, ${ThemingParameters.sapHighlightColor}, rgba(8,84,160,0))`, + left: '100%' + } + }, + anchorBarActionButtonExpandable: {}, + anchorBarActionButtonPinnable: {}, + anchorBarActionPinnableAndExandable: { + '&$anchorBarActionButtonPinnable': { + marginLeft: '0.25rem', + '&:before': { + backgroundImage: 'none' + } + }, + '&$anchorBarActionButtonExpandable': { + marginLeft: '-1.75rem' + } + } +}; + +const useStyles = createUseStyles(anchorBarStyles, { name: 'ObjectPageAnchorBar' }); + +interface Props { + sections: ReactElement | ReactElement[]; + selectedSectionId: string; + handleOnSectionSelected: (e: unknown) => void; + handleOnSubSectionSelected: (e: unknown) => void; + showHideHeaderButton: boolean; + headerContentPinnable: boolean; + headerPinned: boolean; + headerContentHeight: number; + setHeaderPinned: (payload: any) => void; + style?: CSSProperties; + onToggleHeaderContentVisibility: (e: any) => void; + className: string; +} + +const ObjectPageAnchorBar = forwardRef((props: Props, ref: RefObject) => { + const { + sections, + selectedSectionId, + handleOnSectionSelected, + handleOnSubSectionSelected, + showHideHeaderButton, + headerContentPinnable, + onToggleHeaderContentVisibility, + headerPinned, + setHeaderPinned, + headerContentHeight, + style, + className + } = props; + + const classes = useStyles(); + + const shouldRenderHideHeaderButton = showHideHeaderButton; + const shouldRenderHeaderPinnableButton = headerContentPinnable && headerContentHeight > 0; + const showBothActions = shouldRenderHeaderPinnableButton && shouldRenderHideHeaderButton; + const [popoverContent, setPopoverContent] = useState(null); + const popoverRef = useRef(null); + + const onPinHeader = useCallback( + (e) => { + setHeaderPinned(e.getParameter('pressed')); + }, + [setHeaderPinned] + ); + + const onTabItemSelect = useCallback((event) => { + const { sectionId, index } = event.getParameter('item').dataset; + // eslint-disable-next-line eqeqeq + const section = safeGetChildrenArray(sections).find((el) => el.props.id == sectionId); + handleOnSectionSelected( + Event.of(null, {} as any, { + ...section, + index + }) + ); + }, []); + + const onShowSubSectionPopover = useCallback( + (e, section) => { + setPopoverContent(section); + popoverRef.current.openBy(e.target.parentElement); + }, + [setPopoverContent, popoverRef] + ); + + const onSubSectionClick = useCallback( + (e) => { + const selectedId = e.getParameter('item').dataset.key; + const subSection = popoverContent.props.children + .filter((item) => item.props && item.props.isSubSection) + .find((item) => item.props.id === selectedId); + if (subSection) { + handleOnSubSectionSelected(Event.of(null, e.getOriginalEvent(), { section: popoverContent, subSection })); + } + popoverRef.current.close(); + }, + [handleOnSubSectionSelected, popoverRef, popoverContent] + ); + + return ( +
+ + {safeGetChildrenArray(sections).map((section, index) => { + return ( + + ); + })} + + {shouldRenderHideHeaderButton && ( +
+ ); +}); + +ObjectPageAnchorBar.displayName = 'ObjectPageAnchorBar'; + +export { ObjectPageAnchorBar }; diff --git a/packages/main/src/components/ObjectPage/ObjectPageAnchorButton.tsx b/packages/main/src/components/ObjectPage/ObjectPageAnchorButton.tsx index b0a604d1114..6bd5b9b7a6b 100644 --- a/packages/main/src/components/ObjectPage/ObjectPageAnchorButton.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPageAnchorButton.tsx @@ -1,71 +1,17 @@ import '@ui5/webcomponents-icons/dist/icons/slim-arrow-down'; -import { Event } from '@ui5/webcomponents-react-base/lib/Event'; -import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; -import { ScrollLink } from '@ui5/webcomponents-react-base/lib/ScrollLink'; -import { Icon } from '@ui5/webcomponents-react/lib/Icon'; -import { List } from '@ui5/webcomponents-react/lib/List'; -import { ObjectPageMode } from '@ui5/webcomponents-react/lib/ObjectPageMode'; -import { PlacementType } from '@ui5/webcomponents-react/lib/PlacementType'; -import { Popover } from '@ui5/webcomponents-react/lib/Popover'; -import { StandardListItem } from '@ui5/webcomponents-react/lib/StandardListItem'; -import React, { FC, useCallback, useState } from 'react'; -import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; +import { Tab } from '@ui5/webcomponents-react/lib/Tab'; +import React, { FC, useEffect, useRef } from 'react'; interface ObjectPageAnchorPropTypes { section: any; - onSectionSelected: (event: Event) => void; - onSubSectionSelected?: (event: Event) => void; + onShowSubSectionPopover: (event: any, section: any) => void; index: number; selected: boolean; - collapsedHeader: boolean; - mode: ObjectPageMode; } -const anchorButtonStyles = { - anchorButtonContainer: { - position: 'relative', - display: 'inline-flex', - alignItems: 'center', - cursor: 'pointer', - '&:not(:first-child)': { - marginLeft: '2rem' - } - }, - button: { - color: ThemingParameters.sapContent_LabelColor, - fontFamily: ThemingParameters.sapFontFamily, - fontSize: ThemingParameters.sapFontSize, - cursor: 'pointer' - }, - selected: { - color: ThemingParameters.sapSelectedColor, - minWidth: '2rem', - textAlign: 'center', - '&:after': { - content: '""', - borderBottom: `0.188rem solid ${ThemingParameters.sapSelectedColor}`, - width: '100%', - position: 'absolute', - bottom: 0, - left: 0 - } - } -}; -const useStyles = createComponentStyles(anchorButtonStyles, { - name: 'ObjectPageAnchorButton' -}); - export const ObjectPageAnchorButton: FC = (props: ObjectPageAnchorPropTypes) => { - const classes = useStyles(); - const [open, setOpen] = useState(); - const { section, collapsedHeader, index, onSubSectionSelected, onSectionSelected, selected, mode } = props; - - const openModal = useCallback(() => { - setOpen(true); - }, []); - const closeModal = useCallback(() => { - setOpen(false); - }, []); + const ref = useRef(); + const { section, index, selected, onShowSubSectionPopover } = props; let subSectionsAvailable = false; if (section.props.children && section.props.children.filter) { @@ -73,102 +19,35 @@ export const ObjectPageAnchorButton: FC = (props: Obj subSectionsAvailable = subSections.length > 0; } - const onSubSectionClick = useCallback( - (e) => { - const selectedId = e.getParameter('item').dataset.key; - const subSection = section.props.children - .filter((item) => item.props && item.props.isSubSection) - .find((item) => item.props.id === selectedId); - if (subSection) { - onSubSectionSelected(Event.of(null, e.getOriginalEvent(), { section, subSection, sectionIndex: index })); - } - closeModal(); - }, - [onSubSectionSelected, open] - ); - - const navigationIcon = ( - - ); - - const onScrollActive = useCallback(() => { - onSectionSelected( - Event.of(null, {} as any, { - ...section, - index - }) - ); - }, [onSectionSelected]); - - const renderSubSectionListItem = (item) => { - if (mode === ObjectPageMode.IconTabBar) { - return ( - - {item.props.title} - + useEffect(() => { + if (subSectionsAvailable) { + const element = ref.current.parentElement.shadowRoot.querySelector( + `.ui5-tc__headerList li[aria-posinset="${index + 1}"] .ui5-tc__headerItemContent` ); - } - - return ( - - {item.props.title} - - ); - }; - let sectionSelector = null; - if (mode === ObjectPageMode.Default) { - sectionSelector = ( - - {section.props.title} - - ); - } else { - sectionSelector = ( - - {section.props.title} - - ); - } + if (element && !element.querySelector('ui5-icon')) { + const icon = document.createElement('ui5-icon'); + (icon as any).name = 'slim-arrow-down'; + icon.style.verticalAlign = 'text-bottom'; + icon.style.pointerEvents = 'all'; + icon.addEventListener('click', (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + e.stopPropagation(); + onShowSubSectionPopover(e, section); + }); + element.appendChild(icon); + } + } + }, [subSectionsAvailable, ref, onShowSubSectionPopover, section]); return ( -
  • - {sectionSelector} - {subSectionsAvailable && ( - - - {section.props.children - .filter((item) => item.props && item.props.isSubSection) - .map(renderSubSectionListItem)} - - - )} -
  • + ); }; diff --git a/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx b/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx index b0f6a2edc60..f053ac1e4b3 100644 --- a/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx @@ -1,7 +1,7 @@ import { AvatarSize } from '@ui5/webcomponents-react/lib/AvatarSize'; import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection'; -import React, { CSSProperties, FC, ReactElement } from 'react'; +import React, { CSSProperties, FC, forwardRef, ReactElement, useMemo, RefObject } from 'react'; import { safeGetChildrenArray } from './ObjectPageUtils'; interface Props { @@ -14,11 +14,11 @@ interface Props { renderKeyInfos: () => JSX.Element; title: string; subTitle: string; + headerPinned: boolean; + topHeaderHeight: number; } -const positionRelativeStyle: CSSProperties = { position: 'relative' }; - -export const ObjectPageHeader: FC = (props) => { +export const ObjectPageHeader = forwardRef((props: Props, ref: RefObject) => { const { image, classes, @@ -28,14 +28,18 @@ export const ObjectPageHeader: FC = (props) => { renderBreadcrumbs, title, subTitle, - renderKeyInfos + renderKeyInfos, + headerPinned, + topHeaderHeight } = props; - let avatar = null; + const avatar = useMemo(() => { + if (!image) { + return null; + } - if (image) { if (typeof image === 'string') { - avatar = ( + return ( = (props) => { ); } else { - avatar = React.cloneElement(image, { + return React.cloneElement(image, { size: AvatarSize.L, className: image.props?.className ? `${classes.headerImage} ${image.props?.className}` : classes.headerImage } as unknown); } - } + }, [image, classes.headerImage, classes.image, imageShapeCircle]); + + const headerStyles = useMemo(() => { + if (headerPinned) { + return { + top: `${topHeaderHeight}px`, + zIndex: 1 + }; + } + + return null; + }, [headerPinned, topHeaderHeight]); + + let renderedHeaderContent = ( + <> + {avatar} + {renderHeaderContentProp && {renderHeaderContentProp()}} + + ); if (showTitleInHeaderContent) { const headerContents = renderHeaderContentProp && renderHeaderContentProp(); @@ -61,41 +83,38 @@ export const ObjectPageHeader: FC = (props) => { } else { firstElement = headerContents; } - return ( -
    -
    - - {avatar} - -
    {renderBreadcrumbs && renderBreadcrumbs()}
    + renderedHeaderContent = ( + <> + + {avatar} + +
    {renderBreadcrumbs && renderBreadcrumbs()}
    + + +

    {title}

    + {subTitle} + {firstElement} +
    - -

    {title}

    - {subTitle} - {firstElement} -
    - - {contents.map((c, index) => ( -
    - {c} -
    - ))} -
    -
    {renderKeyInfos && renderKeyInfos()}
    + {contents.map((c, index) => ( +
    + {c} +
    + ))}
    +
    {renderKeyInfos && renderKeyInfos()}
    -
    -
    + + ); } return ( -
    -
    - {avatar} - {renderHeaderContentProp && {renderHeaderContentProp()}} -
    +
    + {renderedHeaderContent}
    ); -}; +}); + +ObjectPageHeader.displayName = 'ObjectPageHeader'; diff --git a/packages/main/src/components/ObjectPage/ObjectPageScrollBar.tsx b/packages/main/src/components/ObjectPage/ObjectPageScrollBar.tsx deleted file mode 100644 index 47f5cc0a3c8..00000000000 --- a/packages/main/src/components/ObjectPage/ObjectPageScrollBar.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; -import React, { FC, RefObject, useMemo } from 'react'; -import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; -import { ZIndex } from '../../enums/ZIndex'; - -interface Props { - scrollBarRef: RefObject; - innerScrollBarRef: RefObject; - width: number; -} - -const styles = { - outerScrollbar: { - position: 'absolute', - right: 0, - overflow: 'hidden', - height: '100%', - zIndex: ZIndex.ResponsivePopover, - backgroundColor: ThemingParameters.sapObjectHeader_Background, - '& ::-webkit-scrollbar': { - backgroundColor: '#ffffff' - }, - '& ::-webkit-scrollbar-thumb': { - backgroundColor: '#949494', - '&:hover': { - backgroundColor: '#8c8c8c' - } - }, - '& ::-webkit-scrollbar-corner': { - backgroundColor: '#ffffff' - } - }, - innerScrollbar: { - width: '34px', - overflowY: 'scroll', - overflowX: 'hidden', - height: '100%' - } -}; - -const useScrollBarStyles = createComponentStyles(styles, { name: 'ObjectPageScrollBar' }); - -export const ObjectPageScrollBar: FC = (props) => { - const { scrollBarRef, innerScrollBarRef, width } = props; - - const [scrollBarWidthStyle, scrollBarWidthMargin] = useMemo(() => { - return [{ width: `${width}px` }, { marginLeft: `-${width}px`, width: `${2 * width}px` }]; - }, [width]); - - const classes = useScrollBarStyles(); - - return ( -
    -
    -
    -
    -
    - ); -}; diff --git a/packages/main/src/components/ObjectPage/ObjectPageUtils.ts b/packages/main/src/components/ObjectPage/ObjectPageUtils.ts index 739124eba5b..d552cc16218 100644 --- a/packages/main/src/components/ObjectPage/ObjectPageUtils.ts +++ b/packages/main/src/components/ObjectPage/ObjectPageUtils.ts @@ -1,30 +1,38 @@ import { Children, ReactElement } from 'react'; -export const safeGetChildrenArray = (children): Array => Children.toArray(children).filter(Boolean); +export const safeGetChildrenArray = (children): T[] => Children.toArray(children).filter(Boolean); -export const findSectionIndexById = (sections: ReactElement | Array>, id) => { - const index = safeGetChildrenArray(sections).findIndex((objectPageSection) => objectPageSection.props?.id === id); - if (index === -1) { - return 0; - } - return index; -}; - -export const getProportionateScrollTop = (activeContainer, passiveContainer, base) => { - const activeHeight = activeContainer.current.getBoundingClientRect().height; - const passiveHeight = passiveContainer.current.getBoundingClientRect().height; +// export const findSectionIndexById = (sections: ReactElement | Array>, id) => { +// const index = safeGetChildrenArray(sections).findIndex((objectPageSection) => objectPageSection.props?.id === id); +// if (index === -1) { +// return 0; +// } +// return index; +// }; - return (base / activeHeight) * passiveHeight; +export const getSectionById = (sections: ReactElement | ReactElement[], id) => { + return safeGetChildrenArray(sections).find((objectPageSection) => objectPageSection.props?.id === id); }; -export const bindScrollEvent = (scrollContainer, handler) => { - if (scrollContainer.current && handler.current) { - scrollContainer.current.addEventListener('scroll', handler.current, { passive: true }); - } -}; +// export const getProportionateScrollTop = (activeContainer, passiveContainer, base) => { +// const activeHeight = activeContainer.current.getBoundingClientRect().height; +// const passiveHeight = passiveContainer.current.getBoundingClientRect().height; +// +// return (base / activeHeight) * passiveHeight; +// }; +// +// export const bindScrollEvent = (scrollContainer, handler) => { +// if (scrollContainer.current && handler.current) { +// scrollContainer.current.addEventListener('scroll', handler.current, { passive: true }); +// } +// }; +// +// export const removeScrollEvent = (scrollContainer, handler) => { +// if (scrollContainer.current && handler.current) { +// scrollContainer.current.removeEventListener('scroll', handler.current); +// } +// }; -export const removeScrollEvent = (scrollContainer, handler) => { - if (scrollContainer.current && handler.current) { - scrollContainer.current.removeEventListener('scroll', handler.current); - } +export const extractSectionIdFromHtmlId = (id) => { + return id.replace(/^ObjectPageSection-/, ''); }; diff --git a/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap b/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap index 1991f110d89..268dae6a95b 100644 --- a/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap +++ b/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap @@ -2,24 +2,11 @@ exports[`ObjectPage IconTabBar Mode 1`] = `
    -
    -
    -
    -
    -
    + +
    + + www.myurl.com + + + Address 1 + + + Address 2 + + + Address 3 + +
    +
    +
    + +
    -
    -
    -
    -
    - -
    - - www.myurl.com - - - Address 1 - - - Address 2 - - - Address 3 - -
    -
    -
    -
    - -
    -
    +
    +
    +
    + -
    -
    -
    - Test 1 -
    -
    -
    -
    - - My Content 1 - -
    -
    -
    -
    -
    + My Content 1 +
    -
    +
    `; exports[`ObjectPage Just Some Sections 1`] = `
    -
    -
    -
    -
    -
    -
    + +
    +
    +
    +
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - Test -
    -
    -
    -
    -
    + Test
    -
    +
    `; @@ -418,21 +299,8 @@ exports[`ObjectPage Key Infos 1`] = ` class="ObjectPage-objectPage-0" data-component-name="ObjectPage" > -
    -
    -
    -
    -
    -
    + +
    +
    +
    +
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - Test -
    -
    -
    -
    -
    -
    -
    -
    -
    - Test 2 -
    -
    -
    -
    -
    + Test
    -
    +
    +
    +
    +
    +
    +
    +
    + Test 2 +
    +
    +
    `; exports[`ObjectPage No Header 1`] = `
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - Test -
    -
    -
    -
    -
    -
    -
    -
    - Test 2 -
    + class="ObjectPage-keyInfos-0" + />
    -
    -
    -
    +
    + +
    + + +
    + +
    +
    +
    +
    +
    +
    + Test
    -
    +
    +
    +
    +
    +
    +
    +
    + Test 2 +
    +
    +
    `; exports[`ObjectPage Not crashing with 0 sections 1`] = `
    -
    -
    -
    -
    -
    - + Content of SubSection 5.1 +
    +
    + class="ObjectPageSubSection-objectPageSubSection-0" + data-component-name="ObjectPageSubSection" + id="ObjectPageSubSection-5.2" + role="region" + > +
    + SubSection 5.2 +
    +
    + Content of SubSection 5.2 +
    +
    -
    +
    `; diff --git a/packages/main/src/components/ObjectPage/demo.stories.tsx b/packages/main/src/components/ObjectPage/demo.stories.tsx index 07f8674c57d..bb183bdaf50 100644 --- a/packages/main/src/components/ObjectPage/demo.stories.tsx +++ b/packages/main/src/components/ObjectPage/demo.stories.tsx @@ -78,10 +78,11 @@ export const renderDemo = () => { onSelectedSectionChanged={action('onSelectedSectionChanged')} noHeader={boolean('noHeader', false)} alwaysShowContentHeader={boolean('alwaysShowContentHeader', false)} - showTitleInHeaderContent={boolean('showTitleInHeaderContent', true)} + showTitleInHeaderContent={boolean('showTitleInHeaderContent', false)} renderBreadcrumbs={renderBreadcrumbs} renderKeyInfos={renderKeyInfos} style={{ height: '700px' }} + headerContentPinnable={boolean('headerContentPinnable', true)} >
    Test1
    diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index 90406387804..69e81aae592 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -1,13 +1,9 @@ -import '@ui5/webcomponents-icons/dist/icons/navigation-down-arrow'; -import '@ui5/webcomponents-icons/dist/icons/navigation-up-arrow'; -import { IScroller } from '@ui5/webcomponents-react-base/interfaces/IScroller'; +import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; import { Event } from '@ui5/webcomponents-react-base/lib/Event'; -import { Scroller } from '@ui5/webcomponents-react-base/lib/Scroller'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { useConsolidatedRef } from '@ui5/webcomponents-react-base/lib/useConsolidatedRef'; import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps'; import { getScrollBarWidth } from '@ui5/webcomponents-react-base/lib/Utils'; -import { Button } from '@ui5/webcomponents-react/lib/Button'; import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; import { FlexBoxAlignItems } from '@ui5/webcomponents-react/lib/FlexBoxAlignItems'; import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection'; @@ -24,298 +20,195 @@ import React, { useRef, useState } from 'react'; -import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; import { CommonProps } from '../../interfaces/CommonProps'; import { ObjectPageSectionPropTypes } from '../ObjectPageSection'; import { ObjectPageSubSectionPropTypes } from '../ObjectPageSubSection'; import { CollapsedAvatar } from './CollapsedAvatar'; import styles from './ObjectPage.jss'; -import { ObjectPageAnchorButton } from './ObjectPageAnchorButton'; +import { ObjectPageAnchorBar } from './ObjectPageAnchorBar'; import { ObjectPageHeader } from './ObjectPageHeader'; -import { ObjectPageScrollBar } from './ObjectPageScrollBar'; -import { - bindScrollEvent, - findSectionIndexById, - getProportionateScrollTop, - removeScrollEvent, - safeGetChildrenArray -} from './ObjectPageUtils'; +import { extractSectionIdFromHtmlId, getSectionById, safeGetChildrenArray } from './ObjectPageUtils'; +import { useObserveHeights } from './useObserveHeights'; + +declare const ResizeObserver; + +const SCROLL_BAR_WIDTH = 12; export interface ObjectPagePropTypes extends CommonProps { title?: string; subTitle?: string; image?: string | ReactElement; - imageShapeCircle?: boolean; - headerActions?: Array>; + headerActions?: ReactElement[]; renderHeaderContent?: () => JSX.Element; - children?: ReactElement | Array>; - mode?: ObjectPageMode; + children?: ReactElement | ReactElement[]; + selectedSectionId?: string; selectedSubSectionId?: string; onSelectedSectionChanged?: (event: Event) => void; - showHideHeaderButton?: boolean; - alwaysShowContentHeader?: boolean; - noHeader?: boolean; - showTitleInHeaderContent?: boolean; - scrollerRef?: RefObject; + renderBreadcrumbs?: () => JSX.Element; renderKeyInfos?: () => JSX.Element; + + // appearance + alwaysShowContentHeader?: boolean; + showTitleInHeaderContent?: boolean; + imageShapeCircle?: boolean; + mode?: ObjectPageMode; + noHeader?: boolean; + showHideHeaderButton?: boolean; + headerContentPinnable?: boolean; } const useStyles = createComponentStyles(styles, { name: 'ObjectPage' }); -const defaultScrollbarWidth = 12; /** * import { ObjectPage } from '@ui5/webcomponents-react/lib/ObjectPage'; */ const ObjectPage: FC = forwardRef((props: ObjectPagePropTypes, ref: RefObject) => { const { - title, - image, - subTitle, - headerActions, - renderHeaderContent: renderHeaderContentProp, - mode, - imageShapeCircle, + title = '', + image = null, + subTitle = '', + headerActions = [], + renderHeaderContent = null, + mode = ObjectPageMode.Default, + imageShapeCircle = false, className, style, tooltip, slot, - showHideHeaderButton, + showHideHeaderButton = false, children, - onSelectedSectionChanged, + onSelectedSectionChanged = () => { + /* noop */ + }, selectedSectionId, - noHeader, + noHeader = false, alwaysShowContentHeader, showTitleInHeaderContent, - scrollerRef, renderBreadcrumbs, - renderKeyInfos + renderKeyInfos, + headerContentPinnable } = props; - const [selectedSectionIndex, setSelectedSectionIndex] = useState(findSectionIndexById(children, selectedSectionId)); - const [selectedSubSectionId, setSelectedSubSectionId] = useState(props.selectedSubSectionId); - const [expandHeaderActive, setExpandHeaderActive] = useState(false); - const [collapsedHeader, setCollapsedHeader] = useState(renderHeaderContentProp === null); - - const objectPage: RefObject = useConsolidatedRef(ref); - const fillerDivDomRef: RefObject = useRef(); - const scrollBar: RefObject = useRef(); - const contentContainer: RefObject = useRef(); - const topHeader: RefObject = useRef(); - const innerHeader: RefObject = useRef(); - const innerScrollBar: RefObject = useRef(); - const contentScrollContainer: RefObject = useRef(); - const outerContentContainer: RefObject = useRef(); - const collapsedHeaderFiller: RefObject = useRef(); - const lastScrolledContainer = useRef(); - const hideHeaderButtonPressed = useRef(false); - const stableContentOnScrollRef = useRef(null); - const stableBarOnScrollRef = useRef(null); - const scroller = useConsolidatedRef(scrollerRef); - const [scrollbarWidth, setScrollbarWidth] = useState(defaultScrollbarWidth); - const isMounted = useRef(false); + const firstSectionId = safeGetChildrenArray(children)[0]?.props?.id; - const classes = useStyles(); + const [internalSelectedSectionId, setInternalSelectedSectionId] = useState(selectedSectionId ?? firstSectionId); + const [selectedSubSectionId, setSelectedSubSectionId] = useState(props.selectedSubSectionId); + const [headerPinned, setHeaderPinned] = useState(alwaysShowContentHeader); + const isProgrammaticallyScrolled = useRef(false); - useEffect(() => { - let selectedIndex = findSectionIndexById(children, selectedSectionId); - if (selectedSectionIndex !== selectedIndex) { - setSelectedSectionIndex(selectedIndex); - } - }, [selectedSectionId]); + const objectPageRef: RefObject = useConsolidatedRef(ref); + const topHeaderRef: RefObject = useRef(); + const headerContentRef: RefObject = useRef(); + const anchorBarRef: RefObject = useRef(); - const adjustDummyDivHeight = useCallback(() => { - return new Promise((resolve) => { - requestAnimationFrame(() => { - if (!objectPage.current) { - return; - } + const [scrollbarWidth, setScrollbarWidth] = useState(SCROLL_BAR_WIDTH); + const isMounted = useRef(false); - const sections = objectPage.current.querySelectorAll('[id^="ObjectPageSection"]'); - if (!sections || sections.length < 1) { - return; - } + // observe heights of header parts + const { topHeaderHeight, headerContentHeight, anchorBarHeight, totalHeaderHeight } = useObserveHeights( + objectPageRef, + topHeaderRef, + headerContentRef, + anchorBarRef, + { noHeader } + ); - const lastSectionDomRef = sections[sections.length - 1]; - const subSections = lastSectionDomRef.querySelectorAll('[id^="ObjectPageSubSection"]'); + // ***** + // SECTION SELECTION + // **** - let lastSubSectionHeight; - if (subSections.length > 0) { - lastSubSectionHeight = (subSections[subSections.length - 1] as HTMLElement).offsetHeight; - } else { - lastSubSectionHeight = - (lastSectionDomRef as HTMLElement).offsetHeight - - (lastSectionDomRef.querySelector("[role='heading']") as HTMLElement).offsetHeight; + const scrollToSection = useCallback( + (sectionId) => { + if (!sectionId) { + return; + } + if (firstSectionId === sectionId) { + objectPageRef.current.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + const childOffset = objectPageRef.current.querySelector(`#ObjectPageSection-${sectionId}`) + ?.offsetTop; + if (!isNaN(childOffset)) { + objectPageRef.current.scrollTo({ + top: childOffset - topHeaderHeight - anchorBarHeight - (headerPinned ? headerContentHeight : 0) + 45, + behavior: 'smooth' + }); } + } + isProgrammaticallyScrolled.current = false; + }, + [firstSectionId, objectPageRef, topHeaderHeight, anchorBarHeight, headerPinned, headerContentHeight] + ); - let heightDiff = contentContainer.current.offsetHeight - lastSubSectionHeight; + // change selected section when prop is changed (external change) + useEffect(() => { + isProgrammaticallyScrolled.current = true; + setInternalSelectedSectionId(selectedSectionId ?? firstSectionId); + }, [selectedSectionId, isProgrammaticallyScrolled, firstSectionId]); - heightDiff = heightDiff > 0 ? heightDiff : 0; - fillerDivDomRef.current.style.height = `${heightDiff}px`; - requestAnimationFrame(() => { - if (!contentScrollContainer.current || !topHeader.current) return; - const scrollbarContainerHeight = - contentScrollContainer.current.getBoundingClientRect().height + - topHeader.current.getBoundingClientRect().height; - innerScrollBar.current.style.height = `${scrollbarContainerHeight}px`; - }); - resolve(); + // section was selected by clicking on the anchor bar buttons + const handleOnSectionSelected = useCallback( + (e) => { + isProgrammaticallyScrolled.current = true; + const newSelectionSection = e.getParameter('props')?.id; + setInternalSelectedSectionId((oldSelectedSection) => { + if (oldSelectedSection === newSelectionSection) { + scrollToSection(newSelectionSection); + } + return newSelectionSection; }); - }); - }, [objectPage, contentContainer, fillerDivDomRef, contentScrollContainer, topHeader, innerScrollBar]); - - const adjustContentContainerHeight = useCallback(() => { - if (contentContainer.current && outerContentContainer.current) { - contentContainer.current.style.height = `${outerContentContainer.current.getBoundingClientRect().height}px`; - } - }, [outerContentContainer.current, contentContainer.current]); - - // @ts-ignore - const observer = useRef(new ResizeObserver(adjustDummyDivHeight)); - // @ts-ignore - const outerContainerObserver = useRef(new ResizeObserver(adjustContentContainerHeight)); - - const renderHideHeaderButton = () => { - if (!showHideHeaderButton || renderHeaderContentProp === null || noHeader) return null; - - return ( -