Skip to content
Permalink
Browse files

feat(title): add large iOS toolbar title (#19268)

Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
  • Loading branch information...
liamdebeasi and brandyscarney committed Sep 4, 2019
1 parent d9610cd commit 923312ecd5df101d60708a38942c193ac15b60c5
@@ -75,14 +75,15 @@ export class IonButton {
proxyInputs(IonButton, ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'href', 'mode', 'rel', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type']);

export declare interface IonButtons extends Components.IonButtons {}
@Component({ selector: 'ion-buttons', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>' })
@Component({ selector: 'ion-buttons', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['collapse'] })
export class IonButtons {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
proxyInputs(IonButtons, ['collapse']);

export declare interface IonCard extends Components.IonCard {}
@Component({ selector: 'ion-card', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerDirection', 'target', 'type'] })
@@ -269,15 +270,15 @@ export class IonGrid {
proxyInputs(IonGrid, ['fixed']);

export declare interface IonHeader extends Components.IonHeader {}
@Component({ selector: 'ion-header', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['mode', 'translucent'] })
@Component({ selector: 'ion-header', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['collapse', 'mode', 'translucent'] })
export class IonHeader {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
proxyInputs(IonHeader, ['mode', 'translucent']);
proxyInputs(IonHeader, ['collapse', 'mode', 'translucent']);

export declare interface IonIcon extends Components.IonIcon {}
@Component({ selector: 'ion-icon', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['ariaLabel', 'color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'size', 'src'] })
@@ -895,15 +896,15 @@ export class IonThumbnail {
}

export declare interface IonTitle extends Components.IonTitle {}
@Component({ selector: 'ion-title', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['color'] })
@Component({ selector: 'ion-title', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['color', 'size'] })
export class IonTitle {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
proxyInputs(IonTitle, ['color']);
proxyInputs(IonTitle, ['color', 'size']);

export declare interface IonToggle extends Components.IonToggle {}
@Component({ selector: 'ion-toggle', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['checked', 'color', 'disabled', 'mode', 'name', 'value'] })
@@ -178,6 +178,7 @@ ion-button,css-prop,--ripple-color
ion-button,css-prop,--transition

ion-buttons,scoped
ion-buttons,prop,collapse,boolean,false,false,false

This comment has been minimized.

Copy link
@manucorporat

manucorporat Sep 5, 2019

Member

some concerns about this API, it might be the correct one, but we should discuss more


ion-card,scoped
ion-card,prop,button,boolean,false,false,false
@@ -399,6 +400,7 @@ ion-grid,css-prop,--ion-grid-width-xl
ion-grid,css-prop,--ion-grid-width-xs

ion-header,none
ion-header,prop,collapse,boolean,false,false,false
ion-header,prop,mode,"ios" | "md",undefined,false,false
ion-header,prop,translucent,boolean,false,false,false

@@ -1204,6 +1206,7 @@ ion-thumbnail,css-prop,--size

ion-title,shadow
ion-title,prop,color,string | undefined,undefined,false,false
ion-title,prop,size,"large" | undefined,undefined,false,false
ion-title,css-prop,--color

ion-toast,shadow
@@ -389,7 +389,12 @@ export namespace Components {
*/
'type': 'submit' | 'reset' | 'button';
}
interface IonButtons {}
interface IonButtons {
/**
* If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in `ios` mode with `collapse` set to `true` on `ion-header`
*/
'collapse': boolean;
}
interface IonCard {
/**
* If `true`, a button tag will be rendered and the card will be tappable.
@@ -865,6 +870,10 @@ export namespace Components {
'fixed': boolean;
}
interface IonHeader {
/**
* If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode.
*/
'collapse': boolean;
/**
* The mode determines which platform styles to use.
*/
@@ -2696,6 +2705,10 @@ export namespace Components {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
'color'?: Color;
/**
* The size of the toolbar title. Only applies in `ios` mode.
*/
'size'?: 'large' | undefined;
}
interface IonToast {
/**
@@ -3883,7 +3896,12 @@ declare namespace LocalJSX {
*/
'type'?: 'submit' | 'reset' | 'button';
}
interface IonButtons extends JSXBase.HTMLAttributes<HTMLIonButtonsElement> {}
interface IonButtons extends JSXBase.HTMLAttributes<HTMLIonButtonsElement> {
/**
* If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in `ios` mode with `collapse` set to `true` on `ion-header`
*/
'collapse'?: boolean;
}
interface IonCard extends JSXBase.HTMLAttributes<HTMLIonCardElement> {
/**
* If `true`, a button tag will be rendered and the card will be tappable.
@@ -4371,6 +4389,10 @@ declare namespace LocalJSX {
'fixed'?: boolean;
}
interface IonHeader extends JSXBase.HTMLAttributes<HTMLIonHeaderElement> {
/**
* If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode.
*/
'collapse'?: boolean;
/**
* The mode determines which platform styles to use.
*/
@@ -6012,6 +6034,10 @@ declare namespace LocalJSX {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
'color'?: Color;
/**
* The size of the toolbar title. Only applies in `ios` mode.
*/
'size'?: 'large' | undefined;
}
interface IonToast extends JSXBase.HTMLAttributes<HTMLIonToastElement> {
/**
@@ -1,4 +1,4 @@
import { Component, ComponentInterface, Host, h } from '@stencil/core';
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';

import { getIonMode } from '../../global/ionic-global';

@@ -12,6 +12,18 @@ import { getIonMode } from '../../global/ionic-global';
})
export class Buttons implements ComponentInterface {

/**
* If true, buttons will disappear when its
* parent toolbar has fully collapsed if the toolbar
* is not the first toolbar. If the toolbar is the
* first toolbar, the buttons will be hidden and will
* only be shown once all toolbars have fully collapsed.
*
* Only applies in `ios` mode with `collapse` set to
* `true` on `ion-header`
*/
@Prop() collapse = false;

render() {
return (
<Host class={getIonMode(this)}>
@@ -204,6 +204,13 @@ export const ButtonsExample: React.FC = () => (



## Properties

| Property | Attribute | Description | Type | Default |
| ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------- |
| `collapse` | `collapse` | If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in `ios` mode with `collapse` set to `true` on `ion-header` | `boolean` | `false` |


----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
@@ -16,9 +16,58 @@
.header-translucent-ios {
backdrop-filter: $header-ios-translucent-filter;
}

.header-translucent-ios ion-toolbar {
--opacity: .8;
--backdrop-filter: #{$header-ios-translucent-filter};
}
}

// iOS Header - Collapse
// --------------------------------------------------
.header-collapse-ios {
z-index: 9;
}

.header-collapse-ios ion-toolbar {
position: sticky;
top: 0;
}

.header-collapse-ios ion-toolbar:first-child {
padding-top: 7px;

z-index: 1;
}

.header-collapse-ios ion-toolbar {
z-index: 0;
}

.header-collapse-ios ion-toolbar ion-searchbar {
height: 48px;

padding-top: 0px;
padding-bottom: 13px;
}

ion-toolbar.in-toolbar ion-title,
ion-toolbar.in-toolbar ion-buttons {
transition: all 0.2s ease-in-out;
}

/**
* There is a bug in Safari where animating the opacity
* on an element in a scrollable container while scrolling
* causes the scroll position to jump to the top
*/
.header-collapse-ios ion-toolbar ion-title,
.header-collapse-ios ion-toolbar ion-buttons {
transition: none;
}

.header-collapse-ios-inactive ion-toolbar.in-toolbar ion-title,
.header-collapse-ios-inactive ion-toolbar.in-toolbar ion-buttons[collapse] {
opacity: 0;
pointer-events: none;
}
@@ -25,3 +25,7 @@
.header-md[no-border]::after {
display: none;
}

.header-collapse-md {
display: none;
}
@@ -1,7 +1,8 @@
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';
import { Component, ComponentInterface, Element, Host, Prop, h, readTask, writeTask } from '@stencil/core';

import { getIonMode } from '../../global/ionic-global';

import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive } from './header.utils';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*/
@@ -14,6 +15,17 @@ import { getIonMode } from '../../global/ionic-global';
})
export class Header implements ComponentInterface {

private scrollEl?: HTMLElement;
private contentScrollCallback?: any;

@Element() el!: HTMLElement;

/**
* If `true`, the header will collapse on scroll of the content.
* Only applies in `ios` mode.
*/
@Prop() collapse = false;

/**
* If `true`, the header will be translucent.
* Only applies when the mode is `"ios"` and the device supports
@@ -24,6 +36,76 @@ export class Header implements ComponentInterface {
*/
@Prop() translucent = false;

async componentDidLoad() {
// Determine if the header can collapse
const canCollapse = (this.collapse && getIonMode(this) === 'ios') ? this.collapse : false;

This comment has been minimized.

Copy link
@manucorporat

manucorporat Sep 5, 2019

Member

collapse is not reactive


const tabs = this.el.closest('ion-tabs');
const page = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const contentEl = tabs ? tabs.querySelector('ion-content') : page!.querySelector('ion-content');

This comment has been minimized.

Copy link
@manucorporat

manucorporat Sep 5, 2019

Member

remove page! type assertion


if (canCollapse) {
await this.setupCollapsableHeader(contentEl, (tabs) ? tabs : page!);

This comment has been minimized.

Copy link
@manucorporat

manucorporat Sep 5, 2019

Member

type assertion can be handled better

}
}

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

private async setupCollapsableHeader(contentEl: HTMLIonContentElement | null, pageEl: Element) {
if (!contentEl) { console.error('ion-header requires a content to collapse, make sure there is an ion-content.'); }

this.scrollEl = await contentEl!.getScrollElement();

This comment has been minimized.

Copy link
@manucorporat

manucorporat Sep 5, 2019

Member

we should take advantage of if (!contentEl) { and avoid the ! type assertions whenever it's possible


readTask(() => {
const headers = pageEl.querySelectorAll('ion-header');
const mainHeader = Array.from(headers).find((header: any) => !header.collapse) as HTMLElement | undefined;

if (!mainHeader || !this.scrollEl) { return; }

const mainHeaderIndex = createHeaderIndex(mainHeader);
const scrollHeaderIndex = createHeaderIndex(this.el);

if (!mainHeaderIndex || !scrollHeaderIndex) { return; }

setHeaderActive(mainHeaderIndex, false);

// TODO: Find a better way to do this
let remainingHeight = 0;
for (let i = 1; i <= scrollHeaderIndex.toolbars.length - 1; i++) {
remainingHeight += scrollHeaderIndex.toolbars[i].el.clientHeight;
}

/**
* Handle interaction between toolbar collapse and
* showing/hiding content in the primary ion-header
*/
const toolbarIntersection = (ev: any) => { handleToolbarIntersection(ev, mainHeaderIndex, scrollHeaderIndex); };

readTask(() => {
const mainHeaderHeight = mainHeaderIndex.el.clientHeight;
const intersectionObserver = new IntersectionObserver(toolbarIntersection, { threshold: 0.25, rootMargin: `-${mainHeaderHeight}px 0px 0px 0px` });
intersectionObserver.observe(scrollHeaderIndex.toolbars[0].el);

This comment has been minimized.

Copy link
@manucorporat

manucorporat Sep 5, 2019

Member

do we ever free this observer?

});

/**
* Handle scaling of large iOS titles and
* showing/hiding border on last toolbar
* in primary header
*/
this.contentScrollCallback = () => { handleContentScroll(this.scrollEl!, mainHeaderIndex, scrollHeaderIndex, remainingHeight); };
this.scrollEl.addEventListener('scroll', this.contentScrollCallback);
});

writeTask(() => {
cloneElement('ion-title');
cloneElement('ion-back-button');
});
}

render() {
const mode = getIonMode(this);
return (
@@ -36,6 +118,7 @@ export class Header implements ComponentInterface {
[`header-${mode}`]: true,

[`header-translucent`]: this.translucent,
[`header-collapse-${mode}`]: this.collapse,
[`header-translucent-${mode}`]: this.translucent,
}}
>

0 comments on commit 923312e

Please sign in to comment.
You can’t perform that action at this time.