From 526a3281ab7685a5611042521010ef933e8de905 Mon Sep 17 00:00:00 2001 From: "Harbarth, Lukas" Date: Tue, 20 Jul 2021 13:47:35 +0200 Subject: [PATCH 01/14] cherry-pick 58719ce --- docs/2-MigrationGuide.stories.mdx | 178 +- packages/base/src/dist/Utils.ts | 3 + packages/base/src/hooks/useIsRTL.ts | 4 +- packages/base/src/index.ts | 3 +- packages/base/src/lib/Utils.ts | 4 +- packages/base/src/utils/index.ts | 35 +- .../components/DynamicPage/DynamicPage.jss.ts | 12 +- .../DynamicPage/DynamicPage.stories.mdx | 320 +- .../DynamicPage/DynamicPage.test.tsx | 55 +- .../__snapshots__/DynamicPage.test.tsx.snap | 1191 +++-- .../main/src/components/DynamicPage/index.tsx | 123 +- .../components/DynamicPageAnchorBar/index.tsx | 70 +- .../DynamicPageHeader.jss.ts | 10 +- .../components/DynamicPageHeader/index.tsx | 17 +- .../DynamicPageTitle/DynamicPageTitle.jss.ts | 50 +- .../src/components/DynamicPageTitle/index.tsx | 94 +- .../components/ObjectPage/CollapsedAvatar.tsx | 11 +- .../components/ObjectPage/ObjectPage.jss.ts | 165 +- .../ObjectPage/ObjectPage.stories.mdx | 125 +- .../components/ObjectPage/ObjectPage.test.tsx | 270 +- .../ObjectPage/ObjectPageAnchorBar.tsx | 238 - .../ObjectPage/ObjectPageAnchorButton.tsx | 60 +- .../ObjectPage/ObjectPageAnchorTab.ts | 141 + .../ObjectPage/ObjectPageHeader.tsx | 142 - .../components/ObjectPage/ObjectPageUtils.ts | 8 +- .../__snapshots__/ObjectPage.test.tsx.snap | 4025 ++++++++++++----- .../main/src/components/ObjectPage/index.tsx | 652 ++- .../ObjectPageSection.jss.ts | 3 - .../ObjectPageSection.test.tsx | 4 +- .../ObjectPageSection.test.tsx.snap | 4 + .../components/ObjectPageSection/index.tsx | 27 +- .../ObjectPageSubSection.test.tsx | 4 +- .../ObjectPageSubSection.test.tsx.snap | 8 +- .../components/ObjectPageSubSection/index.tsx | 25 +- .../useObserveHeights.ts | 26 +- .../internal/useResponsiveContentPadding.ts | 40 + 36 files changed, 4975 insertions(+), 3172 deletions(-) create mode 100644 packages/base/src/dist/Utils.ts delete mode 100644 packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx create mode 100644 packages/main/src/components/ObjectPage/ObjectPageAnchorTab.ts delete mode 100644 packages/main/src/components/ObjectPage/ObjectPageHeader.tsx rename packages/main/src/{components/ObjectPage => internal}/useObserveHeights.ts (67%) create mode 100644 packages/main/src/internal/useResponsiveContentPadding.ts diff --git a/docs/2-MigrationGuide.stories.mdx b/docs/2-MigrationGuide.stories.mdx index 116ca85f845..68f559e5be9 100644 --- a/docs/2-MigrationGuide.stories.mdx +++ b/docs/2-MigrationGuide.stories.mdx @@ -1,4 +1,5 @@ import { Meta } from '@storybook/addon-docs/blocks'; +import { MessageStrip, MessageStripType } from '@ui5/webcomponents-react'; @@ -14,6 +15,9 @@ or the [changelog](https://github.com/SAP/ui5-webcomponents-react/blob/master/CH ## Table of Contents +- [0.16.x to 0.17.0](#migrating-from-016x-to-0170) +- [0.15.x to 0.16.0](#migrating-from-015x-to-0160) +- [0.14.x to 0.15.0](#migrating-from-014x-to-0150) - [0.13.x to 0.14.0](#migrating-from-013x-to-0140) - [0.12.x to 0.13.0](#migrating-from-012x-to-0130) - [0.11.x to 0.12.0](#migrating-from-011x-to-0120) @@ -21,6 +25,178 @@ or the [changelog](https://github.com/SAP/ui5-webcomponents-react/blob/master/CH - [0.9.x to 0.10.0](#migrating-from-09x-to-0100) - [0.8.x to 0.9.0](#migrating-from-08x-to-090) +## Migrating from 0.16.x to 0.17.0 + +### Consolidate API of ObjectPage and DynamicPage + +The DynamicPage and the ObjectPage are very similar but had completely different APIs and props. +We streamlined those APIs by adding components used by the `DynamicPage` to the `ObjectPage`. + +#### DynamicPage changes + +- `title` has been renamed to `headerTitle`. +- **`DynamicPageTitle`:** `subHeading` has been renamed to `subheading`. +- **`DynamicPageHeader`:** `children` are no longer displayed as `flex` items to support other display types like `grid`. To align children you now need to add the container (like `FlexBox`) and CSS yourself. +
+
+ Example for aligning items next to each other: + +```jsx +// Before + +
Content 1
+
Content 2
+
+ +// Now + + +
Content 1
+
Content 2
+
+
+``` + +#### ObjectPage changes + +- **`ObjectPageSection`:** `title` and `titleUppercase` has been renamed. Please use `heading` and `headingUppercase` instead. +- **`ObjectPageSubSection`:** `title` has been renamed to `heading`. +- `title` has been renamed to `headerTitle` and is now defining the upper, static, title section of the `ObjectPage`. It expects to receive the `DynamicPageTitle` component. +- `headerContent` has been renamed to `header` and expects now the `DynamicPageHeader` component to be passed. +- `noHeader` has been removed. It is now sufficient not to set `headerTitle` and `header` to achieve the same behavior. +- `title`, `subTitle`, `headerActions`, `breadcrumbs` and `keyInfos` should now be passed to the corresponding `DynamicPageTitle` props. + +Setting the title section of the `ObjectPage`: + +```jsx +//Before + + Breadcrumb 1 + Breadcrumb 2 + + } + headerActions={ + <> + + + + } + keyInfos={keyInfo} +> + ObjectPage Content + + +//Now + + Breadcrumb 1 + Breadcrumb 2 + + } + actions={ /* replaces `headerActions`*/ + <> + + + + } + > + keyInfo {/* replaces `keyInfos` */} + + } +> + ObjectPage Content + +``` + +## Migrating from 0.15.x to 0.16.0 + +
+ +### Changed import path for `ComposedChartPlaceholder` + +The import path of the `ComposedChartPlaceholder` has changed. You can now import the placeholder from `@ui5/webcomponents-react-charts/dist/ComposedChartPlaceholder` or directly from `@ui5/webcomponents-react-charts`. + +```jsx +import { ComposedChartPlaceholder } from '@ui5/webcomponents-react-charts/dist/ComposedChartPlaceholder'; +//or +import { ComposedChartPlaceholder } from '@ui5/webcomponents-react-charts'; +``` + +### API Updates + +#### FilterBar + +If `useToolbar` is set to false, the entire toolbar above the filter items is hidden. The search input, variants, and the "Show/Hide FilterBar" button is not available with this mode. The rest of the buttons are displayed next to the filter items. + +If you have used the `useToolbar` prop to hide the "Show/Hide Filters" button, but still want to display the search or variants, you can now use the `hideToggleFiltersButton`. + +Before: + +```jsx +} useToolbar={false}> + ... + +``` + +Now: + +```jsx +} hideToggleFiltersButton> + ... + +``` + +## Migrating from 0.14.x to 0.15.0 + +
+ +### Replaced `lib` folder with `dist` folder + +UI5 Web Components for React was publishing the individual components in a `lib` folder, while our Assets were published +in `dist`. As UI5 Web Components is also publishing all files in the `dist` directory, we have now dropped our `lib` folder and publish everything to `dist`. + +In case you are importing directly from `@ui5/webcomponents-react`, `@ui5/webcomponents-react-charts` or `@ui5/webcomponents-react-base` +(e.g. `import { Button } from '@ui5/webcomponents-react';`) no action is required. + +If you are importing from the `lib` folders, e.g. `import { Button } from '@ui5/webcomponents-react/lib/Button';` you will have to modify your codebase: + +You can either change all imports manually: + +- replace `@ui5/webcomponents-react/lib/` with `@ui5/webcomponents-react/dist/` +- replace `@ui5/webcomponents-react-base/lib/` with `@ui5/webcomponents-react-base/dist/` +- replace `@ui5/webcomponents-react-charts/lib/` with `@ui5/webcomponents-react-charts/dist/` + +or use [jscodeshift](https://github.com/facebook/jscodeshift) with our codemod by running this command in your project + +```bash +# replace 'src' with the directory where your src files are located +npx jscodeshift --transform node_modules/@ui5/webcomponents-react-base/codemods/transformLibToDist.js --extensions js,jsx src + +# in case you are using typescript +npx jscodeshift --transform node_modules/@ui5/webcomponents-react-base/codemods/transformLibToDist.js --extensions js,jsx,ts,tsx --parser tsx src +``` + + + Please make sure that you have committed all changes before running this codemod. +
+
+ Keep in mind that the codemod output will not always match your project’s coding style, so you might want to run + Prettier + after the codemod finishes for consistent formatting. +
+ ## Migrating from 0.13.x to 0.14.0
@@ -96,7 +272,7 @@ becomes ```js import { createUseStyles } from 'react-jss'; -import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; +import { ThemingParameters } from '@ui5/webcomponents-react-base'; const useStyles = createUseStyles((context) => ({ myClass: { diff --git a/packages/base/src/dist/Utils.ts b/packages/base/src/dist/Utils.ts new file mode 100644 index 00000000000..057b260bf31 --- /dev/null +++ b/packages/base/src/dist/Utils.ts @@ -0,0 +1,3 @@ +import { deprecationNotice, enrichEventWithDetails } from '../utils'; + +export { deprecationNotice, enrichEventWithDetails }; diff --git a/packages/base/src/hooks/useIsRTL.ts b/packages/base/src/hooks/useIsRTL.ts index 5e306c4ecbc..59fb070f0eb 100644 --- a/packages/base/src/hooks/useIsRTL.ts +++ b/packages/base/src/hooks/useIsRTL.ts @@ -5,6 +5,9 @@ import { RefObject, useState } from 'react'; const GLOBAL_DIR_CSS_VAR = '--_ui5_dir'; const detectRTL = (elementRef: RefObject) => { + if (!elementRef.current) { + return getRTL(); + } const doc = window.document; const dirValues = ['ltr', 'rtl']; // exclude "auto" and "" from all calculations const locallyAppliedDir = getComputedStyle(elementRef.current).getPropertyValue(GLOBAL_DIR_CSS_VAR); @@ -29,7 +32,6 @@ const detectRTL = (elementRef: RefObject) => { const useIsRTL = (elementRef: RefObject): boolean => { const [isRTL, setRTL] = useState(getRTL()); // use config RTL as best guess - useIsomorphicLayoutEffect(() => { setRTL(detectRTL(elementRef)); // update immediately while rendering const targets = [document.documentElement, document.body, elementRef.current].filter(Boolean); diff --git a/packages/base/src/index.ts b/packages/base/src/index.ts index bf3e99e3bd7..9d8cbf264c3 100644 --- a/packages/base/src/index.ts +++ b/packages/base/src/index.ts @@ -10,12 +10,11 @@ import { StyleClassHelper } from './lib/StyleClassHelper'; import { ThemingParameters } from './lib/ThemingParameters'; import { useConsolidatedRef } from './lib/useConsolidatedRef'; import { usePassThroughHtmlProps } from './lib/usePassThroughHtmlProps'; -import { deprecationNotice, enrichEventWithDetails, getScrollBarWidth } from './lib/Utils'; +import { deprecationNotice, enrichEventWithDetails } from './lib/Utils'; export { StyleClassHelper, deprecationNotice, - getScrollBarWidth, Logger, LOG_LEVEL, useConsolidatedRef, diff --git a/packages/base/src/lib/Utils.ts b/packages/base/src/lib/Utils.ts index 0bfa180f018..da41e70b54f 100644 --- a/packages/base/src/lib/Utils.ts +++ b/packages/base/src/lib/Utils.ts @@ -1,3 +1,3 @@ -import { deprecationNotice, getScrollBarWidth, enrichEventWithDetails } from '../utils'; +import { deprecationNotice, enrichEventWithDetails } from '../utils'; -export { deprecationNotice, getScrollBarWidth, enrichEventWithDetails }; +export { deprecationNotice, enrichEventWithDetails }; diff --git a/packages/base/src/utils/index.ts b/packages/base/src/utils/index.ts index de4a708aae8..3b89f9a6d54 100644 --- a/packages/base/src/utils/index.ts +++ b/packages/base/src/utils/index.ts @@ -8,35 +8,10 @@ export const deprecationNotice = (component: string, message: string) => { } }; -export const getScrollBarWidth = () => { - const inner = document.createElement('p'); - inner.style.width = '100%'; - inner.style.height = '200px'; - - const outer = document.createElement('div'); - outer.style.position = 'absolute'; - outer.style.top = '0px'; - outer.style.left = '0px'; - outer.style.visibility = 'hidden'; - outer.style.width = '200px'; - outer.style.height = '150px'; - outer.style.overflow = 'hidden'; - outer.appendChild(inner); - - document.body.appendChild(outer); - const w1 = inner.offsetWidth; - outer.style.overflow = 'scroll'; - let w2 = inner.offsetWidth; - - if (w1 === w2) { - w2 = outer.clientWidth; - } - - document.body.removeChild(outer); - return w1 - w2; -}; - -export const enrichEventWithDetails = >(event: UIEvent, payload: T = null) => { +export const enrichEventWithDetails = , ReturnType = CustomEvent>( + event: UIEvent, + payload: T = null +) => { if (event.hasOwnProperty('persist')) { // if there is a persist method, it's an SyntheticEvent so we need to persist it event.persist(); @@ -50,5 +25,5 @@ export const enrichEventWithDetails = >(event: configurable: true }); Object.assign(event.detail, payload); - return (event as unknown) as CustomEvent; + return event as unknown as ReturnType; }; diff --git a/packages/main/src/components/DynamicPage/DynamicPage.jss.ts b/packages/main/src/components/DynamicPage/DynamicPage.jss.ts index 739b8cd883d..df444428b91 100644 --- a/packages/main/src/components/DynamicPage/DynamicPage.jss.ts +++ b/packages/main/src/components/DynamicPage/DynamicPage.jss.ts @@ -5,7 +5,7 @@ export const DynamicPageCssVariables = { headerDisplay: '--ui5wcr_DynamicPage_header_display' }; -const styles = { +export const styles = { dynamicPage: { width: '100%', height: '100%', @@ -43,8 +43,7 @@ const styles = { backgroundColor: ThemingParameters.sapObjectHeader_Background }, contentContainer: { - ...sapUiResponsiveContentPadding, - paddingTop: '1rem !important', + paddingTop: '1rem', boxSizing: 'border-box', width: '100%', height: 'auto', @@ -62,7 +61,10 @@ const styles = { }, backgroundTransparent: { background: 'transparent' + }, + footer: { + position: 'sticky', + bottom: '0.5rem', + margin: '0 0.5rem' } }; - -export default styles; diff --git a/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx b/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx index 9bb2e23a207..ad266d183df 100644 --- a/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx +++ b/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx @@ -1,4 +1,4 @@ -import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; import { DocsHeader } from '@shared/stories/DocsHeader'; import '@ui5/webcomponents-icons/dist/action'; import '@ui5/webcomponents-icons/dist/full-screen'; @@ -17,11 +17,18 @@ import { ValueState } from '@ui5/webcomponents-react/lib/ValueState'; import { ButtonDesign } from '@ui5/webcomponents-react/lib/ButtonDesign'; import { Badge } from '@ui5/webcomponents-react/lib/Badge'; import { ObjectStatus } from '@ui5/webcomponents-react/lib/ObjectStatus'; +import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; +import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection'; +import { FlexBoxWrap } from '@ui5/webcomponents-react/lib/FlexBoxWrap'; import { ProductsTable } from '@shared/stories/ProductsTable'; import { DocsCommonProps } from '@shared/stories/DocsCommonProps'; import { createSelectArgTypes } from '@shared/stories/createSelectArgTypes'; import { VariantManagement } from '@ui5/webcomponents-react/lib/VariantManagement'; -import { useState } from 'react'; +import { Panel } from '@ui5/webcomponents-react/lib/Panel'; +import { BusyIndicator } from '@ui5/webcomponents-react/lib/BusyIndicator'; +import { BarDesign } from '@ui5/webcomponents-react/lib/BarDesign'; +import { Bar } from '@ui5/webcomponents-react/lib/Bar'; +import { useState,useReducer } from 'react'; + Edit + , + , + , + , - , - , - - ]} - navigationActions={[ - , - , - , - - ]} - navigationActions={[ - + + + } /> } - subHeading={} > - Status: OK - - } - header={ - -
- - - -
-
- - In Stock -
-
- } - > - - + + + )} + ); }} diff --git a/packages/main/src/components/DynamicPage/DynamicPage.test.tsx b/packages/main/src/components/DynamicPage/DynamicPage.test.tsx index 22fe9ae5529..5d202772b05 100644 --- a/packages/main/src/components/DynamicPage/DynamicPage.test.tsx +++ b/packages/main/src/components/DynamicPage/DynamicPage.test.tsx @@ -17,6 +17,8 @@ import { ButtonDesign } from '@ui5/webcomponents-react/lib/ButtonDesign'; import { Badge } from '@ui5/webcomponents-react/lib/Badge'; import { Form } from '@ui5/webcomponents-react/lib/Form'; import { FormGroup } from '@ui5/webcomponents-react/lib/FormGroup'; +import { Bar } from '@ui5/webcomponents-react/lib/Bar'; +import { BarDesign } from '@ui5/webcomponents-react/lib/BarDesign'; import { FormItem } from '@ui5/webcomponents-react/lib/FormItem'; import { ObjectStatus } from '@ui5/webcomponents-react/lib/ObjectStatus'; import { ValueState } from '@ui5/webcomponents-react/lib/ValueState'; @@ -24,7 +26,7 @@ import { createPassThroughPropsTest } from '@shared/tests/utils'; const renderComponent = () => ( Edit, @@ -49,12 +51,12 @@ const renderComponent = () => ( } heading={Header Title} - subHeading={} + subheading={} > Status: OK } - header={ + headerContent={
@@ -205,7 +207,7 @@ const renderComponent = () => ( const renderComponentWithoutContent = () => ( Edit, @@ -230,12 +232,12 @@ const renderComponentWithoutContent = () => ( } heading={Header Title} - subHeading={} + subheading={} > Status: OK } - header={ + headerContent={
@@ -254,7 +256,7 @@ const renderComponentWithoutContent = () => ( const renderComponentWithAlwaysShowContentHeader = () => ( Edit, @@ -279,12 +281,12 @@ const renderComponentWithAlwaysShowContentHeader = () => ( } heading={Header Title} - subHeading={} + subheading={} > Status: OK } - header={ + headerContent={
@@ -304,7 +306,7 @@ const renderComponentHideHeaderButton = () => ( Edit, @@ -313,28 +315,11 @@ const renderComponentHideHeaderButton = () => ( , ]} - navigationActions={[ - , - - ], - keyInfos: employed, image: SampleImage, style: { height: '700px' }, - className: '', - tooltip: '', - slot: '', - ref: null + footer: ( + + + + + } + /> + ), + headerTitle: ( + + + + + } + breadcrumbs={ + + Manager Cockpit + My Team + + } + > + employed + + ), + headerContent: ( + + + + +33 6 4512 5158 + DeniseSmith@sap.com + + https://github.com/SAP/ui5-webcomponents-react + + + + + + + + + ) }} /> @@ -97,7 +114,7 @@ import '@ui5/webcomponents-icons/dist/wrench.js'; {(args) => { return ( - +
4 days overdue - Cascaded @@ -110,8 +127,8 @@ import '@ui5/webcomponents-icons/dist/wrench.js';
- - + +
@@ -145,7 +162,7 @@ import '@ui5/webcomponents-icons/dist/wrench.js';
@@ -163,8 +180,12 @@ import '@ui5/webcomponents-icons/dist/wrench.js';
- - + +
@@ -193,7 +214,7 @@ import '@ui5/webcomponents-icons/dist/wrench.js';
@@ -219,7 +240,7 @@ import '@ui5/webcomponents-icons/dist/wrench.js'; diff --git a/packages/main/src/components/ObjectPage/ObjectPage.test.tsx b/packages/main/src/components/ObjectPage/ObjectPage.test.tsx index 4b51c95c4a9..7180b6452c6 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.test.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.test.tsx @@ -1,80 +1,65 @@ -import { fireEvent, render, screen } from '@shared/tests'; -import { Breadcrumbs } from '@ui5/webcomponents-react/lib/Breadcrumbs'; -import { Button } from '@ui5/webcomponents-react/lib/Button'; +import { render } from '@shared/tests'; +import { createPassThroughPropsTest } from '@shared/tests/utils'; +import { Avatar } from '@ui5/webcomponents-react/lib/Avatar'; +import { Bar } from '@ui5/webcomponents-react/lib/Bar'; +import { BarDesign } from '@ui5/webcomponents-react/lib/BarDesign'; +import { DynamicPageHeader } from '@ui5/webcomponents-react/lib/DynamicPageHeader'; +import { DynamicPageTitle } from '@ui5/webcomponents-react/lib/DynamicPageTitle'; import { Label } from '@ui5/webcomponents-react/lib/Label'; -import { Link } from '@ui5/webcomponents-react/lib/Link'; import { ObjectPage } from '@ui5/webcomponents-react/lib/ObjectPage'; import { ObjectPageMode } from '@ui5/webcomponents-react/lib/ObjectPageMode'; import { ObjectPageSection } from '@ui5/webcomponents-react/lib/ObjectPageSection'; import { ObjectPageSubSection } from '@ui5/webcomponents-react/lib/ObjectPageSubSection'; import { Text } from '@ui5/webcomponents-react/lib/Text'; -import { Title } from '@ui5/webcomponents-react/lib/Title'; -import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; import React from 'react'; -const headerContent = ( -
- www.myurl.com - Address 1 - Address 2 - Address 3 -
-); +const headerContent = HeaderContent; +const headerTitle = HeaderTitle; +const footer = Footer
} />; -const renderComponent = (mode = ObjectPageMode.Default) => ( - Action]} - headerContent={headerContent} - showHideHeaderButton - mode={mode} - > - - - - -
Test2
+const renderComponent = (props = {}) => ( + + + - - Test1 + +
Content Section 2
- -

Section 4

- - Test 4 SubSection 1 + + + Content Section 3.1 + + + Content Section 3.2 - - Test 4 SubSection 2 + + Content Section 3.3 - - - Content of SubSection 5.1 + + + Content Section 4.1 - - Content of SubSection 5.2 + + Content Section 4.2 + + + Content Section 4.3
); -const renderComponentWithSections = () => ( - Action]} - headerContent={headerContent} - mode={ObjectPageMode.Default} - > - - +const renderComponentWithSections = (props = {}) => ( + + + Content 1 - - + + - - + + Content 3 ); @@ -88,29 +73,52 @@ afterAll(() => { Element.prototype.scrollTo = original; }); describe('ObjectPage', () => { - test('With Subsections', () => { - const { asFragment } = render(renderComponent()); + test('Default with SubSections', () => { + const { asFragment, getByText } = render(renderComponent()); + expect(getByText('Title Section 1')).not.toBeVisible(); + expect(getByText('Content Section 1')).toBeVisible(); + expect(getByText('Title Section 2')).toBeVisible(); + expect(getByText('Content Section 2')).toBeVisible(); + expect(getByText('Title Section 4')).toBeVisible(); + expect(getByText('Title SubSection 4.1')).toBeVisible(); + expect(getByText('Content Section 4.1')).toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); - test('Only Sections', () => { - const { asFragment } = render(renderComponentWithSections()); + test('Default with Sections', () => { + const { asFragment, getByText } = render(renderComponentWithSections()); + expect(getByText('Title 1')).not.toBeVisible(); + expect(getByText('Title 2')).toBeVisible(); + expect(getByText('Content 1')).toBeVisible(); + expect(getByText('Content 2')).toBeVisible(); + expect(getByText('Content 3')).toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); test('IconTabBar Mode', () => { - const { asFragment } = render(renderComponent(ObjectPageMode.IconTabBar)); - expect(asFragment()).toMatchSnapshot(); - }); - - test('Just Some Sections', () => { - const { asFragment } = render( - - Test - Test 2 - + const cb = jest.fn(); + const { asFragment, getByText, queryByText, container, rerender } = render( + renderComponent({ mode: ObjectPageMode.IconTabBar, onSelectedSectionChanged: cb }) ); - + expect(getByText('Title Section 1')).not.toBeVisible(); + expect(getByText('Content Section 1')).toBeVisible(); + expect(queryByText('Title Section 3')).toBeNull(); + expect(queryByText('Content Section 3')).toBeNull(); + + const tabs = container.querySelector('ui5-tabcontainer').children; + Array.from(tabs).forEach((tab, index) => { + expect(tab).toHaveAttribute('text', `Title Section ${index + 1}`); + }); + //does not work due to shadow-DOM + // fireEvent.click(tabs[2]); + // expect(cb).toHaveBeenCalled(); + rerender( + renderComponent({ mode: ObjectPageMode.IconTabBar, onSelectedSectionChanged: cb, selectedSectionId: '3' }) + ); + expect(getByText('Title Section 3')).not.toBeVisible(); + expect(getByText('Content Section 3.1')).toBeVisible(); + expect(queryByText('Title Section 1')).toBeNull(); + expect(queryByText('Content Section 1')).toBeNull(); expect(asFragment()).toMatchSnapshot(); }); @@ -120,7 +128,6 @@ describe('ObjectPage', () => { Test ); - expect(asFragment()).toMatchSnapshot(); }); @@ -136,80 +143,83 @@ describe('ObjectPage', () => { test('Not crashing with 0 sections', () => { const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with header', () => { + const { asFragment, getByText, queryByRole } = render(renderComponent({ headerContent })); + expect(getByText('HeaderContent')).toBeVisible(); + expect(queryByRole('navigation')).toBeNull(); expect(asFragment()).toMatchSnapshot(); }); - test('Set selected section id', () => { - const { asFragment } = render( - - Test - Test 2 - - ); + test('with title', () => { + const { asFragment, getByText, queryByRole } = render(renderComponent({ headerTitle })); + expect(getByText('HeaderTitle')).toBeVisible(); + expect(queryByRole('navigation')).toBeNull(); expect(asFragment()).toMatchSnapshot(); }); - test.skip('onSelectedSectionChangedHandler', () => { - const callback = jest.fn(); - render( - - - Test - - - Test 2 - - - ); - fireEvent.click(screen.getByText('Section1')); - expect(callback.mock[0][0].detail.selectedSectionId).toEqual('1'); + test('with footer', () => { + const { asFragment, getByText } = render(renderComponent({ footer })); + expect(getByText('Footer')).toBeVisible(); + expect(asFragment()).toMatchSnapshot(); }); - test('No Header', () => { - const { asFragment } = render( - - Test - Test 2 - + test('with title, header & footer', () => { + const { asFragment, getByText } = render(renderComponent({ headerTitle, headerContent, footer })); + expect(getByText('HeaderTitle')).toBeVisible(); + expect(getByText('HeaderContent')).toBeVisible(); + expect(getByText('Footer')).toBeVisible(); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with anchor-bar', () => { + const { asFragment, queryByTitle, rerender, container } = render( + renderComponent({ headerTitle, headerContent, footer }) ); + expect(queryByTitle('Expand Header')).toBeNull(); + expect(queryByTitle('Pin Header')).toBeNull(); + rerender(renderComponent({ headerTitle, headerContent, footer, showHideHeaderButton: true })); + expect(queryByTitle('Expand Header')).toBeVisible(); + expect(queryByTitle('Pin Header')).toBeNull(); + + //needs mocking, otherwise won't work + /* rerender(renderComponent({ headerTitle, headerContent, footer, showHideHeaderButton: true, headerContentPinnable: true })); + expect(queryByTitle('Expand Header')).toBeVisible(); + expect(queryByTitle('Pin Header')).toBeVisible(); + rerender(renderComponent({ headerTitle, headerContent, footer, headerContentPinnable: true })); + expect(queryByTitle('Expand Header')).toBeNull(); + expect(queryByTitle('Pin Header')).toBeVisible(); */ expect(asFragment()).toMatchSnapshot(); }); - const keyInfos = ( - <> -
- Key Info 1 - Value 1 -
-
- Key Info 2 - Value 2 -
-
- Key Info 3 - Value 3 -
- - ); - - const breadcrumbs = ( - - Path1 - Path2 - - - ); - - test('Key Infos', () => { - const { asFragment } = render( - - Test - Test 2 - + test('with img', () => { + const { asFragment, container, rerender } = render(renderComponent({ headerTitle, headerContent, footer })); + const headerContainerChildren = container.querySelector( + 'div[data-component-name="ObjectPageHeaderContainer"]' + ).children; + expect(headerContainerChildren.length).toBe(1); + rerender(renderComponent({ headerTitle, headerContent, footer, image: 'not_a_real_path.orly' })); + expect(headerContainerChildren.length).toBe(2); + rerender( + renderComponent({ headerTitle, headerContent, footer, image: 'not_a_real_path.orly', imageShapeCircle: true }) ); + expect(headerContainerChildren[0]).toHaveStyle(`border-radius: 50%; overflow: hidden;`); expect(asFragment()).toMatchSnapshot(); + rerender(renderComponent({ headerTitle, headerContent, footer, image: })); + expect(headerContainerChildren.length).toBe(2); + expect(headerContainerChildren[0]).toHaveAttribute('size', 'L'); + expect(asFragment()).toMatchSnapshot(); + }); + + //todo: needs mocking + test.skip('title in header', () => { + const { container } = render( + renderComponent({ headerTitle, headerContent, footer, image: , showTitleInHeaderContent: true }) + ); }); - // createPassThroughPropsTest(ObjectPage); + createPassThroughPropsTest(ObjectPage); }); diff --git a/packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx b/packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx deleted file mode 100644 index 209d80fd772..00000000000 --- a/packages/main/src/components/ObjectPage/ObjectPageAnchorBar.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { addCustomCSS } from '@ui5/webcomponents-base/dist/Theming'; -import '@ui5/webcomponents-icons/dist/pushpin-off'; -import '@ui5/webcomponents-icons/dist/slim-arrow-down'; -import '@ui5/webcomponents-icons/dist/slim-arrow-up'; -import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; -import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; -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 { stopPropagation } from '../../internal/stopPropagation'; -import { StandardListItem } from '../../webComponents/StandardListItem'; -import { ObjectPageAnchorButton } from './ObjectPageAnchorButton'; -import { safeGetChildrenArray } from './ObjectPageUtils'; -import { createPortal } from 'react-dom'; -import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks'; -import { - COLLAPSE_HEADER, - EXPAND_HEADER, - PIN_HEADER, - UNPIN_HEADER -} from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults'; - -addCustomCSS( - 'ui5-button', - ` - :host([data-ui5wcr-object-page-header-action]){ - width: 1.375rem; - height: 1.375rem; - min-width: 1.375rem; - } - :host([data-ui5wcr-object-page-header-action]) .ui5-button-root { - padding: 0; - }` -); -addCustomCSS( - 'ui5-togglebutton', - ` - :host([data-ui5wcr-object-page-header-action]){ - width: 1.375rem; - height: 1.375rem; - min-width: 1.375rem; - } - :host([data-ui5wcr-object-page-header-action]) .ui5-button-root { - padding: 0; - }` -); - -const anchorBarStyles = { - anchorBarActionButton: { - position: 'absolute', - 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.target.pressed); - }, - [setHeaderPinned] - ); - - const onTabItemSelect = useCallback( - (event) => { - const { sectionId, index } = event.detail.tab.dataset; - // eslint-disable-next-line eqeqeq - const section = safeGetChildrenArray(sections).find((el) => el.props.id == sectionId); - handleOnSectionSelected( - enrichEventWithDetails({} as any, { - ...section, - index - }) - ); - }, - [sections] - ); - - const onShowSubSectionPopover = useCallback( - (e, section) => { - setPopoverContent(section); - popoverRef.current.openBy(e.target.parentElement); - }, - [setPopoverContent, popoverRef] - ); - - const onSubSectionClick = useCallback( - (e) => { - const selectedId = e.detail.item.dataset.key; - const subSection = popoverContent.props.children - .filter((item) => item.props && item.props.isSubSection) - .find((item) => item.props.id === selectedId); - if (subSection) { - handleOnSubSectionSelected(enrichEventWithDetails(e, { section: popoverContent, subSection })); - } - popoverRef.current.close(); - }, - [handleOnSubSectionSelected, popoverRef, popoverContent] - ); - - const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); - - return ( -
- - {safeGetChildrenArray(sections).map((section: ReactElement, 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 a423e9dd491..8c5603faa88 100644 --- a/packages/main/src/components/ObjectPage/ObjectPageAnchorButton.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPageAnchorButton.tsx @@ -1,9 +1,15 @@ import '@ui5/webcomponents-icons/dist/slim-arrow-down'; -import { Tab } from '@ui5/webcomponents-react/lib/Tab'; -import React, { FC, useEffect, useRef } from 'react'; +import React, { FC, ReactElement, useEffect, useRef } from 'react'; +import { ObjectPageSectionPropTypes } from '../ObjectPageSection'; +import { ObjectPageAnchorTab } from './ObjectPageAnchorTab'; +import { safeGetChildrenArray } from './ObjectPageUtils'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +ObjectPageAnchorTab.define(); interface ObjectPageAnchorPropTypes { - section: any; + section: ReactElement; onShowSubSectionPopover: (event: any, section: any) => void; index: number; selected: boolean; @@ -13,45 +19,29 @@ export const ObjectPageAnchorButton: FC = (props: Obj const ref = useRef(); const { section, index, selected, onShowSubSectionPopover } = props; - let subSectionsAvailable = false; - if (section.props.children && section.props.children.filter) { - const subSections = section.props.children.filter((item) => item.props && item.props.isSubSection); - subSectionsAvailable = subSections.length > 0; - } + const hasSubSections = safeGetChildrenArray(section.props.children).some( + (subSection) => subSection.props?.isSubSection + ); useEffect(() => { - if (subSectionsAvailable) { - try { - const element = ref.current?.parentElement?.shadowRoot?.querySelector( - `.ui5-tc__headerList li[aria-posinset="${index + 1}"] .ui5-tab-strip-itemContent` - ); - - 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); - } - } catch (e) { - // empty catch block, mainly required for tests - } - } - }, [subSectionsAvailable, ref, onShowSubSectionPopover, section]); + const listener = (e) => { + onShowSubSectionPopover(e, section); + }; + const el = ref.current; + el.addEventListener('show-sub-sections', listener); + return () => { + el?.removeEventListener('show-sub-sections', listener); + }; + }, [onShowSubSectionPopover]); return ( - ); }; diff --git a/packages/main/src/components/ObjectPage/ObjectPageAnchorTab.ts b/packages/main/src/components/ObjectPage/ObjectPageAnchorTab.ts new file mode 100644 index 00000000000..7766744517a --- /dev/null +++ b/packages/main/src/components/ObjectPage/ObjectPageAnchorTab.ts @@ -0,0 +1,141 @@ +/* eslint-disable no-underscore-dangle */ +import ifDefined from '@ui5/webcomponents-base/dist/renderer/ifDefined.js'; +import { html, setSuffix, setTags } from '@ui5/webcomponents-base/dist/renderer/LitRenderer.js'; +import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; +import Icon from '@ui5/webcomponents/dist/Icon.js'; +import Tab from '@ui5/webcomponents/dist/Tab.js'; +import TabContainer from '@ui5/webcomponents/dist/TabContainer.js'; +import { DetailedHTMLProps, HTMLAttributes, MouseEventHandler } from 'react'; + +interface ObjectPageAnchorTabPropTypes extends HTMLAttributes { + text: string; + 'with-sub-sections'?: boolean; + selected?: boolean; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + 'ui5-object-page-anchor-tab': DetailedHTMLProps; + } + } +} + +const metadata = { + tag: 'ui5-object-page-anchor-tab', + properties: { + withSubSections: { + type: Boolean + } + }, + events: { + 'show-sub-sections': {} + } +}; + +class ObjectPageAnchorTab extends Tab { + static get metadata() { + return metadata; + } + + static get dependencies() { + return [Icon]; + } + + static get stripTemplate() { + return (context, tags, suffix) => { + setTags(tags); + setSuffix(suffix); + return html` `; + }; + } + + _handleOnSubSectionsClick: MouseEventHandler = (e) => { + e.stopPropagation(); + e.preventDefault(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.fireEvent('show-sub-sections', { + targetRef: (e.target as any).parentElement.parentElement + }); + }; + + // maybe we can look into a custom overflow template as well + // static get overflowTemplate() { + // return (context, tags, suffix) => { + // setTags(tags); + // setSuffix(suffix); + // return html` + //
${ifDefined(context.text)}
+ //
`; + // }; + // } +} + +TabContainer.registerTabStyles(` + +.ui5-tab-strip-itemContent { + display: flex; + align-items: center; + pointer-events: all; +} + +.objectPageSubSectionsIcon { + padding-left: 0.625rem; +} + +.objectPageSubSectionsIcon[interactive]:hover { + color: ${ThemingParameters.sapContent_IconColor}; +} + +.ui5-tab-strip-itemText[data-active]:hover { + color: ${ThemingParameters.sapButton_Lite_Hover_TextColor}; +} +`); +// TabContainer.registerStaticAreaTabStyles(overflowCss); + +export { ObjectPageAnchorTab }; diff --git a/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx b/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx deleted file mode 100644 index 91674ada57b..00000000000 --- a/packages/main/src/components/ObjectPage/ObjectPageHeader.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; -import { isIE } from '@ui5/webcomponents-react-base/lib/Device'; -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 { Label } from '@ui5/webcomponents-react/lib/Label'; -import { Title } from '@ui5/webcomponents-react/lib/Title'; -import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; -import { Toolbar } from '@ui5/webcomponents-react/lib/Toolbar'; -import { ToolbarDesign } from '@ui5/webcomponents-react/lib/ToolbarDesign'; -import { ToolbarSpacer } from '@ui5/webcomponents-react/lib/ToolbarSpacer'; -import { ToolbarStyle } from '@ui5/webcomponents-react/lib/ToolbarStyle'; -import React, { CSSProperties, forwardRef, ReactElement, ReactNode, RefObject, useMemo } from 'react'; -import { safeGetChildrenArray } from './ObjectPageUtils'; - -interface Props { - image: string | ReactElement; - imageShapeCircle: boolean; - classes: any; - showTitleInHeaderContent: boolean; - headerContentProp: ReactElement; - breadcrumbs: ReactNode; - keyInfos: ReactNode; - title: string; - subTitle: string; - headerPinned: boolean; - topHeaderHeight: number; - headerActions: ReactElement[]; -} - -export const ObjectPageHeader = forwardRef((props: Props, ref: RefObject) => { - const { - image, - classes, - imageShapeCircle, - showTitleInHeaderContent, - headerContentProp, - breadcrumbs, - title, - subTitle, - keyInfos, - headerPinned, - topHeaderHeight, - headerActions - } = props; - - const avatar = useMemo(() => { - if (!image) { - return null; - } - - if (typeof image === 'string') { - return ( - - Company Logo - - ); - } else { - 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 || isIE()) { - return { - top: `${topHeaderHeight}px`, - zIndex: 1 - }; - } - - return null; - }, [headerPinned, topHeaderHeight]); - - let renderedHeaderContent = ( - <> - {avatar} - {headerContentProp && {headerContentProp}} - - ); - - if (showTitleInHeaderContent) { - let firstElement; - let contents = []; - - if (headerContentProp?.type === React.Fragment) { - [firstElement, ...contents] = safeGetChildrenArray(headerContentProp.props.children); - } else { - firstElement = headerContentProp; - } - renderedHeaderContent = ( - <> - - {avatar} - -
{breadcrumbs}
- - - - {title} - - - {firstElement} - - - {contents.map((c, index) => ( -
- {c} -
- ))} -
-
{keyInfos}
-
-
- - - {headerActions} - -
- - ); - } - - const headerClasses = StyleClassHelper.of(classes.contentHeader); - if (isIE()) { - headerClasses.put(classes.iEClass); - headerClasses.put(classes.iEClassHeader); - } - - return ( -
- {renderedHeaderContent} -
- ); -}); - -ObjectPageHeader.displayName = 'ObjectPageHeader'; diff --git a/packages/main/src/components/ObjectPage/ObjectPageUtils.ts b/packages/main/src/components/ObjectPage/ObjectPageUtils.ts index 3b517a5772d..421b99fffdb 100644 --- a/packages/main/src/components/ObjectPage/ObjectPageUtils.ts +++ b/packages/main/src/components/ObjectPage/ObjectPageUtils.ts @@ -11,11 +11,17 @@ export const extractSectionIdFromHtmlId = (id: string) => { return id.replace(/^ObjectPageSection-/, ''); }; -export const getLastObjectPageSection = (ref: RefObject): HTMLElement => { +export const getLastObjectPageSection = (ref: RefObject, alwaysSetMargin): HTMLElement => { const sections = ref.current?.querySelectorAll('[id^="ObjectPageSection"]'); + if (!sections || sections.length < 1) { return null; } + if (!alwaysSetMargin && sections.length === 1) { + if (sections[0].querySelectorAll('[id^="ObjectPageSubSection"]').length === 0) { + return null; + } + } return sections[sections.length - 1]; }; 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 e70a22cac54..9cb99931a96 100644 --- a/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap +++ b/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap @@ -1,311 +1,155 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ObjectPage IconTabBar Mode 1`] = ` +exports[`ObjectPage Default with Sections 1`] = `
+ style="grid-auto-columns: min-content 100%; display: grid;" + />
- -
- - www.myurl.com - - - Address 1 - - - Address 2 - - - Address 3 - -
-
-
- -
+
-
- Test 1 +
+ Title 1 +
-
-
- - My Content 1 - + Content 1 +
-
-
-
-
-`; - -exports[`ObjectPage Just Some Sections 1`] = ` - -
- -
- -
-
+
-
-
+ aria-level="3" + class="ObjectPageSection-header" + data-component-name="ObjectPageSectionHeading" + role="heading" + > +
+ Title 3 +
+
- Test +
+ + Content 3 + +
-
-
+ +
`; -exports[`ObjectPage Key Infos 1`] = ` +exports[`ObjectPage Default with SubSections 1`] = `
-
- -
+
-
-
-
+ aria-level="3" + class="ObjectPageSection-header" + data-component-name="ObjectPageSectionHeading" + role="heading" + > +
+ Title Section 1 +
+
- Test +
+ + Content Section 1 + +
-
-
-
-
+
-
-
+ aria-level="3" + class="ObjectPageSection-header" + data-component-name="ObjectPageSectionHeading" + role="heading" + > +
+ Title Section 2 +
+
- Test 2 +
+
+ Content Section 2 +
+
-
-
-
- -`; - -exports[`ObjectPage No Header 1`] = ` - -
-
+
+`; + +exports[`ObjectPage IconTabBar Mode 1`] = ` + +
+
+
+`; + +exports[`ObjectPage Not crashing with 0 sections 1`] = ` + +
+ -
- -
-
+
-
-
-
- Test +
+ Title Section 4 +
-
-
-
- -`; - -exports[`ObjectPage Not crashing with 1 section - IconTabBar Mode 1`] = ` - -
- +
+
- - + +
`; -exports[`ObjectPage Only Sections 1`] = ` +exports[`ObjectPage with img 2`] = `
- +
- - www.myurl.com - - - Address 1 - - - Address 2 - - - Address 3 - + HeaderContent
-
+
-
+
- - - + - -
+
-
- Test 1 +
+ Title Section 1 +
-
-
- - My Content 1 - + + Content Section 1 + +
-
-
-
-
+
- Test 2 +
+ Title Section 2 +
-
-
- - My Content 2 - +
+ Content Section 2 +
+
-
- -
-
+
- Test 3 +
+ Title Section 3 +
-
-
+
+
+
+ Title SubSection 3.1 +
+
+ Content Section 3.1 +
+
+
+
+ Title SubSection 3.2 +
+
+ Content Section 3.2 +
+
+
+
+ Title SubSection 3.3 +
+
+ Content Section 3.3 +
+
+
+
+
+
- - My Content 3 - + Title Section 4 +
-
- +
+
+
+
+ Title SubSection 4.1 +
+
+ Content Section 4.1 +
+
+
+
+ Title SubSection 4.2 +
+
+ Content Section 4.2 +
+
+
+
+ Title SubSection 4.3 +
+
+ Content Section 4.3 +
+
+
+
+ +
+
+
+ +
+ Footer +
+
+
`; -exports[`ObjectPage Set selected section id 1`] = ` +exports[`ObjectPage with title 1`] = `
- -
+
-
-
-
+
+ Title Section 1 +
+
+
+
+ + Content Section 1 + +
+
+
+
- Test 2 +
+ Title Section 2 +
-
- +
+
+
+ Content Section 2 +
+
+
+ +
+
+
+ Title Section 3 +
+
+
+
+
+
+ Title SubSection 3.1 +
+
+ Content Section 3.1 +
+
+
+
+ Title SubSection 3.2 +
+
+ Content Section 3.2 +
+
+
+
+ Title SubSection 3.3 +
+
+ Content Section 3.3 +
+
+
+
+
+
+
+
+ Title Section 4 +
+
+
+
+
+
+ Title SubSection 4.1 +
+
+ Content Section 4.1 +
+
+
+
+ Title SubSection 4.2 +
+
+ Content Section 4.2 +
+
+
+
+ Title SubSection 4.3 +
+
+ Content Section 4.3 +
+
+
+
+
+
`; -exports[`ObjectPage With Subsections 1`] = ` +exports[`ObjectPage with title, header & footer 1`] = `
-
- - www.myurl.com - - - Address 1 - - - Address 2 - - - Address 3 - + HeaderContent
-
+
- + +
+
+ +
+ Footer +
+
+
`; diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index aaab71d7f47..38a32992d12 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -1,20 +1,19 @@ -import { isIE } from '@ui5/webcomponents-react-base/lib/Device'; +import { addCustomCSS } from '@ui5/webcomponents-base/dist/Theming'; +import { useIsRTL } from '@ui5/webcomponents-react-base/lib/hooks'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; +import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; import { useConsolidatedRef } from '@ui5/webcomponents-react-base/lib/useConsolidatedRef'; import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps'; -import { enrichEventWithDetails, getScrollBarWidth } from '@ui5/webcomponents-react-base/lib/Utils'; -import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; -import { FlexBoxAlignItems } from '@ui5/webcomponents-react/lib/FlexBoxAlignItems'; -import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection'; +import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; +import { AvatarPropTypes } from '@ui5/webcomponents-react/lib/Avatar'; +import { AvatarSize } from '@ui5/webcomponents-react/lib/AvatarSize'; import { GlobalStyleClasses } from '@ui5/webcomponents-react/lib/GlobalStyleClasses'; -import { Label } from '@ui5/webcomponents-react/lib/Label'; +import { List } from '@ui5/webcomponents-react/lib/List'; import { ObjectPageMode } from '@ui5/webcomponents-react/lib/ObjectPageMode'; -import { Title } from '@ui5/webcomponents-react/lib/Title'; -import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; -import { Toolbar } from '@ui5/webcomponents-react/lib/Toolbar'; -import { ToolbarDesign } from '@ui5/webcomponents-react/lib/ToolbarDesign'; -import { ToolbarSpacer } from '@ui5/webcomponents-react/lib/ToolbarSpacer'; -import { ToolbarStyle } from '@ui5/webcomponents-react/lib/ToolbarStyle'; +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 { TabContainer } from '@ui5/webcomponents-react/lib/TabContainer'; import { CommonProps } from '@ui5/webcomponents-react/interfaces/CommonProps'; import debounce from 'lodash/debounce'; import React, { @@ -22,57 +21,69 @@ import React, { FC, forwardRef, ReactElement, - ReactNode, RefObject, useCallback, useEffect, + useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { createUseStyles } from 'react-jss'; +import { Ui5PopoverDomRef } from '../../interfaces/Ui5PopoverDomRef'; +import { stopPropagation } from '../../internal/stopPropagation'; +import { useObserveHeights } from '../../internal/useObserveHeights'; +import { useResponsiveContentPadding } from '../../internal/useResponsiveContentPadding'; +import { DynamicPageAnchorBar } from '../DynamicPageAnchorBar'; import { ObjectPageSectionPropTypes } from '../ObjectPageSection'; import { ObjectPageSubSectionPropTypes } from '../ObjectPageSubSection'; import { CollapsedAvatar } from './CollapsedAvatar'; import { ObjectPageCssVariables, styles } from './ObjectPage.jss'; -import { ObjectPageAnchorBar } from './ObjectPageAnchorBar'; -import { ObjectPageHeader } from './ObjectPageHeader'; +import { ObjectPageAnchorButton } from './ObjectPageAnchorButton'; import { extractSectionIdFromHtmlId, getLastObjectPageSection, getSectionById, safeGetChildrenArray } from './ObjectPageUtils'; -import { useObserveHeights } from './useObserveHeights'; +import { PopoverHorizontalAlign } from '../../enums/PopoverHorizontalAlign'; -declare const ResizeObserver; - -const SCROLL_BAR_WIDTH = 12; +addCustomCSS( + 'ui5-tabcontainer', + ` + :host([data-component-name="ObjectPageTabContainer"]) .ui5-tc__header { + box-shadow: inset 0 -0.0625rem ${ThemingParameters.sapPageHeader_BorderColor}, 0 0.125rem 0.25rem 0 rgb(0 0 0 / 8%); + } + ` +); export interface ObjectPagePropTypes extends CommonProps { /** - * Defines the title of the `ObjectPage`. - */ - title?: string; - /** - * Defines the subheading of the `ObjectPage`. + * Defines the the upper, always static, title section of the `ObjectPage`. + * + * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `DynamicPageTitle` in order to preserve the intended design. + * __Note:__ If not defined otherwise the prop `showSubheadingRight` of the `DynamicPageTitle` is set to `true` by default. */ - subTitle?: string; + headerTitle?: ReactElement; /** - * Defines the image of the `ObjectPage`. You can pass a path to an image or an `Avatar` component. + * Defines the dynamic header section of the `ObjectPage`. + * + * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `DynamicPageHeader` in order to preserve the intended design. */ - image?: string | ReactElement; + headerContent?: ReactElement; /** - * Defines the actions in the header toolbar. + * React element which defines the footer content. + * + * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `Bar` with `design={BarDesign.FloatingFooter}` in order to preserve the intended design. */ - headerActions?: ReactElement[]; + footer?: ReactElement; /** - * The header content displays app-specific contextual information. You build the content using containers. - The containers are arranged inline with a left float. If the containers do not all fit on one line, those on the right wrap to the line below. - The header content is hidden by scrolling down the page or clicking the collapse indicator. + * Defines the image of the `ObjectPage`. You can pass a path to an image or an `Avatar` component. */ - headerContent?: ReactNode; + image?: string | ReactElement; /** - * Defines the content area of the `ObjectPage`. It consists of sections and subsections.
+ * Defines the content area of the `ObjectPage`. It consists of sections and subsections. + * * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `ObjectPageSection` and `ObjectPageSubSection` in order to preserve the intended design. */ children?: ReactElement | ReactElement[]; @@ -90,19 +101,10 @@ export interface ObjectPagePropTypes extends CommonProps { onSelectedSectionChanged?: ( event: CustomEvent<{ selectedSectionIndex: number; selectedSectionId: string; section: ComponentType }> ) => void; - /** - * Defines the breadcrumbs above the `ObjectPage` heading.
- * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `Breadcrumbs` in order to preserve the intended design. - */ - breadcrumbs?: ReactNode; - /** - * Defines the key information section in the header of the `ObjectPage`. - */ - keyInfos?: ReactNode; // appearance /** - * Defines whether the header is hidden by scrolling down. + * Defines whether the `headerContent` is hidden by scrolling down. */ alwaysShowContentHeader?: boolean; /** @@ -121,16 +123,12 @@ export interface ObjectPagePropTypes extends CommonProps { * - "IconTabBar": All `ObjectPageSections` are displayed on separate pages. Selecting tabs will lead to the corresponding page. */ mode?: ObjectPageMode; - /** - * Defines whether the header is displayed. - */ - noHeader?: boolean; /** * Defines whether the pin button of the header is displayed. */ showHideHeaderButton?: boolean; /** - * Defines whether the header content is pinnable. + * Defines whether the `headerContent` is pinnable. */ headerContentPinnable?: boolean; } @@ -143,10 +141,9 @@ const useStyles = createUseStyles(styles, { name: 'ObjectPage' }); */ const ObjectPage: FC = forwardRef((props: ObjectPagePropTypes, ref: RefObject) => { const { - title, + headerTitle, image, - subTitle, - headerActions, + footer, mode, imageShapeCircle, className, @@ -157,17 +154,15 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp children, onSelectedSectionChanged, selectedSectionId, - noHeader, alwaysShowContentHeader, showTitleInHeaderContent, - headerContentPinnable, headerContent, - breadcrumbs, - keyInfos + headerContentPinnable } = props; - const firstSectionId = safeGetChildrenArray(children)[0]?.props?.id; + const classes = useStyles(); + const firstSectionId = safeGetChildrenArray(children)[0]?.props?.id; const [internalSelectedSectionId, setInternalSelectedSectionId] = useState(selectedSectionId ?? firstSectionId); const [selectedSubSectionId, setSelectedSubSectionId] = useState(props.selectedSubSectionId); const [headerPinned, setHeaderPinned] = useState(alwaysShowContentHeader); @@ -175,11 +170,14 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp const objectPageRef: RefObject = useConsolidatedRef(ref); const topHeaderRef: RefObject = useRef(); - const headerContentRef: RefObject = useRef(); + //@ts-ignore + const headerContentRef: RefObject = useConsolidatedRef(headerContent?.ref); const anchorBarRef: RefObject = useRef(); + const scrollTimeout = useRef(null); + const [isAfterScroll, setIsAfterScroll] = useState(false); - const [scrollbarWidth, setScrollbarWidth] = useState(SCROLL_BAR_WIDTH); - const isMounted = useRef(false); + const isRTL = useIsRTL(objectPageRef); + const responsivePaddingClass = useResponsiveContentPadding(objectPageRef.current); // observe heights of header parts const { topHeaderHeight, headerContentHeight, anchorBarHeight, totalHeaderHeight } = useObserveHeights( @@ -187,13 +185,38 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp topHeaderRef, headerContentRef, anchorBarRef, - { noHeader } + { noHeader: !headerTitle && !headerContent } ); - // ***** - // SECTION SELECTION - // **** + const avatar = useMemo(() => { + if (!image) { + return null; + } + const headerImageClasses = StyleClassHelper.of(classes.headerImage); + if (isRTL) { + headerImageClasses.put(classes.headerImageRtl); + } + if (typeof image === 'string') { + return ( + + Company Logo + + ); + } else { + if (image.props?.className) { + headerImageClasses.put(image.props?.className); + } + return React.cloneElement(image, { + size: AvatarSize.L, + className: headerImageClasses.className + } as AvatarPropTypes); + } + }, [image, classes.headerImage, classes.headerImageRtl, classes.image, imageShapeCircle, isRTL]); + const prevTopHeaderHeight = useRef(0); const scrollToSection = useCallback( (sectionId) => { if (!sectionId) { @@ -202,18 +225,31 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp if (firstSectionId === sectionId) { objectPageRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); } else { - const childOffset = objectPageRef.current?.querySelector(`#ObjectPageSection-${sectionId}`) - ?.offsetTop; + const childOffset = objectPageRef.current?.querySelector( + `#ObjectPageSection-${sectionId}` + )?.offsetTop; if (!isNaN(childOffset)) { + let safeTopHeaderHeight = topHeaderHeight || prevTopHeaderHeight.current; + if (topHeaderHeight) { + prevTopHeaderHeight.current = topHeaderHeight; + } objectPageRef.current?.scrollTo({ - top: childOffset - topHeaderHeight - anchorBarHeight - (headerPinned ? headerContentHeight : 0) + 45, + top: childOffset - safeTopHeaderHeight - anchorBarHeight - (headerPinned ? headerContentHeight : 0), behavior: 'smooth' }); } } isProgrammaticallyScrolled.current = false; }, - [firstSectionId, objectPageRef, topHeaderHeight, anchorBarHeight, headerPinned, headerContentHeight] + [ + firstSectionId, + objectPageRef, + topHeaderHeight, + anchorBarHeight, + headerPinned, + headerContentHeight, + prevTopHeaderHeight.current + ] ); // change selected section when prop is changed (external change) @@ -240,26 +276,31 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp // do internal scrolling useEffect(() => { - if (!isMounted.current) return; - if (mode === ObjectPageMode.Default && isProgrammaticallyScrolled.current === true) { scrollToSection(internalSelectedSectionId); } - }, [internalSelectedSectionId, isMounted, mode, isProgrammaticallyScrolled, scrollToSection]); + }, [internalSelectedSectionId, mode, isProgrammaticallyScrolled, scrollToSection]); // Scrolling for Sub Section Selection useEffect(() => { if (selectedSubSectionId && isProgrammaticallyScrolled.current === true) { - const childOffset = objectPageRef.current?.querySelector( + const currentSubSection = objectPageRef.current?.querySelector( `div[id="ObjectPageSubSection-${selectedSubSectionId}"]` - )?.offsetTop; + ); + const childOffset = currentSubSection?.offsetTop; if (!isNaN(childOffset)) { + currentSubSection.focus(); objectPageRef.current?.scrollTo({ - top: childOffset - topHeaderHeight - anchorBarHeight - (headerPinned ? headerContentHeight : 0) + 45, + top: + childOffset - + topHeaderHeight - + anchorBarHeight - + 48 /*tabBar*/ - + (headerPinned ? headerContentHeight : 0) - + 16, behavior: 'smooth' }); } - isProgrammaticallyScrolled.current = false; } }, [ @@ -275,8 +316,6 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp setHeaderPinned(alwaysShowContentHeader); }, [setHeaderPinned, alwaysShowContentHeader]); - const classes = useStyles(); - useEffect(() => { setSelectedSubSectionId(props.selectedSubSectionId); if (props.selectedSubSectionId) { @@ -298,7 +337,6 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp ); } }); - if (sectionId) { setInternalSelectedSectionId(sectionId); } @@ -316,14 +354,12 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp useEffect(() => { const fillerDivObserver = new ResizeObserver(() => { let heightDiff = 0; - const maxHeight = Math.min(objectPageRef.current?.clientHeight, window.innerHeight); const availableScrollHeight = maxHeight - totalHeaderHeight; - const lastSectionDomRef = getLastObjectPageSection(objectPageRef); + const lastSectionDomRef = getLastObjectPageSection(objectPageRef, !!footer && mode === ObjectPageMode.IconTabBar); if (lastSectionDomRef) { const subSections = lastSectionDomRef.querySelectorAll('[id^="ObjectPageSubSection"]'); - let lastSubSectionHeight; if (subSections.length > 0) { lastSubSectionHeight = (subSections[subSections.length - 1] as HTMLElement).offsetHeight; @@ -339,8 +375,8 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp heightDiff = 0; } } - - objectPageRef.current?.style.setProperty(ObjectPageCssVariables.lastSectionMargin, `${heightDiff}px`); + const lastSectionMargin = footer ? `calc(${heightDiff}px + 1rem)` : `${heightDiff}px`; + objectPageRef.current?.style.setProperty(ObjectPageCssVariables.lastSectionMargin, lastSectionMargin); }); fillerDivObserver.observe(objectPageRef.current); @@ -348,16 +384,18 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp return () => { fillerDivObserver.disconnect(); }; - }, [totalHeaderHeight, objectPageRef, children]); + }, [totalHeaderHeight, objectPageRef, children, mode, footer]); const fireOnSelectedChangedEvent = debounce((e) => { - onSelectedSectionChanged( - enrichEventWithDetails(e, { - selectedSectionIndex: e.detail.index, - selectedSectionId: e.detail.props.id, - section: e.detail - }) - ); + if (typeof onSelectedSectionChanged === 'function') { + onSelectedSectionChanged( + enrichEventWithDetails(e, { + selectedSectionIndex: e.detail.index, + selectedSectionId: e.detail.props.id, + section: e.detail + }) + ); + } }, 500); const handleOnSubSectionSelected = useCallback( @@ -372,85 +410,45 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp }, [mode, setInternalSelectedSectionId, setSelectedSubSectionId, isProgrammaticallyScrolled] ); - + const [scrolledHeaderExpanded, setScrolledHeaderExpanded] = useState(false); const onToggleHeaderContentVisibility = useCallback( (e) => { - const srcElement = e.target; - const shouldHideHeader = srcElement.icon === 'slim-arrow-up'; - if (shouldHideHeader) { + if (!e.detail.visible) { objectPageRef.current?.classList.add(classes.headerCollapsed); } else { + setScrolledHeaderExpanded(true); objectPageRef.current?.classList.remove(classes.headerCollapsed); } - - requestAnimationFrame(() => { - if (objectPageRef.current?.scrollTop > 0 && !shouldHideHeader) { - const prevHeaderTop = headerContentRef.current.style.top; - headerContentRef.current.style.top = `${topHeaderHeight}px`; - const prevAnchorTop = anchorBarRef.current.style.top; - anchorBarRef.current.style.top = `${headerContentRef.current.offsetHeight + topHeaderHeight}px`; - objectPageRef.current?.addEventListener( - 'scroll', - (e) => { - if (prevHeaderTop ?? true) { - headerContentRef.current.style.top = prevHeaderTop; - } else { - headerContentRef.current.style.removeProperty('top'); - } - if (prevAnchorTop ?? true) { - anchorBarRef.current.style.top = prevAnchorTop; - } else { - anchorBarRef.current.style.removeProperty('top'); - } - }, - { once: true } - ); - } - }); }, - [objectPageRef, classes.headerCollapsed, headerContentHeight, topHeaderHeight] + [objectPageRef.current, classes.headerCollapsed] ); - useEffect(() => { - requestAnimationFrame(() => { - const calculatedScrollBarWidth = getScrollBarWidth(); - if (calculatedScrollBarWidth && calculatedScrollBarWidth !== 0 && calculatedScrollBarWidth !== SCROLL_BAR_WIDTH) { - setScrollbarWidth(calculatedScrollBarWidth); - } - }); - isMounted.current = true; - }, [isMounted, setScrollbarWidth]); - const objectPageClasses = StyleClassHelper.of(classes.objectPage, GlobalStyleClasses.sapScrollBar); if (className) { objectPageClasses.put(className); } - if (showTitleInHeaderContent) { - objectPageClasses.put(classes.titleInHeaderContent); - } - if (mode === ObjectPageMode.IconTabBar) { objectPageClasses.put(classes.iconTabBarMode); } - if (noHeader) { - objectPageClasses.put(classes.noHeader); - } - - const passThroughProps = usePassThroughHtmlProps(props, ['onSelectedSectionChanged']); + const passThroughProps = usePassThroughHtmlProps(props, ['onSelectedSectionChanged', 'onScroll']); useEffect(() => { + const sections = objectPageRef.current?.querySelectorAll('section[data-component-name="ObjectPageSection"]'); const objectPageHeight = objectPageRef.current?.clientHeight ?? 1000; - const marginBottom = objectPageHeight - totalHeaderHeight; + const marginBottom = objectPageHeight - totalHeaderHeight - /*TabContainer*/ 48; const rootMargin = `-${totalHeaderHeight}px 0px -${marginBottom < 0 ? 0 : marginBottom}px 0px`; const observer = new IntersectionObserver( - (elements) => { - elements.forEach((section) => { - if (section.isIntersecting && isProgrammaticallyScrolled.current === false) { + ([section]) => { + if (section.isIntersecting && isProgrammaticallyScrolled.current === false) { + if ( + objectPageRef.current.getBoundingClientRect().top + totalHeaderHeight + 48 <= + section.target.getBoundingClientRect().bottom + ) { setInternalSelectedSectionId(extractSectionIdFromHtmlId(section.target.id)); } - }); + } }, { root: objectPageRef.current, @@ -458,28 +456,188 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp threshold: [0] } ); + // Fallback when scrolling faster than the IntersectionObserver can observe (in most cases faster than 60fps) + if (isAfterScroll) { + let currentSection = sections[sections.length - 1]; + for (let i = 0; i <= sections.length - 1; i++) { + const section = sections[i]; + if ( + objectPageRef.current.getBoundingClientRect().top + totalHeaderHeight + 48 <= + section.getBoundingClientRect().bottom + ) { + currentSection = section; + break; + } + } + if (extractSectionIdFromHtmlId(currentSection.id) !== internalSelectedSectionId) { + setInternalSelectedSectionId(extractSectionIdFromHtmlId(currentSection.id)); + } + setIsAfterScroll(false); + } - objectPageRef.current?.querySelectorAll('section[data-component-name="ObjectPageSection"]').forEach((el) => { + sections.forEach((el) => { observer.observe(el); }); return () => { observer.disconnect(); }; - }, [objectPageRef, children, totalHeaderHeight, setInternalSelectedSectionId, isProgrammaticallyScrolled]); + }, [ + objectPageRef.current, + children, + totalHeaderHeight, + setInternalSelectedSectionId, + isProgrammaticallyScrolled, + isAfterScroll + ]); - const headerClasses = StyleClassHelper.of(classes.header); - const anchorBarClasses = StyleClassHelper.of(classes.anchorBar); - if (isIE()) { - headerClasses.put(classes.iEClass); - anchorBarClasses.put(classes.iEClass); - } + const renderTitleSection = useCallback( + (inHeader = false) => { + const titleStyles = { ...(inHeader ? { padding: 0 } : {}), ...(headerTitle?.props?.style ?? {}) }; + if (headerTitle?.props && headerTitle.props?.showSubheadingRight === undefined) { + return React.cloneElement(headerTitle, { + showSubheadingRight: true, + style: titleStyles, + 'data-not-clickable': + alwaysShowContentHeader || !headerContent || (!showHideHeaderButton && !headerContentPinnable) + }); + } + return React.cloneElement(headerTitle, { + style: titleStyles, + 'data-not-clickable': + alwaysShowContentHeader || !headerContent || (!showHideHeaderButton && !headerContentPinnable) + }); + }, + [headerTitle, showHideHeaderButton, headerContentPinnable, headerContent] + ); + + const renderHeaderContentSection = useCallback(() => { + if (headerContent?.props) { + return React.cloneElement(headerContent, { + ...headerContent.props, + topHeaderHeight, + headerPinned: headerPinned || scrolledHeaderExpanded, + ref: headerContentRef, + children: ( +
+ {avatar} + {headerContent.props.children && ( +
+ {headerTitle && showTitleInHeaderContent && renderTitleSection(true)} + {headerContent.props.children} +
+ )} +
+ ) + }); + } + }, [ + headerContent, + topHeaderHeight, + headerPinned, + scrolledHeaderExpanded, + showTitleInHeaderContent, + avatar, + headerContentRef, + renderTitleSection, + responsivePaddingClass + ]); + + const paddingLeftRtl = isRTL ? 'paddingLeft' : 'paddingRight'; + + const onTabItemSelect = useCallback( + (event) => { + const { sectionId, index } = event.detail.tab.dataset; + const section = safeGetChildrenArray(children).find((el) => el.props.id == sectionId); + handleOnSectionSelected( + enrichEventWithDetails({} as any, { + ...section, + index + }) + ); + }, + [children] + ); + const [popoverContent, setPopoverContent] = useState(null); + const popoverRef = useRef(null); + const onShowSubSectionPopover = useCallback( + (e, section) => { + setPopoverContent(section); + popoverRef.current.openBy(e.detail.targetRef); + }, + [setPopoverContent, popoverRef] + ); + + const onSubSectionClick = useCallback( + (e) => { + const selectedId = e.detail.item.dataset.key; + const subSection = popoverContent.props.children + .filter((item) => item.props && item.props.isSubSection) + .find((item) => item.props.id === selectedId); + if (subSection) { + handleOnSubSectionSelected(enrichEventWithDetails(e, { section: popoverContent, subSection })); + } + popoverRef.current.close(); + }, + [handleOnSubSectionSelected, popoverRef, popoverContent] + ); + const prevScrollTop = useRef(); + const onObjectPageScroll = useCallback( + (e) => { + if (typeof props.onScroll === 'function') { + props.onScroll(e); + } + if (selectedSubSectionId) { + setSelectedSubSectionId(undefined); + } + if (scrollTimeout.current) { + clearTimeout(scrollTimeout.current); + } + scrollTimeout.current = setTimeout(() => { + setIsAfterScroll(true); + }, 100); + if (!headerPinned || e.target.scrollTop === 0) { + objectPageRef.current?.classList.remove(classes.headerCollapsed); + } + if (scrolledHeaderExpanded && e.target.scrollTop !== prevScrollTop.current) { + if (e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight) { + return; + } + prevScrollTop.current = e.target.scrollTop; + setScrolledHeaderExpanded(false); + } + }, + [ + topHeaderHeight, + headerPinned, + props.onScroll, + objectPageRef.current, + scrolledHeaderExpanded, + prevScrollTop.current, + selectedSubSectionId, + scrollTimeout.current + ] + ); - const anchorBarPositionTop = noHeader - ? 0 - : headerPinned || isIE() - ? topHeaderHeight + headerContentHeight - : topHeaderHeight; + const onHoverToggleButton = useCallback( + (e) => { + if (e?.type === 'mouseover') { + topHeaderRef.current?.classList.add(classes.headerHoverStyles); + } else { + topHeaderRef.current?.classList.remove(classes.headerHoverStyles); + } + }, + [classes.headerHoverStyles] + ); + const onTitleClick = useCallback( + (e) => { + onToggleHeaderContentVisibility(enrichEventWithDetails(e, { visible: !headerContentHeight })); + }, + [onToggleHeaderContentVisibility, headerContentHeight] + ); return (
= forwardRef((props: ObjectPagePropTyp style={style} ref={objectPageRef} title={tooltip} + onScroll={onObjectPageScroll} {...passThroughProps} >
-
- {(!showTitleInHeaderContent || headerContentHeight === 0) && ( - - {image && headerContentHeight === 0 && ( -
- -
- )} - - {breadcrumbs} - - - {title} - - -
{keyInfos}
-
-
- - - {headerActions} - -
- )} -
+ {headerTitle && image && headerContentHeight === 0 && ( + + )} + {headerTitle && renderTitleSection()}
- - - {isIE() && ( + {renderHeaderContentSection()} + {headerContent && headerTitle && (
- )} - {isIE() ? ( -
- {mode === ObjectPageMode.IconTabBar ? getSectionById(children, internalSelectedSectionId) : children} + > +
- ) : mode === ObjectPageMode.IconTabBar ? ( - getSectionById(children, internalSelectedSectionId) - ) : ( - children + )} +
+ + {safeGetChildrenArray(children).map((section: ReactElement, index) => { + if (!section.props) return null; + return ( + + ); + })} + + {createPortal( + + + {popoverContent?.props?.children + .filter((item) => item.props && item.props.isSubSection) + .map((item) => ( + + {item.props.heading} + + ))} + + , + document.body + )} +
+
+ {mode === ObjectPageMode.IconTabBar ? getSectionById(children, internalSelectedSectionId) : children} +
+ {footer &&
} + {footer && ( +
+ {footer} +
)}
); @@ -580,17 +765,10 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp ObjectPage.displayName = 'ObjectPage'; ObjectPage.defaultProps = { - title: '', image: null, - subTitle: '', - headerActions: [], mode: ObjectPageMode.Default, imageShapeCircle: false, - showHideHeaderButton: false, - onSelectedSectionChanged: () => { - /* noop */ - }, - noHeader: false + showHideHeaderButton: false }; export { ObjectPage }; diff --git a/packages/main/src/components/ObjectPageSection/ObjectPageSection.jss.ts b/packages/main/src/components/ObjectPageSection/ObjectPageSection.jss.ts index 9ea6303508c..009c6a1a009 100644 --- a/packages/main/src/components/ObjectPageSection/ObjectPageSection.jss.ts +++ b/packages/main/src/components/ObjectPageSection/ObjectPageSection.jss.ts @@ -1,9 +1,7 @@ -import { sapUiResponsiveContentPadding } from '@ui5/webcomponents-react-base/lib/spacing'; import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; const styles = { header: { - ...sapUiResponsiveContentPadding, borderBottom: `1px solid ${ThemingParameters.sapGroup_TitleBorderColor}`, boxSizing: 'border-box', height: '2.75rem' @@ -27,7 +25,6 @@ const styles = { whiteSpace: 'normal' }, sectionContentInner: { - ...sapUiResponsiveContentPadding, paddingTop: '1rem', paddingBottom: '2rem', fontFamily: ThemingParameters.sapFontFamily diff --git a/packages/main/src/components/ObjectPageSection/ObjectPageSection.test.tsx b/packages/main/src/components/ObjectPageSection/ObjectPageSection.test.tsx index ac35fb6bf6e..a1b4a9a3414 100644 --- a/packages/main/src/components/ObjectPageSection/ObjectPageSection.test.tsx +++ b/packages/main/src/components/ObjectPageSection/ObjectPageSection.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; describe('ObjectPageSection', () => { test('Renders with children', () => { const { asFragment } = render( - + This is my Text ); @@ -15,7 +15,7 @@ describe('ObjectPageSection', () => { test('ObjectPage w/ lowercase title', () => { const { asFragment } = render( - + This is my Text ); diff --git a/packages/main/src/components/ObjectPageSection/__snapshots__/ObjectPageSection.test.tsx.snap b/packages/main/src/components/ObjectPageSection/__snapshots__/ObjectPageSection.test.tsx.snap index d1ba7c844da..37693580e0d 100644 --- a/packages/main/src/components/ObjectPageSection/__snapshots__/ObjectPageSection.test.tsx.snap +++ b/packages/main/src/components/ObjectPageSection/__snapshots__/ObjectPageSection.test.tsx.snap @@ -10,6 +10,7 @@ exports[`ObjectPageSection ObjectPage w/ lowercase title 1`] = `
This is my Text
@@ -41,6 +43,7 @@ exports[`ObjectPageSection Renders with children 1`] = `
This is my Text
diff --git a/packages/main/src/components/ObjectPageSection/index.tsx b/packages/main/src/components/ObjectPageSection/index.tsx index 26b7860cc9d..64abf8284c2 100644 --- a/packages/main/src/components/ObjectPageSection/index.tsx +++ b/packages/main/src/components/ObjectPageSection/index.tsx @@ -9,18 +9,18 @@ import styles from './ObjectPageSection.jss'; export interface ObjectPageSectionPropTypes extends CommonProps { /** - * Defines the title of the `ObjectPageSection`. + * Defines the heading of the `ObjectPageSection`. */ - title?: string; + heading?: string; /** * Defines the ID of the `ObjectPageSection`.
* __Note:__ The `id` is taken into account when the section selection changes. */ id: string; /** - * Defines whether the title is always displayed in uppercase. + * Defines whether the heading is always displayed in uppercase. */ - titleUppercase?: boolean; + headingUppercase?: boolean; /** * Defines the content of the `ObjectPageSection`. */ @@ -33,7 +33,7 @@ const useStyles = createUseStyles(styles, { name: 'ObjectPageSection' }); */ const ObjectPageSection: FC = forwardRef( (props: ObjectPageSectionPropTypes, ref: RefObject) => { - const { title, id, children, titleUppercase, className, style, tooltip } = props; + const { heading, id, children, headingUppercase, className, style, tooltip } = props; const classes = useStyles(); if (!id) { @@ -44,12 +44,11 @@ const ObjectPageSection: FC = forwardRef( const htmlId = `ObjectPageSection-${id}`; const titleClasses = StyleClassHelper.of(classes.title); - if (titleUppercase) { + if (headingUppercase) { titleClasses.put(classes.uppercase); } const passThroughProps = usePassThroughHtmlProps(props, ['id']); - return (
= forwardRef( id={htmlId} data-component-name="ObjectPageSection" > -
-
{title}
+
+
{heading}
{/* TODO Check for subsections as they should win over the children */}
-
{children}
+
+ {children} +
); @@ -74,10 +75,8 @@ const ObjectPageSection: FC = forwardRef( ); ObjectPageSection.defaultProps = { - title: '', - // @ts-ignore - isSection: true, - titleUppercase: true + heading: '', + headingUppercase: true }; ObjectPageSection.displayName = 'ObjectPageSection'; diff --git a/packages/main/src/components/ObjectPageSubSection/ObjectPageSubSection.test.tsx b/packages/main/src/components/ObjectPageSubSection/ObjectPageSubSection.test.tsx index 0fc22122898..e795bf2353e 100644 --- a/packages/main/src/components/ObjectPageSubSection/ObjectPageSubSection.test.tsx +++ b/packages/main/src/components/ObjectPageSubSection/ObjectPageSubSection.test.tsx @@ -5,12 +5,12 @@ import React from 'react'; describe('ObjectPageSubSection', () => { test('Render without Crashing', () => { - const { asFragment } = render(); + const { asFragment } = render(Content); expect(asFragment()).toMatchSnapshot(); }); test('No ID should throw', () => { - const renderer = () => render(); + const renderer = () => render(Content); expect(renderer).toThrow(); }); diff --git a/packages/main/src/components/ObjectPageSubSection/__snapshots__/ObjectPageSubSection.test.tsx.snap b/packages/main/src/components/ObjectPageSubSection/__snapshots__/ObjectPageSubSection.test.tsx.snap index 93bc53cd753..7b090123796 100644 --- a/packages/main/src/components/ObjectPageSubSection/__snapshots__/ObjectPageSubSection.test.tsx.snap +++ b/packages/main/src/components/ObjectPageSubSection/__snapshots__/ObjectPageSubSection.test.tsx.snap @@ -4,18 +4,22 @@ exports[`ObjectPageSubSection Render without Crashing 1`] = `
+ data-component-name="ObjectPageSubSectionContent" + > + Content +
`; diff --git a/packages/main/src/components/ObjectPageSubSection/index.tsx b/packages/main/src/components/ObjectPageSubSection/index.tsx index 0c9b098c315..13ac9a2188d 100644 --- a/packages/main/src/components/ObjectPageSubSection/index.tsx +++ b/packages/main/src/components/ObjectPageSubSection/index.tsx @@ -9,9 +9,9 @@ import { EmptyIdPropException } from '../ObjectPage/EmptyIdPropException'; export interface ObjectPageSubSectionPropTypes extends CommonProps { /** - * Defines the title of the `ObjectPageSubSection`. + * Defines the heading of the `ObjectPageSubSection`. */ - title?: string; + heading?: string; /** * Defines the ID of the `ObjectPageSubSection`. */ @@ -24,7 +24,6 @@ export interface ObjectPageSubSectionPropTypes extends CommonProps { const styles = { objectPageSubSection: { - padding: '1rem 0', '&:focus': { outline: `1px dotted ${ThemingParameters.sapContent_FocusColor}`, outlineOffset: '-1px' @@ -47,7 +46,7 @@ const useStyles = createUseStyles(styles, { name: 'ObjectPageSubSection' }); */ const ObjectPageSubSection: FC = forwardRef( (props: ObjectPageSubSectionPropTypes, ref: RefObject) => { - const { children, id, title, className, style, tooltip } = props; + const { children, id, heading, className, style, tooltip } = props; if (!id) { throw new EmptyIdPropException('ObjectPageSubSection requires a unique ID property!'); @@ -67,25 +66,31 @@ const ObjectPageSubSection: FC = forwardRef( return (
-
- {title} +
+ {heading} +
+
+ {children}
-
{children}
); } ); ObjectPageSubSection.defaultProps = { - title: null, // @ts-ignore isSubSection: true }; diff --git a/packages/main/src/components/ObjectPage/useObserveHeights.ts b/packages/main/src/internal/useObserveHeights.ts similarity index 67% rename from packages/main/src/components/ObjectPage/useObserveHeights.ts rename to packages/main/src/internal/useObserveHeights.ts index 13a6684d049..7c7af7bac67 100644 --- a/packages/main/src/components/ObjectPage/useObserveHeights.ts +++ b/packages/main/src/internal/useObserveHeights.ts @@ -6,60 +6,62 @@ export const useObserveHeights = (objectPage, topHeader, headerContentRef, ancho const [topHeaderHeight, setTopHeaderHeight] = useState(0); const [headerContentHeight, setHeaderContentHeight] = useState(0); const [isIntersecting, setIsIntersecting] = useState(true); - useEffect(() => { const headerIntersectionObserver = new IntersectionObserver( ([header]) => { if (header.isIntersecting) { setIsIntersecting(true); - setHeaderContentHeight((header.target as HTMLElement).offsetHeight); } else { setIsIntersecting(false); setHeaderContentHeight(0); } }, - { rootMargin: `-${topHeaderHeight}px 0px 0px 0px`, root: objectPage.current, threshold: 0.3 } + { rootMargin: `-${topHeaderHeight}px 0px 0px 0px`, root: objectPage?.current, threshold: 0.3 } ); - if (headerContentRef.current) { + if (headerContentRef?.current) { headerIntersectionObserver.observe(headerContentRef.current); } return () => { headerIntersectionObserver.disconnect(); }; - }, [topHeaderHeight, setHeaderContentHeight, headerContentRef, setIsIntersecting]); + }, [topHeaderHeight, setHeaderContentHeight, headerContentRef.current, setIsIntersecting]); // top header useEffect(() => { const headerContentResizeObserver = new ResizeObserver(([header]) => { setTopHeaderHeight(header?.contentRect?.height ?? 0); }); - if (topHeader.current) { + if (topHeader?.current) { headerContentResizeObserver.observe(topHeader.current); } return () => { headerContentResizeObserver.disconnect(); }; - }, [topHeader.current, setTopHeaderHeight]); + }, [topHeader?.current, setTopHeaderHeight]); // header content useEffect(() => { const headerContentResizeObserver = new ResizeObserver(([headerContent]) => { if (isIntersecting) { - setHeaderContentHeight(headerContent?.contentRect?.height ?? 0); + // Firefox implements `borderBoxSize` as a single content rect, rather than an array + const borderBoxSize = Array.isArray(headerContent.borderBoxSize) + ? headerContent.borderBoxSize[0] + : headerContent.borderBoxSize; + // Safari doesn't implement `borderBoxSize` + setHeaderContentHeight(borderBoxSize?.blockSize ?? headerContent.target.getBoundingClientRect().height); } }); - if (headerContentRef.current) { + if (headerContentRef?.current) { headerContentResizeObserver.observe(headerContentRef.current); } return () => { headerContentResizeObserver.disconnect(); }; - }, [headerContentRef.current, setHeaderContentHeight, isIntersecting]); - - const anchorBarHeight = anchorBarRef.current?.offsetHeight ?? 33; + }, [headerContentRef?.current, setHeaderContentHeight, isIntersecting]); + const anchorBarHeight = anchorBarRef?.current?.offsetHeight ?? 33; const totalHeaderHeight = (noHeader ? 0 : topHeaderHeight + headerContentHeight) + anchorBarHeight; return { topHeaderHeight, headerContentHeight, anchorBarHeight, totalHeaderHeight }; diff --git a/packages/main/src/internal/useResponsiveContentPadding.ts b/packages/main/src/internal/useResponsiveContentPadding.ts new file mode 100644 index 00000000000..6e19b04233d --- /dev/null +++ b/packages/main/src/internal/useResponsiveContentPadding.ts @@ -0,0 +1,40 @@ +import { getCurrentRange } from '@ui5/webcomponents-react-base/lib/Device'; +import { useEffect, useRef, useState } from 'react'; +import { createUseStyles } from 'react-jss'; + +const useStyles = createUseStyles( + { + Phone: { paddingLeft: '1rem', paddingRight: '1rem' }, + Tablet: { paddingLeft: '2rem', paddingRight: '2rem' }, + Desktop: { paddingLeft: '2rem', paddingRight: '2rem' }, + LargeDesktop: { paddingLeft: '3rem', paddingRight: '3rem' } + }, + { name: 'StdExtPadding' } +); + +export const useResponsiveContentPadding = (element) => { + const [currentRange, setCurrentRange] = useState(getCurrentRange('StdExt', window.innerWidth).name); + let resizeTimeout = useRef(null); + const classes = useStyles(); + + useEffect(() => { + const observer = new ResizeObserver(([el]) => { + // Firefox implements `contentBoxSize` as a single content rect, rather than an array + const contentBoxSize = Array.isArray(el.contentBoxSize) ? el.contentBoxSize[0] : el.contentBoxSize; + if (resizeTimeout.current) { + clearTimeout(resizeTimeout.current); + } + resizeTimeout.current = setTimeout(() => { + setCurrentRange(() => getCurrentRange('StdExt', contentBoxSize.inlineSize).name); + }, 150); + }); + if (element) { + observer.observe(element); + } + return () => { + observer.disconnect(); + }; + }, [element]); + + return classes[currentRange]; +}; From af394aa2b92ef5b6d8c18b11dec48168d4c6d305 Mon Sep 17 00:00:00 2001 From: "Harbarth, Lukas" Date: Tue, 20 Jul 2021 13:47:57 +0200 Subject: [PATCH 02/14] fix DynamicPage story --- .../components/DynamicPage/DynamicPage.stories.mdx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx b/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx index ad266d183df..6ea57609074 100644 --- a/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx +++ b/packages/main/src/components/DynamicPage/DynamicPage.stories.mdx @@ -1,4 +1,4 @@ -import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { DocsHeader } from '@shared/stories/DocsHeader'; import '@ui5/webcomponents-icons/dist/action'; import '@ui5/webcomponents-icons/dist/full-screen'; @@ -28,7 +28,7 @@ import { Panel } from '@ui5/webcomponents-react/lib/Panel'; import { BusyIndicator } from '@ui5/webcomponents-react/lib/BusyIndicator'; import { BarDesign } from '@ui5/webcomponents-react/lib/BarDesign'; import { Bar } from '@ui5/webcomponents-react/lib/Bar'; -import { useState,useReducer } from 'react'; +import { useState, useReducer } from 'react'; { const [collapsed, setCollapsed] = useReducer((coll) => !coll, true); return ( - + {collapsed ? ( @@ -203,7 +203,7 @@ import { useState,useReducer } from 'react'; }; const [collapsed, setCollapsed] = useReducer((coll) => !coll, true); return ( - + {collapsed ? ( @@ -256,8 +256,9 @@ import { useState,useReducer } from 'react'; {(args) => { const [collapsed, setCollapsed] = useReducer((coll) => !coll, true); + console.log(collapsed); return ( - + {collapsed ? ( From 0ddeec7b48bcdcb59bc435fd663f6f8c1be9754f Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 15 Jul 2021 13:26:25 +0200 Subject: [PATCH 03/14] fix(ObjectPage & DynamicPage): active elements in `headerTitle` are always interactive and won't expand the header (#1825) --- docs/2-MigrationGuide.stories.mdx | 5 ++- .../__snapshots__/DynamicPage.test.tsx.snap | 20 +++++----- .../main/src/components/DynamicPage/index.tsx | 4 +- .../DynamicPageTitle/ActionsSpacer.tsx | 19 +++++++++ .../DynamicPageTitle/DynamicPageTitle.jss.ts | 10 ++++- .../src/components/DynamicPageTitle/index.tsx | 39 ++++++++++++++++--- .../components/ObjectPage/ObjectPage.jss.ts | 25 +++++------- .../__snapshots__/ObjectPage.test.tsx.snap | 20 +++++----- .../main/src/components/ObjectPage/index.tsx | 35 ++++++++++------- packages/main/src/internal/stopPropagation.ts | 2 +- 10 files changed, 119 insertions(+), 60 deletions(-) create mode 100644 packages/main/src/components/DynamicPageTitle/ActionsSpacer.tsx diff --git a/docs/2-MigrationGuide.stories.mdx b/docs/2-MigrationGuide.stories.mdx index 68f559e5be9..668c7892711 100644 --- a/docs/2-MigrationGuide.stories.mdx +++ b/docs/2-MigrationGuide.stories.mdx @@ -35,6 +35,7 @@ We streamlined those APIs by adding components used by the `DynamicPage` to the #### DynamicPage changes - `title` has been renamed to `headerTitle`. +- `header` has been renamed to `headerContent`. - **`DynamicPageTitle`:** `subHeading` has been renamed to `subheading`. - **`DynamicPageHeader`:** `children` are no longer displayed as `flex` items to support other display types like `grid`. To align children you now need to add the container (like `FlexBox`) and CSS yourself.
@@ -62,8 +63,8 @@ We streamlined those APIs by adding components used by the `DynamicPage` to the - **`ObjectPageSection`:** `title` and `titleUppercase` has been renamed. Please use `heading` and `headingUppercase` instead. - **`ObjectPageSubSection`:** `title` has been renamed to `heading`. - `title` has been renamed to `headerTitle` and is now defining the upper, static, title section of the `ObjectPage`. It expects to receive the `DynamicPageTitle` component. -- `headerContent` has been renamed to `header` and expects now the `DynamicPageHeader` component to be passed. -- `noHeader` has been removed. It is now sufficient not to set `headerTitle` and `header` to achieve the same behavior. +- `headerContent` now expects the `DynamicPageHeader` component to be passed. +- `noHeader` has been removed. It is now sufficient not to set `headerTitle` and `headerContent` to achieve the same behavior. - `title`, `subTitle`, `headerActions`, `breadcrumbs` and `keyInfos` should now be passed to the corresponding `DynamicPageTitle` props. Setting the title section of the `ObjectPage`: diff --git a/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap b/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap index 4449376eb14..c7769abc869 100644 --- a/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap +++ b/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap @@ -8,7 +8,7 @@ exports[`DynamicPage always show content header 1`] = `
= forwardRef((props: DynamicPageProps, r {headerTitle && cloneElement(headerTitle, { 'data-not-clickable': - alwaysShowContentHeader || !headerContent || (!showHideHeaderButton && !headerContentPinnable), + (alwaysShowContentHeader && !headerContentPinnable) || + !headerContent || + (!showHideHeaderButton && !headerContentPinnable), ref: topHeaderRef, className: headerTitle?.props?.className ? `${responsivePaddingClass} ${headerTitle.props.className}` diff --git a/packages/main/src/components/DynamicPageTitle/ActionsSpacer.tsx b/packages/main/src/components/DynamicPageTitle/ActionsSpacer.tsx new file mode 100644 index 00000000000..fb0f26d9f3b --- /dev/null +++ b/packages/main/src/components/DynamicPageTitle/ActionsSpacer.tsx @@ -0,0 +1,19 @@ +import React, { MouseEventHandler } from 'react'; + +interface ActionsSpacerProps { + onClick: MouseEventHandler; + noHover?: boolean; +} + +export const ActionsSpacer = ({ onClick, noHover }: ActionsSpacerProps) => { + return ( + + ); +}; + +// The Toolbar only recognizes spacers with the 'ToolbarSpacer' displayName +ActionsSpacer.displayName = 'ToolbarSpacer'; diff --git a/packages/main/src/components/DynamicPageTitle/DynamicPageTitle.jss.ts b/packages/main/src/components/DynamicPageTitle/DynamicPageTitle.jss.ts index f09b9f90153..8b8d5d43aa4 100644 --- a/packages/main/src/components/DynamicPageTitle/DynamicPageTitle.jss.ts +++ b/packages/main/src/components/DynamicPageTitle/DynamicPageTitle.jss.ts @@ -15,7 +15,6 @@ export const DynamicPageTitleStyles = { zIndex: 2, cursor: 'pointer', '&[data-not-clickable="true"]': { - pointerEvents: 'none', cursor: 'unset', '&:hover': { backgroundColor: ThemingParameters.sapObjectHeader_Background @@ -60,5 +59,14 @@ export const DynamicPageTitleStyles = { content: { display: 'flex', flexShrink: 1.6 + }, + toolbar: { + cursor: 'auto', + '&:hover': { + backgroundColor: 'inherit' + }, + '&>:first-child': { + height: '100%' + } } }; diff --git a/packages/main/src/components/DynamicPageTitle/index.tsx b/packages/main/src/components/DynamicPageTitle/index.tsx index 819dcfd226f..ad66188fc1f 100644 --- a/packages/main/src/components/DynamicPageTitle/index.tsx +++ b/packages/main/src/components/DynamicPageTitle/index.tsx @@ -20,10 +20,13 @@ import React, { ReactNode, ReactNodeArray, Ref, + useCallback, useEffect, useState } from 'react'; import { createUseStyles } from 'react-jss'; +import { stopPropagation } from '../../internal/stopPropagation'; +import { ActionsSpacer } from './ActionsSpacer'; import { DynamicPageTitleStyles } from './DynamicPageTitle.jss'; import { useIsRTL } from '@ui5/webcomponents-react-base/lib/hooks'; @@ -108,7 +111,19 @@ const DynamicPageTitle: FC = forwardRef((props: InternalP containerClasses.put(classes.iEClass); } containerClasses.putIfPresent(className); - const passThroughProps = usePassThroughHtmlProps(props, ['onToggleHeaderContentVisibility']); + const passThroughProps = usePassThroughHtmlProps(props, ['onToggleHeaderContentVisibility', 'onClick']); + + const onHeaderClick = useCallback( + (e) => { + if (typeof props?.onClick === 'function') { + props.onClick(e); + } + if (typeof onToggleHeaderContentVisibility === 'function' && !props?.['data-not-clickable']) { + onToggleHeaderContentVisibility(e); + } + }, + [props?.onClick, onToggleHeaderContentVisibility, props?.['data-not-clickable']] + ); useEffect(() => { const observer = new ResizeObserver( @@ -143,13 +158,19 @@ const DynamicPageTitle: FC = forwardRef((props: InternalP ref={dynamicPageTitleRef} tooltip={tooltip} data-component-name="DynamicPageTitle" - onClick={onToggleHeaderContentVisibility} + onClick={onHeaderClick} {...passThroughProps} > {(breadcrumbs || (navigationActions && showNavigationInTopArea)) && ( -
{breadcrumbs}
- {showNavigationInTopArea && {navigationActions}} +
+ {breadcrumbs} +
+ {showNavigationInTopArea && ( + + {navigationActions} + + )}
)} @@ -166,8 +187,14 @@ const DynamicPageTitle: FC = forwardRef((props: InternalP
)} - - + + {actions} {!showNavigationInTopArea && Children.count(actions) > 0 && Children.count(navigationActions) > 0 && ( diff --git a/packages/main/src/components/ObjectPage/ObjectPage.jss.ts b/packages/main/src/components/ObjectPage/ObjectPage.jss.ts index 1b0403eeeb8..ac73d1e745c 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.jss.ts +++ b/packages/main/src/components/ObjectPage/ObjectPage.jss.ts @@ -62,9 +62,15 @@ export const styles = { } }, headerHoverStyles: { - backgroundColor: `${ThemingParameters.sapTile_Active_Background} !important`, - '& [data-component-name="DynamicPageTitle"]': { - backgroundColor: ThemingParameters.sapTile_Active_Background + '&[data-not-clickable="true"]': { + cursor: 'unset' + }, + '&[data-not-clickable="false"]': { + // TODO background color should be sapObjectHeader_Hover_Background (same color as sapTile_Active_Background) + backgroundColor: `${ThemingParameters.sapTile_Active_Background}`, + '& [data-component-name="DynamicPageTitle"]': { + backgroundColor: ThemingParameters.sapTile_Active_Background + } } }, header: { @@ -79,18 +85,7 @@ export const styles = { paddingLeft: 0, paddingRight: 0 }, - '&:hover': { - // TODO background color should be sapObjectHeader_Hover_Background (same color as sapTile_Active_Background) - backgroundColor: ThemingParameters.sapTile_Active_Background - }, - cursor: 'pointer', - '&[data-not-clickable="true"]': { - pointerEvents: 'none', - cursor: 'unset', - '&:hover': { - backgroundColor: ThemingParameters.sapObjectHeader_Background - } - } + cursor: 'pointer' }, iEClass: { position: 'fixed', 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 9cb99931a96..18533771f18 100644 --- a/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap +++ b/packages/main/src/components/ObjectPage/__snapshots__/ObjectPage.test.tsx.snap @@ -823,14 +823,14 @@ exports[`ObjectPage with anchor-bar 1`] = `
@@ -1881,14 +1881,14 @@ exports[`ObjectPage with img 1`] = `
@@ -2287,14 +2287,14 @@ exports[`ObjectPage with img 2`] = `
@@ -2678,14 +2678,14 @@ exports[`ObjectPage with title 1`] = `
@@ -3019,14 +3019,14 @@ exports[`ObjectPage with title, header & footer 1`] = `
diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index 38a32992d12..d90086bab9e 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -491,24 +491,39 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp isAfterScroll ]); + const titleHeaderNotClickable = + (alwaysShowContentHeader && !headerContentPinnable) || + !headerContent || + (!showHideHeaderButton && !headerContentPinnable); + + const onTitleClick = useCallback( + (e) => { + if (!titleHeaderNotClickable) { + onToggleHeaderContentVisibility(enrichEventWithDetails(e, { visible: !headerContentHeight })); + } + }, + [onToggleHeaderContentVisibility, headerContentHeight, titleHeaderNotClickable] + ); + const renderTitleSection = useCallback( (inHeader = false) => { const titleStyles = { ...(inHeader ? { padding: 0 } : {}), ...(headerTitle?.props?.style ?? {}) }; + if (headerTitle?.props && headerTitle.props?.showSubheadingRight === undefined) { return React.cloneElement(headerTitle, { showSubheadingRight: true, style: titleStyles, - 'data-not-clickable': - alwaysShowContentHeader || !headerContent || (!showHideHeaderButton && !headerContentPinnable) + 'data-not-clickable': titleHeaderNotClickable, + onToggleHeaderContentVisibility: onTitleClick }); } return React.cloneElement(headerTitle, { style: titleStyles, - 'data-not-clickable': - alwaysShowContentHeader || !headerContent || (!showHideHeaderButton && !headerContentPinnable) + 'data-not-clickable': titleHeaderNotClickable, + onToggleHeaderContentVisibility: onTitleClick }); }, - [headerTitle, showHideHeaderButton, headerContentPinnable, headerContent] + [headerTitle, titleHeaderNotClickable, onTitleClick] ); const renderHeaderContentSection = useCallback(() => { @@ -632,12 +647,6 @@ const ObjectPage: FC = forwardRef((props: ObjectPagePropTyp }, [classes.headerHoverStyles] ); - const onTitleClick = useCallback( - (e) => { - onToggleHeaderContentVisibility(enrichEventWithDetails(e, { visible: !headerContentHeight })); - }, - [onToggleHeaderContentVisibility, headerContentHeight] - ); return (
= forwardRef((props: ObjectPagePropTyp data-component-name="ObjectPageTopHeader" ref={topHeaderRef} role="banner" - data-not-clickable={ - alwaysShowContentHeader || !headerContent || (!showHideHeaderButton && !headerContentPinnable) - } + data-not-clickable={titleHeaderNotClickable} aria-roledescription="Object Page header" className={`${classes.header} ${responsivePaddingClass}`} onClick={onTitleClick} diff --git a/packages/main/src/internal/stopPropagation.ts b/packages/main/src/internal/stopPropagation.ts index cd91367d226..5636cf828da 100644 --- a/packages/main/src/internal/stopPropagation.ts +++ b/packages/main/src/internal/stopPropagation.ts @@ -1,4 +1,4 @@ export const stopPropagation = (e) => { e.stopPropagation(); - e.stopImmediatePropagation(); + e.stopImmediatePropagation?.(); }; From 02fa5a169ef9773b653770b6963a0d14614b571e Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 15 Jul 2021 18:10:58 +0200 Subject: [PATCH 04/14] feat(ObjectPage & DynamicPage): add data attributes to elements (#1831) --- .../__snapshots__/DynamicPage.test.tsx.snap | 23 ++++++++++++++ .../components/DynamicPageAnchorBar/index.tsx | 1 + .../src/components/DynamicPageTitle/index.tsx | 31 +++++++++++++++---- .../components/ObjectPage/CollapsedAvatar.tsx | 7 ++++- .../__snapshots__/ObjectPage.test.tsx.snap | 16 ++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap b/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap index c7769abc869..1e34d84dd36 100644 --- a/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap +++ b/packages/main/src/components/DynamicPage/__snapshots__/DynamicPage.test.tsx.snap @@ -12,6 +12,7 @@ exports[`DynamicPage always show content header 1`] = ` >