diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 9c6b189ed917..23847b6f4ba4 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -9,6 +9,7 @@ import TableHeaderCell from "@ui5/webcomponents/dist/TableHeaderCell.js"; import TableRow from "@ui5/webcomponents/dist/TableRow.js"; import TableCell from "@ui5/webcomponents/dist/TableCell.js"; import TableRowAction from "@ui5/webcomponents/dist/TableRowAction.js"; +import TableGrowing from "@ui5/webcomponents/dist/TableGrowing.js"; import { setAnimationMode } from "@ui5/webcomponents-base"; before(() => { @@ -513,6 +514,58 @@ describe("DynamicPage", () => { }); }); }); + + it("keeps the first table row visible when Shift+Tab returns focus from the growing button", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+ + + Col 1 + Col 2 + + + {Array.from({ length: 30 }, (_, i) => ( + + Row {i + 1}, Col 1 + Row {i + 1}, Col 2 + + ))} +
+
+ ); + + cy.get("[ui5-table-row]").first().realClick(); + cy.get("[ui5-table-row]").first().should("be.focused"); + + cy.realPress("Tab"); + + cy.get("[ui5-table-row]").first().should("not.be.focused"); + + cy.realPress(["Shift", "Tab"]); + + cy.get("[ui5-table-row]").first().should("be.focused"); + + cy.wait(100); + + cy.get("[ui5-dynamic-page]").then(($dp) => { + const dp = $dp[0] as DynamicPage; + const containerRect = dp.scrollContainer!.getBoundingClientRect(); + const contentEl = dp.shadowRoot!.querySelector(".ui5-dynamic-page-content")!; + const contentRect = contentEl.getBoundingClientRect(); + const rowRect = (dp.querySelector("[ui5-table-row]") as HTMLElement).getBoundingClientRect(); + const visibleTop = Math.max(containerRect.top, contentRect.top); + const visibleBottom = containerRect.bottom - dp.endAreaHeight; + + expect(rowRect.bottom).to.be.greaterThan(visibleTop); + expect(rowRect.top).to.be.lessThan(visibleBottom); + }); + }); }); describe("Scroll", () => { @@ -534,8 +587,8 @@ describe("Scroll", () => { cy.get("[data-testid='scroll-down']").then(($btn) => { $btn[0].addEventListener("click", () => { - const scrollContainer = document.querySelector("[ui5-dynamic-page]") - .shadowRoot.querySelector(".ui5-dynamic-page-scroll-container"); + const scrollContainer = document.querySelector("[ui5-dynamic-page]")! + .shadowRoot!.querySelector(".ui5-dynamic-page-scroll-container"); if (scrollContainer) { scrollContainer.scrollTo(0, 500); } @@ -566,8 +619,8 @@ describe("Scroll", () => { cy.get("[data-testid='scroll-to-top']").then(($btn) => { $btn[0].addEventListener("click", () => { - const scrollContainer = document.querySelector("[ui5-dynamic-page]") - .shadowRoot.querySelector(".ui5-dynamic-page-scroll-container"); + const scrollContainer = document.querySelector("[ui5-dynamic-page]")! + .shadowRoot!.querySelector(".ui5-dynamic-page-scroll-container"); if (scrollContainer) { scrollContainer.scrollTo(0, 0); } @@ -678,7 +731,7 @@ describe("Page general interaction", () => { cy.get("[data-testid='toggle-footer']").then(($btn) => { $btn[0].addEventListener("click", () => { - const dynamicPage = document.querySelector("[ui5-dynamic-page]"); + const dynamicPage = document.querySelector("[ui5-dynamic-page]")!; const hasFooter = dynamicPage.hasAttribute("show-footer"); if (hasFooter) { dynamicPage.removeAttribute("show-footer"); diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index 45f37d05cc30..05c203029d21 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -527,6 +527,15 @@ class DynamicPage extends UI5Element { // composedPath()[0] is the actual focused element inside shadow DOM (e.g. a button inside // a web component host). Must be captured synchronously - composedPath() returns [] inside RAF. const target = e.composedPath()[0] as HTMLElement; + + // Ignore transient focus-trap sentinels (e.g. the Table's roving-tabindex before/after + // elements) which have no layout box on at least one axis. Scrolling such an element into + // view would fight the real focus target's own scroll handling and move the viewport away + // from the focused element. Real focusables have a non-zero box on both axes. + if (!target || target.offsetWidth === 0 || target.offsetHeight === 0) { + return; + } + this.setScrollPadding({ start: this.scrollPaddingTop, end: this.endAreaHeight }); // Elements partially hidden behind sticky header/footer appear "in view" to the browser