Skip to content
Merged
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
52 changes: 52 additions & 0 deletions packages/fiori/cypress/specs/DynamicPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DynamicPage showFooter style={{ height: "600px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div>Header Content</div>
</DynamicPageHeader>
<input data-testid="test-input" />
<Bar slot="footerArea" design="FloatingFooter">
<Button slot="endContent">Save</Button>
</Bar>
</DynamicPage>
);

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(
<DynamicPage style={{ height: "400px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div>Header Content</div>
</DynamicPageHeader>
<div style={{ height: "1000px" }}>
<input data-testid="bottom-input" style={{ marginTop: "900px" }} />
</div>
</DynamicPage>
);

cy.get("[data-testid='bottom-input']").focus();

cy.get("[data-testid='bottom-input']").should("be.visible");
});
});

describe("Scroll", () => {
Expand Down
78 changes: 31 additions & 47 deletions packages/fiori/src/DynamicPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ class DynamicPage extends UI5Element {
skipSnapOnScroll = false;
showHeaderInStickArea = false;
isToggled = false;
_focusInHandler?: (e: FocusEvent) => void;

@property({ type: Boolean })
_headerSnapped = false;
Expand All @@ -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 {
Expand Down Expand Up @@ -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`);
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/fiori/src/DynamicPageTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ export default function DynamicPageTemplate(this: DynamicPage) {

{!this.actionsInTitle && headerActions.call(this)}

<div class="ui5-dynamic-page-content" part="content">
<div
part="content"
class="ui5-dynamic-page-content"
onFocusIn={this.onContentFocusIn}
onFocusOut={this.onContentFocusOut}
>
<div class="ui5-dynamic-page-fit-content" part="fit-content">
<slot></slot>
{this.showFooter &&
Expand Down
Loading