diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx
index 871843e8f71c..4f20216a9020 100644
--- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx
+++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx
@@ -224,6 +224,58 @@ describe("DynamicPage", () => {
.find("[ui5-dynamic-page-header-actions]")
.should("have.prop", "hidePinButton", true);
});
+
+ it("sets scroll padding when content receives focus", () => {
+ cy.mount(
+
+
+ Page Title
+
+
+ Header Content
+
+
+
+
+
+
+ );
+
+ cy.get("[data-testid='test-input']").focus();
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-scroll-container")
+ .should("have.css", "scroll-padding-top")
+ .and("not.equal", "0px");
+
+ cy.get("[data-testid='test-input']").blur();
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-scroll-container")
+ .should("have.css", "scroll-padding-top", "0px");
+ });
+
+ it("scrolls focused elements into view", () => {
+ cy.mount(
+
+
+ Page Title
+
+
+ Header Content
+
+
+
+
+
+ );
+
+ cy.get("[data-testid='bottom-input']").focus();
+
+ cy.get("[data-testid='bottom-input']").should("be.visible");
+ });
});
describe("Scroll", () => {
diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts
index 9142c9530a78..aa41142c64ed 100644
--- a/packages/fiori/src/DynamicPage.ts
+++ b/packages/fiori/src/DynamicPage.ts
@@ -188,7 +188,6 @@ class DynamicPage extends UI5Element {
skipSnapOnScroll = false;
showHeaderInStickArea = false;
isToggled = false;
- _focusInHandler?: (e: FocusEvent) => void;
@property({ type: Boolean })
_headerSnapped = false;
@@ -213,52 +212,16 @@ class DynamicPage extends UI5Element {
if (this.dynamicPageHeader) {
this.dynamicPageHeader._snapped = this._headerSnapped;
}
- const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
- const headerHeight = this.dynamicPageHeader?.getBoundingClientRect().height || 0;
- const footerHeight = this.showFooter ? this.footerWrapper?.getBoundingClientRect().height : 0;
-
- if (this.scrollContainer) {
- this.scrollContainer.style.setProperty("scroll-padding-block-end", `${footerHeight}px`);
-
- if (this._headerSnapped) {
- this.scrollContainer.style.setProperty("scroll-padding-block-start", `${titleHeight}px`);
- } else {
- 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);
- }
+ get endAreaHeight() {
+ return this.showFooter ? this.footerWrapper?.getBoundingClientRect().height || 0 : 0;
}
- onExitDOM() {
- if (this.scrollContainer && this._focusInHandler) {
- this.scrollContainer.removeEventListener("focusin", this._focusInHandler);
- this._focusInHandler = undefined;
- }
+ get topAreaHeight() {
+ const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
+ const headerHeight = this.dynamicPageHeader?.getBoundingClientRect().height || 0;
+ return this._headerSnapped ? titleHeight : headerHeight + titleHeight;
}
get dynamicPageTitle(): DynamicPageTitle | null {
@@ -464,14 +427,35 @@ class DynamicPage extends UI5Element {
}
}
- async onExpandHoverIn() {
+ onExpandHoverIn() {
this.dynamicPageTitle?.setAttribute("hovered", "");
- await renderFinished();
}
- async onExpandHoverOut() {
+ onExpandHoverOut() {
this.dynamicPageTitle?.removeAttribute("hovered");
- await renderFinished();
+ }
+
+ onContentFocusIn(e: FocusEvent) {
+ const target = e.target as HTMLElement;
+ this.setScrollPadding({ start: this.topAreaHeight, end: this.endAreaHeight });
+ // 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(() => {
+ target.scrollIntoView({ behavior: "smooth", block: "nearest" });
+ });
+ }
+
+ onContentFocusOut() {
+ // Reset scroll padding when focus leaves content (e.g., moves to sticky header).
+ // The sticky header is part of the scrollable area, so keeping padding causes unwanted scroll.
+ this.setScrollPadding({ start: 0, end: 0 });
+ }
+
+ setScrollPadding(padding: { start: number, end: number }) {
+ this.scrollContainer?.style.setProperty("scroll-padding-top", `${padding.start}px`);
+ this.scrollContainer?.style.setProperty("scroll-padding-bottom", `${padding.end}px`);
}
}
diff --git a/packages/fiori/src/DynamicPageTemplate.tsx b/packages/fiori/src/DynamicPageTemplate.tsx
index 895af65811ff..79a321a06f37 100644
--- a/packages/fiori/src/DynamicPageTemplate.tsx
+++ b/packages/fiori/src/DynamicPageTemplate.tsx
@@ -33,7 +33,12 @@ export default function DynamicPageTemplate(this: DynamicPage) {
{!this.actionsInTitle && headerActions.call(this)}
-