Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions packages/fiori/cypress/specs/DynamicPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -513,6 +514,58 @@ describe("DynamicPage", () => {
});
});
});

it("keeps the first table row visible when Shift+Tab returns focus from the growing button", () => {
cy.mount(
<DynamicPage style={{ height: "400px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div style={{ height: "120px" }}>Header Content</div>
</DynamicPageHeader>
<Table>
<TableHeaderRow slot="headerRow">
<TableHeaderCell>Col 1</TableHeaderCell>
<TableHeaderCell>Col 2</TableHeaderCell>
</TableHeaderRow>
<TableGrowing slot="features" mode="Button" text="More"></TableGrowing>
{Array.from({ length: 30 }, (_, i) => (
<TableRow key={i}>
<TableCell>Row {i + 1}, Col 1</TableCell>
<TableCell>Row {i + 1}, Col 2</TableCell>
</TableRow>
))}
</Table>
</DynamicPage>
);

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<HTMLElement>(".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", () => {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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");
Expand Down
9 changes: 9 additions & 0 deletions packages/fiori/src/DynamicPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading