diff --git a/docs/MigrationGuide.mdx b/docs/MigrationGuide.mdx index 2ecc8bd69a0..44dd74f66a4 100644 --- a/docs/MigrationGuide.mdx +++ b/docs/MigrationGuide.mdx @@ -627,33 +627,88 @@ The regular methods are now general purpose, so they can be used both inside the ### ObjectPage The newly introduced `DynamicPage` web component comes with its own `DynamicPageHeader` and `DynamicPageTitle` components, which are unfortunately incompatible with our `ObjectPage` implementation. -Please use the following components instead: +Please use the `ObjectPageHeader` or `ObjectPageTitle` component instead. -- `headerContent` now only accepts the `ObjectPageHeader` component. -- `headerTitle` now only accepts the `ObjectPageTitle` component. +**Removed Props:** + +- `showHideHeaderButton`: Hiding the expand/collapse button is not supported by design anymore. +- `showTitleInHeaderContent`: Showing the `headerTitle` as part of the `headerContent` is [not supported by design anymore](https://experience.sap.com/fiori-design-web/object-page/#dynamic-page-header-mandatory). + +**Refactored Props:** + +- `headerContent` has been renamed to `headerArea` and now only accepts the `ObjectPageHeader` component. +- `headerTitle` has been renamed to `titleArea` and now only accepts the `ObjectPageTitle` component. +- `headerContentPinnable` has been renamed to `hidePinButton` and the logic has been inverted. The pin button is now shown by default. **Renamed Props:** - `a11yConfig` has been renamed to `accessibilityAttributes` - `a11yConfig.dynamicPageAnchorBar` has been renamed to `accessibilityAttributes.objectPageAnchorBar` - `alwaysShowContentHeader` has been renamed to `headerPinned` -- `headerContentPinnable` has been renamed to `hidePinButton` and the logic has been inverted. The pin button is now shown by default. - -**Removed Props:** - -- `showHideHeaderButton`: Hiding the expand/collapse button is not supported by design anymore. -- `showTitleInHeaderContent`: Showing the `headerTitle` as part of the `headerContent` is [not supported by design anymore](https://experience.sap.com/fiori-design-web/object-page/#dynamic-page-header-mandatory). +- `footer` has been renamed to `footerArea` +- `onToggleHeaderContent` has been renamed to `onToggleHeaderArea` +- `onPinnedStateChange` has been renamed to `onPinButtonToggle` Also, the namings of internal `data-component-name` attributes have been adjusted accordingly. E.g. `data-component-name="DynamicPageTitleSubHeader"` has been renamed to `data-component-name="ObjectPageTitleSubHeader"` -### ObjectPageTitle +### ObjectPageTitle (f.k.a. DynamicPageTitle) -_The `ObjectPageTitle` component is the renamed implementation of the old (React only) `DynamicPageTitle` component._ +_The `ObjectPageTitle` component is the renamed implementation of the old (React only) `DynamicPageTitle` component. Now, it should only be used in the `ObjectPage`._ **Removed Props:** +- `actionsToolbarProps`: Since it's now recommended passing the `Toolbar` component directly, this prop is redundant. +- `navigationActionsToolbarProps`: Since it's now recommended passing the `Toolbar` component directly, this prop is redundant. - `showSubHeaderRight`: Displaying the subheader in the same line as the header is not supported by design anymore. +**Refactored Props:** + +- `actions` has been renamed to `actionsBar`. Instead of single actions, the `Toolbar` component should now be passed. +- `navigationActions` has been renamed to `navigationBar`. Instead of single actions, the `Toolbar` component should now be passed. The `ObjectPageTitle` still offers support for the legacy `Toolbar`. + +_The `ObjectPageTitle` still offers support for the legacy `Toolbar`. You can find out more about this [here](?path=/docs/layouts-floorplans-objectpage--docs#legacy-toolbar-support)._ + +```jsx +// v1 + + + + + } + navigationActions={ + <> + + + + } +/> + +// v2 + + + + + } + navigationBar={ + + + + + } +/> +``` + ### ObjectPageSection The prop `titleText` is now required and the default value `true` has been removed for the `titleTextUppercase` prop to comply with the updated Fiori design guidelines. diff --git a/packages/base/src/utils/index.ts b/packages/base/src/utils/index.ts index 956f427e0d4..835b70f4b91 100644 --- a/packages/base/src/utils/index.ts +++ b/packages/base/src/utils/index.ts @@ -23,6 +23,9 @@ export const enrichEventWithDetails = < event: Event, payload: Detail ): EnrichedEventType => { + if (!event) { + return event; + } // todo: once we drop React 16 support, remove this // the helper accepts both SyntheticEvents and browser events const syntheticEventCast = event as unknown as SyntheticEvent; diff --git a/packages/cli/src/scripts/codemod/transforms/v2/codemodConfig.json b/packages/cli/src/scripts/codemod/transforms/v2/codemodConfig.json index 96527979e56..e6d88553b8b 100644 --- a/packages/cli/src/scripts/codemod/transforms/v2/codemodConfig.json +++ b/packages/cli/src/scripts/codemod/transforms/v2/codemodConfig.json @@ -319,10 +319,16 @@ "changedProps": { "a11yConfig": "accessibilityAttributes", "alwaysShowContentHeader": "headerPinned", - "headerContentPinnable": "hidePinButton" + "headerContentPinnable": "hidePinButton", + "footer": "footerArea", + "onToggleHeaderContent": "onToggleHeaderArea", + "onPinnedStateChange": "onPinButtonToggle" }, "removedProps": ["showHideHeaderButton", "showTitleInHeaderContent"] }, + "ObjectPageTitle": { + "removedProps": ["showSubHeaderRight"] + }, "ObjectStatus": { "changedProps": { "active": "interactive" diff --git a/packages/cli/src/scripts/codemod/transforms/v2/main.cts b/packages/cli/src/scripts/codemod/transforms/v2/main.cts index 71dab56233b..c41160ed752 100644 --- a/packages/cli/src/scripts/codemod/transforms/v2/main.cts +++ b/packages/cli/src/scripts/codemod/transforms/v2/main.cts @@ -347,6 +347,72 @@ export default function transform(file: FileInfo, api: API, options?: Options): headerContentPinnable.remove(); isDirty = true; } + + const headerContent = j(el).find(j.JSXAttribute, { name: { name: 'headerContent' } }); + if (headerContent.size() > 0) { + const attr = headerContent.get(); + if (attr.value.value && attr.value.value.type === 'JSXExpressionContainer') { + headerContent.forEach((el) => { + const dynamicPageHeader = j(el).find(j.JSXElement, { + openingElement: { name: { name: 'DynamicPageHeader' } } + }); + + // remove DynamicPageHeader import only if no DynamicPage is there + if (!componentIsImportedFromWebComponentsReact(j, root, 'DynamicPage')) { + const imports = root.find(j.ImportDeclaration); + const dphImport = imports.find(j.ImportSpecifier, { local: { name: 'DynamicPageHeader' } }); + dphImport.remove(); + } + addWebComponentsReactImport(j, root, 'ObjectPageHeader'); + + // replace JSX tag (component) + dynamicPageHeader.forEach((innerEl) => { + const updatedTagName = j.jsxIdentifier('ObjectPageHeader'); + innerEl.node.openingElement.name = updatedTagName; + if (innerEl.node.closingElement) { + innerEl.node.closingElement.name = updatedTagName; + } + }); + + // replace prop name + headerContent.find(j.JSXIdentifier, { name: 'headerContent' }).replaceWith(j.jsxIdentifier('headerArea')); + }); + isDirty = true; + } + } + + const headerTitle = j(el).find(j.JSXAttribute, { name: { name: 'headerTitle' } }); + if (headerTitle.size() > 0) { + const attr = headerTitle.get(); + if (attr.value.value && attr.value.value.type === 'JSXExpressionContainer') { + headerTitle.forEach((el) => { + const dynamicPageTitle = j(el).find(j.JSXElement, { + openingElement: { name: { name: 'DynamicPageTitle' } } + }); + + // remove DynamicPageTitle import only if no DynamicPage is there + dynamicPageTitle.forEach((innerEl) => { + if (!componentIsImportedFromWebComponentsReact(j, root, 'DynamicPage')) { + const imports = root.find(j.ImportDeclaration); + const dptImport = imports.find(j.ImportSpecifier, { local: { name: 'DynamicPageTitle' } }); + dptImport.remove(); + } + addWebComponentsReactImport(j, root, 'ObjectPageTitle'); + + // replace JSX tag (component) + const updatedTagName = j.jsxIdentifier('ObjectPageTitle'); + innerEl.node.openingElement.name = updatedTagName; + if (innerEl.node.closingElement) { + innerEl.node.closingElement.name = updatedTagName; + } + }); + + // replace prop name + headerTitle.find(j.JSXIdentifier, { name: 'headerTitle' }).replaceWith(j.jsxIdentifier('titleArea')); + }); + isDirty = true; + } + } }); } diff --git a/packages/compat/src/components/Toolbar/index.tsx b/packages/compat/src/components/Toolbar/index.tsx index c182cbdd0c1..873dcc90473 100644 --- a/packages/compat/src/components/Toolbar/index.tsx +++ b/packages/compat/src/components/Toolbar/index.tsx @@ -164,6 +164,8 @@ const Toolbar = forwardRef((props, ref) => { ...rest } = props; + const inObjectPage = props['data-in-object-page-title']; + useStylesheet(styleData, Toolbar.displayName); const [componentRef, outerContainer] = useSyncRef(ref); const controlMetaData = useRef([]); @@ -317,6 +319,9 @@ const Toolbar = forwardRef((props, ref) => { }, [calculateVisibleItems]); const handleToolbarClick = (e) => { + if (inObjectPage && typeof onClick === 'function') { + onClick(e); + } if (active && typeof onClick === 'function') { const isSpaceEnterDown = e.type === 'keydown' && (e.code === 'Enter' || e.code === 'Space'); if (isSpaceEnterDown && e.target !== e.currentTarget) { @@ -379,6 +384,7 @@ const Toolbar = forwardRef((props, ref) => { tabIndex={active ? 0 : undefined} role={active ? 'button' : undefined} data-sap-ui-fastnavgroup="true" + data-component-name="Toolbar" {...rest} >
@@ -420,4 +426,6 @@ const Toolbar = forwardRef((props, ref) => { }); Toolbar.displayName = 'Toolbar'; +//@ts-expect-error: private identifier +Toolbar._displayName = 'UI5WCRToolbar'; export { Toolbar }; diff --git a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx index 4f2fe78a8d2..1e882967093 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx @@ -31,7 +31,9 @@ import { ObjectPageSubSection, ObjectStatus, Text, - Title + Title, + Toolbar, + ToolbarButton } from '../..'; import { cypressPassThroughTestsFactory } from '@/cypress/support/utils'; @@ -40,9 +42,9 @@ describe('ObjectPage', () => { const toggle = cy.spy().as('toggleSpy'); cy.mount( } - headerContent={ObjectPageHeader} - onToggleHeaderContent={toggle} + titleArea={} + headerArea={ObjectPageHeader} + onToggleHeaderArea={toggle} hidePinButton > @@ -62,7 +64,7 @@ describe('ObjectPage', () => { cy.get('@toggleSpy').should('have.been.calledWith', true); cy.get('@toggleSpy').should('have.been.calledTwice'); - cy.findByText('Heading').click(); + cy.findByText('Heading').click({ force: true }); cy.findByText('ObjectPageHeader').should('not.be.visible'); cy.get('@toggleSpy').should('have.been.calledThrice'); cy.get('@toggleSpy').should('have.been.calledWith', false); @@ -74,9 +76,9 @@ describe('ObjectPage', () => { cy.mount( } - headerContent={ObjectPageHeader} - onToggleHeaderContent={toggle} + titleArea={} + headerArea={ObjectPageHeader} + onToggleHeaderArea={toggle} hidePinButton preserveHeaderStateOnClick > @@ -86,7 +88,7 @@ describe('ObjectPage', () => { ); - cy.findByText('Heading').click(); + cy.findByText('Heading').click({ force: true }); cy.findByText('ObjectPageHeader').should('be.visible'); cy.get('@toggleSpy').should('have.callCount', 4); @@ -100,9 +102,9 @@ describe('ObjectPage', () => { cy.mount( } - headerContent={ObjectPageHeader} - onPinnedStateChange={pin} + titleArea={} + headerArea={ObjectPageHeader} + onPinButtonToggle={pin} data-testid="op" > @@ -132,10 +134,10 @@ describe('ObjectPage', () => { it('programmatically pin header (`headerPinned`)', () => { document.body.style.margin = '0px'; - const TestComp = ({ onPinnedStateChange }: ObjectPagePropTypes) => { + const TestComp = ({ onPinButtonToggle }: ObjectPagePropTypes) => { const [pinned, setPinned] = useState(false); const handlePinChange = (pinned) => { - onPinnedStateChange(pinned); + onPinButtonToggle(pinned); setPinned(pinned); }; return ( @@ -150,10 +152,10 @@ describe('ObjectPage', () => { } - headerContent={ObjectPageHeader} + titleArea={} + headerArea={ObjectPageHeader} headerPinned={pinned} - onPinnedStateChange={handlePinChange} + onPinButtonToggle={handlePinChange} data-testid="op" > @@ -164,7 +166,7 @@ describe('ObjectPage', () => { ); }; const pin = cy.spy().as('onPinSpy'); - cy.mount(); + cy.mount(); cy.wait(50); cy.findByTestId('op').scrollTo(0, 500); @@ -223,8 +225,8 @@ describe('ObjectPage', () => { cy.mount( } - headerContent={ + titleArea={} + headerArea={
ObjectPageHeader
@@ -257,7 +259,7 @@ describe('ObjectPage', () => { it('scroll to sections - default mode', () => { document.body.style.margin = '0px'; cy.mount( - + {OPContent} ); @@ -285,7 +287,7 @@ describe('ObjectPage', () => { cy.findByText('Job Relationship').should('be.visible'); cy.mount( - + {OPContent} ); @@ -326,8 +328,8 @@ describe('ObjectPage', () => { document.body.style.margin = '0px'; cy.mount( @@ -355,9 +357,9 @@ describe('ObjectPage', () => { cy.mount( @@ -406,11 +408,11 @@ describe('ObjectPage', () => { return ( @@ -503,11 +505,11 @@ describe('ObjectPage', () => { return ( @@ -590,7 +592,7 @@ describe('ObjectPage', () => { const [hideTitleText2, toggleTitleText2] = useReducer((prev) => !prev, true); const [hideTitleTextSub, toggleTitleTextSub] = useReducer((prev) => !prev, true); return ( - + { const TestComp = () => { const [wrapTitle, toggleWrap] = useReducer((prev) => !prev, false); return ( - + @@ -720,7 +722,7 @@ describe('ObjectPage', () => { const [uppercase, toggleUppercase] = useReducer((prev) => !prev, undefined); const [uppercaseSub, toggleUppercaseSub] = useReducer((prev) => !prev, undefined); return ( - + @@ -759,7 +761,7 @@ describe('ObjectPage', () => { const [level, setLevel] = useState(undefined); const [levelSub, setLevelSub] = useState(undefined); return ( - + @@ -806,20 +808,18 @@ describe('ObjectPage', () => { it('empty content', () => { cy.mount(); cy.findByTestId('op').should('be.visible'); - cy.mount(); + cy.mount(); cy.findByTestId('op').should('be.visible'); }); it('w/ image', () => { - cy.mount( - - ); + cy.mount(); cy.findByAltText('Company Logo').should('be.visible'); cy.mount( @@ -829,7 +829,7 @@ describe('ObjectPage', () => { .parent() .should('have.css', 'border-radius', '50%') .should('have.css', 'overflow', 'hidden'); - cy.mount(} />); + cy.mount(} />); cy.get('[ui5-avatar]').should('have.attr', 'size', 'L').should('be.visible'); }); @@ -837,8 +837,8 @@ describe('ObjectPage', () => { cy.mount( } /> ); @@ -848,8 +848,8 @@ describe('ObjectPage', () => { cy.mount( } > {OPContent} @@ -872,8 +872,8 @@ describe('ObjectPage', () => { cy.mount( @@ -925,13 +925,11 @@ const DPTitle = ( - - - + actionsBar={ + + + + } breadcrumbs={ diff --git a/packages/main/src/components/ObjectPage/ObjectPage.mdx b/packages/main/src/components/ObjectPage/ObjectPage.mdx index e4ba1d5b0a8..48a6b8f7131 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.mdx +++ b/packages/main/src/components/ObjectPage/ObjectPage.mdx @@ -25,14 +25,6 @@ import { ObjectPageSubSection } from '../ObjectPageSubSection'; -## ObjectPage with custom actions and navigationActions overflow button - -The `ObjectPageTitle` offers two props (`actionsToolbarProps` and `navigationActionsToolbarProps`) to configure the props of the `actions` and `navigationActions` toolbars. -With these objectes it's possible to e.g. configure the overflow button displayed within each toolbar by setting the `overflowButton` prop. -`navigationActionsToolbarProps` only has an effect, if the `navigationActions` toolbar is used. (width < 1280px) - - - ## ObjectPageSection with hidden `titleText` and custom `header` @@ -53,6 +45,87 @@ To render a single section in fullscreen mode, set its height to `100%`. ``` +## Legacy Toolbar Support + +The ObjectPage still supports the old (React-only) `Toolbar` implementation. Please only use this toolbar if your app is dependent on some features the `Toolbar` web component is currently not offering. +Also, when using the legacy `Toolbar` there are some things to consider that work out of the box with the recommended Toolbar: + +- To correctly align the actions to the end, use a legacy `ToolbarSpacer` as first child of the `Toolbar`. +- Toggling the `headerArea` by clicking on the empty space inside the legacy `Toolbar` has to be implemented on app side now. Please make sure to add the `data-in-object-page-title` prop to the toolbars, as otherwise the `click` event isn't fired. You can see an example of how to achieve this behavior below. + + + +### Code + +
+ +Show Code + +```tsx +import { useRef } from 'react'; +import { Toolbar as LegacyToolbar, ToolbarSpacer as LegacyToolbarSpacer } from '@ui5/webcomponents-react-compat'; +import { Button, ButtonDesign, ObjectPage, ObjectPageSection, ObjectPageTitle } from '@ui5/webcomponents-react'; +import type { ObjectPageDomRef } from '@ui5/webcomponents-react'; + +function ObjectPageWithLegacyToolbar(props) { + const objectPageRef = useRef(null); + const handleToolbarClick = (e) => { + if (e.target.dataset.componentName === 'ToolbarContent') { + objectPageRef.current.toggleHeaderArea(); + } + }; + return ( + + + + + + } + navigationBar={ + + +
+ {SubcomponentsSection} ## ObjectPageTitle diff --git a/packages/main/src/components/ObjectPage/ObjectPage.module.css b/packages/main/src/components/ObjectPage/ObjectPage.module.css index 65bb1e7980f..b5f797da212 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.module.css +++ b/packages/main/src/components/ObjectPage/ObjectPage.module.css @@ -175,3 +175,8 @@ .snappedContent { grid-column: 1 / span 2; } + +.clickArea { + position: absolute; + inset: 0; +} diff --git a/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx b/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx index 7654546b3e6..664e7d66d45 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx @@ -5,7 +5,13 @@ import BarDesign from '@ui5/webcomponents/dist/types/BarDesign.js'; import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js'; import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js'; +import declineIcon from '@ui5/webcomponents-icons/dist/decline.js'; +import exitFSIcon from '@ui5/webcomponents-icons/dist/exit-full-screen.js'; +import fullscreenIcon from '@ui5/webcomponents-icons/dist/full-screen.js'; import sunIcon from '@ui5/webcomponents-icons/dist/general-leave-request.js'; +import { useRef } from 'react'; +import { Toolbar as LegacyToolbar, ToolbarSpacer as LegacyToolbarSpacer } from '../../../../compat/src/index.js'; +import type { ObjectPageDomRef } from '../../index.js'; import { Bar, Breadcrumbs, @@ -31,7 +37,8 @@ import { ObjectStatus, Text, Title, - ToggleButton + Toolbar, + ToolbarButton } from '../../index.js'; import { ObjectPage } from './index.js'; @@ -39,9 +46,9 @@ const meta = { title: 'Layouts & Floorplans / ObjectPage', component: ObjectPage, argTypes: { - headerTitle: { control: { disable: true } }, - headerContent: { control: { disable: true } }, - footer: { control: { disable: true } }, + titleArea: { control: { disable: true } }, + headerArea: { control: { disable: true } }, + footerArea: { control: { disable: true } }, children: { control: { disable: true } }, placeholder: { control: { disable: true } }, accessibilityAttributes: { table: { category: 'Accessibility props' } } @@ -52,7 +59,7 @@ const meta = { imageShapeCircle: true, image: SampleImage, style: { height: '700px' }, - footer: ( + footerArea: ( ), - headerTitle: ( + titleArea: ( - - - + actionsBar={ + + + + + } + navigationBar={ + + + + + } breadcrumbs={ @@ -88,7 +100,7 @@ const meta = { employed ), - headerContent: ( + headerArea: ( @@ -280,8 +292,8 @@ export const WithIllustratedMessage: Story = { return ( } /> @@ -289,76 +301,6 @@ export const WithIllustratedMessage: Story = { } }; -export const WithCustomOverflowButton: Story = { - name: 'with custom overflow button', - render() { - const titleProps = { - actionsToolbarProps: { - overflowButton: - }, - navigationActionsToolbarProps: { - overflowButton: - }, - actions: ( - <> - - - - - - - - ), - navigationActions: ( - <> - - - - - ) - }; - return ( - <> - - Custom overflow buttons for navigationActions and actions (width: 1000px) - - } - /> - } - /> - Custom overflow buttons for actions (width: 1400px)} - /> - } - /> - - ); - } -}; - export const SectionWithCustomHeader: Story = { name: 'section with custom header', render(args) { @@ -424,3 +366,61 @@ export const FullScreenSingleSection: Story = { ); } }; + +export const LegacyToolbarSupport: Story = { + render(args) { + const objectPageRef = useRef(null); + const handleToolbarClick = (e) => { + if (e.target.dataset.componentName === 'ToolbarContent') { + objectPageRef.current.toggleHeaderArea(); + } + }; + return ( + + + + + + } + navigationBar={ + + + )} - navigationActions={new Array(20).fill()} - actionsToolbarProps={{ overflowPopoverRef: actionsRef }} - navigationActionsToolbarProps={{ overflowPopoverRef: navActionsRef }} + actionsBar={{new Array(10).fill()}} + navigationBar={{new Array(20).fill()}} {...titleProps} /> ), @@ -34,66 +36,28 @@ const PageComponent = ({ titleProps = {}, pageProps = {}, childrenScrollable }: }; return ( <> - - {childrenScrollable && childrenObjectPage} -

{`${!!actionsToolbarInstance}`}

-

{`${!!navActionsToolbarInstance}`}

); }; -const testOverflowRefs = (should = { nav: 'false', actions: 'false' }) => { - cy.findByText('Show actionsRef').click({ force: true }); - cy.wait(200); - cy.findByTestId('actionsInstance').should('have.text', should.actions); - cy.findByText('Show navActionsRef').click({ force: true }); - cy.wait(200); - cy.findByTestId('navActionsInstance').should('have.text', should.nav); -}; - describe('ObjectPageTitle', () => { - it('toolbar instances - ObjectPage', () => { - cy.mount(); - cy.wait(300); - testOverflowRefs({ nav: 'false', actions: 'true' }); - cy.viewport(1000, 1000); - cy.mount(); - cy.wait(300); - testOverflowRefs({ nav: 'true', actions: 'true' }); - cy.viewport(5000, 5000); - cy.mount(); - cy.wait(300); - testOverflowRefs({ nav: 'false', actions: 'false' }); - }); it('show 2nd line content', () => { cy.viewport(320, 700); cy.mount( This is a pretty long title of the ObjectPageTitle, - navigationActions: undefined, + navigationBar: undefined, children:
Content
}} /> ); cy.findByText('This is a pretty long title of the ObjectPageTitle').should('be.visible'); - cy.findByText('Content').should('be.visible'); - cy.get('[data-component-name="ToolbarOverflowButton"]').should('be.visible'); + cy.findByText('Content').should('exist'); // covered by click span + cy.get('[icon="overflow"]').should('be.visible'); }); it('breadcrumbs spread', () => { @@ -101,7 +65,7 @@ describe('ObjectPageTitle', () => { cy.mount( {new Array(14).fill(1337).map((item, index) => ( @@ -145,7 +109,7 @@ describe('ObjectPageTitle', () => { cy.mount( expandedContent
, snappedContent:
snappedContent
@@ -153,18 +117,18 @@ describe('ObjectPageTitle', () => { /> ); if (headerContent) { - cy.findByText('expandedContent').should('be.visible'); + cy.findByText('expandedContent').should('exist'); cy.findByTestId('snappedContent').should('not.exist'); } else { - cy.findByText('snappedContent').should('be.visible'); + cy.findByText('snappedContent').should('exist'); cy.findByTestId('expandedContent').should('not.exist'); } cy.wait(50); cy.findByTestId('page').scrollTo(0, 500); - cy.findByText('snappedContent').should('be.visible'); + cy.findByText('snappedContent').should('exist'); cy.findByTestId('expandedContent').should('not.exist'); if (headerContent && image) { - cy.get('[data-component-name="ATwithImageSnappedContentContainer"]').should('be.visible'); + cy.get('[data-component-name="ATwithImageSnappedContentContainer"]').should('exist'); } else { cy.get('[data-component-name="ATwithImageSnappedContentContainer"]').should('not.exist'); } diff --git a/packages/main/src/components/ObjectPageTitle/ObjectPageTitle.module.css b/packages/main/src/components/ObjectPageTitle/ObjectPageTitle.module.css index 1129e1bd7b1..d3cf877e0aa 100644 --- a/packages/main/src/components/ObjectPageTitle/ObjectPageTitle.module.css +++ b/packages/main/src/components/ObjectPageTitle/ObjectPageTitle.module.css @@ -40,7 +40,7 @@ .titleMainSection { overflow-x: hidden; - flex: 1 1 100%; + flex: 1 1 auto; align-items: baseline; } @@ -51,6 +51,7 @@ } .title { + flex-shrink: 1; min-width: 3rem; overflow-x: hidden; font-family: var(--sapObjectHeader_Title_FontFamily); @@ -61,11 +62,11 @@ hyphens: auto; > * { - font-family: var(--sapObjectHeader_Title_FontFamily); - color: var(--sapObjectHeader_Title_TextColor); - font-size: var(--_ui5wcr_ObjectPage_title_fontsize); - overflow-wrap: break-word; - hyphens: auto; + font-family: inherit; + color: inherit; + font-size: inherit; + overflow-wrap: inherit; + hyphens: inherit; } } @@ -83,10 +84,10 @@ padding-inline-start: 0.5rem; > * { - color: var(--sapObjectHeader_Subtitle_TextColor); - font-size: var(--sapFontSize); - overflow-wrap: break-word; - hyphens: auto; + color: inherit; + font-size: inherit; + overflow-wrap: inherit; + hyphens: inherit; } } @@ -102,27 +103,41 @@ } .toolbar { - flex: 1 1.6 100%; - cursor: auto; + flex-grow: 1; + flex-shrink: 1.6; + min-width: 3rem; + display: flex; + align-items: center; + justify-content: flex-end; - &:hover { - background-color: inherit; + > [ui5-toolbar] { + padding: 0; + border: none; } - & > :first-child { - height: 100%; + > [ui5-toolbar]:not(:first-child):last-child { + flex: 0 1; } - /* toolbar w/o overflow button*/ + > [data-component-name='Toolbar']:not(:first-child):last-child { + width: unset; + flex-shrink: 0; + } - > [data-component-name='ToolbarContent']:first-child:last-child - > [data-component-name='ToolbarChildContainer']:last-child { - margin-inline-end: 0; + > [ui5-toolbar]:only-child { + flex-grow: 1; + flex-shrink: 0; } +} - /* toolbar w/ overflow button */ +.actionsSpacer { + flex-shrink: 0; + height: var(--_ui5_dynamic_page_title_actions_separator_height); + width: 0.0625rem; + background: var(--sapToolbar_SeparatorColor); +} - [data-component-name='ToolbarOverflowButtonContainer'] { - margin-inline-end: 0; - } +.clickArea { + position: absolute; + inset: 0; } diff --git a/packages/main/src/components/ObjectPageTitle/index.tsx b/packages/main/src/components/ObjectPageTitle/index.tsx index 802066eeb7f..0ebf4b35317 100644 --- a/packages/main/src/components/ObjectPageTitle/index.tsx +++ b/packages/main/src/components/ObjectPageTitle/index.tsx @@ -2,29 +2,22 @@ import { debounce, Device, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; -import type { MouseEventHandler, ReactElement, ReactNode, RefObject } from 'react'; -import { Children, cloneElement, forwardRef, isValidElement, useCallback, useEffect, useRef, useState } from 'react'; +import type { ReactElement, ReactNode } from 'react'; +import { cloneElement, forwardRef, isValidElement, useEffect, useRef, useState } from 'react'; import { FlexBoxAlignItems, FlexBoxJustifyContent } from '../../enums/index.js'; import { stopPropagation } from '../../internal/stopPropagation.js'; -import { flattenFragments } from '../../internal/utils.js'; import type { CommonProps } from '../../types/index.js'; -import type { ButtonPropTypes, PopoverDomRef } from '../../webComponents/index.js'; +import type { ToolbarDomRef } from '../../webComponents/index.js'; import { FlexBox } from '../FlexBox/index.js'; -import type { ToolbarPropTypes } from '../Toolbar/index.js'; -import { Toolbar } from '../Toolbar/index.js'; -import { ToolbarSeparator } from '../ToolbarSeparator/index.js'; -import { ActionsSpacer } from './ActionsSpacer.js'; import { classNames, styleData } from './ObjectPageTitle.module.css.js'; export interface ObjectPageTitlePropTypes extends CommonProps { /** - * Defines the actions of the `ObjectPageTitle`. + * Defines the actions bar of the `ObjectPageTitle`. * - * __Note:__ When clicking on an action in the overflow popover it closes the popover. You can use `event.preventDefault()` to prevent this. - * - * __Note:__ Although this prop accepts all `ReactElements`, it is strongly recommended that you only use components that render a single element in order to preserve the intended design. + * __Note:__ Although this prop accepts all `ReactElement`s, it is strongly recommended that you only use the `Toolbar` component in order to preserve the intended design. */ - actions?: ReactElement | ReactElement[]; + actionsBar?: ReactElement; /** * The `breadcrumbs` displayed in the `ObjectPageTitle` top-left area. @@ -49,29 +42,16 @@ export interface ObjectPageTitlePropTypes extends CommonProps { */ subHeader?: ReactNode; /** - * The `ObjectPageTitle` navigation actions.
- * *Note*: The `navigationActions` position depends on the control size. - * If the control size is 1280px or bigger, they are rendered right next to the actions. - * Otherwise, they are rendered in the top-right area, above the actions. - * If a large number of elements(buttons) are used, there could be visual degradations as the space for the `navigationActions` is limited. - * - * __Note:__ Although this prop accepts all `ReactElements`, it is strongly recommended that you only use components that render a single element in order to preserve the intended design. - * - * __Note:__ When clicking on an action in the overflow popover it closes the popover. You can use `event.preventDefault()` to prevent this. - */ - navigationActions?: ReactElement | ReactElement[]; - /** - * Use this prop to customize the "actions" `Toolbar`. + * Defines navigation-actions bar of the `ObjectPageTitle`. * - * __Note:__ It is possible to overwrite internal implementations. Please use with caution! - */ - actionsToolbarProps?: Omit; - /** - * Use this prop to customize the "navigationActions" `Toolbar`. + * *Note*: The `navigationBar` position depends on the control size. + * If the control size is 1280px or bigger, they are rendered right next to the `actionsBar`. + * Otherwise, they are rendered in the top-right area (above the `actionsBar`). + * If a large number of elements(buttons) are used, there could be visual degradations as the space for the `navigationBar` is limited. * - * __Note:__ It is possible to overwrite internal implementations. Please use with caution! + * __Note:__ Although this prop accepts all `ReactElement`s, it is strongly recommended that you only use the `Toolbar` component in order to preserve the intended design. */ - navigationActionsToolbarProps?: Omit; + navigationBar?: ReactElement; /** * The content displayed in the `ObjectPageTitle` in expanded state. */ @@ -101,28 +81,6 @@ export interface InternalProps extends ObjectPageTitlePropTypes { 'data-is-snapped-rendered-outside'?: boolean; } -type ActionsType = - | ReactElement<{ onClick: MouseEventHandler }> - | ReactElement<{ onClick: MouseEventHandler }>[]; - -const enhanceActionsWithClick = (actions: ActionsType, ref: RefObject) => - flattenFragments(actions, Infinity).map((action) => { - if (isValidElement(action)) { - return cloneElement(action, { - onClick: (e) => { - if (typeof action.props?.onClick === 'function') { - action.props.onClick(e); - } - // @ts-expect-error: will be replaced - if (ref.current?.isOpen() && !e.defaultPrevented) { - // @ts-expect-error: will be replaced - ref.current.close(); - } - } - }); - } - }); - /** * The `ObjectPageTitle` component is used to serve as title of the `ObjectPage`. * It can contain Breadcrumbs, Title, Subtitle, Content, KPIs and Actions. @@ -131,17 +89,14 @@ const enhanceActionsWithClick = (actions: ActionsType, ref: RefObject((props, ref) => { const { - actions, + actionsBar, breadcrumbs, children, header, subHeader, - navigationActions, + navigationBar, className, - style, onToggleHeaderContentVisibility, - actionsToolbarProps, - navigationActionsToolbarProps, expandedContent, snappedContent, ...rest @@ -155,13 +110,7 @@ const ObjectPageTitle = forwardRef((pr Device.getCurrentRange(dynamicPageTitleRef.current?.getBoundingClientRect().width)?.name === 'Phone' ); const containerClasses = clsx(classNames.container, isPhone && classNames.phone, className); - - const [actionsOverflowRef, syncedActionsOverflowRef] = useSyncRef( - actionsToolbarProps?.overflowPopoverRef ?? null - ); - const [navActionsOverflowRef, syncedNavActionsOverflowRef] = useSyncRef( - navigationActionsToolbarProps?.overflowPopoverRef ?? null - ); + const toolbarContainerRef = useRef(null); useEffect(() => { isMounted.current = true; @@ -170,19 +119,11 @@ const ObjectPageTitle = forwardRef((pr }; }, [isMounted]); - const { onClick: _0, ...propsWithoutOmitted } = rest; - - 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']] - ); + const onHeaderClick = (e) => { + if (typeof onToggleHeaderContentVisibility === 'function') { + onToggleHeaderContentVisibility(e); + } + }; useEffect(() => { const debouncedObserverFn = debounce(([titleContainer]: ResizeObserverEntry[]) => { @@ -209,55 +150,68 @@ const ObjectPageTitle = forwardRef((pr }; }, [dynamicPageTitleRef.current, showNavigationInTopArea, isMounted]); - const handleActionsToolbarClick = (e) => { - stopPropagation(e); - if (typeof actionsToolbarProps?.onClick === 'function') { - actionsToolbarProps.onClick(e); + const [wcrNavToolbar, setWcrNavToolbar] = useState(null); + useEffect(() => { + //@ts-expect-error: private identifier + if (isValidElement(navigationBar) && navigationBar?.type?._displayName === 'UI5WCRToolbar') { + setWcrNavToolbar( + cloneElement(navigationBar, { + numberOfAlwaysVisibleItems: Infinity + }) + ); } - }; + }, [navigationBar]); + + useEffect(() => { + const toolbarContainer = toolbarContainerRef.current; + + const observer = new MutationObserver(([toolbarContainerMutation]) => { + if (toolbarContainerMutation.type === 'childList') { + const navigationToolbar: ToolbarDomRef | undefined = ( + toolbarContainerMutation.target as HTMLDivElement + ).querySelector(':has(> :nth-last-child(n + 2)) > [ui5-toolbar]:last-child'); + if (navigationToolbar?.children) { + Array.from(navigationToolbar.children).forEach((item) => { + item.setAttribute('overflow-priority', 'NeverOverflow'); + }); + } + } + }); + + const config = { childList: true, subtree: true }; - const handleNavigationActionsToolbarClick = (e) => { - stopPropagation(e); - if (typeof navigationActionsToolbarProps?.onClick === 'function') { - navigationActionsToolbarProps.onClick(e); + if (toolbarContainer) { + const navigationToolbar: ToolbarDomRef | undefined = toolbarContainer.querySelector( + ':has(> :nth-last-child(n + 2)) > [ui5-toolbar]:last-child' + ); + if (navigationToolbar?.children) { + Array.from(navigationToolbar.children).forEach((item) => { + item.setAttribute('overflow-priority', 'NeverOverflow'); + }); + } + observer.observe(toolbarContainer, config); } - }; + + return () => { + observer.disconnect(); + }; + }, []); return ( - - {(breadcrumbs || (navigationActions && showNavigationInTopArea)) && ( + + + {(breadcrumbs || (navigationBar && showNavigationInTopArea)) && ( {breadcrumbs && (
{breadcrumbs}
)} - {navigationActions && showNavigationInTopArea && ( - - - {enhanceActionsWithClick(navigationActions as ActionsType, syncedNavActionsOverflowRef)} - - )} + {showNavigationInTopArea && navigationBar &&
{navigationBar}
}
)} ((pr className={classNames.middleSection} data-component-name="ObjectPageTitleMiddleSection" > - + {header && ( -
+
{header}
)} @@ -277,29 +231,18 @@ const ObjectPageTitle = forwardRef((pr
)}
- {(actions || (!showNavigationInTopArea && navigationActions)) && ( - - - {enhanceActionsWithClick(actions as ActionsType, syncedActionsOverflowRef)} - {!showNavigationInTopArea && Children.count(actions) > 0 && Children.count(navigationActions) > 0 && ( - + {(actionsBar || (!showNavigationInTopArea && navigationBar)) && ( +
+ {actionsBar} + {!showNavigationInTopArea && actionsBar && navigationBar && ( +
)} - {!showNavigationInTopArea && - enhanceActionsWithClick(navigationActions as ActionsType, syncedActionsOverflowRef)} - + {!showNavigationInTopArea && (wcrNavToolbar ? wcrNavToolbar : navigationBar)} +
)} {subHeader && ( diff --git a/packages/main/src/components/Toolbar/index.tsx b/packages/main/src/components/Toolbar/index.tsx index f2246b828d7..d33c1a3d299 100644 --- a/packages/main/src/components/Toolbar/index.tsx +++ b/packages/main/src/components/Toolbar/index.tsx @@ -383,6 +383,7 @@ const Toolbar = forwardRef((props, ref) => { tabIndex={active ? 0 : undefined} role={active ? 'button' : undefined} data-sap-ui-fastnavgroup="true" + data-component-name="Toolbar" {...rest} >
@@ -424,4 +425,6 @@ const Toolbar = forwardRef((props, ref) => { }); Toolbar.displayName = 'Toolbar'; +//@ts-expect-error: private identifier +Toolbar._displayName = 'UI5WCRToolbar'; export { Toolbar }; diff --git a/packages/main/src/webComponents/DynamicPage/DynamicPage.stories.tsx b/packages/main/src/webComponents/DynamicPage/DynamicPage.stories.tsx index 595bd332818..4c8c961e73c 100644 --- a/packages/main/src/webComponents/DynamicPage/DynamicPage.stories.tsx +++ b/packages/main/src/webComponents/DynamicPage/DynamicPage.stories.tsx @@ -72,7 +72,7 @@ const meta = { } actionsBar={ - + @@ -80,7 +80,7 @@ const meta = { } navigationBar={ - +