mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3666ddf0c | ||
|
|
cace1b357e | ||
|
|
ab9d3b2696 | ||
|
|
60aa027903 | ||
|
|
7bd4412889 | ||
|
|
96a5e600e5 | ||
|
|
33cf08bf0e | ||
|
|
7e6e81db55 | ||
|
|
f2d18a2346 | ||
|
|
77d18c7172 | ||
|
|
ec43b8c9b8 | ||
|
|
b4938db5f9 | ||
|
|
9c7beff910 | ||
|
|
16815b3cc8 | ||
|
|
e23d91c6e7 | ||
|
|
1f2b161ccd | ||
|
|
67737bbb54 | ||
|
|
29f8178794 | ||
|
|
d80f45516d | ||
|
|
420aa66392 | ||
|
|
7988720b1c | ||
|
|
67a7e232b9 | ||
|
|
7d417154c5 | ||
|
|
b642b532e8 | ||
|
|
0a7aae28a7 | ||
|
|
39fb8f6720 | ||
|
|
f63d37a4c5 | ||
|
|
9568f228b2 | ||
|
|
a2e1c55911 | ||
|
|
834666c754 | ||
|
|
5acb58a03d | ||
|
|
ce7314db0b | ||
|
|
71fdcbc1b4 |
@@ -158,7 +158,7 @@ function preparePackage(tasks, package, version, install) {
|
||||
});
|
||||
projectTasks.push({
|
||||
title: `${pkg.name}: test`,
|
||||
task: () => execa('npm', ['test'], { cwd: projectRoot })
|
||||
task: async () => await execa('npm', ['test'], { cwd: projectRoot })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,3 +1,35 @@
|
||||
## [4.11.3](https://github.com/ionic-team/ionic/compare/v4.11.1...v4.11.3) (2019-10-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **react:** adding change events to iontabs, fixes [#19665](https://github.com/ionic-team/ionic/issues/19665) ([#19711](https://github.com/ionic-team/ionic/issues/19711)) ([b7baf24](https://github.com/ionic-team/ionic/commit/b7baf24e5053a379156e6c3d82c2b5d3afa999f1))
|
||||
* **react:** adding HashRouter to available ion routers, fixes [#19621](https://github.com/ionic-team/ionic/issues/19621) ([#19683](https://github.com/ionic-team/ionic/issues/19683)) ([fcdbb3c](https://github.com/ionic-team/ionic/commit/fcdbb3ce98747d3b37107904ca110daad95e48bc))
|
||||
* **react:** checking if node is actually an element before treating it like one, fixes [#19769](https://github.com/ionic-team/ionic/issues/19769) ([#19783](https://github.com/ionic-team/ionic/issues/19783)) ([9d0caf6](https://github.com/ionic-team/ionic/commit/9d0caf6de070145c4af618847b27e24c49027b8e))
|
||||
* **react:** checking isOpen again after async call before opening overlay, fixes [#19755](https://github.com/ionic-team/ionic/issues/19755) ([f70e71a](https://github.com/ionic-team/ionic/commit/f70e71a3d461cdab65626a5a7e1b6f4d03b852b1))
|
||||
* **react:** don't remove current view, provide a better method to determine showGoBack fixes [#19731](https://github.com/ionic-team/ionic/issues/19731) and [#19732](https://github.com/ionic-team/ionic/issues/19732) ([31c754d](https://github.com/ionic-team/ionic/commit/31c754dab7ada494ff5f0026d5cf3f7f65198eff))
|
||||
* **react:** removing pages from DOM on nav, fixes [#19701](https://github.com/ionic-team/ionic/issues/19701) ([#19712](https://github.com/ionic-team/ionic/issues/19712)) ([ee21d3a](https://github.com/ionic-team/ionic/commit/ee21d3ae43d8c6b076387a58bca655a56c920bcd))
|
||||
* **react:** unmount leaving view when using browser back button, fixes [#19749](https://github.com/ionic-team/ionic/issues/19749) ([#19781](https://github.com/ionic-team/ionic/issues/19781)) ([2dc5540](https://github.com/ionic-team/ionic/commit/2dc554091056612f1bcd2751d6eeb41cae488751))
|
||||
|
||||
|
||||
|
||||
## [4.11.2](https://github.com/ionic-team/ionic/compare/v4.11.0...v4.11.2) (2019-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** ensure all elements are cleaned up when calling .destroy() ([#19654](https://github.com/ionic-team/ionic/issues/19654)) ([d97e167](https://github.com/ionic-team/ionic/commit/d97e167))
|
||||
* **header:** collapsible header works in tabs ([#19658](https://github.com/ionic-team/ionic/issues/19658)) ([4853909](https://github.com/ionic-team/ionic/commit/4853909)), closes [#19640](https://github.com/ionic-team/ionic/issues/19640)
|
||||
* **ios:** hide leaving view after nav transition to avoid flicker ([#19691](https://github.com/ionic-team/ionic/issues/19691)) ([70e0562](https://github.com/ionic-team/ionic/commit/70e0562)), closes [#19674](https://github.com/ionic-team/ionic/issues/19674)
|
||||
* **menu:** clamp out of bounds swipe value ([#19684](https://github.com/ionic-team/ionic/issues/19684)) ([1535e95](https://github.com/ionic-team/ionic/commit/1535e95)), closes [#18927](https://github.com/ionic-team/ionic/issues/18927)
|
||||
* **react:** add IonPicker as controller component, fixes [#19620](https://github.com/ionic-team/ionic/issues/19620) ([#19643](https://github.com/ionic-team/ionic/issues/19643)) ([ed98d9e](https://github.com/ionic-team/ionic/commit/ed98d9e))
|
||||
* **react:** adding change events to IonTabs, fixes [#19665](https://github.com/ionic-team/ionic/issues/19665) ([#19711](https://github.com/ionic-team/ionic/issues/19711)) ([b7baf24](https://github.com/ionic-team/ionic/commit/b7baf24))
|
||||
* **react:** adding HashRouter to available ion routers, fixes [#19621](https://github.com/ionic-team/ionic/issues/19621) ([#19683](https://github.com/ionic-team/ionic/issues/19683)) ([fcdbb3c](https://github.com/ionic-team/ionic/commit/fcdbb3c))
|
||||
* **react:** pages no longer hidden when navigating between tabs, fixes [#19646](https://github.com/ionic-team/ionic/issues/19646) ([#19647](https://github.com/ionic-team/ionic/issues/19647)) ([8776556](https://github.com/ionic-team/ionic/commit/8776556))
|
||||
* **react:** ensure views are removed from DOM after navigating back, fixes [#19701](https://github.com/ionic-team/ionic/issues/19701) ([#19712](https://github.com/ionic-team/ionic/issues/19712)) ([ee21d3a](https://github.com/ionic-team/ionic/commit/ee21d3a))
|
||||
|
||||
|
||||
|
||||
# [5.0.0-beta.0](https://github.com/ionic-team/ionic/compare/v4.11.1...v5.0.0-beta.0) (2019-10-15)
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export { ToastController } from './providers/toast-controller';
|
||||
export { NavController } from './providers/nav-controller';
|
||||
export { DomController } from './providers/dom-controller';
|
||||
export { Config } from './providers/config';
|
||||
export { AnimationController } from './providers/animation-controller';
|
||||
|
||||
// ROUTER STRATEGY
|
||||
export { IonicRouteStrategy } from './util/ionic-router-reuse-strategy';
|
||||
|
||||
32
angular/src/providers/animation-controller.ts
Normal file
32
angular/src/providers/animation-controller.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Animation, createAnimation, getTimeGivenProgression } from '@ionic/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AnimationController {
|
||||
/**
|
||||
* Create a new animation
|
||||
*/
|
||||
create(animationId?: string): Animation {
|
||||
return createAnimation(animationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL
|
||||
*
|
||||
* Given a progression and a cubic bezier function,
|
||||
* this utility returns the time value(s) at which the
|
||||
* cubic bezier reaches the given time progression.
|
||||
*
|
||||
* If the cubic bezier never reaches the progression
|
||||
* the result will be an empty array.
|
||||
*
|
||||
* This is most useful for switching between easing curves
|
||||
* when doing a gesture animation (i.e. going from linear easing
|
||||
* during a drag, to another easing when `progressEnd` is called)
|
||||
*/
|
||||
easingTime(p0: number[], p1: number[], p2: number[], p3: number[], progression: number): number[] {
|
||||
return getTimeGivenProgression(p0, p1, p2, p3, progression);
|
||||
}
|
||||
}
|
||||
@@ -1098,12 +1098,15 @@ ion-spinner,prop,name,"bubbles" | "circles" | "circular" | "crescent" | "dots" |
|
||||
ion-spinner,prop,paused,boolean,false,false,false
|
||||
ion-spinner,css-prop,--color
|
||||
|
||||
ion-split-pane,none
|
||||
ion-split-pane,shadow
|
||||
ion-split-pane,prop,contentId,string | undefined,undefined,false,false
|
||||
ion-split-pane,prop,disabled,boolean,false,false,false
|
||||
ion-split-pane,prop,when,boolean | string,QUERY['lg'],false,false
|
||||
ion-split-pane,event,ionSplitPaneVisible,{ visible: boolean; },true
|
||||
ion-split-pane,css-prop,--border
|
||||
ion-split-pane,css-prop,--side-max-width
|
||||
ion-split-pane,css-prop,--side-min-width
|
||||
ion-split-pane,css-prop,--side-width
|
||||
|
||||
ion-tab,shadow
|
||||
ion-tab,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false
|
||||
|
||||
@@ -34,18 +34,18 @@
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stencil/core": "1.6.1",
|
||||
"@stencil/core": "1.7.5",
|
||||
"@stencil/sass": "1.0.1",
|
||||
"@types/jest": "24.0.17",
|
||||
"@types/node": "12.7.1",
|
||||
"@types/jest": "24.0.21",
|
||||
"@types/node": "12.12.3",
|
||||
"@types/puppeteer": "1.19.1",
|
||||
"@types/swiper": "4.4.4",
|
||||
"aws-sdk": "^2.497.0",
|
||||
"clean-css-cli": "^4.1.11",
|
||||
"domino": "^2.1.3",
|
||||
"fs-extra": "^8.0.1",
|
||||
"jest": "24.8.0",
|
||||
"jest-cli": "24.8.0",
|
||||
"jest": "24.9.0",
|
||||
"jest-cli": "24.9.0",
|
||||
"np": "^5.0.3",
|
||||
"pixelmatch": "4.0.2",
|
||||
"puppeteer": "1.20.0",
|
||||
@@ -79,10 +79,10 @@
|
||||
"prerelease": "npm run validate && np prerelease --yolo --any-branch --tag next",
|
||||
"prerender.e2e": "node scripts/testing/prerender.js",
|
||||
"start": "npm run build.css && stencil build --dev --watch --serve",
|
||||
"test": "stencil test --spec --e2e",
|
||||
"test.spec": "stencil test --spec",
|
||||
"test": "stencil test --spec --e2e --max-workers=2",
|
||||
"test.spec": "stencil test --spec --max-workers=2",
|
||||
"test.spec.debug": "npx --node-arg=\"--inspect-brk\" stencil test --spec",
|
||||
"test.e2e": "stencil test --e2e",
|
||||
"test.e2e": "stencil test --e2e --max-workers=2",
|
||||
"test.screenshot": "stencil test --e2e --screenshot --screenshot-connector=scripts/screenshot/dev.js",
|
||||
"test.screenshot.ci": "stencil test --e2e --screenshot --screenshot-connector=scripts/screenshot/ci.js --ci",
|
||||
"test.watch": "jest --watch --no-cache",
|
||||
|
||||
1
core/src/components.d.ts
vendored
1
core/src/components.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This is an autogenerated file created by the Stencil compiler.
|
||||
|
||||
@@ -51,23 +51,24 @@
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
|
||||
ion-toolbar.in-toolbar ion-title,
|
||||
ion-toolbar.in-toolbar ion-buttons {
|
||||
.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-condense ion-toolbar ion-title,
|
||||
.header-collapse-condense ion-toolbar ion-buttons {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.header-collapse-condense-inactive ion-toolbar.in-toolbar ion-title,
|
||||
.header-collapse-condense-inactive ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
|
||||
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
|
||||
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is a bug in Safari where changing
|
||||
* the opacity of an element in a scrollable container
|
||||
* while rubber-banding causes the scroll position
|
||||
* to jump to the top
|
||||
*/
|
||||
.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-title,
|
||||
.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Host, Prop, h, readTask, writeT
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
|
||||
import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive } from './header.utils';
|
||||
import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*/
|
||||
@@ -19,6 +19,7 @@ export class Header implements ComponentInterface {
|
||||
private scrollEl?: HTMLElement;
|
||||
private contentScrollCallback?: any;
|
||||
private intersectionObserver?: any;
|
||||
private collapsibleMainHeader?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
@@ -77,6 +78,11 @@ export class Header implements ComponentInterface {
|
||||
this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
|
||||
this.contentScrollCallback = undefined;
|
||||
}
|
||||
|
||||
if (this.collapsibleMainHeader) {
|
||||
this.collapsibleMainHeader.classList.remove('header-collapse-main');
|
||||
this.collapsibleMainHeader = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async setupCollapsibleHeader(contentEl: HTMLIonContentElement | null, pageEl: Element | null) {
|
||||
@@ -84,20 +90,20 @@ export class Header implements ComponentInterface {
|
||||
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
|
||||
const headers = pageEl.querySelectorAll('ion-header');
|
||||
this.collapsibleMainHeader = Array.from(headers).find((header: any) => header.collapse !== 'condense') as HTMLElement | undefined;
|
||||
|
||||
if (!this.collapsibleMainHeader) { return; }
|
||||
|
||||
const mainHeaderIndex = createHeaderIndex(this.collapsibleMainHeader);
|
||||
const scrollHeaderIndex = createHeaderIndex(this.el);
|
||||
|
||||
if (!mainHeaderIndex || !scrollHeaderIndex) { return; }
|
||||
|
||||
setHeaderActive(mainHeaderIndex, false);
|
||||
setToolbarBackgroundOpacity(mainHeaderIndex.toolbars[0], 0);
|
||||
|
||||
readTask(() => {
|
||||
const headers = pageEl.querySelectorAll('ion-header');
|
||||
const mainHeader = Array.from(headers).find((header: any) => header.collapse !== 'condense') as HTMLElement | undefined;
|
||||
|
||||
if (!mainHeader || !this.scrollEl) { return; }
|
||||
|
||||
const mainHeaderIndex = createHeaderIndex(mainHeader);
|
||||
const scrollHeaderIndex = createHeaderIndex(this.el);
|
||||
|
||||
if (!mainHeaderIndex || !scrollHeaderIndex) { return; }
|
||||
|
||||
setHeaderActive(mainHeaderIndex, false);
|
||||
|
||||
readTask(() => {
|
||||
const mainHeaderHeight = mainHeaderIndex.el.clientHeight;
|
||||
|
||||
/**
|
||||
@@ -109,20 +115,21 @@ export class Header implements ComponentInterface {
|
||||
const toolbarIntersection = (ev: any) => { handleToolbarIntersection(ev, mainHeaderIndex, scrollHeaderIndex); };
|
||||
this.intersectionObserver = new IntersectionObserver(toolbarIntersection, { threshold: [0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], rootMargin: `-${mainHeaderHeight}px 0px 0px 0px` });
|
||||
this.intersectionObserver.observe(scrollHeaderIndex.toolbars[0].el);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle scaling of large iOS titles and
|
||||
* showing/hiding border on last toolbar
|
||||
* in primary header
|
||||
*/
|
||||
this.contentScrollCallback = () => { handleContentScroll(this.scrollEl!, scrollHeaderIndex); };
|
||||
this.scrollEl.addEventListener('scroll', this.contentScrollCallback);
|
||||
this.contentScrollCallback = () => { handleContentScroll(this.scrollEl!, scrollHeaderIndex); };
|
||||
this.scrollEl!.addEventListener('scroll', this.contentScrollCallback);
|
||||
});
|
||||
|
||||
writeTask(() => {
|
||||
cloneElement('ion-title');
|
||||
cloneElement('ion-back-button');
|
||||
|
||||
this.collapsibleMainHeader!.classList.add('header-collapse-main');
|
||||
});
|
||||
|
||||
this.collapsibleHeaderInitialized = true;
|
||||
|
||||
@@ -45,7 +45,7 @@ export const createHeaderIndex = (headerEl: HTMLElement | undefined): HeaderInde
|
||||
innerTitleEl: (ionTitleEl) ? ionTitleEl.shadowRoot!.querySelector('.toolbar-title') : null,
|
||||
ionButtonsEl: Array.from(toolbar.querySelectorAll('ion-buttons')) || []
|
||||
} as ToolbarIndex;
|
||||
}) || [[]]
|
||||
}) || []
|
||||
} as HeaderIndex;
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ export const handleContentScroll = (scrollEl: HTMLElement, scrollHeaderIndex: He
|
||||
});
|
||||
};
|
||||
|
||||
const setToolbarBackgroundOpacity = (toolbar: ToolbarIndex, opacity: number | undefined) => {
|
||||
export const setToolbarBackgroundOpacity = (toolbar: ToolbarIndex, opacity?: number) => {
|
||||
if (opacity === undefined) {
|
||||
toolbar.background.style.removeProperty('--opacity');
|
||||
} else {
|
||||
@@ -117,20 +117,18 @@ export const handleToolbarIntersection = (ev: any, mainHeaderIndex: HeaderIndex,
|
||||
if (hasValidIntersection) {
|
||||
setHeaderActive(mainHeaderIndex);
|
||||
setHeaderActive(scrollHeaderIndex, false);
|
||||
setToolbarBackgroundOpacity(mainHeaderIndex.toolbars[0], 1);
|
||||
setToolbarBackgroundOpacity(mainHeaderIndex.toolbars[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
|
||||
writeTask(() => {
|
||||
if (active) {
|
||||
headerIndex.el.classList.remove('header-collapse-condense-inactive');
|
||||
} else {
|
||||
headerIndex.el.classList.add('header-collapse-condense-inactive');
|
||||
}
|
||||
});
|
||||
if (active) {
|
||||
headerIndex.el.classList.remove('header-collapse-condense-inactive');
|
||||
} else {
|
||||
headerIndex.el.classList.add('header-collapse-condense-inactive');
|
||||
}
|
||||
};
|
||||
|
||||
export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, transition = false) => {
|
||||
|
||||
@@ -126,6 +126,12 @@ ion-backdrop {
|
||||
// Menu Split Pane
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.menu-pane-visible) {
|
||||
width: var(--width);
|
||||
min-width: var(--min-width);
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
:host(.menu-pane-visible) .menu-inner {
|
||||
@include position-horizontal(0, 0);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Hos
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, Side } from '../../interface';
|
||||
import { Point, getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { GESTURE_CONTROLLER } from '../../utils/gesture';
|
||||
import { assert, clamp, isEndSide as isEnd } from '../../utils/helpers';
|
||||
import { menuController } from '../../utils/menu-controller';
|
||||
@@ -229,7 +229,7 @@ AFTER:
|
||||
|
||||
@Listen('click', { capture: true })
|
||||
onBackdropClick(ev: any) {
|
||||
if (this._isOpen && this.lastOnEnd < ev.currentTime - 100) {
|
||||
if (this._isOpen && this.lastOnEnd < ev.timeStamp - 100) {
|
||||
const shouldClose = (ev.composedPath)
|
||||
? !ev.composedPath().includes(this.menuInnerEl)
|
||||
: false;
|
||||
@@ -337,7 +337,12 @@ AFTER:
|
||||
const isReversed = !shouldOpen;
|
||||
const ani = (this.animation as Animation)!
|
||||
.direction((isReversed) ? 'reverse' : 'normal')
|
||||
.easing((isReversed) ? this.easingReverse : this.easing);
|
||||
.easing((isReversed) ? this.easingReverse : this.easing)
|
||||
.onFinish(() => {
|
||||
if (ani.getDirection() === 'reverse') {
|
||||
ani.direction('normal');
|
||||
}
|
||||
});
|
||||
|
||||
if (animated) {
|
||||
await ani.play();
|
||||
@@ -384,9 +389,7 @@ AFTER:
|
||||
}
|
||||
|
||||
// the cloned animation should not use an easing curve during seek
|
||||
(this.animation as Animation)
|
||||
.direction((this._isOpen) ? 'reverse' : 'normal')
|
||||
.progressStart(true);
|
||||
(this.animation as Animation).progressStart(true, (this._isOpen) ? 1 : 0);
|
||||
}
|
||||
|
||||
private onMove(detail: GestureDetail) {
|
||||
@@ -398,7 +401,7 @@ AFTER:
|
||||
const delta = computeDelta(detail.deltaX, this._isOpen, this.isEndSide);
|
||||
const stepValue = delta / this.width;
|
||||
|
||||
this.animation.progressStep(stepValue);
|
||||
this.animation.progressStep((this._isOpen) ? 1 - stepValue : stepValue);
|
||||
}
|
||||
|
||||
private onEnd(detail: GestureDetail) {
|
||||
@@ -449,14 +452,16 @@ AFTER:
|
||||
* to the new easing curve, as `stepValue` is going to be given
|
||||
* in terms of a linear curve.
|
||||
*/
|
||||
newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(0.4, 0), new Point(0.6, 1), new Point(1, 1), clamp(0, adjustedStepValue, 1));
|
||||
newStepValue += getTimeGivenProgression([0, 0], [0.4, 0], [0.6, 1], [1, 1], clamp(0, adjustedStepValue, 1))[0];
|
||||
|
||||
const playTo = (this._isOpen) ? !shouldComplete : shouldComplete;
|
||||
|
||||
this.animation
|
||||
.easing('cubic-bezier(0.4, 0.0, 0.6, 1)')
|
||||
.onFinish(
|
||||
() => this.afterAnimation(shouldOpen),
|
||||
{ oneTimeCallback: true })
|
||||
.progressEnd(shouldComplete ? 1 : 0, newStepValue, 300);
|
||||
.progressEnd((playTo) ? 1 : 0, (this._isOpen) ? 1 - newStepValue : newStepValue, 300);
|
||||
}
|
||||
|
||||
private beforeAnimation(shouldOpen: boolean) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Build, Component, Element, Event, EventEmitter, Method, Prop, Watch, h
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
|
||||
import { Point, getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { assert } from '../../utils/helpers';
|
||||
import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition';
|
||||
|
||||
@@ -981,9 +981,9 @@ export class Nav implements NavOutlet {
|
||||
*/
|
||||
if (!shouldComplete) {
|
||||
this.sbAni.easing('cubic-bezier(1, 0, 0.68, 0.28)');
|
||||
newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(1, 0), new Point(0.68, 0.28), new Point(1, 1), stepValue);
|
||||
newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], stepValue)[0];
|
||||
} else {
|
||||
newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(0.32, 0.72), new Point(0, 1), new Point(1, 1), stepValue);
|
||||
newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], stepValue)[0];
|
||||
}
|
||||
|
||||
(this.sbAni as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface, PickerButton, PickerColumn } from '../../interface';
|
||||
import { dismiss, eventMethod, prepareOverlay, present, safeCall } from '../../utils/overlays';
|
||||
import { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
|
||||
import { iosEnterAnimation } from './animations/ios.enter';
|
||||
@@ -163,19 +163,29 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
return Promise.resolve(this.columns.find(column => column.name === name));
|
||||
}
|
||||
|
||||
private buttonClick(button: PickerButton) {
|
||||
// if (this.disabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// keep the time of the most recent button click
|
||||
// a handler has been provided, execute it
|
||||
// pass the handler the values from the inputs
|
||||
const shouldDismiss = safeCall(button.handler, this.getSelected()) !== false;
|
||||
if (shouldDismiss) {
|
||||
return this.dismiss();
|
||||
private async buttonClick(button: PickerButton) {
|
||||
const role = button.role;
|
||||
if (isCancel(role)) {
|
||||
return this.dismiss(undefined, role);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
const shouldDismiss = await this.callButtonHandler(button);
|
||||
if (shouldDismiss) {
|
||||
return this.dismiss(this.getSelected(), button.role);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async callButtonHandler(button: PickerButton | undefined) {
|
||||
if (button) {
|
||||
// a handler has been provided, execute it
|
||||
// pass the handler the values from the inputs
|
||||
const rtn = await safeCall(button.handler);
|
||||
if (rtn === false) {
|
||||
// if the return value of the handler is false then do not dismiss
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getSelected() {
|
||||
@@ -194,11 +204,14 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
private onBackdropTap = () => {
|
||||
const cancelBtn = this.buttons.find(b => b.role === 'cancel');
|
||||
if (cancelBtn) {
|
||||
this.buttonClick(cancelBtn);
|
||||
} else {
|
||||
this.dismiss();
|
||||
this.dismiss(undefined, BACKDROP);
|
||||
}
|
||||
|
||||
private dispatchCancelHandler = (ev: CustomEvent) => {
|
||||
const role = ev.detail.role;
|
||||
if (isCancel(role)) {
|
||||
const cancelButton = this.buttons.find(b => b.role === 'cancel');
|
||||
this.callButtonHandler(cancelButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +232,7 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
zIndex: `${20000 + this.overlayIndex}`
|
||||
}}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
onIonPickerWillDismiss={this.dispatchCancelHandler}
|
||||
>
|
||||
<ion-backdrop
|
||||
visible={this.showBackdrop}
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
const pickerController = document.querySelector('ion-picker-controller');
|
||||
const pickerElement = await pickerController.create({
|
||||
buttons: [{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => console.log('Clicked Cancel!')
|
||||
}, {
|
||||
text: 'Save',
|
||||
handler: () => console.log('Clicked Save!')
|
||||
}, {
|
||||
@@ -112,7 +116,12 @@
|
||||
}],
|
||||
cssClass: customClass
|
||||
});
|
||||
return await pickerElement.present();
|
||||
|
||||
await pickerElement.present();
|
||||
|
||||
const { data, role } = await pickerElement.onDidDismiss();
|
||||
|
||||
console.log('Picker dismissed!', data, role);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, NavOutlet, RouteID, RouteWrite, RouterDirection, RouterOutletOptions, SwipeGestureHandler } from '../../interface';
|
||||
import { Point, getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { transition } from '../../utils/transition';
|
||||
|
||||
@@ -91,9 +91,9 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
|
||||
*/
|
||||
if (!shouldComplete) {
|
||||
this.ani.easing('cubic-bezier(1, 0, 0.68, 0.28)');
|
||||
newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(1, 0), new Point(0.68, 0.28), new Point(1, 1), step);
|
||||
newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], step)[0];
|
||||
} else {
|
||||
newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(0.32, 0.72), new Point(0, 1), new Point(1, 1), step);
|
||||
newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], step)[0];
|
||||
}
|
||||
|
||||
(this.ani as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
|
||||
|
||||
@@ -149,9 +149,12 @@ export const SplitPlaneExample: React.SFC<{}> = () => (
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
| Name | Description |
|
||||
| ---------- | -------------------- |
|
||||
| `--border` | Border between panes |
|
||||
| Name | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------------- |
|
||||
| `--border` | Border between panes |
|
||||
| `--side-max-width` | Maximum width of the side pane. Does not apply when split pane is collapsed. |
|
||||
| `--side-min-width` | Minimum width of the side pane. Does not apply when split pane is collapsed. |
|
||||
| `--side-width` | Width of the side pane. Does not apply when split pane is collapsed. |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
// Split Pane
|
||||
// --------------------------------------------------
|
||||
|
||||
.split-pane-ios {
|
||||
:host {
|
||||
--border: #{$split-pane-ios-border};
|
||||
--side-min-width: #{$split-pane-ios-side-min-width};
|
||||
--side-max-width: #{$split-pane-ios-side-max-width};
|
||||
}
|
||||
|
||||
.split-pane-ios.split-pane-visible > .split-pane-side {
|
||||
min-width: $split-pane-ios-side-min-width;
|
||||
max-width: $split-pane-ios-side-max-width;
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side) {
|
||||
min-width: var(--side-min-width);
|
||||
max-width: var(--side-max-width);
|
||||
|
||||
border-right: var(--border);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.split-pane-ios.split-pane-visible > .split-pane-side[side=end] {
|
||||
min-width: $split-pane-ios-side-min-width;
|
||||
max-width: $split-pane-ios-side-max-width;
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side[side=end]) {
|
||||
min-width: var(--side-min-width);
|
||||
max-width: var(--side-max-width);
|
||||
|
||||
border-right: 0;
|
||||
border-left: var(--border);
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
// Split Pane
|
||||
// --------------------------------------------------
|
||||
|
||||
.split-pane-md {
|
||||
:host {
|
||||
--border: #{$split-pane-md-border};
|
||||
--side-min-width: #{$split-pane-md-side-min-width};
|
||||
--side-max-width: #{$split-pane-md-side-max-width};
|
||||
}
|
||||
|
||||
.split-pane-md.split-pane-visible > .split-pane-side {
|
||||
min-width: $split-pane-md-side-min-width;
|
||||
max-width: $split-pane-md-side-max-width;
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side) {
|
||||
min-width: var(--side-min-width);
|
||||
max-width: var(--side-max-width);
|
||||
|
||||
border-right: var(--border);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.split-pane-md.split-pane-visible > .split-pane-side[side=end] {
|
||||
min-width: $split-pane-md-side-min-width;
|
||||
max-width: $split-pane-md-side-max-width;
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side[side=end]) {
|
||||
min-width: var(--side-min-width);
|
||||
max-width: var(--side-max-width);
|
||||
|
||||
border-right: 0;
|
||||
border-left: var(--border);
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
@import "./split-pane.vars";
|
||||
@import "../menu/menu.vars";
|
||||
|
||||
// Split Pane
|
||||
// --------------------------------------------------
|
||||
|
||||
ion-split-pane {
|
||||
:host {
|
||||
/**
|
||||
* @prop --border: Border between panes
|
||||
* @prop --side-min-width: Minimum width of the side pane. Does not apply when split pane is collapsed.
|
||||
* @prop --side-max-width: Maximum width of the side pane. Does not apply when split pane is collapsed.
|
||||
* @prop --side-width: Width of the side pane. Does not apply when split pane is collapsed.
|
||||
*/
|
||||
--side-width: 100%;
|
||||
|
||||
@include position(0, 0, 0, 0);
|
||||
|
||||
@@ -19,34 +24,50 @@ ion-split-pane {
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.split-pane-visible > .split-pane-side,
|
||||
.split-pane-visible > .split-pane-main {
|
||||
/**
|
||||
* Do not pass CSS Variables down on larger
|
||||
* screens as we want them to affect the outer
|
||||
* `ion-menu` rather than the inner content
|
||||
*/
|
||||
::slotted(ion-menu.menu-pane-visible) {
|
||||
|
||||
flex: 0 1 auto;
|
||||
|
||||
width: var(--side-width);
|
||||
min-width: var(--side-min-width);
|
||||
max-width: var(--side-max-width);
|
||||
}
|
||||
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side),
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-main) {
|
||||
@include position(0, 0, 0, 0);
|
||||
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
box-shadow: none !important;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.split-pane-visible > .split-pane-side:not(ion-menu),
|
||||
.split-pane-visible > ion-menu.split-pane-side.menu-enabled {
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-main) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side:not(ion-menu)),
|
||||
:host(.split-pane-visible) ::slotted(ion-menu.split-pane-side.menu-enabled) {
|
||||
display: flex;
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.split-pane-side:not(ion-menu) {
|
||||
::slotted(.split-pane-side:not(ion-menu)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.split-pane-visible > .split-pane-side {
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side) {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.split-pane-visible > .split-pane-side[side=end] {
|
||||
:host(.split-pane-visible) ::slotted(.split-pane-side[side=end]) {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ const QUERY: { [key: string]: string } = {
|
||||
styleUrls: {
|
||||
ios: 'split-pane.ios.scss',
|
||||
md: 'split-pane.md.scss'
|
||||
}
|
||||
},
|
||||
shadow: true
|
||||
})
|
||||
export class SplitPane implements ComponentInterface {
|
||||
|
||||
@@ -159,6 +160,7 @@ export class SplitPane implements ComponentInterface {
|
||||
'split-pane-visible': this.visible
|
||||
}}
|
||||
>
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,28 @@ test('split-pane: basic', async () => {
|
||||
url: '/src/components/split-pane/test/basic?ionic:_testing=true'
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
const screenshotCompares = [];
|
||||
const MIN_WIDTH = '#side-min-width';
|
||||
const MAX_WIDTH = '#side-max-width';
|
||||
const WIDTH = '#side-width';
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await page.click(MIN_WIDTH);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await page.click(MIN_WIDTH);
|
||||
await page.click(MAX_WIDTH);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await page.click(MAX_WIDTH);
|
||||
await page.click(WIDTH);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,8 +69,22 @@
|
||||
<f></f>
|
||||
<f></f>
|
||||
<f></f>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Toggle Side Min Width</ion-label>
|
||||
<ion-toggle id="side-min-width"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Toggle Side Max Width</ion-label>
|
||||
<ion-toggle id="side-max-width"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Toggle Side Fixed Width</ion-label>
|
||||
<ion-toggle id="side-width"></ion-toggle>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
||||
</div>
|
||||
</ion-split-pane>
|
||||
|
||||
@@ -78,6 +92,16 @@
|
||||
<ion-menu-controller></ion-menu-controller>
|
||||
|
||||
<script>
|
||||
const sideMinWidth = document.querySelector('ion-toggle#side-min-width');
|
||||
const sideMaxWidth = document.querySelector('ion-toggle#side-max-width');
|
||||
const sideWidth = document.querySelector('ion-toggle#side-width');
|
||||
|
||||
const splitPane = document.querySelector('ion-split-pane');
|
||||
|
||||
sideMinWidth.addEventListener('ionChange', () => splitPane.classList.toggle('side-min-width'));
|
||||
sideMaxWidth.addEventListener('ionChange', () => splitPane.classList.toggle('side-max-width'));
|
||||
sideWidth.addEventListener('ionChange', () => splitPane.classList.toggle('side-width'));
|
||||
|
||||
const menuCtrl = document.querySelector('ion-menu-controller');
|
||||
function openStart() {
|
||||
menuCtrl.open('start');
|
||||
@@ -89,6 +113,20 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.side-width {
|
||||
--side-width: 150px;
|
||||
--side-min-width: 150px;
|
||||
}
|
||||
|
||||
.side-min-width {
|
||||
--side-min-width: 500px;
|
||||
}
|
||||
|
||||
.side-max-width {
|
||||
--side-max-width: 75px;
|
||||
--side-min-width: 0px;
|
||||
}
|
||||
|
||||
f {
|
||||
display: block;
|
||||
height: 150px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Build, Component, ComponentInterface, Element, Host, Method, Prop, h } from '@stencil/core';
|
||||
import { Build, Component, ComponentInterface, Element, Host, Method, Prop, Watch, h } from '@stencil/core';
|
||||
|
||||
import { ComponentRef, FrameworkDelegate } from '../../interface';
|
||||
import { attachComponent } from '../../utils/framework-delegate';
|
||||
@@ -30,7 +30,7 @@ export class Tab implements ComponentInterface {
|
||||
*/
|
||||
@Prop() component?: ComponentRef;
|
||||
|
||||
componentWillLoad() {
|
||||
async componentWillLoad() {
|
||||
if (Build.isDev) {
|
||||
if (this.component !== undefined && this.el.childElementCount > 0) {
|
||||
console.error('You can not use a lazy-loaded component in a tab and inlined content at the same time.' +
|
||||
@@ -39,6 +39,9 @@ export class Tab implements ComponentInterface {
|
||||
`- Remove the embedded content inside the ion-tab: <ion-tab></ion-tab>`);
|
||||
}
|
||||
}
|
||||
if (this.active) {
|
||||
await this.setActive();
|
||||
}
|
||||
}
|
||||
|
||||
/** Set the active component for the tab */
|
||||
@@ -48,6 +51,13 @@ export class Tab implements ComponentInterface {
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
@Watch('active')
|
||||
changeActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.prepareLazyLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
private prepareLazyLoaded(): Promise<HTMLElement | undefined> {
|
||||
if (!this.loaded && this.component != null) {
|
||||
this.loaded = true;
|
||||
|
||||
@@ -46,7 +46,9 @@ export class Tabs implements NavOutlet {
|
||||
}
|
||||
if (!this.useRouter) {
|
||||
const tabs = this.tabs;
|
||||
await this.select(tabs[0]);
|
||||
if (tabs.length > 0) {
|
||||
await this.select(tabs[0]);
|
||||
}
|
||||
}
|
||||
this.ionNavWillLoad.emit();
|
||||
}
|
||||
@@ -127,7 +129,8 @@ export class Tabs implements NavOutlet {
|
||||
this.leavingTab = this.selectedTab;
|
||||
this.selectedTab = selectedTab;
|
||||
this.ionTabsWillChange.emit({ tab: selectedTab.tab });
|
||||
return selectedTab.setActive();
|
||||
selectedTab.active = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private tabSwitch() {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="selectTab()">
|
||||
<body>
|
||||
<ion-tabs>
|
||||
<ion-tab tab="tab-one">
|
||||
<div class="content-div div-one">
|
||||
@@ -71,11 +71,11 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function selectTab() {
|
||||
customElements.whenDefined('ion-tabs').then(async () => {
|
||||
const tabs = document.querySelector('ion-tabs');
|
||||
await tabs.componentOnReady();
|
||||
await tabs.select('tab-three');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
appearance: none;
|
||||
|
||||
&::placeholder {
|
||||
@include padding(0);
|
||||
|
||||
color: var(--placeholder-color);
|
||||
|
||||
font-family: inherit;
|
||||
|
||||
@@ -191,7 +191,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
const shouldDismiss = await this.callButtonHandler(button);
|
||||
if (shouldDismiss) {
|
||||
return this.dismiss(undefined, button.role);
|
||||
return this.dismiss(undefined, role);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -213,6 +213,14 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
private dispatchCancelHandler = (ev: CustomEvent) => {
|
||||
const role = ev.detail.role;
|
||||
if (isCancel(role)) {
|
||||
const cancelButton = this.getButtons().find(b => b.role === 'cancel');
|
||||
this.callButtonHandler(cancelButton);
|
||||
}
|
||||
}
|
||||
|
||||
renderButtons(buttons: ToastButton[], side: 'start' | 'end') {
|
||||
if (buttons.length === 0) {
|
||||
return;
|
||||
@@ -265,6 +273,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
...getClassMap(this.cssClass),
|
||||
'toast-translucent': this.translucent
|
||||
}}
|
||||
onIonToastWillDismiss={this.dispatchCancelHandler}
|
||||
>
|
||||
<div class={wrapperClass}>
|
||||
<div class="toast-container">
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
.create ion-icon {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.main-toolbar {
|
||||
--background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -77,8 +81,8 @@
|
||||
connectedCallback() {
|
||||
|
||||
this.innerHTML = `
|
||||
<ion-header class="main-header">
|
||||
<ion-toolbar>
|
||||
<ion-header class="main-header" translucent>
|
||||
<ion-toolbar class="main-toolbar">
|
||||
<ion-buttons collapse>
|
||||
<ion-back-button default-href="/"></ion-back-button>
|
||||
</ion-buttons>
|
||||
@@ -89,7 +93,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="main-content">
|
||||
<ion-content id="content" class="main-content" fullscreen>
|
||||
<ion-header collapse="condense">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">Page Two</ion-title>
|
||||
|
||||
@@ -76,6 +76,10 @@ body.backdrop-no-scroll {
|
||||
z-index: $z-index-page-container;
|
||||
}
|
||||
|
||||
.split-pane-visible > .ion-page.split-pane-main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ion-route,
|
||||
ion-route-redirect,
|
||||
ion-router,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'ionicons';
|
||||
|
||||
export { createAnimation } from './utils/animation/animation';
|
||||
export { getTimeGivenProgression } from './utils/animation/cubic-bezier';
|
||||
export { createGesture } from './utils/gesture';
|
||||
export { isPlatform, Platforms, getPlatforms } from './utils/platform';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface Animation {
|
||||
parentAnimation: Animation | undefined;
|
||||
elements: HTMLElement[];
|
||||
childAnimations: Animation[];
|
||||
id: string | undefined;
|
||||
|
||||
/**
|
||||
* Play the animation
|
||||
@@ -28,7 +29,7 @@ export interface Animation {
|
||||
*/
|
||||
destroy(): void;
|
||||
|
||||
progressStart(forceLinearEasing: boolean): void;
|
||||
progressStart(forceLinearEasing: boolean, step?: number): void;
|
||||
progressStep(step: number): void;
|
||||
progressEnd(playTo: 0 | 1 | undefined, step: number, dur?: number): void;
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ interface AnimationInternal extends Animation {
|
||||
/**
|
||||
* Updates any existing animations.
|
||||
*/
|
||||
update(deep: boolean): Animation;
|
||||
update(deep: boolean, toggleAnimationName: boolean, step?: number): Animation;
|
||||
|
||||
animationFinish(): void;
|
||||
}
|
||||
|
||||
export const createAnimation = (): Animation => {
|
||||
export const createAnimation = (animationId?: string): Animation => {
|
||||
let _delay: number | undefined;
|
||||
let _duration: number | undefined;
|
||||
let _easing: string | undefined;
|
||||
@@ -32,6 +32,8 @@ export const createAnimation = (): Animation => {
|
||||
let _fill: AnimationFill | undefined;
|
||||
let _direction: AnimationDirection | undefined;
|
||||
let _keyframes: AnimationKeyFrames = [];
|
||||
const id: string | undefined = animationId;
|
||||
|
||||
let beforeAddClasses: string[] = [];
|
||||
let beforeRemoveClasses: string[] = [];
|
||||
let initialized = false;
|
||||
@@ -533,7 +535,7 @@ export const createAnimation = (): Animation => {
|
||||
elements.forEach(element => {
|
||||
if (_keyframes.length > 0) {
|
||||
const keyframeRules = generateKeyframeRules(_keyframes);
|
||||
keyframeName = generateKeyframeName(keyframeRules);
|
||||
keyframeName = (animationId !== undefined) ? animationId : generateKeyframeName(keyframeRules);
|
||||
const stylesheet = createKeyframeStylesheet(keyframeName, keyframeRules, element);
|
||||
stylesheets.push(stylesheet);
|
||||
|
||||
@@ -564,6 +566,7 @@ export const createAnimation = (): Animation => {
|
||||
const initializeWebAnimation = () => {
|
||||
elements.forEach(element => {
|
||||
const animation = element.animate(_keyframes, {
|
||||
id,
|
||||
delay: getDelay(),
|
||||
duration: getDuration(),
|
||||
easing: getEasing(),
|
||||
@@ -608,8 +611,7 @@ export const createAnimation = (): Animation => {
|
||||
});
|
||||
|
||||
} else {
|
||||
const animationDelay = getDelay() || 0;
|
||||
const animationDuration = `-${animationDelay + (getDuration()! * step)}ms`;
|
||||
const animationDuration = `-${getDuration()! * step}ms`;
|
||||
|
||||
elements.forEach(element => {
|
||||
if (_keyframes.length > 0) {
|
||||
@@ -620,7 +622,7 @@ export const createAnimation = (): Animation => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateWebAnimation = () => {
|
||||
const updateWebAnimation = (step?: number) => {
|
||||
webAnimations.forEach(animation => {
|
||||
animation.effect.updateTiming({
|
||||
delay: getDelay(),
|
||||
@@ -631,15 +633,19 @@ export const createAnimation = (): Animation => {
|
||||
direction: getDirection()
|
||||
});
|
||||
});
|
||||
|
||||
if (step !== undefined) {
|
||||
setAnimationStep(step);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCSSAnimation = (toggleAnimationName = true) => {
|
||||
const updateCSSAnimation = (toggleAnimationName = true, step?: number) => {
|
||||
raf(() => {
|
||||
elements.forEach(element => {
|
||||
setStyleProperty(element, 'animation-name', keyframeName || null);
|
||||
setStyleProperty(element, 'animation-duration', `${getDuration()}ms`);
|
||||
setStyleProperty(element, 'animation-timing-function', getEasing());
|
||||
setStyleProperty(element, 'animation-delay', `${getDelay()}ms`);
|
||||
setStyleProperty(element, 'animation-delay', (step !== undefined) ? `-${step! * getDuration()}ms` : `${getDelay()}ms`);
|
||||
setStyleProperty(element, 'animation-fill-mode', getFill() || null);
|
||||
setStyleProperty(element, 'animation-direction', getDirection() || null);
|
||||
|
||||
@@ -660,25 +666,25 @@ export const createAnimation = (): Animation => {
|
||||
});
|
||||
};
|
||||
|
||||
const update = (deep = false, toggleAnimationName = true) => {
|
||||
const update = (deep = false, toggleAnimationName = true, step?: number) => {
|
||||
if (deep) {
|
||||
childAnimations.forEach(animation => {
|
||||
animation.update(deep);
|
||||
animation.update(deep, toggleAnimationName, step);
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsWebAnimations) {
|
||||
updateWebAnimation();
|
||||
updateWebAnimation(step);
|
||||
} else {
|
||||
updateCSSAnimation(toggleAnimationName);
|
||||
updateCSSAnimation(toggleAnimationName, step);
|
||||
}
|
||||
|
||||
return ani;
|
||||
};
|
||||
|
||||
const progressStart = (forceLinearEasing = false) => {
|
||||
const progressStart = (forceLinearEasing = false, step?: number) => {
|
||||
childAnimations.forEach(animation => {
|
||||
animation.progressStart(forceLinearEasing);
|
||||
animation.progressStart(forceLinearEasing, step);
|
||||
});
|
||||
|
||||
pauseAnimation();
|
||||
@@ -687,8 +693,7 @@ export const createAnimation = (): Animation => {
|
||||
if (!initialized) {
|
||||
initializeAnimation();
|
||||
} else {
|
||||
update();
|
||||
setAnimationStep(0);
|
||||
update(false, true, step);
|
||||
}
|
||||
|
||||
return ani;
|
||||
@@ -957,6 +962,7 @@ export const createAnimation = (): Animation => {
|
||||
parentAnimation,
|
||||
elements,
|
||||
childAnimations,
|
||||
id,
|
||||
animationFinish,
|
||||
from,
|
||||
to,
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
* TODO: Reduce rounding error
|
||||
*/
|
||||
|
||||
export class Point {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL
|
||||
* Given a cubic-bezier curve, get the x value (time) given
|
||||
* the y value (progression).
|
||||
* Ex: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
@@ -19,11 +16,12 @@ export class Point {
|
||||
* P3: (1, 1)
|
||||
*
|
||||
* If you give a cubic bezier curve that never reaches the
|
||||
* provided progression, this function will return NaN.
|
||||
* provided progression, this function will return an empty array.
|
||||
*/
|
||||
export const getTimeGivenProgression = (p0: Point, p1: Point, p2: Point, p3: Point, progression: number) => {
|
||||
const tValues = solveCubicBezier(p0.y, p1.y, p2.y, p3.y, progression);
|
||||
return solveCubicParametricEquation(p0.x, p1.x, p2.x, p3.x, tValues[0]); // TODO: Add better strategy for dealing with multiple solutions
|
||||
export const getTimeGivenProgression = (p0: number[], p1: number[], p2: number[], p3: number[], progression: number): number[] => {
|
||||
return solveCubicBezier(p0[1], p1[1], p2[1], p3[1], progression).map(tValue => {
|
||||
return solveCubicParametricEquation(p0[0], p1[0], p2[0], p3[0], tValue);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAnimation } from '../animation';
|
||||
import { getTimeGivenProgression, Point } from '../cubic-bezier';
|
||||
import { getTimeGivenProgression } from '../cubic-bezier';
|
||||
import { Animation } from '../animation-interface';
|
||||
|
||||
describe('Animation Class', () => {
|
||||
@@ -313,70 +313,83 @@ describe('cubic-bezier conversion', () => {
|
||||
describe('should properly get a time value (x value) given a progression value (y value)', () => {
|
||||
it('cubic-bezier(0.32, 0.72, 0, 1)', () => {
|
||||
const equation = [
|
||||
new Point(0, 0),
|
||||
new Point(0.32, 0.72),
|
||||
new Point(0, 1),
|
||||
new Point(1, 1)
|
||||
[0, 0],
|
||||
[0.32, 0.72],
|
||||
[0, 1],
|
||||
[1, 1]
|
||||
];
|
||||
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), 0.16);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), 0.56);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), 0.11);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.16]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), [0.56]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), [0.11]);
|
||||
});
|
||||
|
||||
it('cubic-bezier(1, 0, 0.68, 0.28)', () => {
|
||||
const equation = [
|
||||
new Point(0, 0),
|
||||
new Point(1, 0),
|
||||
new Point(0.68, 0.28),
|
||||
new Point(1, 1)
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[0.68, 0.28],
|
||||
[1, 1]
|
||||
];
|
||||
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), 0.60);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.50), 0.84);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), 0.98);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), [0.60]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.50), [0.84]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), [0.98]);
|
||||
})
|
||||
|
||||
it('cubic-bezier(0.4, 0, 0.6, 1)', () => {
|
||||
const equation = [
|
||||
new Point(0, 0),
|
||||
new Point(0.4, 0),
|
||||
new Point(0.6, 1),
|
||||
new Point(1, 1)
|
||||
[0, 0],
|
||||
[0.4, 0],
|
||||
[0.6, 1],
|
||||
[1, 1]
|
||||
];
|
||||
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), 0.43);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), 0.11);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), 0.78);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), [0.43]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), [0.11]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), [0.78]);
|
||||
})
|
||||
|
||||
it('cubic-bezier(0, 0, 0.2, 1)', () => {
|
||||
const equation = [
|
||||
new Point(0, 0),
|
||||
new Point(0, 0),
|
||||
new Point(0.2, 1),
|
||||
new Point(1, 1)
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0.2, 1],
|
||||
[1, 1]
|
||||
];
|
||||
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), 0.71);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), 0.03);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.70), 0.35);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), [0.71]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), [0.03]);
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.70), [0.35]);
|
||||
})
|
||||
|
||||
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
|
||||
const equation = [
|
||||
new Point(0, 0),
|
||||
new Point(0.05, 0.2),
|
||||
new Point(.14, 1.72),
|
||||
new Point(1, 1)
|
||||
[0, 0],
|
||||
[0.05, 0.2],
|
||||
[.14, 1.72],
|
||||
[1, 1]
|
||||
];
|
||||
|
||||
expect(getTimeGivenProgression(...equation, 1.32)).toBeNaN();
|
||||
expect(getTimeGivenProgression(...equation, -0.32)).toBeNaN();
|
||||
expect(getTimeGivenProgression(...equation, 1.32)[0]).toBeUndefined();
|
||||
expect(getTimeGivenProgression(...equation, -0.32)[0]).toBeUndefined();
|
||||
})
|
||||
|
||||
it('cubic-bezier(0.21, 1.71, 0.88, 0.9) (multiple solutions)', () => {
|
||||
const equation = [
|
||||
[0, 0],
|
||||
[0.21, 1.71],
|
||||
[0.88, 0.9],
|
||||
[1, 1]
|
||||
];
|
||||
|
||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 1.02), [0.35, 0.87]);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const shouldApproximatelyEqual = (givenValue: number, expectedValue: number): boolean => {
|
||||
expect(Math.abs(expectedValue - givenValue)).toBeLessThanOrEqual(0.01);
|
||||
const shouldApproximatelyEqual = (givenValues: number[], expectedValues: number[]): void => {
|
||||
givenValues.forEach((givenValue, i) => {
|
||||
expect(Math.abs(expectedValues[i] - givenValue)).toBeLessThanOrEqual(0.01);
|
||||
});
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
const squareA = document.querySelector('.square-a');
|
||||
|
||||
const rootAnimation = createAnimation();
|
||||
const rootAnimation = createAnimation('root-animation-id');
|
||||
|
||||
rootAnimation
|
||||
.addElement(squareA)
|
||||
|
||||
@@ -92,8 +92,8 @@ export const createPointerEvents = (
|
||||
stopMouse();
|
||||
};
|
||||
|
||||
const enable = (enable = true) => {
|
||||
if (!enable) {
|
||||
const enable = (isEnabled = true) => {
|
||||
if (!isEnabled) {
|
||||
if (rmTouchStart) {
|
||||
rmTouchStart();
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ const getBackButton = (refEl: any, backDirection: boolean) => {
|
||||
const activeHeader = parentHeader && !parentHeader.classList.contains('header-collapse-condense-inactive');
|
||||
const backButton = buttons.querySelector('ion-back-button');
|
||||
const buttonsCollapse = buttons.classList.contains('buttons-collapse');
|
||||
const startSlot = buttons.slot === 'start' || buttons.slot === '';
|
||||
|
||||
if (backButton !== null && ((buttonsCollapse && activeHeader && backDirection) || !buttonsCollapse)) {
|
||||
if (backButton !== null && startSlot && ((buttonsCollapse && activeHeader && backDirection) || !buttonsCollapse)) {
|
||||
return backButton;
|
||||
}
|
||||
}
|
||||
|
||||
15
packages/react-router/src/ReactRouter/IonReactHashRouter.tsx
Normal file
15
packages/react-router/src/ReactRouter/IonReactHashRouter.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { HashRouter, HashRouterProps } from 'react-router-dom';
|
||||
|
||||
import { RouteManagerWithRouter } from './Router';
|
||||
|
||||
export class IonReactHashRouter extends React.Component<HashRouterProps> {
|
||||
render() {
|
||||
const { children, ...props } = this.props;
|
||||
return (
|
||||
<HashRouter {...props}>
|
||||
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
15
packages/react-router/src/ReactRouter/IonReactRouter.tsx
Normal file
15
packages/react-router/src/ReactRouter/IonReactRouter.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter, BrowserRouterProps } from 'react-router-dom';
|
||||
|
||||
import { RouteManagerWithRouter } from './Router';
|
||||
|
||||
export class IonReactRouter extends React.Component<BrowserRouterProps> {
|
||||
render() {
|
||||
const { children, ...props } = this.props;
|
||||
return (
|
||||
<BrowserRouter {...props}>
|
||||
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ interface NavManagerProps extends RouteComponentProps {
|
||||
findViewInfoByLocation: (location: HistoryLocation) => { view?: ViewItem, viewStack?: ViewStack };
|
||||
findViewInfoById: (id: string) => { view?: ViewItem, viewStack?: ViewStack };
|
||||
getActiveIonPage: () => { view?: ViewItem, viewStack?: ViewStack };
|
||||
onNavigate: (type: 'push' | 'replace', path: string, state?: any) => void;
|
||||
}
|
||||
|
||||
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
|
||||
@@ -27,8 +28,6 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
|
||||
this.state = {
|
||||
goBack: this.goBack.bind(this),
|
||||
hasIonicRouter: () => true,
|
||||
getHistory: this.getHistory.bind(this),
|
||||
getLocation: this.getLocation.bind(this),
|
||||
navigate: this.navigate.bind(this),
|
||||
getStackManager: this.getStackManager.bind(this),
|
||||
getPageManager: this.getPageManager.bind(this),
|
||||
@@ -66,36 +65,28 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
|
||||
if (enteringView) {
|
||||
const lastLocation = this.locationHistory.findLastLocation(enteringView.routeData.match.url);
|
||||
if (lastLocation) {
|
||||
this.props.history.replace(lastLocation.pathname + lastLocation.search, { direction: 'back' });
|
||||
this.props.onNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
|
||||
} else {
|
||||
this.props.history.replace(enteringView.routeData.match.url, { direction: 'back' });
|
||||
this.props.onNavigate('replace', enteringView.routeData.match.url, 'back');
|
||||
}
|
||||
} else {
|
||||
if (defaultHref) {
|
||||
this.props.history.replace(defaultHref, { direction: 'back' });
|
||||
this.props.onNavigate('replace', defaultHref, 'back');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (defaultHref) {
|
||||
this.props.history.replace(defaultHref, { direction: 'back' });
|
||||
this.props.onNavigate('replace', defaultHref, 'back');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHistory() {
|
||||
return this.props.history as any;
|
||||
}
|
||||
|
||||
getLocation() {
|
||||
return this.props.location as any;
|
||||
}
|
||||
|
||||
navigate(path: string, direction?: RouterDirection | 'none') {
|
||||
this.props.history.push(path, { direction });
|
||||
this.props.onNavigate('push', path, direction);
|
||||
}
|
||||
|
||||
tabNavigate(url: string) {
|
||||
this.props.history.replace(url, { direction: 'back' });
|
||||
tabNavigate(path: string) {
|
||||
this.props.onNavigate('replace', path, 'back');
|
||||
}
|
||||
|
||||
getPageManager() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NavDirection } from '@ionic/core';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { ViewStacks } from './ViewStacks';
|
||||
@@ -9,7 +8,6 @@ export interface RouteManagerContextState {
|
||||
viewStacks: ViewStacks;
|
||||
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => Promise<void>;
|
||||
removeViewStack: (stack: string) => void;
|
||||
transitionView: (enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) => void;
|
||||
}
|
||||
|
||||
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
|
||||
@@ -17,8 +15,7 @@ export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManager
|
||||
syncView: () => { navContextNotFoundError(); },
|
||||
hideView: () => { navContextNotFoundError(); },
|
||||
setupIonRouter: () => Promise.reject(navContextNotFoundError()),
|
||||
removeViewStack: () => { navContextNotFoundError(); },
|
||||
transitionView: () => { navContextNotFoundError(); }
|
||||
removeViewStack: () => { navContextNotFoundError(); }
|
||||
});
|
||||
|
||||
function navContextNotFoundError() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NavDirection } from '@ionic/core';
|
||||
import { RouterDirection } from '@ionic/react';
|
||||
import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, BrowserRouterProps, RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
|
||||
import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
|
||||
|
||||
import { generateId } from '../utils';
|
||||
|
||||
@@ -20,17 +20,18 @@ interface RouteManagerState extends RouteManagerContextState {
|
||||
class RouteManager extends React.Component<RouteComponentProps, RouteManagerState> {
|
||||
listenUnregisterCallback: UnregisterCallback | undefined;
|
||||
activeIonPageId?: string;
|
||||
currentDirection?: RouterDirection;
|
||||
|
||||
constructor(props: RouteComponentProps) {
|
||||
super(props);
|
||||
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
|
||||
this.handleNavigate = this.handleNavigate.bind(this);
|
||||
this.state = {
|
||||
viewStacks: new ViewStacks(),
|
||||
hideView: this.hideView.bind(this),
|
||||
setupIonRouter: this.setupIonRouter.bind(this),
|
||||
removeViewStack: this.removeViewStack.bind(this),
|
||||
syncView: this.syncView.bind(this),
|
||||
transitionView: this.transitionView.bind(this),
|
||||
syncView: this.syncView.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +42,12 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listenUnregisterCallback) {
|
||||
this.listenUnregisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
hideView(viewId: string) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
@@ -57,6 +64,8 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
|
||||
historyChange(location: HistoryLocation, action: HistoryAction) {
|
||||
location.state = location.state || { direction: this.currentDirection };
|
||||
this.currentDirection = undefined;
|
||||
this.setState({
|
||||
location,
|
||||
action
|
||||
@@ -65,7 +74,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
|
||||
setActiveView(location: HistoryLocation, action: HistoryAction) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
let direction: RouterDirection = location.state && location.state.direction || 'forward';
|
||||
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
|
||||
let leavingView: ViewItem | undefined;
|
||||
const viewStackKeys = viewStacks.getKeys();
|
||||
|
||||
@@ -91,19 +100,26 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
* record the view that originally directed to the new view for back button purposes.
|
||||
*/
|
||||
enteringView.prevId = enteringView.prevId || leavingView.id;
|
||||
} else if (action === 'POP') {
|
||||
direction = leavingView.prevId === enteringView.id ? 'back' : 'none';
|
||||
} else {
|
||||
direction = direction || 'back';
|
||||
leavingView.mount = false;
|
||||
}
|
||||
} else if (action === 'REPLACE') {
|
||||
} else if (direction === 'back' || action === 'REPLACE') {
|
||||
leavingView.mount = false;
|
||||
}
|
||||
} else {
|
||||
// If there is not a leavingView, then we shouldn't provide a direction
|
||||
direction = undefined;
|
||||
}
|
||||
this.removeOrphanedViews(enteringView, enteringViewStack);
|
||||
} else {
|
||||
enteringView.show = true;
|
||||
enteringView.mount = true;
|
||||
enteringView.routeData.match = match!;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (leavingView) {
|
||||
@@ -120,7 +136,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
if (enteringView && viewStack) {
|
||||
const enteringEl = enteringView.ionPageElement ? enteringView.ionPageElement : undefined;
|
||||
const leavingEl = leavingView && leavingView.ionPageElement ? leavingView.ionPageElement : undefined;
|
||||
|
||||
if (enteringEl) {
|
||||
// Don't animate from an empty view
|
||||
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
|
||||
@@ -128,7 +143,8 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
enteringEl!,
|
||||
leavingEl!,
|
||||
viewStack.routerOutlet,
|
||||
navDirection);
|
||||
navDirection,
|
||||
!!enteringView.prevId);
|
||||
} else if (leavingEl) {
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
@@ -137,10 +153,24 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listenUnregisterCallback) {
|
||||
this.listenUnregisterCallback();
|
||||
}
|
||||
removeOrphanedViews(view: ViewItem, viewStack: ViewStack) {
|
||||
const viewsToRemove = viewStack.views.filter(v => v.prevId === view.id);
|
||||
viewsToRemove.forEach(v => {
|
||||
// Don't remove if view is currently active
|
||||
if (v.id !== this.activeIonPageId) {
|
||||
this.removeOrphanedViews(v, viewStack);
|
||||
|
||||
// If view is not currently visible, go ahead and remove it from DOM
|
||||
if (v.ionPageElement!.classList.contains('ion-page-hidden')) {
|
||||
v.show = false;
|
||||
v.ionPageElement = undefined;
|
||||
v.isIonRoute = false;
|
||||
v.prevId = undefined;
|
||||
v.key = generateId();
|
||||
}
|
||||
v.mount = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
|
||||
@@ -231,21 +261,21 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
});
|
||||
}
|
||||
|
||||
transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet?: HTMLIonRouterOutletElement, direction?: NavDirection) {
|
||||
transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement | undefined, direction: NavDirection | undefined, showGoBack: boolean) {
|
||||
/**
|
||||
* Super hacky workaround to make sure ionRouterOutlet is available
|
||||
* since transitionView might be called before IonRouterOutlet is fully mounted
|
||||
*/
|
||||
if (ionRouterOutlet && ionRouterOutlet.componentOnReady) {
|
||||
this.commitView(enteringEl, leavingEl, ionRouterOutlet, direction);
|
||||
this.commitView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.transitionView(enteringEl, leavingEl, ionRouterOutlet, direction);
|
||||
this.transitionView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection) {
|
||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) {
|
||||
|
||||
if (enteringEl === leavingEl) {
|
||||
return;
|
||||
@@ -255,7 +285,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack: direction === 'forward',
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
|
||||
@@ -266,11 +296,21 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
}
|
||||
|
||||
handleNavigate(type: 'push' | 'replace', path: string, direction?: RouterDirection) {
|
||||
this.currentDirection = direction;
|
||||
if (type === 'push') {
|
||||
this.props.history.push(path);
|
||||
} else {
|
||||
this.props.history.replace(path);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RouteManagerContext.Provider value={this.state}>
|
||||
<NavManager
|
||||
{...this.props}
|
||||
onNavigate={this.handleNavigate}
|
||||
findViewInfoById={(id: string) => this.state.viewStacks.findViewInfoById(id)}
|
||||
findViewInfoByLocation={(location: HistoryLocation) => this.state.viewStacks.findViewInfoByLocation(location)}
|
||||
getActiveIonPage={() => this.state.viewStacks.findViewInfoById(this.activeIonPageId)}
|
||||
@@ -282,16 +322,5 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
}
|
||||
|
||||
const RouteManagerWithRouter = withRouter(RouteManager);
|
||||
export const RouteManagerWithRouter = withRouter(RouteManager);
|
||||
RouteManagerWithRouter.displayName = 'RouteManager';
|
||||
|
||||
export class IonReactRouter extends React.Component<BrowserRouterProps> {
|
||||
render() {
|
||||
const { children, ...props } = this.props;
|
||||
return (
|
||||
<BrowserRouter {...props}>
|
||||
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,16 +85,4 @@ export class ViewStacks {
|
||||
return { view, viewStack };
|
||||
}
|
||||
|
||||
setHiddenViews() {
|
||||
const keys = this.getKeys();
|
||||
keys.forEach(key => {
|
||||
const viewStack = this.viewStacks[key];
|
||||
viewStack!.views.forEach(view => {
|
||||
if (!view.routeData.match && !view.isIonRoute) {
|
||||
view.show = false;
|
||||
view.mount = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
|
||||
export { IonReactRouter } from './Router';
|
||||
export { IonReactRouter } from './IonReactRouter';
|
||||
export { IonReactHashRouter } from './IonReactHashRouter';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## @ionic/react (beta)
|
||||
## @ionic/react
|
||||
|
||||
These are React specific building blocks on top of [@ionic/core](https://www.npmjs.com/package/@ionic/core) components/services.
|
||||
|
||||
@@ -27,9 +27,6 @@ ionic init "My React App" --type=react
|
||||
ionic integrations enable capacitor
|
||||
```
|
||||
|
||||
Open the './capacitor.config.json' file in your projects root.
|
||||
Change `"webDir": "www"` to be `"webDir": "build"` (dependent on your config but Ionic React defaults with this as the build directory)
|
||||
|
||||
Then run the following command to get started with either `ios` or `android` platforms.
|
||||
```
|
||||
ionic capacitor add <android|ios>
|
||||
|
||||
@@ -54,13 +54,19 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
|
||||
|
||||
async present(prevProps?: Props) {
|
||||
const { isOpen, onDidDismiss, ...cProps } = this.props;
|
||||
const overlay = this.overlay = await controller.create({
|
||||
...cProps as any
|
||||
});
|
||||
let overlay = this.overlay;
|
||||
if (!overlay) {
|
||||
overlay = this.overlay = await controller.create({
|
||||
...cProps as any
|
||||
});
|
||||
}
|
||||
attachProps(overlay, {
|
||||
[dismissEventName]: onDidDismiss
|
||||
}, prevProps);
|
||||
await overlay.present();
|
||||
// Check isOpen again since the value could of changed during the async call to controller.create
|
||||
if (this.props.isOpen === true) {
|
||||
await overlay.present();
|
||||
}
|
||||
}
|
||||
|
||||
render(): null {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { IonTabBarInner } from '../inner-proxies';
|
||||
import { IonTabButton } from '../proxies';
|
||||
|
||||
type Props = LocalJSX.IonTabBar & {
|
||||
onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void;
|
||||
onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void;
|
||||
currentPath?: string;
|
||||
slot?: 'bottom' | 'top';
|
||||
};
|
||||
@@ -75,6 +77,12 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Pro
|
||||
this.context.navigate(originalHref, 'back');
|
||||
}
|
||||
} else {
|
||||
if (this.props.onIonTabsWillChange) {
|
||||
this.props.onIonTabsWillChange(new CustomEvent('ionTabWillChange', { detail: { tab: e.detail.tab } }));
|
||||
}
|
||||
if (this.props.onIonTabsDidChange) {
|
||||
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
||||
}
|
||||
this.context.navigate(this.state.tabs[e.detail.tab].currentHref, 'none');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../../contexts/NavContext';
|
||||
@@ -5,7 +6,7 @@ import { IonRouterOutlet } from '../IonRouterOutlet';
|
||||
|
||||
import { IonTabBar } from './IonTabBar';
|
||||
|
||||
interface Props {
|
||||
interface Props extends LocalJSX.IonTabs {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ const tabsInner: React.CSSProperties = {
|
||||
contain: 'layout size style'
|
||||
};
|
||||
|
||||
export const IonTabs = /*@__PURE__*/(() => class extends React.Component<Props> {
|
||||
export class IonTabs extends React.Component<Props> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
|
||||
|
||||
@@ -38,7 +39,7 @@ export const IonTabs = /*@__PURE__*/(() => class extends React.Component<Props>
|
||||
|
||||
render() {
|
||||
let outlet: React.ReactElement<{}> | undefined;
|
||||
let tabBar: React.ReactElement<{ slot: 'bottom' | 'top' }> | undefined;
|
||||
let tabBar: React.ReactElement | undefined;
|
||||
|
||||
React.Children.forEach(this.props.children, (child: any) => {
|
||||
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
|
||||
@@ -48,7 +49,8 @@ export const IonTabs = /*@__PURE__*/(() => class extends React.Component<Props>
|
||||
outlet = child;
|
||||
}
|
||||
if (child.type === IonTabBar) {
|
||||
tabBar = child;
|
||||
const { onIonTabsDidChange, onIonTabsWillChange } = this.props;
|
||||
tabBar = React.cloneElement(child, { onIonTabsDidChange, onIonTabsWillChange });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,11 +73,7 @@ export const IonTabs = /*@__PURE__*/(() => class extends React.Component<Props>
|
||||
);
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonTabs';
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { camelToDashCase } from './case';
|
||||
|
||||
export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => {
|
||||
// add any classes in className to the class list
|
||||
const className = getClassName(node.classList, newProps, oldProps);
|
||||
if (className !== '') {
|
||||
node.className = className;
|
||||
}
|
||||
|
||||
Object.keys(newProps).forEach(name => {
|
||||
if (name === 'children' || name === 'style' || name === 'ref' || name === 'class' || name === 'className' || name === 'forwardedRef') {
|
||||
return;
|
||||
// some test frameworks don't render DOM elements, so we test here to make sure we are dealing with DOM first
|
||||
if (node instanceof Element) {
|
||||
// add any classes in className to the class list
|
||||
const className = getClassName(node.classList, newProps, oldProps);
|
||||
if (className !== '') {
|
||||
node.className = className;
|
||||
}
|
||||
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
|
||||
const eventName = name.substring(2);
|
||||
const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1);
|
||||
|
||||
if (!isCoveredByReact(eventNameLc)) {
|
||||
syncEvent(node, eventNameLc, newProps[name]);
|
||||
Object.keys(newProps).forEach(name => {
|
||||
if (name === 'children' || name === 'style' || name === 'ref' || name === 'class' || name === 'className' || name === 'forwardedRef') {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
(node as any)[name] = newProps[name];
|
||||
const propType = typeof newProps[name];
|
||||
if (propType === 'string') {
|
||||
node.setAttribute(camelToDashCase(name), newProps[name]);
|
||||
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
|
||||
const eventName = name.substring(2);
|
||||
const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1);
|
||||
|
||||
if (!isCoveredByReact(eventNameLc)) {
|
||||
syncEvent(node, eventNameLc, newProps[name]);
|
||||
}
|
||||
} else {
|
||||
(node as any)[name] = newProps[name];
|
||||
const propType = typeof newProps[name];
|
||||
if (propType === 'string') {
|
||||
node.setAttribute(camelToDashCase(name), newProps[name]);
|
||||
} else {
|
||||
(node as any)[name] = newProps[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => {
|
||||
|
||||
@@ -2,8 +2,6 @@ import { RouterDirection } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
export interface NavContextState {
|
||||
getHistory: () => History;
|
||||
getLocation: () => Location;
|
||||
getPageManager: () => any;
|
||||
getStackManager: () => any;
|
||||
goBack: (defaultHref?: string) => void;
|
||||
@@ -15,8 +13,6 @@ export interface NavContextState {
|
||||
}
|
||||
|
||||
export const NavContext = /*@__PURE__*/React.createContext<NavContextState>({
|
||||
getHistory: () => window.history,
|
||||
getLocation: () => window.location,
|
||||
getPageManager: () => undefined,
|
||||
getStackManager: () => undefined,
|
||||
goBack: (defaultHref?: string) => {
|
||||
|
||||
Reference in New Issue
Block a user