Skip to content

Commit cd6da99

Browse files
authored
fix(ui5-dynamic-page): prevent scroll when focusing title/header (#12588)
fix(ui5-dynamic-page): prevent unintentional scroll when focusing title/header
1 parent 74aa404 commit cd6da99

File tree

3 files changed

+89
-48
lines changed

3 files changed

+89
-48
lines changed

packages/fiori/cypress/specs/DynamicPage.cy.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,58 @@ describe("DynamicPage", () => {
224224
.find("[ui5-dynamic-page-header-actions]")
225225
.should("have.prop", "hidePinButton", true);
226226
});
227+
228+
it("sets scroll padding when content receives focus", () => {
229+
cy.mount(
230+
<DynamicPage showFooter style={{ height: "600px" }}>
231+
<DynamicPageTitle slot="titleArea">
232+
<div slot="heading">Page Title</div>
233+
</DynamicPageTitle>
234+
<DynamicPageHeader slot="headerArea">
235+
<div>Header Content</div>
236+
</DynamicPageHeader>
237+
<input data-testid="test-input" />
238+
<Bar slot="footerArea" design="FloatingFooter">
239+
<Button slot="endContent">Save</Button>
240+
</Bar>
241+
</DynamicPage>
242+
);
243+
244+
cy.get("[data-testid='test-input']").focus();
245+
246+
cy.get("[ui5-dynamic-page]")
247+
.shadow()
248+
.find(".ui5-dynamic-page-scroll-container")
249+
.should("have.css", "scroll-padding-top")
250+
.and("not.equal", "0px");
251+
252+
cy.get("[data-testid='test-input']").blur();
253+
254+
cy.get("[ui5-dynamic-page]")
255+
.shadow()
256+
.find(".ui5-dynamic-page-scroll-container")
257+
.should("have.css", "scroll-padding-top", "0px");
258+
});
259+
260+
it("scrolls focused elements into view", () => {
261+
cy.mount(
262+
<DynamicPage style={{ height: "400px" }}>
263+
<DynamicPageTitle slot="titleArea">
264+
<div slot="heading">Page Title</div>
265+
</DynamicPageTitle>
266+
<DynamicPageHeader slot="headerArea">
267+
<div>Header Content</div>
268+
</DynamicPageHeader>
269+
<div style={{ height: "1000px" }}>
270+
<input data-testid="bottom-input" style={{ marginTop: "900px" }} />
271+
</div>
272+
</DynamicPage>
273+
);
274+
275+
cy.get("[data-testid='bottom-input']").focus();
276+
277+
cy.get("[data-testid='bottom-input']").should("be.visible");
278+
});
227279
});
228280

229281
describe("Scroll", () => {

packages/fiori/src/DynamicPage.ts

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ class DynamicPage extends UI5Element {
188188
skipSnapOnScroll = false;
189189
showHeaderInStickArea = false;
190190
isToggled = false;
191-
_focusInHandler?: (e: FocusEvent) => void;
192191

193192
@property({ type: Boolean })
194193
_headerSnapped = false;
@@ -213,52 +212,16 @@ class DynamicPage extends UI5Element {
213212
if (this.dynamicPageHeader) {
214213
this.dynamicPageHeader._snapped = this._headerSnapped;
215214
}
216-
const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
217-
const headerHeight = this.dynamicPageHeader?.getBoundingClientRect().height || 0;
218-
const footerHeight = this.showFooter ? this.footerWrapper?.getBoundingClientRect().height : 0;
219-
220-
if (this.scrollContainer) {
221-
this.scrollContainer.style.setProperty("scroll-padding-block-end", `${footerHeight}px`);
222-
223-
if (this._headerSnapped) {
224-
this.scrollContainer.style.setProperty("scroll-padding-block-start", `${titleHeight}px`);
225-
} else {
226-
this.scrollContainer.style.setProperty("scroll-padding-block-start", `${headerHeight + titleHeight}px`);
227-
}
228-
}
229215
}
230216

231-
onAfterRendering() {
232-
if (this.scrollContainer) {
233-
if (this._focusInHandler) {
234-
this.scrollContainer.removeEventListener("focusin", this._focusInHandler);
235-
}
236-
237-
this._focusInHandler = (e: FocusEvent) => {
238-
const target = e.target as HTMLElement;
239-
240-
if (!target || target === this.scrollContainer) {
241-
return;
242-
}
243-
244-
if (this.dynamicPageHeader?.contains(target) || this.dynamicPageTitle?.contains(target)) {
245-
return;
246-
}
247-
248-
requestAnimationFrame(() => {
249-
target.scrollIntoView({ behavior: "smooth", block: "nearest" });
250-
});
251-
};
252-
253-
this.scrollContainer.addEventListener("focusin", this._focusInHandler);
254-
}
217+
get endAreaHeight() {
218+
return this.showFooter ? this.footerWrapper?.getBoundingClientRect().height || 0 : 0;
255219
}
256220

257-
onExitDOM() {
258-
if (this.scrollContainer && this._focusInHandler) {
259-
this.scrollContainer.removeEventListener("focusin", this._focusInHandler);
260-
this._focusInHandler = undefined;
261-
}
221+
get topAreaHeight() {
222+
const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
223+
const headerHeight = this.dynamicPageHeader?.getBoundingClientRect().height || 0;
224+
return this._headerSnapped ? titleHeight : headerHeight + titleHeight;
262225
}
263226

264227
get dynamicPageTitle(): DynamicPageTitle | null {
@@ -464,14 +427,35 @@ class DynamicPage extends UI5Element {
464427
}
465428
}
466429

467-
async onExpandHoverIn() {
430+
onExpandHoverIn() {
468431
this.dynamicPageTitle?.setAttribute("hovered", "");
469-
await renderFinished();
470432
}
471433

472-
async onExpandHoverOut() {
434+
onExpandHoverOut() {
473435
this.dynamicPageTitle?.removeAttribute("hovered");
474-
await renderFinished();
436+
}
437+
438+
onContentFocusIn(e: FocusEvent) {
439+
const target = e.target as HTMLElement;
440+
this.setScrollPadding({ start: this.topAreaHeight, end: this.endAreaHeight });
441+
// textareas and similar elements appear "in view" even when partially
442+
// hidden behind sticky header/footer.
443+
// manual scroll brings them fully into view.
444+
// another issue is that browsers do not reflect dynamic changes of scroll-padding
445+
requestAnimationFrame(() => {
446+
target.scrollIntoView({ behavior: "smooth", block: "nearest" });
447+
});
448+
}
449+
450+
onContentFocusOut() {
451+
// Reset scroll padding when focus leaves content (e.g., moves to sticky header).
452+
// The sticky header is part of the scrollable area, so keeping padding causes unwanted scroll.
453+
this.setScrollPadding({ start: 0, end: 0 });
454+
}
455+
456+
setScrollPadding(padding: { start: number, end: number }) {
457+
this.scrollContainer?.style.setProperty("scroll-padding-top", `${padding.start}px`);
458+
this.scrollContainer?.style.setProperty("scroll-padding-bottom", `${padding.end}px`);
475459
}
476460
}
477461

packages/fiori/src/DynamicPageTemplate.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ export default function DynamicPageTemplate(this: DynamicPage) {
3333

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

36-
<div class="ui5-dynamic-page-content" part="content">
36+
<div
37+
part="content"
38+
class="ui5-dynamic-page-content"
39+
onFocusIn={this.onContentFocusIn}
40+
onFocusOut={this.onContentFocusOut}
41+
>
3742
<div class="ui5-dynamic-page-fit-content" part="fit-content">
3843
<slot></slot>
3944
{this.showFooter &&

0 commit comments

Comments
 (0)