From 3c8c18f39f8697593048318f11420bfd7f5da71b Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 29 Feb 2024 15:14:33 +0100 Subject: [PATCH] feat(ObjectPage): add `onBeforeNavigate` event (#5557) Closes #5342 --- .../components/ObjectPage/ObjectPage.cy.tsx | 39 ++++++++++++++++ .../main/src/components/ObjectPage/index.tsx | 46 +++++++++++++++++-- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx index 755258e29dc..3b8eb073bea 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx @@ -33,6 +33,7 @@ import { TitleLevel, ValueState } from '../..'; +import { cypressPassThroughTestsFactory } from '@/cypress/support/utils'; describe('ObjectPage', () => { it('toggle header', () => { @@ -837,6 +838,44 @@ describe('ObjectPage', () => { cy.get('[ui5-tabcontainer]').should('not.exist'); cy.get('[data-component-name="ObjectPageAnchorBar"]').should('not.be.visible'); }); + + it('onBeforeNavigate', () => { + const beforeNavigateHandlerDefaultPrevented = (e) => { + // deleted as not relevant for the test + delete e.detail.tab; + delete e.detail.tabIndex; + e.preventDefault(); + }; + const beforeNavigate = cy.spy(beforeNavigateHandlerDefaultPrevented).as('beforeNavigateSpy'); + const sectionChange = cy.spy().as('sectionChangeSpy'); + cy.mount( + + {OPContent} + + ); + cy.get('[ui5-tabcontainer]').findUi5TabByText('Personal').click(); + cy.get('@beforeNavigateSpy') + .should('have.been.calledOnce') + .its('firstCall.args[0].detail') + .should('deep.equal', { sectionIndex: 2, sectionId: 'personal', subSectionId: undefined }); + cy.get('@sectionChangeSpy').should('not.have.been.called'); + + cy.get('[ui5-tabcontainer]').findUi5TabOpenPopoverButtonByText('Employment').click(); + cy.realPress('Enter'); + cy.get('@beforeNavigateSpy') + .should('have.been.calledTwice') + .its('secondCall.args[0].detail') + .should('deep.equal', { sectionIndex: 3, sectionId: 'employment', subSectionId: 'employment-job-information' }); + cy.get('@sectionChangeSpy').should('not.have.been.called'); + }); + + cypressPassThroughTestsFactory(ObjectPage); }); const DPTitle = ( diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index 01dcb7d49eb..078a4dea204 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -1,5 +1,6 @@ 'use client'; +import type { TabContainerTabSelectEventDetail } from '@ui5/webcomponents/dist/TabContainer.js'; import { debounce, enrichEventWithDetails, ThemingParameters, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties, ReactElement, ReactNode } from 'react'; @@ -18,8 +19,8 @@ import { AvatarSize, GlobalStyleClasses, ObjectPageMode } from '../../enums/inde import { addCustomCSSWithScoping } from '../../internal/addCustomCSSWithScoping.js'; import { safeGetChildrenArray } from '../../internal/safeGetChildrenArray.js'; import { useObserveHeights } from '../../internal/useObserveHeights.js'; -import type { CommonProps } from '../../types/index.js'; -import type { AvatarPropTypes } from '../../webComponents/index.js'; +import type { CommonProps, Ui5CustomEvent } from '../../types/index.js'; +import type { AvatarPropTypes, TabContainerDomRef } from '../../webComponents/index.js'; import { Tab, TabContainer } from '../../webComponents/index.js'; import { DynamicPageCssVariables } from '../DynamicPage/DynamicPage.jss.js'; import { DynamicPageAnchorBar } from '../DynamicPageAnchorBar/index.js'; @@ -43,6 +44,14 @@ const TAB_CONTAINER_HEADER_HEIGHT = 48; type ObjectPageSectionType = ReactElement | boolean; +interface BeforeNavigateDetail { + sectionIndex: number; + sectionId: string; + subSectionId: string | undefined; +} + +type ObjectPageTabSelectEventDetail = TabContainerTabSelectEventDetail & BeforeNavigateDetail; + export interface ObjectPagePropTypes extends Omit { /** * Defines the upper, always static, title section of the `ObjectPage`. @@ -79,11 +88,13 @@ export interface ObjectPagePropTypes extends Omit { */ children?: ObjectPageSectionType | ObjectPageSectionType[]; /** - * Defines the ID of the currently `ObjectPageSection` section. + * Sets the current selected `ObjectPageSection` by `id`. + * + * __Note:__ If a valid `selectedSubSectionId` is set, this prop has no effect. */ selectedSectionId?: string; /** - * Defines the ID of the currently `ObjectPageSubSection` section. + * Sets the current selected `ObjectPageSubSection` by `id`. */ selectedSubSectionId?: string; /** @@ -132,6 +143,12 @@ export interface ObjectPagePropTypes extends Omit { * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use placeholder components like the `IllustratedMessage` or custom skeletons pages in order to preserve the intended design. */ placeholder?: ReactNode; + /** + * The event is fired before the selected section is changed using the navigation. It can be aborted by the application with `preventDefault()`, which means that there will be no navigation. + * + * __Note:__ This event is only fired when navigating via tab-bar. + */ + onBeforeNavigate?: (event: Ui5CustomEvent) => void; /** * Fired when the selected section changes. */ @@ -177,6 +194,7 @@ const ObjectPage = forwardRef((props, ref) onSelectedSectionChange, onToggleHeaderContent, onPinnedStateChange, + onBeforeNavigate, ...rest } = props; @@ -584,6 +602,7 @@ const ObjectPage = forwardRef((props, ref) const snappedHeaderInObjPage = headerTitle && headerTitle.props.snappedContent && headerCollapsed === true && !!image; + const hasHeaderContent = !!headerContent; const renderTitleSection = useCallback( (inHeader = false) => { const titleInHeaderClass = inHeader ? classes.titleInHeader : undefined; @@ -606,7 +625,7 @@ const ObjectPage = forwardRef((props, ref) 'data-is-snapped-rendered-outside': snappedHeaderInObjPage }); }, - [headerTitle, titleHeaderNotClickable, onTitleClick, headerCollapsed, snappedHeaderInObjPage, !!headerContent] + [headerTitle, titleHeaderNotClickable, onTitleClick, headerCollapsed, snappedHeaderInObjPage, hasHeaderContent] ); const isInitial = useRef(true); @@ -668,6 +687,22 @@ const ObjectPage = forwardRef((props, ref) ]); const onTabItemSelect = (event) => { + if (typeof onBeforeNavigate === 'function') { + const selectedTabDataset = event.detail.tab.dataset; + const sectionIndex = parseInt(selectedTabDataset.index, 10); + const sectionId = selectedTabDataset.parentId ?? selectedTabDataset.sectionId; + const subSectionId = selectedTabDataset.isSubTab ? selectedTabDataset.sectionId : undefined; + onBeforeNavigate( + enrichEventWithDetails(event, { + sectionIndex, + sectionId, + subSectionId + }) + ); + if (event.defaultPrevented) { + return; + } + } event.preventDefault(); const { sectionId, index, isSubTab, parentId } = event.detail.tab.dataset; if (isSubTab) { @@ -845,6 +880,7 @@ const ObjectPage = forwardRef((props, ref) data-section-id={item.props.id} text={item.props.titleText} selected={item.props.id === selectedSubSectionId || undefined} + data-index={index} > {/*ToDo: workaround for nested tab selection*/}