Skip to content

Commit 2a438da

Browse files
authored
feat(content, reorder-group, header, footer, infinite-scroll, refresher): add custom scroll target to improve compatibility with virtual scroll (#24883)
Resolves #23437
1 parent 171020e commit 2a438da

File tree

38 files changed

+1305
-178
lines changed

38 files changed

+1305
-178
lines changed

core/src/components.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2991,7 +2991,7 @@ export namespace Components {
29912991
*/
29922992
"checkEnd": () => Promise<void>;
29932993
/**
2994-
* This method marks a subset of items as dirty, so they can be re-rendered. Items should be marked as dirty any time the content or their style changes. The subset of items to be updated can are specifing by an offset and a length.
2994+
* This method marks a subset of items as dirty, so they can be re-rendered. Items should be marked as dirty any time the content or their style changes. The subset of items to be updated can are specifying by an offset and a length.
29952995
*/
29962996
"checkRange": (offset: number, len?: number) => Promise<void>;
29972997
"domRender"?: DomRenderFn;

core/src/components/footer/footer.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/core';
2+
import { findIonContent, getScrollElement, printIonContentErrorMsg } from '@utils/content';
23

34
import { getIonMode } from '../../global/ionic-global';
4-
import { componentOnReady } from '../../utils/helpers';
55

66
import { handleFooterFade } from './footer.utils';
77

@@ -56,17 +56,19 @@ export class Footer implements ComponentInterface {
5656

5757
if (hasFade) {
5858
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
59-
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
59+
const contentEl = (pageEl) ? findIonContent(pageEl) : null;
60+
61+
if (!contentEl) {
62+
printIonContentErrorMsg(this.el);
63+
return;
64+
}
6065

6166
this.setupFadeFooter(contentEl);
6267
}
6368
}
6469

65-
private setupFadeFooter = async (contentEl: HTMLIonContentElement | null) => {
66-
if (!contentEl) { console.error('ion-footer requires a content to collapse. Make sure there is an ion-content.'); return; }
67-
68-
await new Promise(resolve => componentOnReady(contentEl, resolve));
69-
const scrollEl = this.scrollEl = await contentEl.getScrollElement();
70+
private setupFadeFooter = async (contentEl: HTMLElement) => {
71+
const scrollEl = this.scrollEl = await getScrollElement(contentEl);
7072

7173
/**
7274
* Handle fading of toolbars on scroll
@@ -102,7 +104,7 @@ export class Footer implements ComponentInterface {
102104
[`footer-collapse-${collapse}`]: collapse !== undefined,
103105
}}
104106
>
105-
{ mode === 'ios' && translucent &&
107+
{mode === 'ios' && translucent &&
106108
<div class="footer-background"></div>
107109
}
108110
<slot></slot>

core/src/components/footer/readme.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ Footer can be a wrapper for ion-toolbar to make sure the content area is sized c
77

88
The `collapse` property can be set to `'fade'` on a page's `ion-footer` to have the background color of the toolbars fade in as users scroll. This provides the same fade effect that is found in many native iOS applications.
99

10+
### Usage with Virtual Scroll
11+
12+
Fade footer requires a scroll container to function. When using a virtual scrolling solution, you will need to disable scrolling on the `ion-content` and indicate which element container is responsible for the scroll container with the `.ion-content-scroll-host` class target.
13+
14+
```html
15+
<ion-content scroll-y="false">
16+
<virtual-scroll-element class="ion-content-scroll-host">
17+
<!-- Your virtual scroll content -->
18+
</virtual-scroll-element>
19+
</ion-content>
20+
<ion-footer collapse="fade">
21+
<ion-toolbar>
22+
<ion-title>Footer</ion-title>
23+
</ion-toolbar>
24+
</ion-footer>
25+
```
26+
1027
<!-- Auto Generated Below -->
1128

1229

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { E2EPage } from '@stencil/core/testing';
2+
import { newE2EPage } from '@stencil/core/testing';
3+
4+
import { scrollToBottom } from '@utils/test';
5+
6+
/**
7+
* This test suite verifies that the fade effect for iOS is working correctly
8+
* when the `ion-footer` is using a custom scroll target with the `.ion-content-scroll-host`
9+
* selector.
10+
*/
11+
describe('footer: fade with custom scroll target: iOS', () => {
12+
13+
let page: E2EPage;
14+
15+
beforeEach(async () => {
16+
page = await newE2EPage({
17+
url: '/src/components/footer/test/scroll-target?ionic:_testing=true&ionic:mode=ios'
18+
});
19+
});
20+
21+
it('should match existing visual screenshots', async () => {
22+
const compares = [];
23+
24+
compares.push(await page.compareScreenshot('footer: blurred'));
25+
26+
await scrollToBottom(page, '#scroll-target');
27+
28+
compares.push(await page.compareScreenshot('footer: not blurred'));
29+
30+
for (const compare of compares) {
31+
expect(compare).toMatchScreenshot();
32+
}
33+
});
34+
35+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<title>Footer - Fade (custom scroll host)</title>
7+
<meta name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
9+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
10+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
11+
<script src="../../../../../scripts/testing/scripts.js"></script>
12+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
13+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
14+
<style>
15+
.red {
16+
background-color: #ea445a;
17+
}
18+
19+
.green {
20+
background-color: #76d672;
21+
}
22+
23+
.blue {
24+
background-color: #3478f6;
25+
}
26+
27+
.yellow {
28+
background-color: #ffff80;
29+
}
30+
31+
.pink {
32+
background-color: #ff6b86;
33+
}
34+
35+
.purple {
36+
background-color: #7e34f6;
37+
}
38+
39+
.black {
40+
background-color: #000;
41+
}
42+
43+
.orange {
44+
background-color: #f69234;
45+
}
46+
47+
.grid {
48+
display: grid;
49+
grid-template-columns: 1fr 1fr;
50+
grid-gap: 10px;
51+
}
52+
53+
.grid-item {
54+
height: 200px;
55+
}
56+
57+
#scroll-target {
58+
position: absolute;
59+
top: 0;
60+
left: 0;
61+
62+
height: 100%;
63+
width: 100%;
64+
overflow-y: auto;
65+
66+
padding: 50px 0;
67+
}
68+
</style>
69+
</head>
70+
71+
<body>
72+
<ion-app>
73+
<div class="ion-page">
74+
<ion-header translucent="true">
75+
<ion-toolbar>
76+
<ion-title>Mailboxes</ion-title>
77+
</ion-toolbar>
78+
</ion-header>
79+
<ion-content fullscreen="true" scroll-y="false">
80+
<div id="scroll-target" class="ion-content-scroll-host">
81+
<div class="grid ion-padding">
82+
<div class="grid-item red"></div>
83+
<div class="grid-item green"></div>
84+
<div class="grid-item blue"></div>
85+
<div class="grid-item yellow"></div>
86+
<div class="grid-item pink"></div>
87+
<div class="grid-item purple"></div>
88+
<div class="grid-item black"></div>
89+
<div class="grid-item orange"></div>
90+
</div>
91+
</div>
92+
</ion-content>
93+
<ion-footer collapse="fade" translucent="true">
94+
<ion-toolbar>
95+
<ion-title>Updated Just Now</ion-title>
96+
</ion-toolbar>
97+
</ion-footer>
98+
</div>
99+
</ion-app>
100+
</body>
101+
102+
</html>

core/src/components/header/header.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Component, ComponentInterface, Element, Host, Prop, h, writeTask } from '@stencil/core';
2+
import { findIonContent, getScrollElement, printIonContentErrorMsg } from '@utils/content';
23

34
import { getIonMode } from '../../global/ionic-global';
4-
import { Attributes, componentOnReady, inheritAttributes } from '../../utils/helpers';
5+
import { Attributes, inheritAttributes } from '../../utils/helpers';
56
import { hostContext } from '../../utils/theme';
67

78
import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
@@ -72,7 +73,8 @@ export class Header implements ComponentInterface {
7273

7374
if (hasCondense) {
7475
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
75-
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
76+
77+
const contentEl = (pageEl) ? findIonContent(pageEl) : null;
7678

7779
// Cloned elements are always needed in iOS transition
7880
writeTask(() => {
@@ -84,22 +86,25 @@ export class Header implements ComponentInterface {
8486
await this.setupCondenseHeader(contentEl, pageEl);
8587
} else if (hasFade) {
8688
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
87-
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
88-
const condenseHeader = (contentEl) ? contentEl.querySelector('ion-header[collapse="condense"]') as HTMLElement | null : null;
89+
const contentEl = (pageEl) ? findIonContent(pageEl) : null;
90+
91+
if (!contentEl) {
92+
printIonContentErrorMsg(this.el);
93+
return;
94+
}
95+
96+
const condenseHeader = contentEl.querySelector('ion-header[collapse="condense"]') as HTMLElement | null;
97+
8998
await this.setupFadeHeader(contentEl, condenseHeader);
9099
}
91100
}
92101

93-
private setupFadeHeader = async (contentEl: HTMLIonContentElement | null, condenseHeader: HTMLElement | null) => {
94-
if (!contentEl) { console.error('ion-header requires a content to collapse. Make sure there is an ion-content.'); return; }
95-
96-
await new Promise(resolve => componentOnReady(contentEl, resolve));
97-
const scrollEl = this.scrollEl = await contentEl.getScrollElement();
102+
private setupFadeHeader = async (contentEl: HTMLElement, condenseHeader: HTMLElement | null) => {
103+
const scrollEl = this.scrollEl = await getScrollElement(contentEl);
98104

99105
/**
100106
* Handle fading of toolbars on scroll
101107
*/
102-
103108
this.contentScrollCallback = () => { handleHeaderFade(this.scrollEl!, this.el, condenseHeader); };
104109
scrollEl!.addEventListener('scroll', this.contentScrollCallback);
105110

@@ -123,12 +128,14 @@ export class Header implements ComponentInterface {
123128
}
124129
}
125130

126-
private async setupCondenseHeader(contentEl: HTMLIonContentElement | null, pageEl: Element | null) {
127-
if (!contentEl || !pageEl) { console.error('ion-header requires a content to collapse, make sure there is an ion-content.'); return; }
131+
private async setupCondenseHeader(contentEl: HTMLElement | null, pageEl: Element | null) {
132+
if (!contentEl || !pageEl) {
133+
printIonContentErrorMsg(this.el);
134+
return;
135+
}
128136
if (typeof (IntersectionObserver as any) === 'undefined') { return; }
129137

130-
await new Promise(resolve => componentOnReady(contentEl, resolve));
131-
this.scrollEl = await contentEl.getScrollElement();
138+
this.scrollEl = await getScrollElement(contentEl);
132139

133140
const headers = pageEl.querySelectorAll('ion-header');
134141
this.collapsibleMainHeader = Array.from(headers).find((header: any) => header.collapse !== 'condense') as HTMLElement | undefined;
@@ -192,7 +199,7 @@ export class Header implements ComponentInterface {
192199
}}
193200
{...inheritedAttributes}
194201
>
195-
{ mode === 'ios' && translucent &&
202+
{mode === 'ios' && translucent &&
196203
<div class="header-background"></div>
197204
}
198205
<slot></slot>

core/src/components/header/readme.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ The `collapse` property can be set to `'fade'` on a page's main `ion-header` to
99

1010
This functionality can be combined with [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) as well. The `collapse="condense"` value should be set on the `ion-header` inside of your `ion-content`. The `collapse="fade"` value should be set on the `ion-header` outside of your `ion-content`.
1111

12+
### Usage with Virtual Scroll
13+
14+
Fade and collapsible large titles require a scroll container to function. When using a virtual scrolling solution, you will need to disable scrolling on the `ion-content` and indicate which element container is responsible for the scroll container with the `.ion-content-scroll-host` class target.
15+
16+
```html
17+
<ion-header collapse="fade">
18+
<ion-toolbar>
19+
<ion-title>Header</ion-title>
20+
</ion-toolbar>
21+
</ion-header>
22+
<ion-content fullscreen="true" scroll-y="false">
23+
<ion-header collapse="condense">
24+
<ion-toolbar>
25+
<ion-title size="large">Header</ion-title>
26+
</ion-toolbar>
27+
</ion-header>
28+
<virtual-scroll-element class="ion-content-scroll-host">
29+
<!-- Your virtual scroll content -->
30+
</virtual-scroll-element>
31+
</ion-content>
32+
```
1233

1334
<!-- Auto Generated Below -->
1435

0 commit comments

Comments
 (0)