Skip to content

Commit

Permalink
feat(title): add large iOS toolbar title (#19268)
Browse files Browse the repository at this point in the history
Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
  • Loading branch information
liamdebeasi and brandyscarney committed Sep 4, 2019
1 parent d9610cd commit 923312e
Show file tree
Hide file tree
Showing 21 changed files with 964 additions and 80 deletions.
11 changes: 6 additions & 5 deletions angular/src/directives/proxies.ts
Expand Up @@ -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'] })
Expand Down Expand Up @@ -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'] })
Expand Down Expand Up @@ -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'] })
Expand Down
3 changes: 3 additions & 0 deletions core/api.txt
Expand Up @@ -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

Contributor

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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
30 changes: 28 additions & 2 deletions core/src/components.d.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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> {
/**
Expand Down
14 changes: 13 additions & 1 deletion core/src/components/buttons/buttons.tsx
@@ -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';

Expand All @@ -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)}>
Expand Down
7 changes: 7 additions & 0 deletions core/src/components/buttons/readme.md
Expand Up @@ -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/)*
51 changes: 50 additions & 1 deletion core/src/components/header/header.ios.scss
Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions core/src/components/header/header.md.scss
Expand Up @@ -25,3 +25,7 @@
.header-md[no-border]::after {
display: none;
}

.header-collapse-md {
display: none;
}
85 changes: 84 additions & 1 deletion core/src/components/header/header.tsx
@@ -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.
*/
Expand All @@ -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
Expand All @@ -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

Contributor

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

Contributor

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

Contributor

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

Contributor

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

Contributor

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 (
Expand All @@ -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,
}}
>
Expand Down

0 comments on commit 923312e

Please sign in to comment.