Skip to content

Commit 7c04fe2

Browse files
feat(DynamicPageTitle): add expandedContent & snappedContent props (#4997)
This PR adds the `expandedContent` and `snappedContent` props. These props allow rendering components below the title of the DynamicPageTitle. Closes #4987 --------- Co-authored-by: Marcus Notheis <[email protected]>
1 parent 49ebe68 commit 7c04fe2

File tree

7 files changed

+132
-12
lines changed

7 files changed

+132
-12
lines changed

packages/main/src/components/DynamicPage/DynamicPage.stories.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import exitFSIcon from '@ui5/webcomponents-icons/dist/exit-full-screen.js';
77
import fullscreenIcon from '@ui5/webcomponents-icons/dist/full-screen.js';
88
import menu2Icon from '@ui5/webcomponents-icons/dist/menu2';
99
import navDownArrowIcon from '@ui5/webcomponents-icons/dist/navigation-down-arrow.js';
10-
import { useEffect, useReducer, useRef, useState } from 'react';
10+
import { useReducer, useState } from 'react';
1111
import {
1212
Badge,
1313
Bar,
@@ -23,6 +23,7 @@ import {
2323
FlexBoxDirection,
2424
FlexBoxWrap,
2525
Label,
26+
MessageStrip,
2627
ObjectStatus,
2728
PageBackgroundDesign,
2829
Panel,
@@ -91,6 +92,8 @@ const meta = {
9192
}
9293
header={<Title>Header Title</Title>}
9394
subHeader={<Label>This is a sub header</Label>}
95+
expandedContent={<MessageStrip>Information (only visible if header content is expanded)</MessageStrip>}
96+
snappedContent={<MessageStrip>Information (only visible if header content is collapsed/snapped)</MessageStrip>}
9497
>
9598
<Badge>Status: OK</Badge>
9699
</DynamicPageTitle>

packages/main/src/components/DynamicPage/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
284284
(!showHideHeaderButton && !headerContentPinnable),
285285
ref: componentRefTopHeader,
286286
className: clsx(classes.title, headerTitle?.props?.className),
287-
onToggleHeaderContentVisibility: onToggleHeaderContentInternal
287+
onToggleHeaderContentVisibility: onToggleHeaderContentInternal,
288+
'data-header-content-visible': headerContent && headerCollapsed !== true
288289
})}
289290
{headerContent &&
290291
cloneElement(headerContent, {

packages/main/src/components/DynamicPageTitle/DynamicPageTitle.cy.tsx

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
import { useRef, useState } from 'react';
2-
import type { DynamicPageTitlePropTypes } from '../..';
3-
import { Breadcrumbs, BreadcrumbsItem, DynamicPage, ObjectPage, Title } from '../..';
2+
import type { DynamicPagePropTypes, DynamicPageTitlePropTypes, ObjectPagePropTypes } from '../..';
3+
import {
4+
Avatar,
5+
Breadcrumbs,
6+
BreadcrumbsItem,
7+
DynamicPage,
8+
DynamicPageHeader,
9+
ObjectPage,
10+
ObjectPageSection,
11+
Title
12+
} from '../..';
413
import { Button } from '../../webComponents/index.js';
514
import { DynamicPageTitle } from './';
615

716
interface PropTypes {
817
dynamicPageTitleProps?: DynamicPageTitlePropTypes;
18+
pageProps?: ObjectPagePropTypes | DynamicPagePropTypes;
919
isObjectPage: boolean;
20+
childrenScrollable?: boolean;
1021
}
1122

12-
const PageComponent = ({ dynamicPageTitleProps = {}, isObjectPage }: PropTypes) => {
23+
const PageComponent = ({ dynamicPageTitleProps = {}, isObjectPage, pageProps = {}, childrenScrollable }: PropTypes) => {
1324
const actionsRef = useRef(null);
1425
const navActionsRef = useRef(null);
1526
const [actionsToolbarInstance, setActionsToolbarInstance] = useState();
1627
const [navActionsToolbarInstance, setNavActionsToolbarInstance] = useState();
17-
const pageProps = {
28+
const childrenObjectPage = (
29+
<ObjectPageSection id={'0'}>
30+
<div style={{ height: '1600px', background: 'cadetblue' }}></div>
31+
</ObjectPageSection>
32+
);
33+
const childrenDynamicPage = <div style={{ height: '1600px', background: 'cadetblue' }}></div>;
34+
const localPageProps = {
1835
headerTitle: (
1936
<DynamicPageTitle
2037
actions={new Array(10).fill(<Button>Test</Button>)}
@@ -23,7 +40,8 @@ const PageComponent = ({ dynamicPageTitleProps = {}, isObjectPage }: PropTypes)
2340
navigationActionsToolbarProps={{ overflowPopoverRef: navActionsRef }}
2441
{...dynamicPageTitleProps}
2542
/>
26-
)
43+
),
44+
...pageProps
2745
};
2846
return (
2947
<>
@@ -41,7 +59,15 @@ const PageComponent = ({ dynamicPageTitleProps = {}, isObjectPage }: PropTypes)
4159
>
4260
Show navActionsRef
4361
</Button>
44-
{isObjectPage ? <ObjectPage {...pageProps} /> : <DynamicPage {...pageProps} />}
62+
{isObjectPage ? (
63+
<ObjectPage data-testid="page" {...localPageProps}>
64+
{childrenScrollable && childrenObjectPage}
65+
</ObjectPage>
66+
) : (
67+
<DynamicPage data-testid="page" {...localPageProps}>
68+
{childrenScrollable && childrenDynamicPage}
69+
</DynamicPage>
70+
)}
4571
<p data-testid="actionsInstance">{`${!!actionsToolbarInstance}`}</p>
4672
<p data-testid="navActionsInstance">{`${!!navActionsToolbarInstance}`}</p>
4773
</>
@@ -144,4 +170,44 @@ describe('DynamicPageTitle', () => {
144170
cy.findByTestId('breadcrumbs').parent().should('have.css', 'width', '460px' /*50% (min-width)*/);
145171
});
146172
});
173+
174+
it('expandedContent & snappedContent', () => {
175+
[true, 'withImage', false].forEach((isObjectPage) => {
176+
const image = isObjectPage === 'withImage' && <Avatar />;
177+
[
178+
undefined,
179+
<DynamicPageHeader key="headerContent" style={{ height: '100px', background: 'lightblue' }}>
180+
Header Section
181+
</DynamicPageHeader>
182+
].forEach((headerContent) => {
183+
cy.mount(
184+
<PageComponent
185+
childrenScrollable
186+
isObjectPage={!!isObjectPage}
187+
pageProps={{ image, headerContent, style: { height: '800px' } }}
188+
dynamicPageTitleProps={{
189+
expandedContent: <div data-testid="expandedContent">expandedContent</div>,
190+
snappedContent: <div data-testid="snappedContent">snappedContent</div>
191+
}}
192+
/>
193+
);
194+
if (headerContent) {
195+
cy.findByText('expandedContent').should('be.visible');
196+
cy.findByTestId('snappedContent').should('not.exist');
197+
} else {
198+
cy.findByText('snappedContent').should('be.visible');
199+
cy.findByTestId('expandedContent').should('not.exist');
200+
}
201+
cy.wait(50);
202+
cy.findByTestId('page').scrollTo(0, 500);
203+
cy.findByText('snappedContent').should('be.visible');
204+
cy.findByTestId('expandedContent').should('not.exist');
205+
if (headerContent && image) {
206+
cy.get('[data-component-name="ATwithImageSnappedContentContainer"]').should('be.visible');
207+
} else {
208+
cy.get('[data-component-name="ATwithImageSnappedContentContainer"]').should('not.exist');
209+
}
210+
});
211+
});
212+
});
147213
});

packages/main/src/components/DynamicPageTitle/index.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,33 @@ export interface DynamicPageTitlePropTypes extends CommonProps {
8282
* __Note:__ It is possible to overwrite internal implementations. Please use with caution!
8383
*/
8484
navigationActionsToolbarProps?: Omit<ToolbarPropTypes, 'design' | 'toolbarStyle' | 'active'>;
85+
/**
86+
* The content displayed in the `DynamicPageTitle` in expanded state.
87+
*/
88+
expandedContent?: ReactNode | ReactNode[];
89+
/**
90+
* The content displayed in the `DynamicPageTitle` in collapsed (snapped) state.
91+
*/
92+
snappedContent?: ReactNode | ReactNode[];
8593
}
8694

8795
interface InternalProps extends DynamicPageTitlePropTypes {
8896
/**
8997
* The onToggleHeaderContentVisibility show or hide the header section
9098
*/
9199
onToggleHeaderContentVisibility?: (e: any) => boolean;
100+
/**
101+
* Defines whether the content area can be toggled
102+
*/
103+
'data-not-clickable'?: boolean;
104+
/**
105+
* Defines whether the content area is visible
106+
*/
107+
'data-header-content-visible'?: boolean;
108+
/**
109+
* Defines if the `snappedContent` should be rendered by the `DynamicPageTitle`
110+
*/
111+
'data-is-snapped-rendered-outside'?: boolean;
92112
}
93113

94114
const useStyles = createUseStyles(DynamicPageTitleStyles, { name: 'DynamicPageTitle' });
@@ -128,6 +148,8 @@ const DynamicPageTitle = forwardRef<HTMLDivElement, DynamicPageTitlePropTypes>((
128148
onToggleHeaderContentVisibility,
129149
actionsToolbarProps,
130150
navigationActionsToolbarProps,
151+
expandedContent,
152+
snappedContent,
131153
...rest
132154
} = props as InternalProps;
133155

@@ -300,6 +322,11 @@ const DynamicPageTitle = forwardRef<HTMLDivElement, DynamicPageTitlePropTypes>((
300322
</div>
301323
</FlexBox>
302324
)}
325+
{props?.['data-header-content-visible']
326+
? expandedContent
327+
: props['data-is-snapped-rendered-outside']
328+
? undefined
329+
: snappedContent}
303330
</FlexBox>
304331
);
305332
});

packages/main/src/components/ObjectPage/ObjectPage.jss.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,6 @@ export const styles = {
136136
padding: 0
137137
}
138138
},
139-
titleInHeader: { padding: 0 }
139+
titleInHeader: { padding: 0 },
140+
snappedContent: { gridColumn: '1 / span 2' }
140141
};

packages/main/src/components/ObjectPage/ObjectPage.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ const meta = {
8585
<BreadcrumbsItem>Employee Details</BreadcrumbsItem>
8686
</Breadcrumbs>
8787
}
88+
expandedContent={<MessageStrip>Information (only visible if header content is expanded)</MessageStrip>}
89+
snappedContent={<MessageStrip>Information (only visible if header content is collapsed/snapped)</MessageStrip>}
8890
>
8991
<ObjectStatus state={ValueState.Success}>employed</ObjectStatus>
9092
</DynamicPageTitle>

packages/main/src/components/ObjectPage/index.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,8 @@ const ObjectPage = forwardRef<HTMLDivElement, ObjectPagePropTypes>((props, ref)
611611
[onToggleHeaderContentVisibility, headerCollapsed, titleHeaderNotClickable]
612612
);
613613

614+
const snappedHeaderInObjPage = headerTitle && headerTitle.props.snappedContent && headerCollapsed === true && !!image;
615+
614616
const renderTitleSection = useCallback(
615617
(inHeader = false) => {
616618
const titleInHeaderClass = inHeader ? classes.titleInHeader : undefined;
@@ -620,18 +622,31 @@ const ObjectPage = forwardRef<HTMLDivElement, ObjectPagePropTypes>((props, ref)
620622
showSubHeaderRight: true,
621623
className: clsx(titleInHeaderClass, headerTitle?.props?.className),
622624
'data-not-clickable': titleHeaderNotClickable,
623-
onToggleHeaderContentVisibility: onTitleClick
625+
onToggleHeaderContentVisibility: onTitleClick,
626+
'data-header-content-visible': headerContent && headerCollapsed !== true,
627+
'data-is-snapped-rendered-outside': snappedHeaderInObjPage
624628
});
625629
}
626630
return cloneElement(headerTitle, {
627631
className: clsx(titleInHeaderClass, headerTitle?.props?.className),
628632
'data-not-clickable': titleHeaderNotClickable,
629-
onToggleHeaderContentVisibility: onTitleClick
633+
onToggleHeaderContentVisibility: onTitleClick,
634+
'data-header-content-visible': headerContent && headerCollapsed !== true,
635+
'data-is-snapped-rendered-outside': snappedHeaderInObjPage
630636
});
631637
},
632-
[headerTitle, titleHeaderNotClickable, onTitleClick]
638+
[headerTitle, titleHeaderNotClickable, onTitleClick, headerCollapsed, snappedHeaderInObjPage, !!headerContent]
633639
);
634640

641+
const isInitial = useRef(true);
642+
useEffect(() => {
643+
if (!isInitial.current) {
644+
scrollTimeout.current = performance.now() + 200;
645+
} else {
646+
isInitial.current = false;
647+
}
648+
}, [snappedHeaderInObjPage]);
649+
635650
const renderHeaderContentSection = useCallback(() => {
636651
if (headerContent?.props) {
637652
return cloneElement(headerContent, {
@@ -763,6 +778,11 @@ const ObjectPage = forwardRef<HTMLDivElement, ObjectPagePropTypes>((props, ref)
763778
<CollapsedAvatar image={image} imageShapeCircle={imageShapeCircle} />
764779
)}
765780
{headerTitle && renderTitleSection()}
781+
{snappedHeaderInObjPage && (
782+
<div className={classes.snappedContent} data-component-name="ATwithImageSnappedContentContainer">
783+
{headerTitle.props.snappedContent}
784+
</div>
785+
)}
766786
</header>
767787
{renderHeaderContentSection()}
768788
{headerContent && headerTitle && (

0 commit comments

Comments
 (0)