From ab8e47e1699d78f6a7a8e2397fc10b1f2b661283 Mon Sep 17 00:00:00 2001 From: Plamen Ivanov Date: Mon, 20 Apr 2026 15:21:35 +0300 Subject: [PATCH 1/2] fix(ui5-dynamic-page): prevent unwanted content scroll on focus Keep pointer interactions stable by skipping focus-driven auto-scroll after pointer down in the content area. For keyboard navigation, scroll only when the focused element is actually clipped by the effective visible content viewport. Keep smooth scrolling when auto-scroll is needed. Add and update DynamicPage Cypress coverage for: - no scroll shift on content button click with partially hidden header - no scroll shift on tab focus when target is already visible Fixes: #13332 --- .../fiori/cypress/specs/DynamicPage.cy.tsx | 103 ++++++++++++++++++ packages/fiori/src/DynamicPage.ts | 36 ++++++ packages/fiori/src/DynamicPageTemplate.tsx | 1 + 3 files changed, 140 insertions(+) diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 9fc6b28a869f..c9cf14e6828a 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -305,6 +305,109 @@ 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); + }); + }); + }); }); describe("Scroll", () => { diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index c3aab7f13465..3e3c2b01554d 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -190,6 +190,7 @@ class DynamicPage extends UI5Element { skipSnapOnScroll = false; showHeaderInStickArea = false; isToggled = false; + _skipContentScrollOnFocus = false; @property({ type: Boolean }) _headerSnapped = false; @@ -197,6 +198,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; @@ -453,14 +457,32 @@ class DynamicPage extends UI5Element { this.dynamicPageTitle?.removeAttribute("hovered"); } + onContentPointerDown() { + this._skipContentScrollOnFocus = true; + + requestAnimationFrame(() => { + this._skipContentScrollOnFocus = false; + }); + } + onContentFocusIn(e: FocusEvent) { const target = e.target as HTMLElement; this.setScrollPadding({ start: this.topAreaHeight, end: this.endAreaHeight }); + + if (this._skipContentScrollOnFocus) { + this._skipContentScrollOnFocus = false; + return; + } + // 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 +497,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(); diff --git a/packages/fiori/src/DynamicPageTemplate.tsx b/packages/fiori/src/DynamicPageTemplate.tsx index 91c1e81f320c..394d5ca89a50 100644 --- a/packages/fiori/src/DynamicPageTemplate.tsx +++ b/packages/fiori/src/DynamicPageTemplate.tsx @@ -36,6 +36,7 @@ export default function DynamicPageTemplate(this: DynamicPage) {
From 1f9884ff0cf65baa03d361806aa52ce2f69bbbc9 Mon Sep 17 00:00:00 2001 From: Plamen Ivanov Date: Fri, 24 Apr 2026 14:08:55 +0300 Subject: [PATCH 2/2] - fixed review comments --- .../fiori/cypress/specs/DynamicPage.cy.tsx | 51 +++++++++++++++++++ packages/fiori/src/DynamicPage.ts | 28 ++++------ packages/fiori/src/DynamicPageTemplate.tsx | 1 - 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index c9cf14e6828a..d3bfd3dc60f1 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -408,6 +408,57 @@ describe("DynamicPage", () => { }); }); }); + + 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
+
+
+ +