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