Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ ion-fab-list,prop,side,"bottom" | "end" | "start" | "top",'bottom',false,false
ion-fab-list,prop,theme,"ios" | "md" | "ionic",undefined,false,false

ion-footer,none
ion-footer,prop,collapse,"fade" | undefined,undefined,false,false
ion-footer,prop,collapse,"fade" | "hide" | undefined,undefined,false,false
ion-footer,prop,mode,"ios" | "md",undefined,false,false
ion-footer,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-footer,prop,translucent,boolean,false,false,false
Expand Down Expand Up @@ -923,7 +923,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" | "fade" | undefined,undefined,false,false
ion-header,prop,collapse,"condense" | "fade" | "hide" | undefined,undefined,false,false
ion-header,prop,divider,boolean,false,false,false
ion-header,prop,mode,"ios" | "md",undefined,false,false
ion-header,prop,theme,"ios" | "md" | "ionic",undefined,false,false
Expand Down
20 changes: 10 additions & 10 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1454,9 +1454,9 @@ export namespace Components {
}
interface IonFooter {
/**
* Describes the scroll effect that will be applied to the footer. Only applies when the theme is `"ios"`.
* Describes the scroll effect that will be applied to the footer. - `"fade"` only applies when the theme is `"ios"`. - `"hide"` applies to all themes (`"ios"`, `"md"`, and `"ionic"`): the footer slides down and fades out after cumulative downward scrolling on the page content, and returns on any upward scroll (same behavior as `ion-header[collapse="hide"]`).
*/
"collapse"?: 'fade';
"collapse"?: 'fade' | 'hide';
/**
* The mode determines the platform behaviors of the component.
*/
Expand Down Expand Up @@ -1517,9 +1517,9 @@ export namespace Components {
}
interface IonHeader {
/**
* Describes the scroll effect that will be applied to the header. Only applies when the theme is `"ios"`. 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. - `"condense"` and `"fade"` only apply when the theme is `"ios"`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles). - `"hide"` applies to all themes (`"ios"`, `"md"`, and `"ionic"`): the header slides up and fades out after cumulative downward scrolling on the page content, and returns on any upward scroll.
*/
"collapse"?: 'condense' | 'fade';
"collapse"?: 'condense' | 'fade' | 'hide';
/**
* If `true`, the header will have a line at the bottom. TODO(ROU-10855): add support for this prop on ios/md themes
* @default false
Expand Down Expand Up @@ -7502,9 +7502,9 @@ declare namespace LocalJSX {
}
interface IonFooter {
/**
* Describes the scroll effect that will be applied to the footer. Only applies when the theme is `"ios"`.
* Describes the scroll effect that will be applied to the footer. - `"fade"` only applies when the theme is `"ios"`. - `"hide"` applies to all themes (`"ios"`, `"md"`, and `"ionic"`): the footer slides down and fades out after cumulative downward scrolling on the page content, and returns on any upward scroll (same behavior as `ion-header[collapse="hide"]`).
*/
"collapse"?: 'fade';
"collapse"?: 'fade' | 'hide';
/**
* The mode determines the platform behaviors of the component.
*/
Expand Down Expand Up @@ -7565,9 +7565,9 @@ declare namespace LocalJSX {
}
interface IonHeader {
/**
* Describes the scroll effect that will be applied to the header. Only applies when the theme is `"ios"`. 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. - `"condense"` and `"fade"` only apply when the theme is `"ios"`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles). - `"hide"` applies to all themes (`"ios"`, `"md"`, and `"ionic"`): the header slides up and fades out after cumulative downward scrolling on the page content, and returns on any upward scroll.
*/
"collapse"?: 'condense' | 'fade';
"collapse"?: 'condense' | 'fade' | 'hide';
/**
* If `true`, the header will have a line at the bottom. TODO(ROU-10855): add support for this prop on ios/md themes
* @default false
Expand Down Expand Up @@ -10964,7 +10964,7 @@ declare namespace LocalJSX {
"side": 'start' | 'end' | 'top' | 'bottom';
}
interface IonFooterAttributes {
"collapse": 'fade';
"collapse": 'fade' | 'hide';
"translucent": boolean;
}
interface IonGalleryAttributes {
Expand All @@ -10977,7 +10977,7 @@ declare namespace LocalJSX {
"fixed": boolean;
}
interface IonHeaderAttributes {
"collapse": 'condense' | 'fade';
"collapse": 'condense' | 'fade' | 'hide';
"divider": boolean;
"translucent": boolean;
}
Expand Down
110 changes: 110 additions & 0 deletions core/src/components/content/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,113 @@
*/
transform: translateZ(0);
}

// `ion-header` / `ion-footer` `[collapse="hide"]`.
// Slide distances are set on this host at runtime through @utils/on-scroll/collapse-hide.utils.
// `transform` is composited (GPU). `height` is transitioned in sync for layout; interpolating height
// still triggers layout work—`backface-visibility: hidden` keeps the layer friendly for the transform.
$transition-curve-base: cubic-bezier(0.4, 0, 1, 1);
$transition-curve-quick: cubic-bezier(0, 0, 0.2, 1);

// --- Header only -------------------------------------------------
:host(.content-header-hide-scroll-partner:not(.content-footer-hide-scroll-partner)) .inner-scroll {
@include transform(translateY(0));

backface-visibility: hidden;

height: 100%;

transition: transform 300ms $transition-curve-quick, height 300ms $transition-curve-quick;
}

:host(.content-header-hide-scroll-partner:not(.content-footer-hide-scroll-partner).content-header-hide-scroll-hidden)
.inner-scroll {
@include transform(translateY(calc(-1 * var(--header-hide-slide-y, 0px))));

backface-visibility: hidden;

height: calc(100% + var(--header-hide-slide-y, 0px));

transition: transform 200ms $transition-curve-base, height 200ms $transition-curve-base;
}

// --- Footer only -------------------------------------------------
:host(.content-footer-hide-scroll-partner:not(.content-header-hide-scroll-partner)) .inner-scroll {
@include transform(translateY(0));

backface-visibility: hidden;

height: 100%;

transition: transform 300ms $transition-curve-quick, height 300ms $transition-curve-quick;
}

:host(.content-footer-hide-scroll-partner:not(.content-header-hide-scroll-partner).content-footer-hide-scroll-hidden)
.inner-scroll {
@include transform(translateY(0));

backface-visibility: hidden;

height: calc(100% + var(--footer-hide-slide-y, 0px));

transition: transform 200ms $transition-curve-base, height 200ms $transition-curve-base;
}

// --- Both --------------------------------------------------------
:host(
.content-header-hide-scroll-partner.content-footer-hide-scroll-partner:not(.content-header-hide-scroll-hidden):not(
.content-footer-hide-scroll-hidden
)
)
.inner-scroll {
@include transform(translateY(0));

backface-visibility: hidden;

height: 100%;

transition: transform 300ms $transition-curve-quick, height 300ms $transition-curve-quick;
}

:host(
.content-header-hide-scroll-partner.content-footer-hide-scroll-partner.content-header-hide-scroll-hidden:not(
.content-footer-hide-scroll-hidden
)
)
.inner-scroll {
@include transform(translateY(calc(-1 * var(--header-hide-slide-y, 0px))));

backface-visibility: hidden;

height: calc(100% + var(--header-hide-slide-y, 0px));

transition: transform 200ms $transition-curve-base, height 200ms $transition-curve-base;
}

:host(
.content-header-hide-scroll-partner.content-footer-hide-scroll-partner:not(
.content-header-hide-scroll-hidden
).content-footer-hide-scroll-hidden
)
.inner-scroll {
@include transform(translateY(0));

backface-visibility: hidden;

height: calc(100% + var(--footer-hide-slide-y, 0px));

transition: transform 200ms $transition-curve-base, height 200ms $transition-curve-base;
}

:host(
.content-header-hide-scroll-partner.content-footer-hide-scroll-partner.content-header-hide-scroll-hidden.content-footer-hide-scroll-hidden
)
.inner-scroll {
@include transform(translateY(calc(-1 * var(--header-hide-slide-y, 0px))));

backface-visibility: hidden;

height: calc(100% + var(--header-hide-slide-y, 0px) + var(--footer-hide-slide-y, 0px));

transition: transform 200ms $transition-curve-base, height 200ms $transition-curve-base;
}
24 changes: 24 additions & 0 deletions core/src/components/footer/footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,27 @@ ion-footer {
ion-footer.footer-toolbar-padding ion-toolbar:last-of-type {
padding-bottom: var(--ion-safe-area-bottom, 0);
}

// `collapse="hide"` — same motion pattern as `ion-header` (slide + opacity); TS in @utils/on-scroll.
$footer-hide-curve-base: cubic-bezier(0.4, 0, 1, 1);
$footer-hide-curve-quick: cubic-bezier(0, 0, 0.2, 1);

ion-footer.footer-collapse-hide {
// --footer-hide-slide-y is set at runtime through @utils/on-scroll/collapse-hide.utils.

@include transform(translateY(0));

transition: transform 300ms $footer-hide-curve-quick, opacity 300ms $footer-hide-curve-quick;

opacity: 1;
}

ion-footer.footer-collapse-hide.footer-collapse-hide-hidden {
@include transform(translateY(var(--footer-hide-slide-y, 0px)));

pointer-events: none;

transition: transform 200ms $footer-hide-curve-base, opacity 300ms $footer-hide-curve-base;

opacity: 0;
}
92 changes: 77 additions & 15 deletions core/src/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createKeyboardController } from '@utils/keyboard/keyboard-controller';
import { config } from '../../global/config';
import { getIonTheme } from '../../global/ionic-global';

import { handleFooterFade } from './footer.utils';
import { handleFooterFade, createFooterHideInteraction } from './footer.utils';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
Expand All @@ -24,6 +24,11 @@ import { handleFooterFade } from './footer.utils';
export class Footer implements ComponentInterface {
private scrollEl?: HTMLElement;
private contentScrollCallback?: () => void;
private footerHideCleanup?: () => void;
private appliedCollapse?: 'fade' | 'hide';
private appliedTheme?: string;
private didLoad = false;
private setupToken = 0;
private keyboardCtrl: KeyboardController | null = null;
private keyboardCtrlPromise: Promise<KeyboardController> | null = null;

Expand All @@ -33,9 +38,13 @@ export class Footer implements ComponentInterface {

/**
* Describes the scroll effect that will be applied to the footer.
* Only applies when the theme is `"ios"`.
*
* - `"fade"` only applies when the theme is `"ios"`.
* - `"hide"` applies to all themes (`"ios"`, `"md"`, and `"ionic"`): the footer
* slides down and fades out after cumulative downward scrolling on the page content,
* and returns on any upward scroll (same behavior as `ion-header[collapse="hide"]`).
*/
@Prop() collapse?: 'fade';
@Prop() collapse?: 'fade' | 'hide';

/**
* If `true`, the footer will be translucent.
Expand All @@ -48,6 +57,7 @@ export class Footer implements ComponentInterface {
@Prop() translucent = false;

componentDidLoad() {
this.didLoad = true;
this.checkCollapsibleFooter();
}

Expand All @@ -56,6 +66,12 @@ export class Footer implements ComponentInterface {
}

async connectedCallback() {
// On re-attach (didLoad already true but disconnectedCallback ran since),
// componentDidLoad will not fire again — re-run setup here.
if (this.didLoad) {
this.checkCollapsibleFooter();
}

const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
/**
* If the keyboard is hiding, then we need to wait
Expand Down Expand Up @@ -86,6 +102,8 @@ export class Footer implements ComponentInterface {
}

disconnectedCallback() {
this.destroyCollapsibleFooter();

if (this.keyboardCtrlPromise) {
this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
this.keyboardCtrlPromise = null;
Expand All @@ -99,31 +117,64 @@ export class Footer implements ComponentInterface {

private checkCollapsibleFooter = () => {
const theme = getIonTheme(this);
if (theme !== 'ios') {
const { collapse } = this;
const hasFade = collapse === 'fade';
const hasHide = collapse === 'hide';

const runIosFade = theme === 'ios' && hasFade;

if (!runIosFade && !hasHide) {
this.destroyCollapsibleFooter();
return;
}

const { collapse } = this;
const hasFade = collapse === 'fade';
// Skip teardown/rebuild when the collapse mode and theme have not changed
// since the last setup — avoids thrashing listeners and resetting scroll
// accumulators on unrelated re-renders (e.g. keyboardVisible state flips).
const activeMode = hasHide ? 'hide' : 'fade';
if (this.appliedCollapse === activeMode && this.appliedTheme === theme) {
return;
}

this.destroyCollapsibleFooter();

if (hasFade) {
const appRootSelector = config.get('appRootSelector', 'ion-app');
const pageEl = this.el.closest(`${appRootSelector},ion-page,.ion-page,page-inner`);
const contentEl = pageEl ? findIonContent(pageEl) : null;
const appRootSelector = config.get('appRootSelector', 'ion-app');
const pageEl = this.el.closest(`${appRootSelector},ion-page,.ion-page,page-inner`);
const contentEl = pageEl ? findIonContent(pageEl) : null;

if (!contentEl) {
printIonContentErrorMsg(this.el);
return;
}
if (!contentEl) {
printIonContentErrorMsg(this.el);
return;
}

this.appliedCollapse = activeMode;
this.appliedTheme = theme;

if (runIosFade) {
this.setupFadeFooter(contentEl);
} else if (hasHide) {
void this.setupHideFooter(contentEl);
}
};

private async setupHideFooter(contentEl: HTMLElement) {
const token = ++this.setupToken;
const scrollEl = await getScrollElement(contentEl);
// A newer checkCollapsibleFooter ran while we were awaiting — abandon.
if (token !== this.setupToken) {
return;
}
this.scrollEl = scrollEl;
this.footerHideCleanup = createFooterHideInteraction(this.el, scrollEl);
}

private setupFadeFooter = async (contentEl: HTMLElement) => {
const scrollEl = (this.scrollEl = await getScrollElement(contentEl));
const token = ++this.setupToken;
const scrollEl = await getScrollElement(contentEl);
if (token !== this.setupToken) {
return;
}
this.scrollEl = scrollEl;

/**
* Handle fading of toolbars on scroll
Expand All @@ -137,10 +188,21 @@ export class Footer implements ComponentInterface {
};

private destroyCollapsibleFooter() {
// Invalidate any in-flight setupHideFooter/setupFadeFooter awaits.
this.setupToken++;

if (this.footerHideCleanup) {
this.footerHideCleanup();
this.footerHideCleanup = undefined;
}

if (this.scrollEl && this.contentScrollCallback) {
this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
this.contentScrollCallback = undefined;
}

this.appliedCollapse = undefined;
this.appliedTheme = undefined;
}

render() {
Expand Down
Loading
Loading