Skip to content

Commit ccf4bcc

Browse files
fix(ui5-dynamic-page): correct scroll-padding-top for focus auto-scroll (#13418)
Replace the pointer-down focus-skip workaround with a scroll-padding-top calculation based on the actually overlapped sticky area (title/header and current scrollTop), so visible content no longer shifts on click while partially clipped controls can still scroll into view on keyboard focus. Keep smooth scroll behavior when auto-scroll is needed. Add/update DynamicPage Cypress coverage for: no scroll shift on content button click with partially hidden header no scroll shift on tab focus when target is already visible partially clipped textarea is scrolled into view on Tab focus Fixes: #13332
1 parent b5f72c4 commit ccf4bcc

2 files changed

Lines changed: 167 additions & 3 deletions

File tree

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

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,161 @@ describe("DynamicPage", () => {
305305

306306
cy.get("[data-testid='bottom-input']").should("be.visible");
307307
});
308+
309+
it("does not scroll content when a button is clicked while the header is partially hidden", () => {
310+
let clickCount = 0;
311+
312+
cy.mount(
313+
<DynamicPage style={{ height: "400px" }}>
314+
<DynamicPageTitle slot="titleArea">
315+
<div slot="heading">Page Title</div>
316+
</DynamicPageTitle>
317+
<DynamicPageHeader slot="headerArea">
318+
<div style={{ height: "180px" }}>
319+
<div>Line 1</div>
320+
<div>Line 2</div>
321+
<div>Line 3</div>
322+
<div>Line 4</div>
323+
<div>Rack: 34</div>
324+
</div>
325+
</DynamicPageHeader>
326+
<Button data-testid="content-button" onClick={() => { clickCount += 1; }}>test</Button>
327+
<div style={{ height: "1000px" }}></div>
328+
</DynamicPage>
329+
);
330+
331+
cy.get("[ui5-dynamic-page]")
332+
.shadow()
333+
.find(".ui5-dynamic-page-scroll-container")
334+
.then(($container) => {
335+
$container[0].scrollTop = 120;
336+
});
337+
338+
cy.get("[ui5-dynamic-page]")
339+
.shadow()
340+
.find(".ui5-dynamic-page-scroll-container")
341+
.then(($container) => {
342+
const initialScrollTop = $container[0].scrollTop;
343+
344+
cy.get("[data-testid='content-button']")
345+
.realClick();
346+
347+
cy.then(() => {
348+
expect(clickCount).to.equal(1);
349+
});
350+
351+
cy.get("[ui5-dynamic-page]")
352+
.shadow()
353+
.find(".ui5-dynamic-page-scroll-container")
354+
.should(($updatedContainer) => {
355+
expect($updatedContainer[0].scrollTop).to.be.closeTo(initialScrollTop, 1);
356+
});
357+
});
358+
});
359+
360+
it("does not scroll content when a visible button receives keyboard focus while the header is partially hidden", () => {
361+
cy.mount(
362+
<DynamicPage style={{ height: "400px" }}>
363+
<DynamicPageTitle slot="titleArea">
364+
<div slot="heading">Page Title</div>
365+
</DynamicPageTitle>
366+
<DynamicPageHeader slot="headerArea">
367+
<div style={{ height: "180px" }}>
368+
<div>Line 1</div>
369+
<div>Line 2</div>
370+
<div>Line 3</div>
371+
<div>Line 4</div>
372+
<div>Rack: 34</div>
373+
</div>
374+
</DynamicPageHeader>
375+
<button data-testid="first-content-button">first</button>
376+
<Button data-testid="content-button">test</Button>
377+
<div style={{ height: "1000px" }}></div>
378+
</DynamicPage>
379+
);
380+
381+
cy.get("[ui5-dynamic-page]")
382+
.shadow()
383+
.find(".ui5-dynamic-page-scroll-container")
384+
.then(($container) => {
385+
$container[0].scrollTop = 120;
386+
});
387+
388+
cy.get("[data-testid='first-content-button']")
389+
.focus()
390+
.should("be.focused");
391+
392+
cy.get("[ui5-dynamic-page]")
393+
.shadow()
394+
.find(".ui5-dynamic-page-scroll-container")
395+
.then(($container) => {
396+
const initialScrollTop = $container[0].scrollTop;
397+
398+
cy.realPress("Tab");
399+
400+
cy.get("[data-testid='content-button']")
401+
.should("be.focused");
402+
403+
cy.get("[ui5-dynamic-page]")
404+
.shadow()
405+
.find(".ui5-dynamic-page-scroll-container")
406+
.should(($updatedContainer) => {
407+
expect($updatedContainer[0].scrollTop).to.be.closeTo(initialScrollTop, 1);
408+
});
409+
});
410+
});
411+
412+
it("scrolls a partially clipped textarea into view when focused via Tab", () => {
413+
cy.mount(
414+
<DynamicPage style={{ height: "400px" }}>
415+
<DynamicPageTitle slot="titleArea">
416+
<div slot="heading">Page Title</div>
417+
</DynamicPageTitle>
418+
<DynamicPageHeader slot="headerArea">
419+
<div style={{ height: "180px" }}>
420+
<div>Line 1</div>
421+
<div>Line 2</div>
422+
<div>Line 3</div>
423+
<div>Line 4</div>
424+
<div>Rack: 34</div>
425+
</div>
426+
</DynamicPageHeader>
427+
<button data-testid="before-textarea">Before</button>
428+
<textarea data-testid="target-textarea" style={{ display: "block", marginTop: "8px", height: "120px" }} />
429+
<div style={{ height: "1000px" }}></div>
430+
</DynamicPage>
431+
);
432+
433+
cy.get("[ui5-dynamic-page]")
434+
.shadow()
435+
.find(".ui5-dynamic-page-scroll-container")
436+
.then(($container) => {
437+
$container[0].scrollTop = 120;
438+
});
439+
440+
cy.get("[data-testid='before-textarea']")
441+
.focus()
442+
.should("be.focused");
443+
444+
cy.realPress("Tab");
445+
446+
cy.get("[data-testid='target-textarea']")
447+
.should("be.focused");
448+
449+
cy.get("[ui5-dynamic-page]")
450+
.then(($dp) => {
451+
const dp = $dp[0] as DynamicPage;
452+
const containerRect = dp.scrollContainer!.getBoundingClientRect();
453+
const contentEl = dp.shadowRoot!.querySelector<HTMLElement>(".ui5-dynamic-page-content")!;
454+
const contentRect = contentEl.getBoundingClientRect();
455+
const targetRect = (dp.querySelector("[data-testid='target-textarea']") as HTMLTextAreaElement).getBoundingClientRect();
456+
const visibleTop = Math.max(containerRect.top, contentRect.top);
457+
const visibleBottom = containerRect.bottom - dp.endAreaHeight;
458+
459+
expect(targetRect.top).to.be.at.least(visibleTop);
460+
expect(targetRect.bottom).to.be.at.most(visibleBottom);
461+
});
462+
});
308463
});
309464

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

packages/fiori/src/DynamicPage.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,18 @@ class DynamicPage extends UI5Element {
220220
return this.showFooter ? this.footerWrapper?.getBoundingClientRect().height || 0 : 0;
221221
}
222222

223-
get topAreaHeight() {
223+
get scrollPaddingTop() {
224224
const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
225225
const headerHeight = this.dynamicPageHeader?.getBoundingClientRect().height || 0;
226-
return this._headerSnapped ? titleHeight : headerHeight + titleHeight;
226+
227+
if (this._headerSnapped) {
228+
return titleHeight;
229+
}
230+
231+
const fullHeight = headerHeight + titleHeight;
232+
const scrollTop = this.scrollContainer?.scrollTop || 0;
233+
234+
return Math.max(titleHeight, fullHeight - scrollTop);
227235
}
228236

229237
get dynamicPageTitle(): DynamicPageTitle | null {
@@ -455,7 +463,8 @@ class DynamicPage extends UI5Element {
455463

456464
onContentFocusIn(e: FocusEvent) {
457465
const target = e.target as HTMLElement;
458-
this.setScrollPadding({ start: this.topAreaHeight, end: this.endAreaHeight });
466+
this.setScrollPadding({ start: this.scrollPaddingTop, end: this.endAreaHeight });
467+
459468
// textareas and similar elements appear "in view" even when partially
460469
// hidden behind sticky header/footer.
461470
// manual scroll brings them fully into view.

0 commit comments

Comments
 (0)