feat(content, reorder-group, header, footer, infinite-scroll, refresher): add custom scroll target to improve compatibility with virtual scroll (#24883)

Resolves #23437
This commit is contained in:
Sean Perkins
2022-03-15 11:47:46 -04:00
committed by GitHub
parent 171020e9d2
commit 2a438da010
38 changed files with 1303 additions and 176 deletions

View File

@@ -1,7 +1,8 @@
import { Component, ComponentInterface, Element, Host, Prop, h, writeTask } from '@stencil/core';
import { findIonContent, getScrollElement, printIonContentErrorMsg } from '@utils/content';
import { getIonMode } from '../../global/ionic-global';
import { Attributes, componentOnReady, inheritAttributes } from '../../utils/helpers';
import { Attributes, inheritAttributes } from '../../utils/helpers';
import { hostContext } from '../../utils/theme';
import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
@@ -72,7 +73,8 @@ export class Header implements ComponentInterface {
if (hasCondense) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
const contentEl = (pageEl) ? findIonContent(pageEl) : null;
// Cloned elements are always needed in iOS transition
writeTask(() => {
@@ -84,22 +86,25 @@ export class Header implements ComponentInterface {
await this.setupCondenseHeader(contentEl, pageEl);
} else if (hasFade) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
const condenseHeader = (contentEl) ? contentEl.querySelector('ion-header[collapse="condense"]') as HTMLElement | null : null;
const contentEl = (pageEl) ? findIonContent(pageEl) : null;
if (!contentEl) {
printIonContentErrorMsg(this.el);
return;
}
const condenseHeader = contentEl.querySelector('ion-header[collapse="condense"]') as HTMLElement | null;
await this.setupFadeHeader(contentEl, condenseHeader);
}
}
private setupFadeHeader = async (contentEl: HTMLIonContentElement | null, condenseHeader: HTMLElement | null) => {
if (!contentEl) { console.error('ion-header requires a content to collapse. Make sure there is an ion-content.'); return; }
await new Promise(resolve => componentOnReady(contentEl, resolve));
const scrollEl = this.scrollEl = await contentEl.getScrollElement();
private setupFadeHeader = async (contentEl: HTMLElement, condenseHeader: HTMLElement | null) => {
const scrollEl = this.scrollEl = await getScrollElement(contentEl);
/**
* Handle fading of toolbars on scroll
*/
this.contentScrollCallback = () => { handleHeaderFade(this.scrollEl!, this.el, condenseHeader); };
scrollEl!.addEventListener('scroll', this.contentScrollCallback);
@@ -123,12 +128,14 @@ export class Header implements ComponentInterface {
}
}
private async setupCondenseHeader(contentEl: HTMLIonContentElement | null, pageEl: Element | null) {
if (!contentEl || !pageEl) { console.error('ion-header requires a content to collapse, make sure there is an ion-content.'); return; }
private async setupCondenseHeader(contentEl: HTMLElement | null, pageEl: Element | null) {
if (!contentEl || !pageEl) {
printIonContentErrorMsg(this.el);
return;
}
if (typeof (IntersectionObserver as any) === 'undefined') { return; }
await new Promise(resolve => componentOnReady(contentEl, resolve));
this.scrollEl = await contentEl.getScrollElement();
this.scrollEl = await getScrollElement(contentEl);
const headers = pageEl.querySelectorAll('ion-header');
this.collapsibleMainHeader = Array.from(headers).find((header: any) => header.collapse !== 'condense') as HTMLElement | undefined;
@@ -192,7 +199,7 @@ export class Header implements ComponentInterface {
}}
{...inheritedAttributes}
>
{ mode === 'ios' && translucent &&
{mode === 'ios' && translucent &&
<div class="header-background"></div>
}
<slot></slot>

View File

@@ -9,6 +9,27 @@ The `collapse` property can be set to `'fade'` on a page's main `ion-header` to
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`.
### Usage with Virtual Scroll
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.
```html
<ion-header collapse="fade">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true" scroll-y="false">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
<virtual-scroll-element class="ion-content-scroll-host">
<!-- Your virtual scroll content -->
</virtual-scroll-element>
</ion-content>
```
<!-- Auto Generated Below -->

View File

@@ -1,89 +1,93 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Header - Fade</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.red {
background-color: #ea445a;
}
.green {
background-color: #76d672;
}
<head>
<meta charset="UTF-8">
<title>Header - Fade</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.red {
background-color: #ea445a;
}
.blue {
background-color: #3478f6;
}
.green {
background-color: #76d672;
}
.yellow {
background-color: #ffff80;
}
.blue {
background-color: #3478f6;
}
.pink {
background-color: #ff6b86;
}
.yellow {
background-color: #ffff80;
}
.purple {
background-color: #7e34f6;
}
.pink {
background-color: #ff6b86;
}
.black {
background-color: #000;
}
.purple {
background-color: #7e34f6;
}
.orange {
background-color: #f69234;
}
.black {
background-color: #000;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.orange {
background-color: #f69234;
}
.grid-item {
height: 200px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.grid-item {
height: 200px;
}
</style>
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header collapse="fade" translucent="true">
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header collapse="fade" translucent="true">
<ion-toolbar>
<ion-title>Mailboxes</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title>Mailboxes</ion-title>
<ion-title size="large">Mailboxes</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Mailboxes</ion-title>
</ion-toolbar>
</ion-header>
<div class="grid ion-padding">
<div class="grid-item red"></div>
<div class="grid-item green"></div>
<div class="grid-item blue"></div>
<div class="grid-item yellow"></div>
<div class="grid-item pink"></div>
<div class="grid-item purple"></div>
<div class="grid-item black"></div>
<div class="grid-item orange"></div>
</div>
</ion-content>
<ion-footer translucent="true">
<ion-toolbar>
<ion-title>Updated Just Now</ion-title>
</ion-toolbar>
</ion-footer>
</div>
</ion-app>
</body>
<div class="grid ion-padding">
<div class="grid-item red"></div>
<div class="grid-item green"></div>
<div class="grid-item blue"></div>
<div class="grid-item yellow"></div>
<div class="grid-item pink"></div>
<div class="grid-item purple"></div>
<div class="grid-item black"></div>
<div class="grid-item orange"></div>
</div>
</ion-content>
<ion-footer translucent="true">
<ion-toolbar>
<ion-title>Updated Just Now</ion-title>
</ion-toolbar>
</ion-footer>
</div>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,59 @@
import { newE2EPage } from '@stencil/core/testing';
import type { E2EPage } from '@stencil/core/testing';
import { scrollToBottom } from '@utils/test';
describe('ion-header: custom scroll target', () => {
let page: E2EPage;
beforeEach(async () => {
page = await newE2EPage({
url: '/src/components/header/test/scroll-target?ionic:_testing=true&ionic:mode=ios'
});
});
it('should match existing visual screenshots', async () => {
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
describe('large title', () => {
it('should display the large title initially', async () => {
const largeHeader = await page.find('ion-header[collapse="condense"]');
const collapseHeader = await page.find('ion-header[collapse="fade"]');
expect(largeHeader.className).not.toContain('header-collapse-condense-inactive');
expect(collapseHeader.className).toContain('header-collapse-condense-inactive');
});
describe('when the scroll container has overflow', () => {
it('should display the collapsed title on scroll', async () => {
const screenshotCompares = [];
screenshotCompares.push(await page.compareScreenshot('large title expanded'));
const largeHeader = await page.find('ion-header[collapse="condense"]');
const collapseHeader = await page.find('ion-header[collapse="fade"]');
await scrollToBottom(page, '#scroll-target');
await page.waitForChanges();
expect(largeHeader.className).toContain('header-collapse-condense-inactive');
expect(collapseHeader.className).not.toContain('header-collapse-condense-inactive');
screenshotCompares.push(await page.compareScreenshot('large title collapsed'));
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
});
});
});

View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Header - Custom Scroll Target</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.red {
background-color: #ea445a;
}
.green {
background-color: #76d672;
}
.blue {
background-color: #3478f6;
}
.yellow {
background-color: #ffff80;
}
.pink {
background-color: #ff6b86;
}
.purple {
background-color: #7e34f6;
}
.black {
background-color: #000;
}
.orange {
background-color: #f69234;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.grid-item {
height: 200px;
}
#scroll-target {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow-y: auto;
padding: 50px 0;
}
</style>
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header collapse="fade" translucent="true">
<ion-toolbar>
<ion-title>Mailboxes</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true" scroll-y="false">
<div id="scroll-target" class="ion-content-scroll-host">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Mailboxes</ion-title>
</ion-toolbar>
</ion-header>
<div class="grid ion-padding">
<div class="grid-item red"></div>
<div class="grid-item green"></div>
<div class="grid-item blue"></div>
<div class="grid-item yellow"></div>
<div class="grid-item pink"></div>
<div class="grid-item purple"></div>
<div class="grid-item black"></div>
<div class="grid-item orange"></div>
</div>
</div>
</ion-content>
<ion-footer translucent="true">
<ion-toolbar>
<ion-title>Updated Just Now</ion-title>
</ion-toolbar>
</ion-footer>
</div>
</ion-app>
</body>
</html>