diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx
index 9fc6b28a869f..d3bfd3dc60f1 100644
--- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx
+++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx
@@ -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(
+
+
+ Page Title
+
+
+
+
Line 1
+
Line 2
+
Line 3
+
Line 4
+
Rack: 34
+
+
+
+
+
+ );
+
+ 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(
+
+
+ Page Title
+
+
+
+
Line 1
+
Line 2
+
Line 3
+
Line 4
+
Rack: 34
+
+
+
+
+
+
+ );
+
+ 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(
+
+
+ Page Title
+
+
+
+
Line 1
+
Line 2
+
Line 3
+
Line 4
+
Rack: 34
+
+
+
+
+
+
+ );
+
+ 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", () => {
diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts
index c3aab7f13465..f5cb8391c1bc 100644
--- a/packages/fiori/src/DynamicPage.ts
+++ b/packages/fiori/src/DynamicPage.ts
@@ -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;
@@ -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 {
@@ -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" });
});
}
@@ -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();