feat(header, footer): add ios fading header style (#24011)

This commit is contained in:
Liam DeBeasi
2021-10-05 16:44:23 -04:00
committed by GitHub
parent 225a278740
commit 7ce3959b66
27 changed files with 704 additions and 60 deletions

View File

@ -519,13 +519,13 @@ export class IonFabList {
export declare interface IonFooter extends Components.IonFooter {}
@ProxyCmp({
inputs: ['mode', 'translucent']
inputs: ['collapse', 'mode', 'translucent']
})
@Component({
selector: 'ion-footer',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['mode', 'translucent']
inputs: ['collapse', 'mode', 'translucent']
})
export class IonFooter {
protected el: HTMLElement;

View File

@ -463,6 +463,7 @@ ion-fab-list,prop,activated,boolean,false,false,false
ion-fab-list,prop,side,"bottom" | "end" | "start" | "top",'bottom',false,false
ion-footer,none
ion-footer,prop,collapse,"fade" | undefined,undefined,false,false
ion-footer,prop,mode,"ios" | "md",undefined,false,false
ion-footer,prop,translucent,boolean,false,false,false
@ -482,7 +483,7 @@ ion-grid,css-prop,--ion-grid-width-xl
ion-grid,css-prop,--ion-grid-width-xs
ion-header,none
ion-header,prop,collapse,"condense" | undefined,undefined,false,false
ion-header,prop,collapse,"condense" | "fade" | undefined,undefined,false,false
ion-header,prop,mode,"ios" | "md",undefined,false,false
ion-header,prop,translucent,boolean,false,false,false

View File

@ -920,6 +920,10 @@ export namespace Components {
"side": 'start' | 'end' | 'top' | 'bottom';
}
interface IonFooter {
/**
* Describes the scroll effect that will be applied to the footer. Only applies in iOS mode.
*/
"collapse"?: 'fade';
/**
* The mode determines which platform styles to use.
*/
@ -937,9 +941,9 @@ export namespace Components {
}
interface IonHeader {
/**
* Describes the scroll effect that will be applied to the header `condense` only applies in iOS mode. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
* Describes the scroll effect that will be applied to the header. Only applies in iOS mode. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
*/
"collapse"?: 'condense';
"collapse"?: 'condense' | 'fade';
/**
* The mode determines which platform styles to use.
*/
@ -4606,6 +4610,10 @@ declare namespace LocalJSX {
"side"?: 'start' | 'end' | 'top' | 'bottom';
}
interface IonFooter {
/**
* Describes the scroll effect that will be applied to the footer. Only applies in iOS mode.
*/
"collapse"?: 'fade';
/**
* The mode determines which platform styles to use.
*/
@ -4623,9 +4631,9 @@ declare namespace LocalJSX {
}
interface IonHeader {
/**
* Describes the scroll effect that will be applied to the header `condense` only applies in iOS mode. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
* Describes the scroll effect that will be applied to the header. Only applies in iOS mode. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
*/
"collapse"?: 'condense';
"collapse"?: 'condense' | 'fade';
/**
* The mode determines which platform styles to use.
*/

View File

@ -24,3 +24,10 @@
.footer-ios.ion-no-border ion-toolbar:first-of-type {
--border-width: 0;
}
// iOS Footer - Collapse Fade
// --------------------------------------------------
.footer-collapse-fade ion-toolbar {
--opacity-scale: inherit;
}

View File

@ -1,6 +1,9 @@
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';
import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { componentOnReady } from '../../utils/helpers';
import { handleFooterFade } from './footer.utils';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -13,6 +16,16 @@ import { getIonMode } from '../../global/ionic-global';
}
})
export class Footer implements ComponentInterface {
private scrollEl?: HTMLElement;
private contentScrollCallback: any;
@Element() el!: HTMLIonFooterElement;
/**
* Describes the scroll effect that will be applied to the footer.
* Only applies in iOS mode.
*/
@Prop() collapse?: 'fade';
/**
* If `true`, the footer will be translucent.
@ -24,9 +37,56 @@ export class Footer implements ComponentInterface {
*/
@Prop() translucent = false;
render() {
componentDidLoad() {
this.checkCollapsibleFooter();
}
componentDidUpdate() {
this.checkCollapsibleFooter();
}
private checkCollapsibleFooter = () => {
const mode = getIonMode(this);
if (mode !== 'ios') { return; }
const { collapse } = this;
const hasFade = collapse === 'fade';
this.destroyCollapsibleFooter();
if (hasFade) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
this.setupFadeFooter(contentEl);
}
}
private setupFadeFooter = async (contentEl: HTMLIonContentElement | null) => {
if (!contentEl) { console.error('ion-footer 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();
/**
* Handle fading of toolbars on scroll
*/
this.contentScrollCallback = () => { handleFooterFade(scrollEl, this.el); };
scrollEl.addEventListener('scroll', this.contentScrollCallback);
handleFooterFade(scrollEl, this.el);
}
private destroyCollapsibleFooter() {
if (this.scrollEl && this.contentScrollCallback) {
this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
this.contentScrollCallback = undefined;
}
}
render() {
const { translucent, collapse } = this;
const mode = getIonMode(this);
const translucent = this.translucent;
return (
<Host
role="contentinfo"
@ -38,6 +98,8 @@ export class Footer implements ComponentInterface {
[`footer-translucent`]: translucent,
[`footer-translucent-${mode}`]: translucent,
[`footer-collapse-${collapse}`]: collapse !== undefined,
}}
>
{ mode === 'ios' && translucent &&

View File

@ -0,0 +1,36 @@
import { readTask, writeTask } from '@stencil/core';
import { clamp } from '../../utils/helpers';
export const handleFooterFade = (scrollEl: HTMLElement, baseEl: HTMLElement) => {
readTask(() => {
const scrollTop = scrollEl.scrollTop;
const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
/**
* Toolbar background will fade
* out over fadeDuration in pixels.
*/
const fadeDuration = 10;
/**
* Begin fading out maxScroll - 30px
* from the bottom of the content.
* Also determine how close we are
* to starting the fade. If we are
* before the starting point, the
* scale value will get clamped to 0.
* If we are after the maxScroll (rubber
* band scrolling), the scale value will
* get clamped to 1.
*/
const fadeStart = maxScroll - fadeDuration;
const distanceToStart = scrollTop - fadeStart;
const scale = clamp(0, 1 - (distanceToStart / fadeDuration), 1);
writeTask(() => {
baseEl.style.setProperty('--opacity-scale', scale.toString());
})
});
}

View File

@ -3,6 +3,10 @@
Footer is a root component of a page that sits at the bottom of the page.
Footer can be a wrapper for ion-toolbar to make sure the content area is sized correctly.
## Fade Footer
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.
<!-- Auto Generated Below -->
@ -25,6 +29,13 @@ Footer can be a wrapper for ion-toolbar to make sure the content area is sized c
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
<!-- Fade Footer -->
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
```
@ -50,6 +61,13 @@ export const FooterExample: React.FC = () => (
<IonTitle>Footer</IonTitle>
</IonToolbar>
</IonFooter>
{/*-- Fade Footer --*/}
<IonFooter collapse="fade">
<IonToolbar>
<IonTitle>Footer</IonTitle>
</IonToolbar>
</IonFooter>
</>
);
```
@ -69,7 +87,7 @@ export class FooterExample {
return [
<ion-content></ion-content>,
// Footer without a border
{/*-- Footer without a border --*/}
<ion-footer class="ion-no-border">
<ion-toolbar>
<ion-title>Footer - No Border</ion-title>
@ -80,6 +98,13 @@ export class FooterExample {
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>,
{/*-- Fade Footer --*/}
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
];
}
@ -105,6 +130,13 @@ export class FooterExample {
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
<!-- Fade Footer -->
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
</template>
<script>
@ -122,7 +154,8 @@ export default defineComponent({
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ----------- |
| `collapse` | `collapse` | Describes the scroll effect that will be applied to the footer. Only applies in iOS mode. | `"fade" \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `translucent` | `translucent` | If `true`, the footer will be translucent. Only applies when the mode is `"ios"` and the device supports [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility). Note: In order to scroll content behind the footer, the `fullscreen` attribute needs to be set on the content. | `boolean` | `false` |

View File

@ -0,0 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('footer: fade', async () => {
const page = await newE2EPage({
url: '/src/components/footer/test/fade?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Footer - 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;
}
.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;
}
</style>
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header 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 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 collapse="fade" translucent="true">
<ion-toolbar>
<ion-title>Updated Just Now</ion-title>
</ion-toolbar>
</ion-footer>
</div>
</ion-app>
</body>
</html>

View File

@ -13,4 +13,11 @@
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
<!-- Fade Footer -->
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
```

View File

@ -13,4 +13,11 @@
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
<!-- Fade Footer -->
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
```

View File

@ -18,6 +18,13 @@ export const FooterExample: React.FC = () => (
<IonTitle>Footer</IonTitle>
</IonToolbar>
</IonFooter>
{/*-- Fade Footer --*/}
<IonFooter collapse="fade">
<IonToolbar>
<IonTitle>Footer</IonTitle>
</IonToolbar>
</IonFooter>
</>
);
```

View File

@ -10,7 +10,7 @@ export class FooterExample {
return [
<ion-content></ion-content>,
// Footer without a border
{/*-- Footer without a border --*/}
<ion-footer class="ion-no-border">
<ion-toolbar>
<ion-title>Footer - No Border</ion-title>
@ -21,6 +21,13 @@ export class FooterExample {
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>,
{/*-- Fade Footer --*/}
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
];
}

View File

@ -14,6 +14,13 @@
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
<!-- Fade Footer -->
<ion-footer collapse="fade">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
</template>
<script>

View File

@ -33,7 +33,13 @@
--border-width: 0;
}
// iOS Header - Collapse
// iOS Header - Collapse Fade
// --------------------------------------------------
.header-collapse-fade ion-toolbar {
--opacity-scale: inherit;
}
// iOS Header - Collapse Condense
// --------------------------------------------------
.header-collapse-condense {
z-index: 9;
@ -71,6 +77,15 @@
padding-bottom: 13px;
}
.header-collapse-main {
--opacity-scale: 1;
}
.header-collapse-main ion-toolbar {
--opacity-scale: inherit;
}
.header-collapse-main ion-toolbar.in-toolbar ion-title,
.header-collapse-main ion-toolbar.in-toolbar ion-buttons {
transition: all 0.2s ease-in-out;

View File

@ -1,9 +1,9 @@
import { Component, ComponentInterface, Element, Host, Prop, h, writeTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { inheritAttributes } from '../../utils/helpers';
import { componentOnReady, inheritAttributes } from '../../utils/helpers';
import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -16,8 +16,6 @@ import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarInte
}
})
export class Header implements ComponentInterface {
private collapsibleHeaderInitialized = false;
private scrollEl?: HTMLElement;
private contentScrollCallback?: any;
private intersectionObserver?: any;
@ -27,12 +25,12 @@ export class Header implements ComponentInterface {
@Element() el!: HTMLElement;
/**
* Describes the scroll effect that will be applied to the header
* `condense` only applies in iOS mode.
* Describes the scroll effect that will be applied to the header.
* Only applies in iOS mode.
*
* Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
*/
@Prop() collapse?: 'condense';
@Prop() collapse?: 'condense' | 'fade';
/**
* If `true`, the header will be translucent.
@ -48,12 +46,12 @@ export class Header implements ComponentInterface {
this.inheritedAttributes = inheritAttributes(this.el, ['role']);
}
async componentDidLoad() {
await this.checkCollapsibleHeader();
componentDidLoad() {
this.checkCollapsibleHeader();
}
async componentDidUpdate() {
await this.checkCollapsibleHeader();
componentDidUpdate() {
this.checkCollapsibleHeader();
}
disconnectedCallback() {
@ -61,13 +59,17 @@ export class Header implements ComponentInterface {
}
private async checkCollapsibleHeader() {
const mode = getIonMode(this);
if (mode !== 'ios') { return; }
const { collapse } = this;
const hasCondense = collapse === 'condense';
const hasFade = collapse === 'fade';
// Determine if the header can collapse
const hasCollapse = this.collapse === 'condense';
const canCollapse = (hasCollapse && getIonMode(this) === 'ios') ? hasCollapse : false;
if (!canCollapse && this.collapsibleHeaderInitialized) {
this.destroyCollapsibleHeader();
} else if (canCollapse && !this.collapsibleHeaderInitialized) {
if (hasCondense) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null;
@ -78,10 +80,31 @@ export class Header implements ComponentInterface {
cloneElement('ion-back-button');
});
await this.setupCollapsibleHeader(contentEl, pageEl);
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;
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();
/**
* Handle fading of toolbars on scroll
*/
this.contentScrollCallback = () => { handleHeaderFade(this.scrollEl!, this.el, condenseHeader); };
scrollEl!.addEventListener('scroll', this.contentScrollCallback);
handleHeaderFade(this.scrollEl!, this.el, condenseHeader);
}
private destroyCollapsibleHeader() {
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
@ -99,10 +122,11 @@ export class Header implements ComponentInterface {
}
}
private async setupCollapsibleHeader(contentEl: HTMLIonContentElement | null, pageEl: Element | null) {
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; }
if (typeof (IntersectionObserver as any) === 'undefined') { return; }
await new Promise(resolve => componentOnReady(contentEl, resolve));
this.scrollEl = await contentEl.getScrollElement();
const headers = pageEl.querySelectorAll('ion-header');
@ -116,10 +140,7 @@ export class Header implements ComponentInterface {
if (!mainHeaderIndex || !scrollHeaderIndex) { return; }
setHeaderActive(mainHeaderIndex, false);
mainHeaderIndex.toolbars.forEach(toolbar => {
setToolbarBackgroundOpacity(toolbar, 0);
});
setToolbarBackgroundOpacity(mainHeaderIndex.el, 0);
/**
* Handle interaction between toolbar collapse and
@ -145,8 +166,6 @@ export class Header implements ComponentInterface {
this.collapsibleMainHeader.classList.add('header-collapse-main');
}
});
this.collapsibleHeaderInitialized = true;
}
render() {

View File

@ -5,7 +5,7 @@ import { clamp } from '../../utils/helpers';
const TRANSITION = 'all 0.2s ease-in-out';
interface HeaderIndex {
el: HTMLElement;
el: HTMLIonHeaderElement;
toolbars: ToolbarIndex[] | [];
}
@ -64,11 +64,19 @@ export const handleContentScroll = (scrollEl: HTMLElement, scrollHeaderIndex: He
});
};
export const setToolbarBackgroundOpacity = (toolbar: ToolbarIndex, opacity?: number) => {
export const setToolbarBackgroundOpacity = (headerEl: HTMLIonHeaderElement, opacity?: number) => {
/**
* Fading in the backdrop opacity
* should happen after the large title
* has collapsed, so it is handled
* by handleHeaderFade()
*/
if (headerEl.collapse === 'fade') { return; }
if (opacity === undefined) {
toolbar.background.style.removeProperty('--opacity');
headerEl.style.removeProperty('--opacity-scale');
} else {
toolbar.background.style.setProperty('--opacity', opacity.toString());
headerEl.style.setProperty('--opacity-scale', opacity.toString());
}
};
@ -88,9 +96,7 @@ const handleToolbarBorderIntersection = (ev: any, mainHeaderIndex: HeaderIndex,
*/
const scale = (ev[0].intersectionRatio > 0.9 || scrollTop <= 0) ? 0 : ((1 - ev[0].intersectionRatio) * 100) / 75;
mainHeaderIndex.toolbars.forEach(toolbar => {
setToolbarBackgroundOpacity(toolbar, (scale === 1) ? undefined : scale);
});
setToolbarBackgroundOpacity(mainHeaderIndex.el, (scale === 1) ? undefined : scale);
};
/**
@ -136,7 +142,7 @@ export const handleToolbarIntersection = (ev: any, mainHeaderIndex: HeaderIndex,
if (hasValidIntersection && scrollTop > 0) {
setHeaderActive(mainHeaderIndex);
setHeaderActive(scrollHeaderIndex, false);
setToolbarBackgroundOpacity(mainHeaderIndex.toolbars[0]);
setToolbarBackgroundOpacity(mainHeaderIndex.el);
}
}
});
@ -160,3 +166,37 @@ export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, trans
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
});
};
export const handleHeaderFade = (scrollEl: HTMLElement, baseEl: HTMLElement, condenseHeader: HTMLElement | null) => {
readTask(() => {
const scrollTop = scrollEl.scrollTop;
const baseElHeight = baseEl.clientHeight;
const fadeStart = (condenseHeader) ? condenseHeader.clientHeight : 0;
/**
* If we are using fade header with a condense
* header, then the toolbar backgrounds should
* not begin to fade in until the condense
* header has fully collapsed.
*
* Additionally, the main content should not
* overflow out of the container until the
* condense header has fully collapsed. When
* using just the condense header the content
* should overflow out of the container.
*/
if ((condenseHeader !== null) && (scrollTop < fadeStart)) {
baseEl.style.setProperty('--opacity-scale', '0');
scrollEl.style.setProperty('clip-path', `inset(${baseElHeight}px 0px 0px 0px)`);
return;
}
const distanceToStart = scrollTop - fadeStart;
const fadeDuration = 10;
const scale = clamp(0, (distanceToStart / fadeDuration), 1);
writeTask(() => {
scrollEl.style.removeProperty('clip-path');
baseEl.style.setProperty('--opacity-scale', scale.toString());
})
});
}

View File

@ -3,6 +3,11 @@
Header is a parent component that holds the toolbar component.
It's important to note that ion-header needs to be the one of the three root elements of a page
## Fade Header
The `collapse` property can be set to `'fade'` on a page's main `ion-header` 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.
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`.
<!-- Auto Generated Below -->
@ -10,7 +15,7 @@ It's important to note that ion-header needs to be the one of the three root ele
## Usage
### Angular / javascript
### Angular
```html
<ion-header>
@ -40,6 +45,69 @@ It's important to note that ion-header needs to be the one of the three root ele
</ion-toolbar>
</ion-header>
</ion-content>
<!-- Fade Header with collapse header -->
<ion-header collapse="fade" [translucent]="true">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
```
### Javascript
```html
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>My Navigation Bar</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-title>Subheader</ion-title>
</ion-toolbar>
</ion-header>
<!-- Header without a border -->
<ion-header class="ion-no-border">
<ion-toolbar>
<ion-title>Header - No Border</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">My Navigation Bar</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
<!-- Fade Header with collapse header -->
<ion-header collapse="fade" translucent="true">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
```
@ -78,6 +146,21 @@ export const HeaderExample: React.FC = () => (
</IonToolbar>
</IonHeader>
</IonContent>
{/*-- Fade Header with collapse header --*/}
<IonHeader collapse="fade" translucent={true}>
<IonToolbar>
<IonTitle>Header</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Header</IonTitle>
</IonToolbar>
</IonHeader>
</IonContent>
</>
);
```
@ -108,7 +191,7 @@ export class HeaderExample {
</ion-toolbar>
</ion-header>,
// Header without a border
{/*-- Header without a border --*/}
<ion-header class="ion-no-border">
<ion-toolbar>
<ion-title>Header - No Border</ion-title>
@ -121,6 +204,21 @@ export class HeaderExample {
<ion-title size="large">My Navigation Bar</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>,
{/*-- Fade Header with collapse header --*/}
<ion-header collapse="fade" translucent={true}>
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen={true}>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
];
}
@ -159,6 +257,21 @@ export class HeaderExample {
</ion-toolbar>
</ion-header>
</ion-content>
<!-- Fade Header with collapse header -->
<ion-header collapse="fade" :translucent="true">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
</template>
<script>
@ -190,8 +303,8 @@ export default defineComponent({
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------- |
| `collapse` | `collapse` | Describes the scroll effect that will be applied to the header `condense` only applies in iOS mode. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) | `"condense" \| undefined` | `undefined` |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------- |
| `collapse` | `collapse` | Describes the scroll effect that will be applied to the header. Only applies in iOS mode. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) | `"condense" \| "fade" \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `translucent` | `translucent` | If `true`, the header will be translucent. Only applies when the mode is `"ios"` and the device supports [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility). Note: In order to scroll content behind the header, the `fullscreen` attribute needs to be set on the content. | `boolean` | `false` |

View File

@ -0,0 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('header: fade', async () => {
const page = await newE2EPage({
url: '/src/components/header/test/fade?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@ -0,0 +1,89 @@
<!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;
}
.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;
}
</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">
<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>
</html>

View File

@ -26,4 +26,19 @@
</ion-toolbar>
</ion-header>
</ion-content>
<!-- Fade Header with collapse header -->
<ion-header collapse="fade" [translucent]="true">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
```

View File

@ -26,4 +26,19 @@
</ion-toolbar>
</ion-header>
</ion-content>
<!-- Fade Header with collapse header -->
<ion-header collapse="fade" translucent="true">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
```

View File

@ -31,6 +31,21 @@ export const HeaderExample: React.FC = () => (
</IonToolbar>
</IonHeader>
</IonContent>
{/*-- Fade Header with collapse header --*/}
<IonHeader collapse="fade" translucent={true}>
<IonToolbar>
<IonTitle>Header</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Header</IonTitle>
</IonToolbar>
</IonHeader>
</IonContent>
</>
);
```

View File

@ -21,7 +21,7 @@ export class HeaderExample {
</ion-toolbar>
</ion-header>,
// Header without a border
{/*-- Header without a border --*/}
<ion-header class="ion-no-border">
<ion-toolbar>
<ion-title>Header - No Border</ion-title>
@ -34,6 +34,21 @@ export class HeaderExample {
<ion-title size="large">My Navigation Bar</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>,
{/*-- Fade Header with collapse header --*/}
<ion-header collapse="fade" translucent={true}>
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen={true}>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
];
}

View File

@ -27,6 +27,21 @@
</ion-toolbar>
</ion-header>
</ion-content>
<!-- Fade Header with collapse header -->
<ion-header collapse="fade" :translucent="true">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Header</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
</template>
<script>

View File

@ -25,6 +25,7 @@
--border-width: 0;
--border-style: solid;
--opacity: 1;
--opacity-scale: 1;
@include font-smoothing();
@include padding-horizontal(var(--ion-safe-area-left), var(--ion-safe-area-right));
@ -91,7 +92,7 @@
background: var(--background);
contain: strict;
opacity: var(--opacity);
opacity: calc(var(--opacity) * var(--opacity-scale));
z-index: $z-index-toolbar-background;
pointer-events: none;
}

View File

@ -334,6 +334,7 @@ export const IonFabList = /*@__PURE__*/ defineContainer<JSX.IonFabList>('ion-fab
export const IonFooter = /*@__PURE__*/ defineContainer<JSX.IonFooter>('ion-footer', IonFooterCmp, [
'collapse',
'translucent'
]);