From 37648b559391335dd66a6681af89c8b1ca54f87a Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Thu, 25 Sep 2025 14:03:35 +0300 Subject: [PATCH 1/4] fix(ui5-dynamic-page): improve focus visibility for keyboard navigation --- .../fiori/cypress/specs/DynamicPage.cy.tsx | 75 +++++++++++++++++++ packages/fiori/src/DynamicPage.ts | 14 ++++ 2 files changed, 89 insertions(+) diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 871843e8f71c..1cd70b96d500 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -3,6 +3,7 @@ import DynamicPageTitle from "../../src/DynamicPageTitle.js"; import DynamicPageHeader from "../../src/DynamicPageHeader.js"; import Bar from "@ui5/webcomponents/dist/Bar.js"; import Button from "@ui5/webcomponents/dist/Button.js"; +import Input from "@ui5/webcomponents/dist/Input.js"; import { setAnimationMode } from "@ui5/webcomponents-base"; before(() => { @@ -732,6 +733,80 @@ describe("Page general interaction", () => { }); }); +describe("Focus Accessibility", () => { + it("scrolls focused elements into view when they are clipped by header/footer", () => { + cy.mount( + + +
Page Title
+
+ +
+ Header Content +
+
+
+
+ +
+
+ +
+
+ +
+
+ + + +
+ ); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container") + .scrollTo(0, 200); + + cy.get("[data-testid='middle-input']") + .shadow() + .find("input") + .focus(); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container") + .then(($container) => { + const scrollTop = $container[0].scrollTop; + + expect(scrollTop).to.not.equal(200); + }); + + cy.get("[data-testid='middle-input']") + .shadow() + .find("input") + .should("be.focused"); + + cy.get("[data-testid='bottom-input']") + .shadow() + .find("input") + .focus(); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container") + .then(($container) => { + const scrollTop = $container[0].scrollTop; + + expect(scrollTop).to.be.greaterThan(300); + }); + + cy.get("[data-testid='bottom-input']") + .shadow() + .find("input") + .should("be.focused"); + }); +}); + describe("Page layout when content has 100% height", () => { it("footer does not hide the content", () => { cy.mount( diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index 587367c267a5..b08c5ccf0a23 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -202,6 +202,20 @@ class DynamicPage extends UI5Element { super(); } + onAfterRendering() { + if (this.scrollContainer && !this.scrollContainer.dataset.focusListenerAdded) { + this.scrollContainer.addEventListener("focusin", (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && target !== this.scrollContainer) { + requestAnimationFrame(() => { + target.scrollIntoView({ behavior: "smooth", block: "nearest" }); + }); + } + }); + this.scrollContainer.dataset.focusListenerAdded = "true"; + } + } + onBeforeRendering() { if (this.dynamicPageTitle) { this.dynamicPageTitle.snapped = this._headerSnapped; From 42d5cf8b57066f7b5fcd526041625b55f017960c Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Thu, 2 Oct 2025 14:39:56 +0300 Subject: [PATCH 2/4] fix double snap of the header --- .../fiori/cypress/specs/DynamicPage.cy.tsx | 74 ------------------- packages/fiori/src/DynamicPage.ts | 53 +++++++++---- 2 files changed, 37 insertions(+), 90 deletions(-) diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 1cd70b96d500..8fe5d90e76cd 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -733,80 +733,6 @@ describe("Page general interaction", () => { }); }); -describe("Focus Accessibility", () => { - it("scrolls focused elements into view when they are clipped by header/footer", () => { - cy.mount( - - -
Page Title
-
- -
- Header Content -
-
-
-
- -
-
- -
-
- -
-
- - - -
- ); - - cy.get("[ui5-dynamic-page]") - .shadow() - .find(".ui5-dynamic-page-scroll-container") - .scrollTo(0, 200); - - cy.get("[data-testid='middle-input']") - .shadow() - .find("input") - .focus(); - - cy.get("[ui5-dynamic-page]") - .shadow() - .find(".ui5-dynamic-page-scroll-container") - .then(($container) => { - const scrollTop = $container[0].scrollTop; - - expect(scrollTop).to.not.equal(200); - }); - - cy.get("[data-testid='middle-input']") - .shadow() - .find("input") - .should("be.focused"); - - cy.get("[data-testid='bottom-input']") - .shadow() - .find("input") - .focus(); - - cy.get("[ui5-dynamic-page]") - .shadow() - .find(".ui5-dynamic-page-scroll-container") - .then(($container) => { - const scrollTop = $container[0].scrollTop; - - expect(scrollTop).to.be.greaterThan(300); - }); - - cy.get("[data-testid='bottom-input']") - .shadow() - .find("input") - .should("be.focused"); - }); -}); - describe("Page layout when content has 100% height", () => { it("footer does not hide the content", () => { cy.mount( diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index b08c5ccf0a23..e2ba371f5bee 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -188,6 +188,7 @@ class DynamicPage extends UI5Element { skipSnapOnScroll = false; showHeaderInStickArea = false; isToggled = false; + _focusInHandler?: (e: FocusEvent) => void; @property({ type: Boolean }) _headerSnapped = false; @@ -202,20 +203,6 @@ class DynamicPage extends UI5Element { super(); } - onAfterRendering() { - if (this.scrollContainer && !this.scrollContainer.dataset.focusListenerAdded) { - this.scrollContainer.addEventListener("focusin", (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && target !== this.scrollContainer) { - requestAnimationFrame(() => { - target.scrollIntoView({ behavior: "smooth", block: "nearest" }); - }); - } - }); - this.scrollContainer.dataset.focusListenerAdded = "true"; - } - } - onBeforeRendering() { if (this.dynamicPageTitle) { this.dynamicPageTitle.snapped = this._headerSnapped; @@ -234,10 +221,44 @@ class DynamicPage extends UI5Element { this.scrollContainer.style.setProperty("scroll-padding-block-end", `${footerHeight}px`); if (this._headerSnapped) { - this.scrollContainer.style.setProperty("scroll-padding-block-start", `${titleHeight}px`); + this.scrollContainer.style.setProperty("scroll-padding-block-start", `${titleHeight}px`); } else { - this.scrollContainer.style.setProperty("scroll-padding-block-start", `${headerHeight + titleHeight}px`); + this.scrollContainer.style.setProperty("scroll-padding-block-start", `${headerHeight + titleHeight}px`); + } + } + } + + onAfterRendering() { + if (this.scrollContainer) { + if (this._focusInHandler) { + this.scrollContainer.removeEventListener("focusin", this._focusInHandler); } + + this._focusInHandler = (e: FocusEvent) => { + const target = e.target as HTMLElement; + + if (!target || target === this.scrollContainer) { + return; + } + + if (this.dynamicPageHeader?.contains(target) || + this.dynamicPageTitle?.contains(target)) { + return; + } + + requestAnimationFrame(() => { + target.scrollIntoView({ behavior: "smooth", block: "nearest" }); + }); + }; + + this.scrollContainer.addEventListener("focusin", this._focusInHandler); + } + } + + onExitDOM() { + if (this.scrollContainer && this._focusInHandler) { + this.scrollContainer.removeEventListener("focusin", this._focusInHandler); + this._focusInHandler = undefined; } } From c0fbace1c25488bddd46c96be651ba0b8d27854f Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Thu, 2 Oct 2025 14:40:55 +0300 Subject: [PATCH 3/4] remove unused import --- packages/fiori/cypress/specs/DynamicPage.cy.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 8fe5d90e76cd..871843e8f71c 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -3,7 +3,6 @@ import DynamicPageTitle from "../../src/DynamicPageTitle.js"; import DynamicPageHeader from "../../src/DynamicPageHeader.js"; import Bar from "@ui5/webcomponents/dist/Bar.js"; import Button from "@ui5/webcomponents/dist/Button.js"; -import Input from "@ui5/webcomponents/dist/Input.js"; import { setAnimationMode } from "@ui5/webcomponents-base"; before(() => { From 005a157db8ddd7ed5b30ab599ff497db0887bcb4 Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Thu, 2 Oct 2025 14:57:27 +0300 Subject: [PATCH 4/4] fix lint issue --- packages/fiori/src/DynamicPage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index e2ba371f5bee..9142c9530a78 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -241,8 +241,7 @@ class DynamicPage extends UI5Element { return; } - if (this.dynamicPageHeader?.contains(target) || - this.dynamicPageTitle?.contains(target)) { + if (this.dynamicPageHeader?.contains(target) || this.dynamicPageTitle?.contains(target)) { return; }