Skip to content

Commit 8ccd2eb

Browse files
feat(DynamicPage): enable sticky sub-headers support (#4377)
Closes #3989 --------- Co-authored-by: Marcus Notheis <[email protected]>
1 parent 47a08d4 commit 8ccd2eb

File tree

4 files changed

+210
-10
lines changed

4 files changed

+210
-10
lines changed

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

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,5 +359,108 @@ describe('DynamicPage', () => {
359359
cy.get('[icon="question-mark"]').should('have.length', 1).should('be.visible');
360360
});
361361

362+
it('with sticky sub-headers', () => {
363+
const renderContent = (stickyHeaderHeight) => (
364+
<>
365+
<div
366+
style={{
367+
position: 'sticky',
368+
width: '100%',
369+
height: '4rem',
370+
background: 'lightgreen',
371+
top: `${stickyHeaderHeight}px`
372+
}}
373+
>
374+
<span>Sticky Header</span>
375+
</div>
376+
<div style={{ width: '100%', background: 'orange', height: '10rem' }}>
377+
<span>Content 1</span>
378+
</div>
379+
<div
380+
style={{
381+
position: 'sticky',
382+
width: '100%',
383+
height: '8rem',
384+
background: 'lightgreen',
385+
top: `calc(${stickyHeaderHeight}px + 4rem)`
386+
}}
387+
>
388+
<span>Sticky Header 2</span>
389+
</div>
390+
<div style={{ background: 'lightblue', height: '2000px', width: '100%' }}>
391+
<span>Content 2</span>
392+
</div>
393+
</>
394+
);
395+
396+
function checksWithScroll() {
397+
cy.findByText('Sticky Header').should('be.visible');
398+
cy.findByText('Content 1').should('be.visible');
399+
cy.findByText('Sticky Header 2').should('be.visible');
400+
cy.findByText('Content 2').should('be.visible');
401+
402+
cy.findByTestId('dp').scrollTo(0, 800);
403+
404+
cy.findByText('Sticky Header').should('be.visible');
405+
cy.findByText('Content 1').should('not.be.visible');
406+
cy.findByText('Sticky Header 2').should('be.visible');
407+
cy.findByText('Content 2').should('not.be.visible');
408+
}
409+
410+
cy.mount(
411+
<DynamicPage
412+
data-testid="dp"
413+
style={{ height: '90vh' }}
414+
headerContent={<DynamicPageHeader>headerContent</DynamicPageHeader>}
415+
headerTitle={<DynamicPageTitle header={<div>Header</div>}>Status</DynamicPageTitle>}
416+
>
417+
{({ stickyHeaderHeight }) => {
418+
return renderContent(stickyHeaderHeight);
419+
}}
420+
</DynamicPage>
421+
);
422+
423+
checksWithScroll();
424+
425+
cy.mount(
426+
<DynamicPage
427+
data-testid="dp"
428+
style={{ height: '90vh' }}
429+
headerTitle={<DynamicPageTitle header={<div>Header</div>}>Status</DynamicPageTitle>}
430+
>
431+
{({ stickyHeaderHeight }) => {
432+
return renderContent(stickyHeaderHeight);
433+
}}
434+
</DynamicPage>
435+
);
436+
437+
checksWithScroll();
438+
439+
cy.mount(
440+
<DynamicPage data-testid="dp" style={{ height: '90vh' }}>
441+
{({ stickyHeaderHeight }) => {
442+
return renderContent(stickyHeaderHeight);
443+
}}
444+
</DynamicPage>
445+
);
446+
447+
checksWithScroll();
448+
449+
cy.mount(
450+
<DynamicPage
451+
data-testid="dp"
452+
style={{ height: '90vh' }}
453+
headerContent={<DynamicPageHeader>headerContent</DynamicPageHeader>}
454+
headerTitle={<DynamicPageTitle header={<div>Header</div>}>Status</DynamicPageTitle>}
455+
footer={<div>footer</div>}
456+
>
457+
{({ stickyHeaderHeight }) => {
458+
return renderContent(stickyHeaderHeight);
459+
}}
460+
</DynamicPage>
461+
);
462+
463+
checksWithScroll();
464+
});
362465
cypressPassThroughTestsFactory(DynamicPage);
363466
});

packages/main/src/components/DynamicPage/DynamicPage.mdx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,50 @@ With these objects it's possible to e.g. configure the overflow button displayed
4141

4242
<Canvas of={ComponentStories.WithCustomOverflowButton} />
4343

44+
## With sticky sub-headers
45+
46+
To display sticky sub-headers that line-up with the standard header of the `DynamicPage` you're able to render `children` as function.
47+
There you receive an object as parameter that offers the following properties:
48+
49+
- `stickyHeaderHeight`: The height of the `DynamicPage` header sections which will stick to the top of the page.
50+
51+
<Canvas of={ComponentStories.StickySubHeaders} sourceState="none" />
52+
53+
### Code
54+
55+
```jsx
56+
<DynamicPage {...props}>
57+
{({ stickyHeaderHeight }) => (
58+
<>
59+
<div
60+
style={{
61+
position: 'sticky',
62+
width: '100%',
63+
height: '4rem',
64+
background: 'lightgreen',
65+
top: `${stickyHeaderHeight}px`
66+
}}
67+
>
68+
Sticky Header
69+
</div>
70+
<div style={{ width: '100%', background: 'orange', height: '10rem' }}>Content</div>
71+
<div
72+
style={{
73+
position: 'sticky',
74+
width: '100%',
75+
height: '8rem',
76+
background: 'lightgreen',
77+
top: `calc(${stickyHeaderHeight}px + 4rem)`
78+
}}
79+
>
80+
Sticky Header 2
81+
</div>
82+
<div style={{ background: 'lightblue', height: '2000px', width: '100%' }}>Content</div>
83+
</>
84+
)}
85+
</DynamicPage>
86+
```
87+
4488
<Markdown>{SubcomponentsSection}</Markdown>
4589

4690
## DynamicPageTitle

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

Lines changed: 52 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 { useReducer, useState } from 'react';
10+
import { useEffect, useReducer, useRef, useState } from 'react';
1111
import {
1212
Badge,
1313
Bar,
@@ -343,3 +343,54 @@ export const WithCustomOverflowButton: Story = {
343343
);
344344
}
345345
};
346+
347+
export const StickySubHeaders: Story = {
348+
name: 'with sticky sub-headers',
349+
render(args) {
350+
return (
351+
<DynamicPage
352+
{...args}
353+
footer={
354+
<Bar
355+
design={BarDesign.FloatingFooter}
356+
endContent={
357+
<>
358+
<Button design={ButtonDesign.Positive}>Accept</Button>
359+
<Button design={ButtonDesign.Negative}>Reject</Button>
360+
</>
361+
}
362+
/>
363+
}
364+
>
365+
{({ stickyHeaderHeight }) => (
366+
<>
367+
<div
368+
style={{
369+
position: 'sticky',
370+
width: '100%',
371+
height: '4rem',
372+
background: 'lightgreen',
373+
top: `${stickyHeaderHeight}px`
374+
}}
375+
>
376+
Sticky Header
377+
</div>
378+
<div style={{ width: '100%', background: 'orange', height: '10rem' }}>Content</div>
379+
<div
380+
style={{
381+
position: 'sticky',
382+
width: '100%',
383+
height: '8rem',
384+
background: 'lightgreen',
385+
top: `calc(${stickyHeaderHeight}px + 4rem)`
386+
}}
387+
>
388+
Sticky Header 2
389+
</div>
390+
<div style={{ background: 'lightblue', height: '2000px', width: '100%' }}>Content</div>
391+
</>
392+
)}
393+
</DynamicPage>
394+
);
395+
}
396+
};

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { DynamicPageAnchorBar } from '../DynamicPageAnchorBar';
1111
import { FlexBox } from '../FlexBox';
1212
import { DynamicPageCssVariables, styles } from './DynamicPage.jss';
1313

14-
export interface DynamicPagePropTypes extends Omit<CommonProps, 'title'> {
14+
export interface DynamicPagePropTypes extends Omit<CommonProps, 'title' | 'children'> {
1515
/**
1616
* Determines the background color of DynamicPage.
1717
*/
@@ -49,8 +49,10 @@ export interface DynamicPagePropTypes extends Omit<CommonProps, 'title'> {
4949
footer?: ReactElement;
5050
/**
5151
* React element or node array which defines the content.
52+
*
53+
* __Note:__ Assigning `children` as function is recommended when implementing sticky sub-headers. You can find out more about this [here](https://sap.github.io/ui5-webcomponents-react/?path=/story/layouts-floorplans-dynamicpage--sticky-sub-headers).
5254
*/
53-
children?: ReactNode | ReactNode[];
55+
children?: ReactNode | ReactNode[] | ((payload: { stickyHeaderHeight: number }) => ReactElement);
5456
/**
5557
* Defines internally used a11y properties.
5658
*/
@@ -260,6 +262,11 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
260262
}
261263
}, [headerCollapsed]);
262264

265+
const top =
266+
headerState === HEADER_STATES.VISIBLE_PINNED || headerState === HEADER_STATES.VISIBLE
267+
? (headerContentRef?.current?.offsetHeight ?? 0) + topHeaderHeight
268+
: topHeaderHeight;
269+
263270
return (
264271
<div
265272
ref={componentRef}
@@ -290,12 +297,7 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
290297
data-component-name="DynamicPageAnchorBarContainer"
291298
className={classes.anchorBar}
292299
ref={anchorBarRef}
293-
style={{
294-
top:
295-
headerState === HEADER_STATES.VISIBLE_PINNED || headerState === HEADER_STATES.VISIBLE
296-
? (headerContentRef?.current?.offsetHeight ?? 0) + topHeaderHeight
297-
: topHeaderHeight
298-
}}
300+
style={{ top }}
299301
>
300302
<DynamicPageAnchorBar
301303
headerContentPinnable={headerContentPinnable}
@@ -317,7 +319,7 @@ const DynamicPage = forwardRef<HTMLDivElement, DynamicPagePropTypes>((props, ref
317319
paddingBlockEnd: footer ? '1rem' : 0
318320
}}
319321
>
320-
{children}
322+
{typeof children === 'function' ? children({ stickyHeaderHeight: top + 1 /*anchorBar height */ }) : children}
321323
</div>
322324
{footer && (
323325
<div

0 commit comments

Comments
 (0)