diff --git a/core/api.txt b/core/api.txt index 89ff60efdf..edf8519823 100644 --- a/core/api.txt +++ b/core/api.txt @@ -400,7 +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,collapse,"condense" | undefined,undefined,false,false ion-header,prop,mode,"ios" | "md",undefined,false,false ion-header,prop,translucent,boolean,false,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index d6c1d09d49..c25d203a40 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -391,7 +391,7 @@ export namespace Components { } 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` + * 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`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) */ 'collapse': boolean; } @@ -871,9 +871,9 @@ export namespace Components { } interface IonHeader { /** - * If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode. + * 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) */ - 'collapse': boolean; + 'collapse'?: 'condense'; /** * The mode determines which platform styles to use. */ @@ -3898,7 +3898,7 @@ declare namespace LocalJSX { } interface IonButtons extends JSXBase.HTMLAttributes { /** - * 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` + * 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`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) */ 'collapse'?: boolean; } @@ -4390,9 +4390,9 @@ declare namespace LocalJSX { } interface IonHeader extends JSXBase.HTMLAttributes { /** - * If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode. + * 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) */ - 'collapse'?: boolean; + 'collapse'?: 'condense'; /** * The mode determines which platform styles to use. */ diff --git a/core/src/components/buttons/buttons.tsx b/core/src/components/buttons/buttons.tsx index adaf48aafe..84cb3f1504 100644 --- a/core/src/components/buttons/buttons.tsx +++ b/core/src/components/buttons/buttons.tsx @@ -20,13 +20,21 @@ export class Buttons implements ComponentInterface { * only be shown once all toolbars have fully collapsed. * * Only applies in `ios` mode with `collapse` set to - * `true` on `ion-header` + * `true` on `ion-header`. + * + * Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) */ @Prop() collapse = false; render() { + const mode = getIonMode(this); return ( - + ); } diff --git a/core/src/components/buttons/readme.md b/core/src/components/buttons/readme.md index 8dbc0a1724..b654e4879c 100644 --- a/core/src/components/buttons/readme.md +++ b/core/src/components/buttons/readme.md @@ -55,6 +55,15 @@ The `` element can be positioned inside of the toolbar using a name + + + + + + + + Collapsible Buttons + ``` @@ -100,6 +109,15 @@ The `` element can be positioned inside of the toolbar using a name + + + + + + + + Collapsible Buttons + ``` @@ -155,6 +173,15 @@ export const ButtonsExample: React.FC = () => ( + + + + + + + + Collapsible Buttons + ); ``` @@ -199,6 +226,15 @@ export const ButtonsExample: React.FC = () => ( + + + + + + + + Collapsible Buttons + ``` @@ -206,9 +242,9 @@ 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` | +| 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`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles) | `boolean` | `false` | ---------------------------------------------- diff --git a/core/src/components/buttons/usage/angular.md b/core/src/components/buttons/usage/angular.md index d60803aa07..029528963c 100644 --- a/core/src/components/buttons/usage/angular.md +++ b/core/src/components/buttons/usage/angular.md @@ -34,4 +34,13 @@ + + + + + + + + Collapsible Buttons + ``` \ No newline at end of file diff --git a/core/src/components/buttons/usage/javascript.md b/core/src/components/buttons/usage/javascript.md index 1149590d8a..d7b9fbd5eb 100644 --- a/core/src/components/buttons/usage/javascript.md +++ b/core/src/components/buttons/usage/javascript.md @@ -38,4 +38,13 @@ + + + + + + + + Collapsible Buttons + ``` \ No newline at end of file diff --git a/core/src/components/buttons/usage/react.md b/core/src/components/buttons/usage/react.md index 8205d7bc56..89c089cece 100644 --- a/core/src/components/buttons/usage/react.md +++ b/core/src/components/buttons/usage/react.md @@ -48,6 +48,15 @@ export const ButtonsExample: React.FC = () => ( + + + + + + + + Collapsible Buttons + ); ``` \ No newline at end of file diff --git a/core/src/components/buttons/usage/vue.md b/core/src/components/buttons/usage/vue.md index 7b926acd96..887a96c729 100644 --- a/core/src/components/buttons/usage/vue.md +++ b/core/src/components/buttons/usage/vue.md @@ -35,5 +35,14 @@ + + + + + + + + Collapsible Buttons + ``` \ No newline at end of file diff --git a/core/src/components/header/header.ios.scss b/core/src/components/header/header.ios.scss index 0b42259dec..4a85dc2311 100644 --- a/core/src/components/header/header.ios.scss +++ b/core/src/components/header/header.ios.scss @@ -25,26 +25,26 @@ // iOS Header - Collapse // -------------------------------------------------- -.header-collapse-ios { +.header-collapse-condense { z-index: 9; } -.header-collapse-ios ion-toolbar { +.header-collapse-condense ion-toolbar { position: sticky; top: 0; } -.header-collapse-ios ion-toolbar:first-child { +.header-collapse-condense ion-toolbar:first-child { padding-top: 7px; z-index: 1; } -.header-collapse-ios ion-toolbar { +.header-collapse-condense ion-toolbar { z-index: 0; } -.header-collapse-ios ion-toolbar ion-searchbar { +.header-collapse-condense ion-toolbar ion-searchbar { height: 48px; padding-top: 0px; @@ -61,13 +61,13 @@ ion-toolbar.in-toolbar ion-buttons { * 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 { +.header-collapse-condense ion-toolbar ion-title, +.header-collapse-condense 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] { +.header-collapse-condense-inactive ion-toolbar.in-toolbar ion-title, +.header-collapse-condense-inactive ion-toolbar.in-toolbar ion-buttons.buttons-collapse { opacity: 0; pointer-events: none; } \ No newline at end of file diff --git a/core/src/components/header/header.md.scss b/core/src/components/header/header.md.scss index 71d03249b2..a60e9e80d4 100644 --- a/core/src/components/header/header.md.scss +++ b/core/src/components/header/header.md.scss @@ -26,6 +26,6 @@ display: none; } -.header-collapse-md { +.header-collapse-condense { display: none; } \ No newline at end of file diff --git a/core/src/components/header/header.tsx b/core/src/components/header/header.tsx index 3002104c2d..c814198280 100644 --- a/core/src/components/header/header.tsx +++ b/core/src/components/header/header.tsx @@ -15,16 +15,20 @@ import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarInte }) export class Header implements ComponentInterface { + private collapsibleHeaderInitialized = false; private scrollEl?: HTMLElement; private contentScrollCallback?: any; + private intersectionObserver?: any; @Element() el!: HTMLElement; /** - * If `true`, the header will collapse on scroll of the content. - * Only applies in `ios` mode. + * 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) */ - @Prop() collapse = false; + @Prop() collapse?: 'condense'; /** * If `true`, the header will be translucent. @@ -37,32 +41,56 @@ 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; + await this.checkCollapsibleHeader(); + } - 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'); - - if (canCollapse) { - await this.setupCollapsableHeader(contentEl, (tabs) ? tabs : page!); - } + async componentDidUpdate() { + await this.checkCollapsibleHeader(); } componentDidUnload() { - if (this.scrollEl && this.contentScrollCallback) { - this.scrollEl.removeEventListener('scroll', this.contentScrollCallback); + this.destroyCollapsibleHeader(); + } + + private async checkCollapsibleHeader() { + + // 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) { + const tabs = this.el.closest('ion-tabs'); + const page = this.el.closest('ion-app,ion-page,.ion-page,page-inner'); + + const pageEl = (tabs) ? tabs : (page) ? page : null; + const contentEl = (pageEl) ? pageEl.querySelector('ion-content') : null; + + await this.setupCollapsibleHeader(contentEl, pageEl); } } - 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.'); } + private destroyCollapsibleHeader() { + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + this.intersectionObserver = undefined; + } - this.scrollEl = await contentEl!.getScrollElement(); + if (this.scrollEl && this.contentScrollCallback) { + this.scrollEl.removeEventListener('scroll', this.contentScrollCallback); + this.contentScrollCallback = undefined; + } + } + + private async setupCollapsibleHeader(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; } + + this.scrollEl = await contentEl.getScrollElement(); readTask(() => { const headers = pageEl.querySelectorAll('ion-header'); - const mainHeader = Array.from(headers).find((header: any) => !header.collapse) as HTMLElement | undefined; + const mainHeader = Array.from(headers).find((header: any) => header.collapse !== 'condense') as HTMLElement | undefined; if (!mainHeader || !this.scrollEl) { return; } @@ -87,8 +115,8 @@ export class Header implements ComponentInterface { 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.intersectionObserver = new IntersectionObserver(toolbarIntersection, { threshold: 0.25, rootMargin: `-${mainHeaderHeight}px 0px 0px 0px` }); + this.intersectionObserver.observe(scrollHeaderIndex.toolbars[0].el); }); /** @@ -104,10 +132,13 @@ export class Header implements ComponentInterface { cloneElement('ion-title'); cloneElement('ion-back-button'); }); + + this.collapsibleHeaderInitialized = true; } render() { const mode = getIonMode(this); + const collapse = this.collapse || 'none'; return ( diff --git a/core/src/components/header/header.utils.ts b/core/src/components/header/header.utils.ts index b796220dca..88ff101529 100644 --- a/core/src/components/header/header.utils.ts +++ b/core/src/components/header/header.utils.ts @@ -81,7 +81,6 @@ const setToolbarBackgroundOpacity = (toolbar: ToolbarIndex, opacity: number | un * hide the primary toolbar content and show the scrollable toolbar content */ export const handleToolbarIntersection = (ev: any, mainHeaderIndex: HeaderIndex, scrollHeaderIndex: HeaderIndex) => { - console.log(ev); writeTask(() => { const event = ev[0]; const intersection = event.intersectionRect; @@ -121,9 +120,9 @@ export const handleToolbarIntersection = (ev: any, mainHeaderIndex: HeaderIndex, export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => { writeTask(() => { if (active) { - headerIndex.el.classList.remove('header-collapse-ios-inactive'); + headerIndex.el.classList.remove('header-collapse-condense-inactive'); } else { - headerIndex.el.classList.add('header-collapse-ios-inactive'); + headerIndex.el.classList.add('header-collapse-condense-inactive'); } setToolbarBackgroundOpacity(headerIndex.toolbars[0], (active) ? undefined : 0); }); diff --git a/core/src/components/header/readme.md b/core/src/components/header/readme.md index 633dbbc780..929aca7bf5 100644 --- a/core/src/components/header/readme.md +++ b/core/src/components/header/readme.md @@ -10,7 +10,7 @@ It's important to note that ion-header needs to be the one of the three root ele ## Usage -### Javascript +### Angular / javascript ```html @@ -26,7 +26,13 @@ It's important to note that ion-header needs to be the one of the three root ele - + + + + My Navigation Bar + + + ``` @@ -45,26 +51,60 @@ export const HeaderExample: React.FC = () => ( My Navigation Bar - + Subheader - - + + + + + My Navigation Bar + + + ); ``` +### Vue + +```html + +``` + + ## Properties -| Property | Attribute | Description | Type | Default | -| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | -| `collapse` | `collapse` | If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode. | `boolean` | `false` | -| `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` | +| 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` | +| `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` | ---------------------------------------------- diff --git a/core/src/components/header/usage/angular.md b/core/src/components/header/usage/angular.md new file mode 100644 index 0000000000..272c1af54b --- /dev/null +++ b/core/src/components/header/usage/angular.md @@ -0,0 +1,22 @@ +```html + + + + + + My Navigation Bar + + + + Subheader + + + + + + + My Navigation Bar + + + +``` diff --git a/core/src/components/header/usage/javascript.md b/core/src/components/header/usage/javascript.md index 2942fbd154..272c1af54b 100644 --- a/core/src/components/header/usage/javascript.md +++ b/core/src/components/header/usage/javascript.md @@ -12,5 +12,11 @@ - + + + + My Navigation Bar + + + ``` diff --git a/core/src/components/header/usage/react.md b/core/src/components/header/usage/react.md index 1a6ec5bc26..e65f281af0 100644 --- a/core/src/components/header/usage/react.md +++ b/core/src/components/header/usage/react.md @@ -11,13 +11,19 @@ export const HeaderExample: React.FC = () => ( My Navigation Bar - + Subheader - - + + + + + My Navigation Bar + + + ); ``` \ No newline at end of file diff --git a/core/src/components/header/usage/vue.md b/core/src/components/header/usage/vue.md new file mode 100644 index 0000000000..941cb3e7fb --- /dev/null +++ b/core/src/components/header/usage/vue.md @@ -0,0 +1,24 @@ +```html + +``` diff --git a/core/src/components/title/readme.md b/core/src/components/title/readme.md index 7cc330e825..b2860c231b 100644 --- a/core/src/components/title/readme.md +++ b/core/src/components/title/readme.md @@ -2,8 +2,6 @@ `ion-title` is a component that sets the title of the `Toolbar`. - - @@ -24,50 +22,281 @@ Default Title + + + + Large Title + ``` +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `ion-title`, `ion-header`, and (optionally) `ion-buttons` elements. + +```html + + + Settings + + + + + + + Settings + + + + + + + ... + + +``` + +In the example above, notice there are two `ion-header` elements. The first `ion-header` represents the "collapsed" state of your collapsible header, and the second `ion-header` represents the "expanded" state of your collapsible header. Notice that the second `ion-header` must have `collapse="condense"` and must exist within `ion-content`. Additionally, in order to get the large title styling, `ion-title` must have `size="large"`. + +```html + + + + Click Me + + Settings + + + + + + + + Click Me + + Settings + + + + + + + ... + + +``` + +In this example, notice that we have added two sets of `ion-buttons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `ion-title` element. + +`ion-buttons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. + ### React ```tsx import React from 'react'; import { - IonToolbar, - IonTitle + IonTitle, + IonToolbar } from '@ionic/react'; export const ToolbarExample: React.FC = () => ( + {/*-- Default title --*/} Default Title - + + {/*-- Small title --*/} Small Title above a Default Title Default Title + + {/*-- Large title --*/} + + Large Title + ); ``` +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `IonTitle`, `IonHeader`, and (optionally) `IonButtons` elements. + +```tsx +import React from 'react'; +import { + IonContent, + IonHeader, + IonSearchbar, + IonTitle, + IonToolbar +} from '@ionic/react'; + +export const LargeTitleExample: React.FC = () => ( + <> + + + Settings + + + + + + + Settings + + + + + + + ... + + + +); +``` + +In the example above, notice there are two `IonHeader` elements. The first `IonHeader` represents the "collapsed" state of your collapsible header, and the second `IonHeader` represents the "expanded" state of your collapsible header. Notice that the second `IonHeader` must have `collapse="condense"` and must exist within `IonContent`. Additionally, in order to get the large title styling, `IonTitle` must have `size="large"`. + +```tsx +import React from 'react'; +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonSearchbar, + IonTitle, + IonToolbar +} from '@ionic/react'; + +export const LargeTitleExample: React.FC = () => ( + <> + + + + Click Me + + Settings + + + + + + + + Click Me + + Settings + + + + + + + ... + + + +); +``` + +In this example, notice that we have added two sets of `IonButtons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `IonTitle` element. + +`IonButtons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. + ### Vue ```html ``` +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `ion-title`, `ion-header`, and (optionally) `ion-buttons` elements. + +```html + +``` + +In the example above, notice there are two `ion-header` elements. The first `ion-header` represents the "collapsed" state of your collapsible header, and the second `ion-header` represents the "expanded" state of your collapsible header. Notice that the second `ion-header` must have `collapse="condense"` and must exist within `ion-content`. Additionally, in order to get the large title styling, `ion-title` must have `size="large"`. + +```html + +``` + +In this example, notice that we have added two sets of `ion-buttons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `ion-title` element. + +`ion-buttons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. + ## Properties diff --git a/core/src/components/title/title.scss b/core/src/components/title/title.scss index 65d1640e67..f35f7096ac 100644 --- a/core/src/components/title/title.scss +++ b/core/src/components/title/title.scss @@ -111,5 +111,5 @@ font-size: 34px; font-weight: 700; - text-align: left; + text-align: start; } diff --git a/core/src/components/title/usage/angular.md b/core/src/components/title/usage/angular.md index adc9c394b6..53172232b6 100644 --- a/core/src/components/title/usage/angular.md +++ b/core/src/components/title/usage/angular.md @@ -11,4 +11,69 @@ Default Title + + + + Large Title + ``` + +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `ion-title`, `ion-header`, and (optionally) `ion-buttons` elements. + +```html + + + Settings + + + + + + + Settings + + + + + + + ... + + +``` + +In the example above, notice there are two `ion-header` elements. The first `ion-header` represents the "collapsed" state of your collapsible header, and the second `ion-header` represents the "expanded" state of your collapsible header. Notice that the second `ion-header` must have `collapse="condense"` and must exist within `ion-content`. Additionally, in order to get the large title styling, `ion-title` must have `size="large"`. + +```html + + + + Click Me + + Settings + + + + + + + + Click Me + + Settings + + + + + + + ... + + +``` + +In this example, notice that we have added two sets of `ion-buttons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `ion-title` element. + +`ion-buttons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. \ No newline at end of file diff --git a/core/src/components/title/usage/javascript.md b/core/src/components/title/usage/javascript.md index adc9c394b6..53172232b6 100644 --- a/core/src/components/title/usage/javascript.md +++ b/core/src/components/title/usage/javascript.md @@ -11,4 +11,69 @@ Default Title + + + + Large Title + ``` + +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `ion-title`, `ion-header`, and (optionally) `ion-buttons` elements. + +```html + + + Settings + + + + + + + Settings + + + + + + + ... + + +``` + +In the example above, notice there are two `ion-header` elements. The first `ion-header` represents the "collapsed" state of your collapsible header, and the second `ion-header` represents the "expanded" state of your collapsible header. Notice that the second `ion-header` must have `collapse="condense"` and must exist within `ion-content`. Additionally, in order to get the large title styling, `ion-title` must have `size="large"`. + +```html + + + + Click Me + + Settings + + + + + + + + Click Me + + Settings + + + + + + + ... + + +``` + +In this example, notice that we have added two sets of `ion-buttons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `ion-title` element. + +`ion-buttons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. \ No newline at end of file diff --git a/core/src/components/title/usage/react.md b/core/src/components/title/usage/react.md index fa3849bce5..621f6c89d6 100644 --- a/core/src/components/title/usage/react.md +++ b/core/src/components/title/usage/react.md @@ -1,20 +1,115 @@ ```tsx import React from 'react'; import { - IonToolbar, - IonTitle + IonTitle, + IonToolbar } from '@ionic/react'; export const ToolbarExample: React.FC = () => ( + {/*-- Default title --*/} Default Title - + + {/*-- Small title --*/} Small Title above a Default Title Default Title + + {/*-- Large title --*/} + + Large Title + ); ``` + +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `IonTitle`, `IonHeader`, and (optionally) `IonButtons` elements. + +```tsx +import React from 'react'; +import { + IonContent, + IonHeader, + IonSearchbar, + IonTitle, + IonToolbar +} from '@ionic/react'; + +export const LargeTitleExample: React.FC = () => ( + <> + + + Settings + + + + + + + Settings + + + + + + + ... + + + +); +``` + +In the example above, notice there are two `IonHeader` elements. The first `IonHeader` represents the "collapsed" state of your collapsible header, and the second `IonHeader` represents the "expanded" state of your collapsible header. Notice that the second `IonHeader` must have `collapse="condense"` and must exist within `IonContent`. Additionally, in order to get the large title styling, `IonTitle` must have `size="large"`. + +```tsx +import React from 'react'; +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonSearchbar, + IonTitle, + IonToolbar +} from '@ionic/react'; + +export const LargeTitleExample: React.FC = () => ( + <> + + + + Click Me + + Settings + + + + + + + + Click Me + + Settings + + + + + + + ... + + + +); +``` + +In this example, notice that we have added two sets of `IonButtons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `IonTitle` element. + +`IonButtons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. diff --git a/core/src/components/title/usage/vue.md b/core/src/components/title/usage/vue.md index 69c6fb454f..95fbdba3ad 100644 --- a/core/src/components/title/usage/vue.md +++ b/core/src/components/title/usage/vue.md @@ -1,14 +1,85 @@ ```html -``` \ No newline at end of file +``` + +### Collapsible Large Titles + +Ionic provides a way to create the collapsible titles that exist on stock iOS apps. Getting this setup requires configuring your `ion-title`, `ion-header`, and (optionally) `ion-buttons` elements. + +```html + +``` + +In the example above, notice there are two `ion-header` elements. The first `ion-header` represents the "collapsed" state of your collapsible header, and the second `ion-header` represents the "expanded" state of your collapsible header. Notice that the second `ion-header` must have `collapse="condense"` and must exist within `ion-content`. Additionally, in order to get the large title styling, `ion-title` must have `size="large"`. + +```html + +``` + +In this example, notice that we have added two sets of `ion-buttons` both with `collapse="true"`. When the secondary header collapses, the buttons in the secondary header will hide, and the buttons in the primary header will show. This is useful for ensuring that your header buttons always appear next to an `ion-title` element. + +`ion-buttons` elements that do not have `collapse` set will always be visible, regardless of collapsed state. diff --git a/core/src/components/toolbar/test/title/e2e.ts b/core/src/components/toolbar/test/title/e2e.ts new file mode 100644 index 0000000000..4ea7ac5c7e --- /dev/null +++ b/core/src/components/toolbar/test/title/e2e.ts @@ -0,0 +1,19 @@ +import { newE2EPage } from '@stencil/core/testing'; + +test('toolbar: title', async () => { + const page = await newE2EPage({ + url: '/src/components/toolbar/test/title?ionic:_testing=true' + }); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); + +test('toolbar:rtl: title', async () => { + const page = await newE2EPage({ + url: '/src/components/toolbar/test/title?ionic:_testing=true&rtl=true' + }); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); diff --git a/core/src/components/toolbar/test/title/index.html b/core/src/components/toolbar/test/title/index.html index 93effe25f9..db542b65ed 100644 --- a/core/src/components/toolbar/test/title/index.html +++ b/core/src/components/toolbar/test/title/index.html @@ -30,27 +30,9 @@ .create ion-icon { transform: scale(0.9); } - -/* - ion-toolbar { - --background: red; - --border-color: #dfe1e0; - } - - .main-content { - --background: #f1f2f6; - } -*/ - - + @@ -71,7 +53,7 @@ diff --git a/core/src/utils/transition/ios.transition.ts b/core/src/utils/transition/ios.transition.ts index 26886ca0fb..7876ea32e7 100644 --- a/core/src/utils/transition/ios.transition.ts +++ b/core/src/utils/transition/ios.transition.ts @@ -16,7 +16,7 @@ export const shadow = (el: T): ShadowRoot | T => { }; const getLargeTitle = (refEl: any) => { - return refEl.querySelector('ion-header:not(.header-collapse-ios-inactive) ion-title[size=large]'); + return refEl.querySelector('ion-header:not(.header-collapse-condense-inactive) ion-title[size=large]'); }; const getBackButton = (refEl: any, backDirection: boolean) => { @@ -24,10 +24,11 @@ const getBackButton = (refEl: any, backDirection: boolean) => { for (const buttons of buttonsList) { const parentHeader = buttons.closest('ion-header'); - const activeHeader = parentHeader && !parentHeader.classList.contains('header-collapse-ios-inactive'); + const activeHeader = parentHeader && !parentHeader.classList.contains('header-collapse-condense-inactive'); const backButton = buttons.querySelector('ion-back-button'); + const buttonsCollapse = buttons.classList.contains('buttons-collapse'); - if (backButton !== null && ((buttons.collapse && activeHeader && backDirection) || !buttons.collapse)) { + if (backButton !== null && ((buttonsCollapse && activeHeader && backDirection) || !buttonsCollapse)) { return backButton; } } @@ -60,26 +61,33 @@ const createLargeTitleTransition = (rootAnimation: IonicAnimation, rtl: boolean, }; const animateBackButton = (rootAnimation: IonicAnimation, rtl: boolean, backDirection: boolean, backButtonEl: any) => { - console.log(rtl); + const START_TEXT_TRANSLATE = (rtl) ? '7px' : '-7px'; + const END_TEXT_TRANSLATE = (rtl) ? '-4px' : '4px'; + + const ICON_TRANSLATE = (rtl) ? '-4px' : '4px'; + + const TEXT_TRANSFORM_ORIGIN_X = (rtl) ? 'right' : 'left'; + const ICON_TRANSFORM_ORIGIN_X = (rtl) ? 'left' : 'right'; + const FORWARD_TEXT_KEYFRAMES = [ - { offset: 0, opacity: 0, transform: `translate(-7px, ${addSafeArea(8)}) scale(2.1)` }, - { offset: 1, opacity: 1, transform: `translate(4px, ${addSafeArea(-40)}) scale(1)` } + { offset: 0, opacity: 0, transform: `translate(${START_TEXT_TRANSLATE}, ${addSafeArea(8)}) scale(2.1)` }, + { offset: 1, opacity: 1, transform: `translate(${END_TEXT_TRANSLATE}, ${addSafeArea(-40)}) scale(1)` } ]; const BACKWARD_TEXT_KEYFRAMES = [ - { offset: 0, opacity: 1, transform: `translate(4px, ${addSafeArea(-40)}) scale(1)` }, + { offset: 0, opacity: 1, transform: `translate(${END_TEXT_TRANSLATE}, ${addSafeArea(-40)}) scale(1)` }, { offset: 0.6, opacity: 0 }, - { offset: 1, opacity: 0, transform: `translate(-7px, ${addSafeArea(8)}) scale(2.1)` } + { offset: 1, opacity: 0, transform: `translate(${START_TEXT_TRANSLATE}, ${addSafeArea(8)}) scale(2.1)` } ]; const TEXT_KEYFRAMES = (backDirection) ? BACKWARD_TEXT_KEYFRAMES : FORWARD_TEXT_KEYFRAMES; const FORWARD_ICON_KEYFRAMES = [ - { offset: 0, opacity: 0, transform: `translate3d(4px, ${addSafeArea(-35)}, 0) scale(0.6)` }, - { offset: 1, opacity: 1, transform: `translate3d(4px, ${addSafeArea(-40)}, 0) scale(1)` } + { offset: 0, opacity: 0, transform: `translate3d(${ICON_TRANSLATE}, ${addSafeArea(-35)}, 0) scale(0.6)` }, + { offset: 1, opacity: 1, transform: `translate3d(${ICON_TRANSLATE}, ${addSafeArea(-40)}, 0) scale(1)` } ]; const BACKWARD_ICON_KEYFRAMES = [ - { offset: 0, opacity: 1, transform: `translate(4px, ${addSafeArea(-40)}) scale(1)` }, - { offset: 0.2, opacity: 0, transform: `translate(4px, ${addSafeArea(-35)}) scale(0.6)` }, - { offset: 1, opacity: 0, transform: `translate(4px, ${addSafeArea(-35)}) scale(0.6)` } + { offset: 0, opacity: 1, transform: `translate(${ICON_TRANSLATE}, ${addSafeArea(-40)}) scale(1)` }, + { offset: 0.2, opacity: 0, transform: `translate(${ICON_TRANSLATE}, ${addSafeArea(-35)}) scale(0.6)` }, + { offset: 1, opacity: 0, transform: `translate(${ICON_TRANSLATE}, ${addSafeArea(-35)}) scale(0.6)` } ]; const ICON_KEYFRAMES = (backDirection) ? BACKWARD_ICON_KEYFRAMES : FORWARD_ICON_KEYFRAMES; @@ -105,7 +113,7 @@ const animateBackButton = (rootAnimation: IonicAnimation, rtl: boolean, backDire enteringBackButtonTextAnimation .beforeStyles({ - 'transform-origin': 'left center' + 'transform-origin': `${TEXT_TRANSFORM_ORIGIN_X} center` }) .beforeAddWrite(() => { backButtonEl.style.setProperty('display', 'none'); @@ -118,7 +126,7 @@ const animateBackButton = (rootAnimation: IonicAnimation, rtl: boolean, backDire enteringBackButtonIconAnimation .beforeStyles({ - 'transform-origin': 'right center' + 'transform-origin': `${ICON_TRANSFORM_ORIGIN_X} center` }) .keyframes(ICON_KEYFRAMES); @@ -126,16 +134,18 @@ const animateBackButton = (rootAnimation: IonicAnimation, rtl: boolean, backDire }; const animateLargeTitle = (rootAnimation: IonicAnimation, rtl: boolean, backDirection: boolean, largeTitleEl: any) => { - const TRANSLATE = (rtl) ? '-18px' : '18px'; + const START_TRANSLATE = (rtl) ? '-18px' : '18px'; + const TRANSFORM_ORIGIN_X = (rtl) ? 'right' : 'left'; + const BACKWARDS_KEYFRAMES = [ - { offset: 0, opacity: 0, transform: `translate(${TRANSLATE}, ${addSafeArea(0)}) scale(0.49)` }, + { offset: 0, opacity: 0, transform: `translate(${START_TRANSLATE}, ${addSafeArea(0)}) scale(0.49)` }, { offset: 0.1, opacity: 0 }, { offset: 1, opacity: 1, transform: `translate(0, ${addSafeArea(49)}) scale(1)` } ]; const FORWARDS_KEYFRAMES = [ { offset: 0, opacity: 0.99, transform: `translate(0, ${addSafeArea(49)}) scale(1)` }, { offset: 0.6, opacity: 0 }, - { offset: 1, opacity: 0, transform: `translate(${TRANSLATE}, ${addSafeArea(0)}) scale(0.5)` } + { offset: 1, opacity: 0, transform: `translate(${START_TRANSLATE}, ${addSafeArea(0)}) scale(0.5)` } ]; const KEYFRAMES = (backDirection) ? BACKWARDS_KEYFRAMES : FORWARDS_KEYFRAMES; @@ -150,7 +160,7 @@ const animateLargeTitle = (rootAnimation: IonicAnimation, rtl: boolean, backDire clonedLargeTitleAnimation .beforeStyles({ - 'transform-origin': 'left center', + 'transform-origin': `${TRANSFORM_ORIGIN_X} center`, 'height': '46px', 'display': '', 'position': 'relative' @@ -254,7 +264,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio } } - const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-ios'); + const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-condense'); const { forward, backward } = createLargeTitleTransition(rootAnimation, isRTL, backDirection, enteringEl, leavingEl); @@ -270,16 +280,16 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio const buttons = Array.from(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); const parentHeader = enteringToolBarEl.closest('ion-header'); - const inactiveHeader = parentHeader && parentHeader.classList.contains('header-collapse-ios-inactive'); + const inactiveHeader = parentHeader && parentHeader.classList.contains('header-collapse-condense-inactive'); let buttonsToAnimate; if (backDirection) { buttonsToAnimate = buttons.filter(button => { - const isCollapseButton = (button as any).collapse; + const isCollapseButton = button.classList.contains('buttons-collapse'); return (isCollapseButton && !inactiveHeader) || !isCollapseButton; }); } else { - buttonsToAnimate = buttons.filter(button => !(button as any).collapse); + buttonsToAnimate = buttons.filter(button => !button.classList.contains('buttons-collapse')); } enteringToolBarButtons.addElement(buttonsToAnimate); @@ -409,10 +419,10 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio const buttons = leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'); const parentHeader = leavingToolBarEl.closest('ion-header'); - const inactiveHeader = parentHeader && parentHeader.classList.contains('header-collapse-ios-inactive'); + const inactiveHeader = parentHeader && parentHeader.classList.contains('header-collapse-condense-inactive'); const buttonsToAnimate = Array.from(buttons).filter(button => { - const isCollapseButton = (button as any).collapse; + const isCollapseButton = button.classList.contains('buttons-collapse'); return (isCollapseButton && !inactiveHeader) || !isCollapseButton; });