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
154 changes: 154 additions & 0 deletions packages/fiori/cypress/specs/DynamicPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,160 @@ describe("DynamicPage", () => {

cy.get("[data-testid='bottom-input']").should("be.visible");
});

it("does not scroll content when a button is clicked while the header is partially hidden", () => {
let clickCount = 0;

cy.mount(
<DynamicPage style={{ height: "400px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div style={{ height: "180px" }}>
<div>Line 1</div>
<div>Line 2</div>
<div>Line 3</div>
<div>Line 4</div>
<div>Rack: 34</div>
</div>
</DynamicPageHeader>
<Button data-testid="content-button" onClick={() => { clickCount += 1; }}>test</Button>
<div style={{ height: "1000px" }}></div>
</DynamicPage>
);

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.then(($container) => {
$container[0].scrollTop = 120;
});

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.then(($container) => {
const initialScrollTop = $container[0].scrollTop;

cy.get("[data-testid='content-button']")
.realClick();

cy.then(() => {
expect(clickCount).to.equal(1);
});

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.should(($updatedContainer) => {
expect($updatedContainer[0].scrollTop).to.be.closeTo(initialScrollTop, 1);
});
});
});

it("does not scroll content when a visible button receives keyboard focus while the header is partially hidden", () => {
cy.mount(
<DynamicPage style={{ height: "400px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div style={{ height: "180px" }}>
<div>Line 1</div>
<div>Line 2</div>
<div>Line 3</div>
<div>Line 4</div>
<div>Rack: 34</div>
</div>
</DynamicPageHeader>
<button data-testid="first-content-button">first</button>
<Button data-testid="content-button">test</Button>
<div style={{ height: "1000px" }}></div>
</DynamicPage>
);

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.then(($container) => {
$container[0].scrollTop = 120;
});

cy.get("[data-testid='first-content-button']")
.focus()
.should("be.focused");

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.then(($container) => {
const initialScrollTop = $container[0].scrollTop;

cy.realPress("Tab");

cy.get("[data-testid='content-button']")
.should("be.focused");

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.should(($updatedContainer) => {
expect($updatedContainer[0].scrollTop).to.be.closeTo(initialScrollTop, 1);
});
});
});

it("scrolls a partially clipped textarea into view when focused via Tab", () => {
cy.mount(
<DynamicPage style={{ height: "400px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div style={{ height: "180px" }}>
<div>Line 1</div>
<div>Line 2</div>
<div>Line 3</div>
<div>Line 4</div>
<div>Rack: 34</div>
</div>
</DynamicPageHeader>
<button data-testid="before-textarea">Before</button>
<textarea data-testid="target-textarea" style={{ display: "block", marginTop: "8px", height: "120px" }} />
<div style={{ height: "1000px" }}></div>
</DynamicPage>
);

cy.get("[ui5-dynamic-page]")
.shadow()
.find(".ui5-dynamic-page-scroll-container")
.then(($container) => {
$container[0].scrollTop = 120;
});

cy.get("[data-testid='before-textarea']")
.focus()
.should("be.focused");

cy.realPress("Tab");

cy.get("[data-testid='target-textarea']")
.should("be.focused");

cy.get("[ui5-dynamic-page]")
.then(($dp) => {
const dp = $dp[0] as DynamicPage;
const containerRect = dp.scrollContainer!.getBoundingClientRect();
const contentRect = dp.contentArea!.getBoundingClientRect();
const targetRect = (dp.querySelector("[data-testid='target-textarea']") as HTMLTextAreaElement).getBoundingClientRect();
const visibleTop = Math.max(containerRect.top, contentRect.top);
const visibleBottom = containerRect.bottom - dp.endAreaHeight;

expect(targetRect.top).to.be.at.least(visibleTop);
expect(targetRect.bottom).to.be.at.most(visibleBottom);
});
});
});

describe("Scroll", () => {
Expand Down
36 changes: 33 additions & 3 deletions packages/fiori/src/DynamicPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ class DynamicPage extends UI5Element {
@query(".ui5-dynamic-page-scroll-container")
scrollContainer?: HTMLElement;

@query(".ui5-dynamic-page-content")
contentArea?: HTMLElement;

@query("[ui5-dynamic-page-header-actions]")
headerActions?: DynamicPageHeaderActions;

Expand All @@ -220,10 +223,18 @@ class DynamicPage extends UI5Element {
return this.showFooter ? this.footerWrapper?.getBoundingClientRect().height || 0 : 0;
}

get topAreaHeight() {
get scrollPaddingTop() {
const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
const headerHeight = this.dynamicPageHeader?.getBoundingClientRect().height || 0;
return this._headerSnapped ? titleHeight : headerHeight + titleHeight;

if (this._headerSnapped) {
return titleHeight;
}

const fullHeight = headerHeight + titleHeight;
const scrollTop = this.scrollContainer?.scrollTop || 0;

return Math.max(titleHeight, fullHeight - scrollTop);
}

get dynamicPageTitle(): DynamicPageTitle | null {
Expand Down Expand Up @@ -455,12 +466,17 @@ class DynamicPage extends UI5Element {

onContentFocusIn(e: FocusEvent) {
const target = e.target as HTMLElement;
this.setScrollPadding({ start: this.topAreaHeight, end: this.endAreaHeight });
this.setScrollPadding({ start: this.scrollPaddingTop, end: this.endAreaHeight });

// textareas and similar elements appear "in view" even when partially
// hidden behind sticky header/footer.
// manual scroll brings them fully into view.
// another issue is that browsers do not reflect dynamic changes of scroll-padding
requestAnimationFrame(() => {
if (!this.isContentElementClipped(target)) {
return;
}

target.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
}
Expand All @@ -475,6 +491,20 @@ class DynamicPage extends UI5Element {
this.scrollContainer?.style.setProperty("scroll-padding-top", `${padding.start}px`);
this.scrollContainer?.style.setProperty("scroll-padding-bottom", `${padding.end}px`);
}

isContentElementClipped(target: HTMLElement) {
if (!this.scrollContainer || !target?.getBoundingClientRect) {
return false;
}

const targetRect = target.getBoundingClientRect();
const containerRect = this.scrollContainer.getBoundingClientRect();
const contentRect = this.contentArea?.getBoundingClientRect();
const visibleTop = Math.max(containerRect.top, contentRect?.top || containerRect.top);
const visibleBottom = containerRect.bottom - this.endAreaHeight;

return targetRect.top < visibleTop || targetRect.bottom > visibleBottom;
}
}

DynamicPage.define();
Expand Down
Loading