Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 93 additions & 18 deletions packages/main/src/components/ObjectPage/ObjectPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('ObjectPage', () => {
cy.get('[ui5-tabcontainer]').findUi5TabByText('Section 15').should('have.attr', 'aria-selected', 'true');

if (mode === ObjectPageMode.Default) {
cy.findByTestId('op').scrollTo(0, 4750);
cy.findByTestId('op').scrollTo(0, 4858);

cy.findByText('Content 7').should('be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Section 7').should('have.attr', 'aria-selected', 'true');
Expand All @@ -124,7 +124,7 @@ describe('ObjectPage', () => {
for (let i = 0; i < 15; i++) {
cy.findByText('Add').click();
}
cy.findByTestId('op').scrollTo(0, 4750);
cy.findByTestId('op').scrollTo(0, 4858);

cy.findByText('Content 7').should('be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Section 7').should('have.attr', 'aria-selected', 'true');
Expand Down Expand Up @@ -374,12 +374,6 @@ describe('ObjectPage', () => {
);
cy.wait(200);

// first titleText should never be displayed (not.be.visible doesn't work here - only invisible for sighted users)
cy.findByText('Goals')
.parent()
.should('have.css', 'width', '1px')
.and('have.css', 'margin', '-1px')
.and('have.css', 'position', 'absolute');
cy.findByText('Employment').should('not.be.visible');
cy.findByText('Test').should('be.visible');

Expand Down Expand Up @@ -712,19 +706,19 @@ describe('ObjectPage', () => {
};
cy.mount(<TestComp height="2000px" mode={ObjectPageMode.Default} />);
cy.findByText('Update Heights').click();
cy.findByText('{"offset":1080,"scroll":2290}').should('exist');
cy.findByText('{"offset":1080,"scroll":2330}').should('exist');

cy.findByTestId('op').scrollTo('bottom');
cy.findByText('Update Heights').click({ force: true });
cy.findByText('{"offset":1080,"scroll":2290}').should('exist');
cy.findByText('{"offset":1080,"scroll":2330}').should('exist');

cy.mount(<TestComp height="2000px" withFooter mode={ObjectPageMode.Default} />);
cy.findByText('Update Heights').click();
cy.findByText('{"offset":1080,"scroll":2330}').should('exist');
cy.findByText('{"offset":1080,"scroll":2370}').should('exist');

cy.findByTestId('op').scrollTo('bottom');
cy.findByText('Update Heights').click({ force: true });
cy.findByText('{"offset":1080,"scroll":2330}').should('exist');
cy.findByText('{"offset":1080,"scroll":2370}').should('exist');

cy.mount(<TestComp height="400px" mode={ObjectPageMode.Default} />);
cy.findByText('Update Heights').click();
Expand Down Expand Up @@ -923,12 +917,6 @@ describe('ObjectPage', () => {
cy.get('[ui5-tabcontainer]').findUi5TabByText('Goals').click();
cy.findByText('Custom Header Section One').should('be.visible');
cy.findByText('toggle titleText1').click({ scrollBehavior: false, force: true });
// first titleText should never be displayed (not.be.visible doesn't work here - only invisible for sighted users)
cy.findByText('Goals')
.parent()
.should('have.css', 'width', '1px')
.and('have.css', 'margin', '-1px')
.and('have.css', 'position', 'absolute');
cy.findByText('Custom Header Section One').should('be.visible');

cy.get('[ui5-tabcontainer]').findUi5TabByText('Personal').click();
Expand Down Expand Up @@ -1843,6 +1831,55 @@ describe('ObjectPage', () => {
}
cy.focused().should('be.visible').and('have.attr', 'ui5-table-row');
});

it('sticky headers', () => {
cy.mount(
<ObjectPage
titleArea={DPTitle}
headerArea={DPContent}
mode="IconTabBar"
style={{ height: '1000px' }}
data-testid="op"
>
{OPContent}
{OPContentWithCustomHeaderSections}
</ObjectPage>,
);

cy.findByText('Goals').should('not.be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Employment').click();
cy.findByText('Employment').should('not.be.visible');
cy.findByText('Employee Details').parent().should('have.css', 'position', 'sticky');

cy.mount(
<ObjectPage titleArea={DPTitle} headerArea={DPContent} style={{ height: '1000px' }} data-testid="op">
{OPContent}
{OPContentWithCustomHeaderSections}
</ObjectPage>,
);

cy.findByText('Goals').should('be.visible').parent().should('have.css', 'position', 'sticky');
cy.findByTestId('op').scrollTo(0, 500);
cy.findByText('Goals').should('be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Personal').click();
// has subsections -> only subsection headers are sticky
cy.findByText('Personal').should('be.visible').parent().should('have.css', 'position', 'static');
cy.findByText('Connect').should('be.visible').parent().should('have.css', 'position', 'sticky');
cy.findByTestId('op').scrollTo(0, 2500);
cy.findByText('Goals').should('not.be.visible');
cy.findByText('Payment Information').should('be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Custom Header Section One').click();
cy.findByText('Custom Header Section One').should('be.visible').parent().should('have.css', 'position', 'sticky');
cy.findByTestId('op').scrollTo(0, 3500);
cy.findByText('Custom Header Section One').should('be.visible');
cy.get('[ui5-tabcontainer]').findUi5TabByText('Custom Header Section Two').click();
// has subsections -> only subsection headers are sticky
cy.findByText('Custom Header Section Two').should('be.visible').parent().should('have.css', 'position', 'static');
cy.findByText('Subsection1').should('be.visible').parent().should('have.css', 'position', 'sticky');
cy.findByTestId('op').scrollTo(0, 4000);
cy.findByText('Custom Header Section Two').should('not.be.visible');
cy.findByText('Subsection1').should('be.visible');
});
});

const DPTitle = (
Expand Down Expand Up @@ -1942,6 +1979,44 @@ const OPContent = [
</ObjectPageSection>,
];

const OPContentWithCustomHeaderSections = [
<ObjectPageSection
key={'customheader1'}
titleText="Custom Header Section One"
hideTitleText
id="custom1"
header={<Title>Custom Header Section One</Title>}
>
<div style={{ width: '100%', height: '200px', background: 'lightgreen' }} />
</ObjectPageSection>,
<ObjectPageSection
key={'customheader2'}
titleText="Custom Header Section Two"
hideTitleText
id="custom2"
header={<MessageStrip hideCloseButton>Custom Header Section Two</MessageStrip>}
>
<ObjectPageSubSection
titleText="Subsection1"
id="sub1"
actions={
<>
<Button design={ButtonDesign.Emphasized} style={{ minWidth: '120px' }}>
Custom Action
</Button>
<Button design={ButtonDesign.Transparent} icon="action-settings" tooltip="settings" />
<Button design={ButtonDesign.Transparent} icon="download" tooltip="download" />
</>
}
>
<div style={{ width: '100%', height: '300px', background: 'cadetblue' }} />
</ObjectPageSubSection>
<ObjectPageSubSection titleText="Subsection2" id="sub2">
<div style={{ width: '100%', height: '300px', background: 'cadetblue' }} />
</ObjectPageSubSection>
</ObjectPageSection>,
];

const HeaderWithLargeForm = (
<ObjectPageHeader>
<Form layout="S1 M2 L2 XL2">
Expand Down
21 changes: 5 additions & 16 deletions packages/main/src/components/ObjectPage/ObjectPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
container: objectPage / inline-size;
--_ui5wcr_ObjectPage_header_display: block;
--_ui5wcr_ObjectPage_title_fontsize: var(--sapObjectHeader_Title_FontSize);
--_ui5wcr_ObjectPage_header_height: 0;

box-sizing: border-box;
position: relative;
Expand All @@ -23,19 +24,6 @@
&[data-in-iframe='true'] {
scroll-behavior: auto;
}

/*invisible first heading*/
section[id*='ObjectPageSection-']:first-of-type > div[role='heading'] {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
white-space: nowrap;
}
}

.iconTabBarMode section[data-component-name='ObjectPageSection'] > div[role='heading'] {
Expand All @@ -49,7 +37,7 @@
background-color: var(--sapObjectHeader_Background);
position: sticky;
inset-block-start: 0;
z-index: 4;
z-index: 5;
cursor: pointer;
display: grid;
grid-auto-columns: 100%;
Expand Down Expand Up @@ -102,7 +90,7 @@

.anchorBar {
position: sticky;
z-index: 4;
z-index: 5;
}

.tabContainerSpacer {
Expand All @@ -112,7 +100,7 @@

.tabContainer {
position: sticky;
z-index: 3;
z-index: 4;
background: var(--sapObjectHeader_Background);
}

Expand Down Expand Up @@ -169,6 +157,7 @@
position: sticky;
inset-block-end: 0.5rem;
margin: 0 0.5rem;
z-index: 4;
}

.footerSpacer {
Expand Down
63 changes: 62 additions & 1 deletion packages/main/src/components/ObjectPage/ObjectPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,68 @@ export const SectionWithCustomHeader: Story = {
aria-label="Personal"
header={<MessageStrip hideCloseButton>Custom Header Section Two</MessageStrip>}
>
<div style={{ width: '100%', height: '500px', background: 'cadetblue' }} />
<ObjectPageSubSection
titleText="Connect"
id="personal-connect"
aria-label="Connect"
actions={
<>
<Button design={ButtonDesign.Emphasized} style={{ minWidth: '120px' }}>
Custom Action
</Button>
<Button design={ButtonDesign.Transparent} icon="action-settings" tooltip="settings" />
<Button design={ButtonDesign.Transparent} icon="download" tooltip="download" />
</>
}
>
<Form style={{ alignItems: 'baseline' }}>
<FormGroup headerText="Phone Numbers">
<FormItem labelContent={<Label showColon>Home</Label>}>
<Text>+1 234-567-8901</Text>
<Text>+1 234-567-5555</Text>
</FormItem>
</FormGroup>
<FormGroup headerText="Social Accounts">
<FormItem labelContent={<Label showColon>LinkedIn</Label>}>
<Text>/DeniseSmith</Text>
</FormItem>
<FormItem labelContent={<Label showColon>Twitter</Label>}>
<Text>@DeniseSmith</Text>
</FormItem>
</FormGroup>
<FormGroup headerText="Addresses">
<FormItem labelContent={<Label showColon>Home Address</Label>}>
<Text>2096 Mission Street</Text>
</FormItem>
<FormItem labelContent={<Label showColon>Mailing Address</Label>}>
<Text>PO Box 32114</Text>
</FormItem>
</FormGroup>
<FormGroup headerText="Mailing Address">
<FormItem labelContent={<Label showColon>Work</Label>}>
<Text>[email protected]</Text>
</FormItem>
</FormGroup>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Payment Information"
id="personal-payment-information"
aria-label="Payment Information"
>
<Form>
<FormGroup headerText="Salary">
<FormItem labelContent={<Label showColon>Bank Transfer</Label>}>
<Text>Money Bank, Inc.</Text>
</FormItem>
</FormGroup>
<FormGroup headerText="Payment method for Expenses">
<FormItem labelContent={<Label showColon>Extra Travel Expenses</Label>}>
<Text>Cash 100 USD</Text>
</FormItem>
</FormGroup>
</Form>
</ObjectPageSubSection>
</ObjectPageSection>
<ObjectPageSection
titleText="Employment"
Expand Down
7 changes: 6 additions & 1 deletion packages/main/src/components/ObjectPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { useOnScrollEnd } from './useOnScrollEnd.js';
const ObjectPageCssVariables = {
headerDisplay: '--_ui5wcr_ObjectPage_header_display',
titleFontSize: '--_ui5wcr_ObjectPage_title_fontsize',
fullHeaderHeight: '--_ui5wcr_ObjectPage_header_height',
};

const TAB_CONTAINER_HEADER_HEIGHT = 44 + 4; // tabbar height + custom 4px padding-block-start
Expand Down Expand Up @@ -610,7 +611,11 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
});
const objectPageStyles: CSSProperties = {
...style,
};
[ObjectPageCssVariables.fullHeaderHeight]:
headerPinned || scrolledHeaderExpanded
? `${topHeaderHeight + (headerCollapsed === true ? 0 : headerContentHeight) + TAB_CONTAINER_HEADER_HEIGHT}px`
: `${topHeaderHeight + TAB_CONTAINER_HEADER_HEIGHT}px`,
} as CSSProperties;
if (headerCollapsed === true && headerArea) {
objectPageStyles[ObjectPageCssVariables.titleFontSize] = ThemingParameters.sapObjectHeader_Title_SnappedFontSize;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.section {
box-sizing: border-box;
background: var(--sapBackgroundColor);

&:first-of-type {
margin-block-start: 1px;
Expand All @@ -16,6 +17,10 @@
outline-offset: calc(-1 * var(--sapContent_FocusWidth));
}

.outlineSpacerDiv {
height: 2px;
}

.headerContainer {
padding-block: 0.5rem;
color: var(--sapGroup_TitleTextColor);
Expand All @@ -28,6 +33,14 @@
height: 2.25rem;
}

.sticky {
position: sticky;
background: var(--sapBackgroundColor);
/*-1 -> subpixel rounding errors */
inset-block-start: calc(var(--_ui5wcr_ObjectPage_header_height) - 1px);
z-index: 3;
}

.title {
height: 2.25rem;
line-height: 2.25rem;
Expand Down
7 changes: 5 additions & 2 deletions packages/main/src/components/ObjectPageSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,15 @@ const ObjectPageSection = forwardRef<HTMLElement, ObjectPageSectionPropTypes>((p
onBlur={objectPageMode === ObjectPageMode.Default ? handleBlur : props.onBlur}
onKeyDown={objectPageMode === ObjectPageMode.Default ? handleKeyDown : props.onKeyDown}
>
{!!header && <div className={classNames.headerContainer}>{header}</div>}
<div className={classNames.outlineSpacerDiv} aria-hidden="true" />
{!!header && (
<div className={clsx(classNames.headerContainer, !hasSubSection ? classNames.sticky : undefined)}>{header}</div>
)}
{!hideTitleText && (
<div
role="heading"
aria-level={parseInt(titleTextLevel.slice(1))}
className={classNames.titleContainer}
className={clsx(classNames.titleContainer, !header && !hasSubSection ? classNames.sticky : undefined)}
data-component-name="ObjectPageSectionTitleText"
>
<div className={titleClasses}>{titleText}</div>
Expand Down
Loading
Loading