Compare commits

..

40 Commits

Author SHA1 Message Date
Liam DeBeasi
4476817825 5.3.1 2020-07-27 09:33:54 -04:00
Liam DeBeasi
97e32f3b0b chore(): bump stencil to 1.17.1 (#21822) 2020-07-27 09:24:31 -04:00
Ely Lucas
bfddb17065 fix(react): using autonomous web component for safari support, closes #21803
* fix(react): using autonomous web component for safari support, closes #21803

* taking out extends
2020-07-24 12:25:28 -06:00
Adam Bradley
3c9d6ea5f5 chore(deps): update to stencil 1.17.0, bump deps (#21811) 2020-07-24 12:44:09 -05:00
Sebastián Ferreras
8e1178b98b docs(loading): remove duplicated cssClass property in usage (#21784) 2020-07-23 17:42:58 -04:00
Liam DeBeasi
470478d387 docs(route): add correct interface name (#21802) 2020-07-23 13:28:46 -04:00
Liam DeBeasi
cc73a1063e merge release-5.3.0
5.3.0
2020-07-23 12:18:57 -04:00
Liam DeBeasi
349e8cd5b0 5.3.0 2020-07-23 11:43:23 -04:00
Liam DeBeasi
bcbe8cbb8d fix(overlay): do not try to trap focus on hidden inputs (#21799) 2020-07-23 11:19:31 -04:00
Liam DeBeasi
03ca0c5968 docs(modal): add correct card-style modal usage for react (#21780)
resolves #21773
2020-07-22 13:49:32 -04:00
Liam DeBeasi
fff4aec6cf fix(overlays): trap focus inside overlay components except toast (#21716)
fixes #21647
2020-07-22 12:09:31 -04:00
Liam DeBeasi
eb592b8917 fix(nav): insertPages method correctly inserts multiple pages with props (#21725)
fixes #21724
2020-07-22 12:06:29 -04:00
Ely Lucas
a15cd01bc3 fix(react): fixes swipe to go back regression (#21791) 2020-07-21 22:30:44 -06:00
Adam Bradley
4199762d3e chore(): export ionicons jsx interface (#21788) 2020-07-21 14:45:19 -04:00
Adam Bradley
79518468dd fix(overlays): move prepareOverlay to connectedCallback
For custom elements builds, overlays cannot use hasAttribute() in the constructor, so moving it to connectedCallback instead.
2020-07-21 13:07:54 -05:00
Ely Lucas
f4a08b7ed4 fix(react): fixng ion-router-outlet ref regresssion (#21786) 2020-07-21 08:48:25 -06:00
Liam DeBeasi
dbe6853884 fix(title): allow overriding of large title transform origin (#21770)
resolves #21761
2020-07-20 13:40:27 -04:00
Liam DeBeasi
096eef4a79 feat(card): expose global card css variable (#21756)
resolves #21694
2020-07-20 12:46:58 -04:00
Ely Lucas
d4a5fbd955 fix(react): adding custom history to IonReactRouter, closes #20297 (#21775) 2020-07-20 10:03:35 -06:00
Brandy Carney
591c133344 docs(readme): Ionic -> Ionic Framework 2020-07-20 11:55:59 -04:00
Christian Lüdemann
d297ecb87a fix(virtual-scroll): properly calculate top offset when nested (#21581) 2020-07-20 10:29:39 -04:00
Liam DeBeasi
a625c837a6 feat(input, textarea): expose native events for ionBlur and ionFocus (#21777)
resolves #17363
2020-07-17 17:43:17 -04:00
Liam DeBeasi
77464ef21a feat(router): add navigation hooks (#21709) 2020-07-17 11:08:16 -04:00
Liam DeBeasi
fa93dffdb4 feat(input): accept datetime-local, month, and week type values (#21758)
resolves #21757
2020-07-17 10:46:07 -04:00
Brandy Carney
6f200ac751 chore(release): update repository name (#21699) 2020-07-14 17:01:08 -04:00
Simon Hänisch
7c2d0c981a feat(select): add optional generic typings (#21514)
resolves https://github.com/ionic-team/ionic-framework/issues/20220
2020-07-14 16:06:48 -04:00
Ely Lucas
1351b2eafc fix(react): fix regression on IonTabsContext not working properly (#21751) 2020-07-14 13:26:04 -06:00
Liam DeBeasi
88f1828bd8 fix(segment-button): allow min-width to be overridden (#21722)
fixes #21105
2020-07-14 10:58:40 -04:00
Liam DeBeasi
020f3cc56c fix(keyboard): keyboard events now consistently fire on android (#21741)
fixes #21734
2020-07-14 10:43:58 -04:00
Liam DeBeasi
3cbf9e7c4c fix(ios): improve scroll assist reliability on password inputs (#21703)
fixes #21688
2020-07-10 09:41:43 -04:00
Liam DeBeasi
2664587749 fix(angular): per-page animations now work with swipe to go back (#21706)
resolves #21692
2020-07-10 09:38:58 -04:00
Liam DeBeasi
f00ad8a835 fix(datetime): remove unneeded combox role (#21708)
resolves https://github.com/ionic-team/ionic-framework/issues/21667
2020-07-10 09:37:39 -04:00
Ely Lucas
81ef3f1ecd fix(react): fix regression with history.replace in new react router (#21698) 2020-07-08 10:22:08 -06:00
Ely Lucas
c171ccbd37 feat(react): React Router Enhancements (#21693) 2020-07-07 12:02:05 -05:00
Ely Lucas
a0735b97bf fix(build): Fixes to dev build for react (#21684)
* fix(react): removing lock files in react to get npm llink to work

* fix(scripts) updating ionic/react dep in react-router

* fix(scripts): linking ionic/react in router

* removing log
2020-07-06 21:11:45 -05:00
Adam Bradley
b4423a816f chore(deps): update stencil and rollup (#21680) 2020-07-06 17:37:10 +02:00
Liam DeBeasi
ff23e4f267 merge release-5.2.3
5.2.3
2020-07-02 09:11:27 -04:00
Liam DeBeasi
1dcd9de50a fix(input): clear button can now be tabbed to (#21633)
fixes https://github.com/ionic-team/ionic/issues/21549
2020-07-01 10:13:12 -04:00
Adam Bradley
c458523b0d chore(stencil): update to stencil 1.15.0 (#21653) 2020-06-30 11:34:16 -05:00
Liam DeBeasi
a5e4669c4b feat(segment-button, toast): add additional parts docs (#21532) 2020-06-30 10:31:54 -05:00
338 changed files with 29415 additions and 20999 deletions

View File

@@ -209,6 +209,13 @@ function prepareDevPackage(tasks, package, version) {
title: `${pkg.name}: npm link @ionic/core`,
task: () => execa('npm', ['link', '@ionic/core'], { cwd: projectRoot })
});
if (package === 'packages/react-router') {
projectTasks.push({
title: `${pkg.name}: npm link @ionic/react`,
task: () => execa('npm', ['link', '@ionic/react'], { cwd: projectRoot })
});
}
}
projectTasks.push({

View File

@@ -78,6 +78,9 @@ async function setPackageVersionChanges(packages, version) {
if (package !== 'core') {
const pkg = common.readPkg(package);
common.updateDependency(pkg, '@ionic/core', version);
if(package === 'packages/react-router') {
common.updateDependency(pkg, '@ionic/react', version);
}
common.writePkg(package, pkg);
}
const projectRoot = common.projectPath(package);

View File

@@ -132,7 +132,7 @@ async function publishGithub(version, gitTag, changelog, npmTag) {
await octokit.repos.createRelease({
owner: 'ionic-team',
repo: 'ionic',
repo: 'ionic-framework',
target_commitish: branch,
tag_name: gitTag,
name: version,

View File

@@ -1,3 +1,42 @@
## [5.3.1](https://github.com/ionic-team/ionic/compare/v5.3.0...v5.3.1) (2020-07-27)
### Bug Fixes
* **react:** properly extend HTMLElement for tabs ([bfddb17](https://github.com/ionic-team/ionic/commit/bfddb170659224d0f826762744dfe44a85813d36)), closes [#21803](https://github.com/ionic-team/ionic/issues/21803)
# [5.3.0 Phosphorus](https://github.com/ionic-team/ionic/compare/v5.2.3...v5.3.0) (2020-07-23)
### Bug Fixes
* **angular:** per-page animations now work with swipe to go back ([#21706](https://github.com/ionic-team/ionic/issues/21706)) ([2664587](https://github.com/ionic-team/ionic/commit/2664587749e45100a04f70796733de162b26cdf7)), closes [#21692](https://github.com/ionic-team/ionic/issues/21692)
* **datetime:** remove unneeded combobox role ([#21708](https://github.com/ionic-team/ionic/issues/21708)) ([f00ad8a](https://github.com/ionic-team/ionic/commit/f00ad8a8357ccd7fe85631dad0c841f2d4c72487))
* **input:** clear button can now be tabbed to ([#21633](https://github.com/ionic-team/ionic/issues/21633)) ([1dcd9de](https://github.com/ionic-team/ionic/commit/1dcd9de50ae16bfa102e98120a022de5b0287baa))
* **ios:** improve scroll assist reliability on password inputs ([#21703](https://github.com/ionic-team/ionic/issues/21703)) ([3cbf9e7](https://github.com/ionic-team/ionic/commit/3cbf9e7c4c225d6b02237d8ea8f16fb924ba360e)), closes [#21688](https://github.com/ionic-team/ionic/issues/21688)
* **keyboard:** keyboard events now consistently fire on android ([#21741](https://github.com/ionic-team/ionic/issues/21741)) ([020f3cc](https://github.com/ionic-team/ionic/commit/020f3cc56cb6dac23dd8766a3802422500b510e2)), closes [#21734](https://github.com/ionic-team/ionic/issues/21734)
* **nav:** insertPages method correctly inserts multiple pages with props ([#21725](https://github.com/ionic-team/ionic/issues/21725)) ([eb592b8](https://github.com/ionic-team/ionic/commit/eb592b8917b8a7412d8c346f41b47d3b79002b95)), closes [#21724](https://github.com/ionic-team/ionic/issues/21724)
* **overlays:** trap focus inside overlay components except toast ([#21716](https://github.com/ionic-team/ionic/issues/21716)) ([fff4aec](https://github.com/ionic-team/ionic/commit/fff4aec6cfbd48566594a05f4af57dd0578977a8)), closes [#21647](https://github.com/ionic-team/ionic/issues/21647)
* **segment-button:** allow min-width to be overridden ([#21722](https://github.com/ionic-team/ionic/issues/21722)) ([88f1828](https://github.com/ionic-team/ionic/commit/88f1828bd8f6b9a1c1f3dcb220d93067bed7f404)), closes [#21105](https://github.com/ionic-team/ionic/issues/21105)
* **title:** allow overriding of large title transform-origin ([#21770](https://github.com/ionic-team/ionic/issues/21770)) ([dbe6853](https://github.com/ionic-team/ionic/commit/dbe6853884bd76c3d8e229cd58e1571d9b3a7249)), closes [#21761](https://github.com/ionic-team/ionic/issues/21761)
* **virtual-scroll:** properly calculate top offset when nested ([#21581](https://github.com/ionic-team/ionic/issues/21581)) ([d297ecb](https://github.com/ionic-team/ionic/commit/d297ecb87ad3e1c8f0988f0571a475081ce368f8))
### Features
* **card:** expose global card css variables ([#21756](https://github.com/ionic-team/ionic/issues/21756)) ([096eef4](https://github.com/ionic-team/ionic/commit/096eef4a79c2d05c37eb224466c6d7d512d2be20)), closes [#21694](https://github.com/ionic-team/ionic/issues/21694)
* **input:** accept datetime-local, month, and week type values ([#21758](https://github.com/ionic-team/ionic/issues/21758)) ([fa93dff](https://github.com/ionic-team/ionic/commit/fa93dffdb4f350e8db8acc7f06b06761974eea8e)), closes [#21757](https://github.com/ionic-team/ionic/issues/21757)
* **input, textarea:** expose native events for ionBlur and ionFocus ([#21777](https://github.com/ionic-team/ionic/issues/21777)) ([a625c83](https://github.com/ionic-team/ionic/commit/a625c837a60abc07ad71c696196a89f1a25a4c27)), closes [#17363](https://github.com/ionic-team/ionic/issues/17363)
* **react:** add custom history to IonReactRouter ([#21775](https://github.com/ionic-team/ionic/issues/21775)) ([d4a5fbd](https://github.com/ionic-team/ionic/commit/d4a5fbd955e8ecccba8b77491943d81fdf5a5ef4)), closes [#20297](https://github.com/ionic-team/ionic/issues/20297)
* **react:** add new react router ([#21693](https://github.com/ionic-team/ionic/issues/21693)) ([c171ccb](https://github.com/ionic-team/ionic/commit/c171ccbd37c1ee4b4934758a3a759170ff357cb2))
* **router:** add navigation hooks ([#21709](https://github.com/ionic-team/ionic/issues/21709)) ([77464ef](https://github.com/ionic-team/ionic/commit/77464ef21aaaa5afa7a02e5417f3ec295b240601))
* **segment-button, toast:** expose additional shadow parts ([#21532](https://github.com/ionic-team/ionic/issues/21532)) ([a5e4669](https://github.com/ionic-team/ionic/commit/a5e4669c4bcbcb2cdd605ed17c35e42438bd5596))
* **select:** add optional generic typings ([#21514](https://github.com/ionic-team/ionic/issues/21514)) ([7c2d0c9](https://github.com/ionic-team/ionic/commit/7c2d0c981ab91930478c4b76220ce4ec4ed4e471))
## [5.2.3](https://github.com/ionic-team/ionic/compare/v5.2.2...v5.2.3) (2020-07-01)

View File

@@ -1,9 +1,9 @@
# Ionic
# Ionic Framework
[Ionic](https://ionicframework.com/) is the open-source mobile app development framework that makes it easy to
[Ionic Framework](https://ionicframework.com/) is the open-source mobile app development framework that makes it easy to
build top quality native and progressive web apps with web technologies.
Ionic is based on [Web Components](https://www.webcomponents.org/introduction) and comes with many significant performance, usability, and feature improvements over the past versions.
Ionic Framework is based on [Web Components](https://www.webcomponents.org/introduction) and comes with many significant performance, usability, and feature improvements over the past versions.
### Packages
@@ -42,7 +42,7 @@ It is the perfect starting point for learning and building your own app.
### Future Goals
As Ionic components migrate to the web component standard, a goal of ours is to have Ionic components easily work within all of the popular frameworks.
As Ionic Framework components migrate to the web component standard, a goal of ours is to have Ionic Framework easily work within all of the popular frameworks.
### Earlier Versions

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "5.2.3",
"version": "5.3.1",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -42,7 +42,7 @@
"validate": "npm i && npm run lint && npm run test && npm run build"
},
"dependencies": {
"@ionic/core": "5.2.3",
"@ionic/core": "5.3.1",
"tslib": "^1.9.3"
},
"peerDependencies": {

View File

@@ -127,7 +127,7 @@ export class StackController {
* Save any custom animation so that navigating
* back will use this custom animation by default.
*/
if (animationBuilder !== undefined && leavingView) {
if (leavingView) {
leavingView.animationBuilder = animationBuilder;
}
@@ -190,13 +190,16 @@ export class StackController {
if (leavingView) {
const views = this.getStack(leavingView.stackId);
const enteringView = views[views.length - 2];
const customAnimation = enteringView.animationBuilder;
return this.wait(() => {
return this.transition(
enteringView, // entering view
leavingView, // leaving view
'back',
this.canGoBack(2),
true
true,
customAnimation
);
});
}

View File

@@ -46,4 +46,4 @@ export { IonicModule } from './ionic-module';
export { IonicSafeString, getPlatforms, isPlatform, createAnimation } from '@ionic/core';
// CORE TYPES
export { Animation, AnimationBuilder, AnimationCallbackOptions, AnimationDirection, AnimationFill, AnimationKeyFrames, AnimationLifecycle, Gesture, GestureConfig, GestureDetail, mdTransitionAnimation, iosTransitionAnimation } from '@ionic/core';
export { Animation, AnimationBuilder, AnimationCallbackOptions, AnimationDirection, AnimationFill, AnimationKeyFrames, AnimationLifecycle, Gesture, GestureConfig, GestureDetail, mdTransitionAnimation, iosTransitionAnimation, NavComponentWithProps } from '@ionic/core';

View File

@@ -475,13 +475,13 @@ ion-input,prop,required,boolean,false,false,false
ion-input,prop,size,number | undefined,undefined,false,false
ion-input,prop,spellcheck,boolean,false,false,false
ion-input,prop,step,string | undefined,undefined,false,false
ion-input,prop,type,"date" | "email" | "number" | "password" | "search" | "tel" | "text" | "time" | "url",'text',false,false
ion-input,prop,type,"date" | "datetime-local" | "email" | "month" | "number" | "password" | "search" | "tel" | "text" | "time" | "url" | "week",'text',false,false
ion-input,prop,value,null | number | string | undefined,'',false,false
ion-input,method,getInputElement,getInputElement() => Promise<HTMLInputElement>
ion-input,method,setFocus,setFocus() => Promise<void>
ion-input,event,ionBlur,void,true
ion-input,event,ionBlur,FocusEvent,true
ion-input,event,ionChange,InputChangeEventDetail,true
ion-input,event,ionFocus,void,true
ion-input,event,ionFocus,FocusEvent,true
ion-input,event,ionInput,KeyboardEvent,true
ion-input,css-prop,--background
ion-input,css-prop,--color
@@ -745,13 +745,13 @@ ion-nav,method,getActive,getActive() => Promise<ViewController | undefined>
ion-nav,method,getByIndex,getByIndex(index: number) => Promise<ViewController | undefined>
ion-nav,method,getPrevious,getPrevious(view?: ViewController | undefined) => Promise<ViewController | undefined>
ion-nav,method,insert,insert<T extends NavComponent>(insertIndex: number, component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,insertPages,insertPages(insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,insertPages,insertPages(insertIndex: number, insertComponents: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,pop,pop(opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,popTo,popTo(indexOrViewCtrl: number | ViewController, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,popToRoot,popToRoot(opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,push,push<T extends NavComponent>(component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,removeIndex,removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,setPages,setPages(views: any[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,setPages,setPages(views: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,method,setRoot,setRoot<T extends NavComponent>(component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
ion-nav,event,ionNavDidChange,void,false
ion-nav,event,ionNavWillChange,void,false
@@ -933,6 +933,8 @@ ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false
ion-ripple-effect,method,addRipple,addRipple(x: number, y: number) => Promise<() => void>
ion-route,none
ion-route,prop,beforeEnter,(() => boolean | NavigationHookOptions | Promise<NavigationHookResult>) | undefined,undefined,false,false
ion-route,prop,beforeLeave,(() => boolean | NavigationHookOptions | Promise<NavigationHookResult>) | undefined,undefined,false,false
ion-route,prop,component,string,undefined,true,false
ion-route,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false
ion-route,prop,url,string,'',false,false
@@ -1051,6 +1053,7 @@ ion-segment-button,css-prop,--padding-start
ion-segment-button,css-prop,--padding-top
ion-segment-button,css-prop,--transition
ion-segment-button,part,indicator
ion-segment-button,part,indicator-background
ion-segment-button,part,native
ion-select,shadow
@@ -1069,7 +1072,7 @@ ion-select,prop,value,any,undefined,false,false
ion-select,method,open,open(event?: UIEvent | undefined) => Promise<any>
ion-select,event,ionBlur,void,true
ion-select,event,ionCancel,void,true
ion-select,event,ionChange,SelectChangeEventDetail,true
ion-select,event,ionChange,SelectChangeEventDetail<any>,true
ion-select,event,ionFocus,void,true
ion-select,css-prop,--padding-bottom
ion-select,css-prop,--padding-end
@@ -1226,9 +1229,9 @@ ion-textarea,prop,value,null | string | undefined,'',false,false
ion-textarea,prop,wrap,"hard" | "off" | "soft" | undefined,undefined,false,false
ion-textarea,method,getInputElement,getInputElement() => Promise<HTMLTextAreaElement>
ion-textarea,method,setFocus,setFocus() => Promise<void>
ion-textarea,event,ionBlur,void,true
ion-textarea,event,ionBlur,FocusEvent,true
ion-textarea,event,ionChange,TextareaChangeEventDetail,true
ion-textarea,event,ionFocus,void,true
ion-textarea,event,ionFocus,FocusEvent,true
ion-textarea,event,ionInput,KeyboardEvent,true
ion-textarea,css-prop,--background
ion-textarea,css-prop,--border-radius
@@ -1290,6 +1293,10 @@ ion-toast,css-prop,--min-width
ion-toast,css-prop,--start
ion-toast,css-prop,--white-space
ion-toast,css-prop,--width
ion-toast,part,button
ion-toast,part,container
ion-toast,part,header
ion-toast,part,message
ion-toggle,shadow
ion-toggle,prop,checked,boolean,false,false,false

360
core/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "5.2.3",
"version": "5.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1310,18 +1310,18 @@
"dev": true
},
"@rollup/plugin-node-resolve": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.1.0.tgz",
"integrity": "sha512-ovq7ZM3JJYUUmEjjO+H8tnUdmQmdQudJB7xruX8LFZ1W2q8jXdPUS6SsIYip8ByOApu4RR7729Am9WhCeCMiHA==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.4.0.tgz",
"integrity": "sha512-LFqKdRLn0ShtQyf6SBYO69bGE1upV6wUhBX0vFOUnLAyzx5cwp8svA0eHUnu8+YU57XOkrMtfG63QOpQx25pHQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.8",
"@types/resolve": "0.0.8",
"@rollup/pluginutils": "^3.1.0",
"@types/resolve": "1.17.1",
"builtin-modules": "^3.1.0",
"deep-freeze": "^0.0.1",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.14.2"
"resolve": "^1.17.0"
}
},
"@rollup/plugin-virtual": {
@@ -1339,14 +1339,6 @@
"@types/estree": "0.0.39",
"estree-walker": "^1.0.1",
"picomatch": "^2.2.2"
},
"dependencies": {
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
}
}
},
"@samverschueren/stream-to-observable": {
@@ -1391,12 +1383,12 @@
}
},
"@stencil/core": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.14.0.tgz",
"integrity": "sha512-3u3O8y8HIORiKuDmPowJiUu9nNmpxq4ak1kR3aVuWH6YTQ7+Uexy0dAxabn3CUoQmh6cpYsk9l5P5yIX2EQHoQ==",
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.17.1.tgz",
"integrity": "sha512-1OsRAMP9wo79mmZc4kz2DGnN/hqXLjdrTNGzqXd8K8K/6Mdua3Te+Zb3gmKMGP7ZaIzqIHbWDvCD2XZ4Sb0dFw==",
"dev": true,
"requires": {
"typescript": "3.9.3"
"typescript": "3.9.7"
}
},
"@stencil/sass": {
@@ -1519,9 +1511,9 @@
}
},
"@types/jest": {
"version": "26.0.3",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.3.tgz",
"integrity": "sha512-v89ga1clpVL/Y1+YI0eIu1VMW+KU7Xl8PhylVtDKVWaSUHBHYPLXMQGBdrpHewaKoTvlXkksbYqPgz8b4cmRZg==",
"version": "26.0.7",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.7.tgz",
"integrity": "sha512-+x0077/LoN6MjqBcVOe1y9dpryWnfDZ+Xfo3EqGeBcfPRJlQp3Lw62RvNlWxuGv7kOEwlHriAa54updi3Jvvwg==",
"dev": true,
"requires": {
"jest-diff": "^25.2.1",
@@ -1535,9 +1527,9 @@
"dev": true
},
"@types/node": {
"version": "14.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz",
"integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==",
"version": "14.0.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.25.tgz",
"integrity": "sha512-okMqUHqrMlGOxfDZliX1yFX5MV6qcd5PpRz96XYtjkM0Ws/hwg23FMUqt6pETrVRZS+EKUB5HY19mmo54EuQbA==",
"dev": true
},
"@types/normalize-package-data": {
@@ -1553,18 +1545,18 @@
"dev": true
},
"@types/puppeteer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-3.0.0.tgz",
"integrity": "sha512-59+fkfHHXHzX5rgoXIMnZyzum7ZLx/Wc3fhsOduFThpTpKbzzdBHMZsrkKGLunimB4Ds/tI5lXTRLALK8Mmnhg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-3.0.1.tgz",
"integrity": "sha512-t03eNKCvWJXhQ8wkc5C6GYuSqMEdKLOX0GLMGtks25YZr38wKZlKTwGM/BoAPVtdysX7Bb9tdwrDS1+NrW3RRA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -1623,6 +1615,16 @@
"integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==",
"dev": true
},
"@types/yauzl": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz",
"integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==",
"dev": true,
"optional": true,
"requires": {
"@types/node": "*"
}
},
"abab": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
@@ -1652,13 +1654,10 @@
"dev": true
},
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"dev": true,
"requires": {
"es6-promisify": "^5.0.0"
}
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
"dev": true
},
"ajv": {
"version": "6.12.2",
@@ -1811,12 +1810,6 @@
"integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
"dev": true
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1859,9 +1852,9 @@
}
},
"aws-sdk": {
"version": "2.705.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.705.0.tgz",
"integrity": "sha512-eD3YD3UgCdFIYlkAVYW1rbriKSNHm7nsZbYsBuo91pbyq4XYT56w2oTyNECorYPoq//Y4LQTTx8cbOuEjsNi3w==",
"version": "2.719.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.719.0.tgz",
"integrity": "sha512-vqVAeZ2C8VLvL1hJIBCRFnKMomIoWSIUeYjULRiwRlBYH95EgcDY559Mq2rkx+E0733jdf3wNHA+eRVmwHyXvQ==",
"dev": true,
"requires": {
"buffer": "4.9.2",
@@ -2104,11 +2097,34 @@
}
},
"binary-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
"dev": true
},
"bl": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
"integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"dev": true,
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
}
}
},
"boxen": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-3.2.0.tgz",
@@ -2421,9 +2437,9 @@
"dev": true
},
"chokidar": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
"integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz",
"integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
@@ -2471,6 +2487,12 @@
}
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@@ -2733,18 +2755,6 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"configstore": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-4.0.0.tgz",
@@ -3040,6 +3050,12 @@
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true
},
"devtools-protocol": {
"version": "0.0.781568",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.781568.tgz",
"integrity": "sha512-9Uqnzy6m6zEStluH9iyJ3iHyaQziFnMnLeC8vK0eN6smiJmIx7+yB64d67C2lH/LZra+5cGscJAJsNXO+MdPMg==",
"dev": true
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -3151,9 +3167,9 @@
}
},
"domino": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.5.tgz",
"integrity": "sha512-vMDo7f6ogUV9PkzmxXLiXzJkJZqU09Le4C40mj+HmAGS/2FPmdetoNOQZXpu2kekn0GJKvtwKMAVoruTj60Xww==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
"integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==",
"dev": true
},
"domutils": {
@@ -3233,21 +3249,6 @@
"is-arrayish": "^0.2.1"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"dev": true
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"requires": {
"es6-promise": "^4.0.3"
}
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -3279,6 +3280,12 @@
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
},
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -3550,15 +3557,41 @@
}
},
"extract-zip": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
"integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"requires": {
"concat-stream": "^1.6.2",
"debug": "^2.6.9",
"mkdirp": "^0.5.4",
"@types/yauzl": "^2.9.1",
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"extsprintf": {
@@ -3759,6 +3792,12 @@
"map-cache": "^0.2.2"
}
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
@@ -4119,19 +4158,19 @@
}
},
"https-proxy-agent": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
"integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
"dev": true,
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
"agent-base": "5",
"debug": "4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
@@ -8023,6 +8062,12 @@
"minimist": "^1.2.5"
}
},
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -9176,12 +9221,6 @@
}
}
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -9233,19 +9272,23 @@
"dev": true
},
"puppeteer": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.20.0.tgz",
"integrity": "sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.2.1.tgz",
"integrity": "sha512-PZoZG7u+T6N1GFWBQmGVG162Ak5MAy8nYSVpeeQrwJK2oYUlDWpHEJPcd/zopyuEMTv7DiztS1blgny1txR2qw==",
"dev": true,
"requires": {
"debug": "^4.1.0",
"extract-zip": "^1.6.6",
"https-proxy-agent": "^2.2.1",
"devtools-protocol": "0.0.781568",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"mime": "^2.0.3",
"pkg-dir": "^4.2.0",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^2.6.1",
"ws": "^6.1.0"
"rimraf": "^3.0.2",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"ws": "^7.2.3"
},
"dependencies": {
"debug": {
@@ -9263,13 +9306,13 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"
"glob": "^7.1.3"
}
}
}
@@ -9373,18 +9416,14 @@
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"readdirp": {
@@ -9683,9 +9722,9 @@
}
},
"rollup": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.18.1.tgz",
"integrity": "sha512-w4X77ADA+WTGlapC8Z6yggdJtODw3SBl6R2LSkA7ZW5MtdkgcB7sfaSD1UWyx8diXbMcGIb0eI9gCx/dyqOgNQ==",
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.23.0.tgz",
"integrity": "sha512-vLNmZFUGVwrnqNAJ/BvuLk1MtWzu4IuoqsH9UWK5AIdO3rt8/CSiJNvPvCIvfzrbNsqKbNzPAG1V2O4eTe2XZg==",
"dev": true,
"requires": {
"fsevents": "~2.1.2"
@@ -9772,9 +9811,9 @@
}
},
"sass": {
"version": "1.26.9",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.9.tgz",
"integrity": "sha512-t8AkRVi+xvba4yZiLWkJdgJHBFCB3Dh4johniQkPy9ywkgFHNasXFEFP+RG/F6LhQ+aoE4aX+IorIWQjS0esVw==",
"version": "1.26.10",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz",
"integrity": "sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw==",
"dev": true,
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
@@ -10597,6 +10636,31 @@
}
}
},
"tar-fs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz",
"integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"tar-stream": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz",
"integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==",
"dev": true,
"requires": {
"bl": "^4.0.1",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
}
},
"term-size": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
@@ -10975,12 +11039,6 @@
"integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
"dev": true
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -10991,11 +11049,33 @@
}
},
"typescript": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz",
"integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==",
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
},
"unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dev": true,
"requires": {
"buffer": "^5.2.1",
"through": "^2.3.8"
},
"dependencies": {
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"dev": true,
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
}
}
},
"unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "5.2.3",
"version": "5.3.1",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -34,25 +34,25 @@
"tslib": "^1.10.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^8.1.0",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/core": "1.14.0",
"@stencil/core": "1.17.1",
"@stencil/sass": "1.3.2",
"@types/jest": "26.0.3",
"@types/node": "14.0.14",
"@types/puppeteer": "3.0.0",
"@types/jest": "26.0.7",
"@types/node": "14.0.25",
"@types/puppeteer": "3.0.1",
"@types/swiper": "5.3.1",
"aws-sdk": "^2.705.0",
"aws-sdk": "^2.719.0",
"clean-css-cli": "^4.1.11",
"domino": "^2.1.3",
"domino": "^2.1.6",
"fs-extra": "^9.0.1",
"jest": "26.1.0",
"jest-cli": "26.1.0",
"np": "^5.0.3",
"pixelmatch": "4.0.2",
"puppeteer": "1.20.0",
"rollup": "^2.18.1",
"sass": "^1.26.9",
"puppeteer": "5.2.1",
"rollup": "^2.23.0",
"sass": "^1.26.10",
"stylelint": "10.1.0",
"stylelint-order": "3.0.1",
"swiper": "5.4.1",

View File

@@ -5,8 +5,9 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimeOptions, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, ViewController } from "./interface";
import { ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimeOptions, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, ViewController } from "./interface";
import { IonicSafeString } from "./utils/sanitization";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SelectCompareFn } from "./components/select/select-interface";
export namespace Components {
interface IonActionSheet {
@@ -1412,7 +1413,7 @@ export namespace Components {
* @param opts The navigation options.
* @param done The transition complete function.
*/
"insertPages": (insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
"insertPages": (insertIndex: number, insertComponents: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
/**
* Pop a component off of the navigation stack. Navigates back from the current component.
* @param opts The navigation options.
@@ -1462,7 +1463,7 @@ export namespace Components {
* @param opts The navigation options.
* @param done The transition complete function.
*/
"setPages": (views: any[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
"setPages": (views: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
/**
* Set the root for the current navigation stack to a component.
* @param component The component to set as the root of the navigation stack.
@@ -1849,6 +1850,14 @@ export namespace Components {
"type": 'bounded' | 'unbounded';
}
interface IonRoute {
/**
* A navigation hook that is fired when the route tries to enter. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
*/
"beforeEnter"?: NavigationHookCallback;
/**
* A navigation hook that is fired when the route tries to leave. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
*/
"beforeLeave"?: NavigationHookCallback;
/**
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
*/
@@ -1877,6 +1886,7 @@ export namespace Components {
* Go back to previous page in the window.history.
*/
"back": () => Promise<void>;
"canTransition": () => Promise<string | boolean>;
"navChanged": (direction: RouterDirection) => Promise<boolean>;
"printDebug": () => Promise<void>;
/**
@@ -4215,7 +4225,7 @@ declare namespace LocalJSX {
/**
* Emitted when the input loses focus.
*/
"onIonBlur"?: (event: CustomEvent<void>) => void;
"onIonBlur"?: (event: CustomEvent<FocusEvent>) => void;
/**
* Emitted when the value has changed.
*/
@@ -4223,7 +4233,7 @@ declare namespace LocalJSX {
/**
* Emitted when the input has focus.
*/
"onIonFocus"?: (event: CustomEvent<void>) => void;
"onIonFocus"?: (event: CustomEvent<FocusEvent>) => void;
/**
* Emitted when a keyboard input occurred.
*/
@@ -5095,6 +5105,14 @@ declare namespace LocalJSX {
"type"?: 'bounded' | 'unbounded';
}
interface IonRoute {
/**
* A navigation hook that is fired when the route tries to enter. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
*/
"beforeEnter"?: NavigationHookCallback;
/**
* A navigation hook that is fired when the route tries to leave. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
*/
"beforeLeave"?: NavigationHookCallback;
/**
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
*/
@@ -5731,7 +5749,7 @@ declare namespace LocalJSX {
/**
* Emitted when the input loses focus.
*/
"onIonBlur"?: (event: CustomEvent<void>) => void;
"onIonBlur"?: (event: CustomEvent<FocusEvent>) => void;
/**
* Emitted when the input value has changed.
*/
@@ -5739,7 +5757,7 @@ declare namespace LocalJSX {
/**
* Emitted when the input has focus.
*/
"onIonFocus"?: (event: CustomEvent<void>) => void;
"onIonFocus"?: (event: CustomEvent<FocusEvent>) => void;
/**
* Emitted when a keyboard input occurred.
*/

View File

@@ -26,6 +26,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
export class ActionSheet implements ComponentInterface, OverlayInterface {
presented = false;
lastFocus?: HTMLElement;
animation?: any;
private wrapperEl?: HTMLElement;
private groupEl?: HTMLElement;
@@ -117,7 +118,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
return present(this, 'actionSheetEnter', iosEnterAnimation, mdEnterAnimation);
}
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}
@@ -250,7 +251,10 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
onIonBackdropTap={this.onBackdropTap}
>
<ion-backdrop tappable={this.backdropDismiss}/>
<div class="action-sheet-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
<div tabindex="0"></div>
<div class="action-sheet-wrapper ion-overlay-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
<div class="action-sheet-container">
<div class="action-sheet-group" ref={el => this.groupEl = el}>
{this.header !== undefined &&
@@ -292,6 +296,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
}
</div>
</div>
<div tabindex="0"></div>
</Host>
);
}

View File

@@ -1,6 +1,40 @@
import { testActionSheet, testActionSheetAlert, testActionSheetBackdrop } from '../test.utils';
import { newE2EPage } from '@stencil/core/testing';
const DIRECTORY = 'basic';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.textContent, activeElement);
}
test('action-sheet: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/action-sheet/test/basic?ionic:_testing=true' });
await page.click('#basic');
await page.waitForSelector('#basic');
let actionSheet = await page.find('ion-action-sheet');
expect(actionSheet).not.toBe(null);
await actionSheet.waitForVisible();
await page.keyboard.press('Tab');
const activeElementText = await getActiveElementText(page);
expect(activeElementText).toEqual('Delete');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
const activeElementTextTwo = await getActiveElementText(page);
expect(activeElementTextTwo).toEqual('Cancel');
await page.keyboard.press('Tab');
const activeElementTextThree = await getActiveElementText(page);
expect(activeElementTextThree).toEqual('Delete');
});
test('action-sheet: basic', async () => {
await testActionSheet(DIRECTORY, '#basic');

View File

@@ -34,6 +34,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
private gesture?: Gesture;
presented = false;
lastFocus?: HTMLElement;
@Element() el!: HTMLIonAlertElement;
@@ -167,7 +168,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
}) as AlertInput);
}
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}
@@ -514,7 +515,9 @@ export class Alert implements ComponentInterface, OverlayInterface {
<ion-backdrop tappable={this.backdropDismiss}/>
<div class="alert-wrapper" ref={el => this.wrapperEl = el}>
<div tabindex="0"></div>
<div class="alert-wrapper ion-overlay-wrapper" ref={el => this.wrapperEl = el}>
<div class="alert-head">
{header && <h2 id={hdrId} class="alert-title">{header}</h2>}
@@ -527,6 +530,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
{this.renderAlertButtons()}
</div>
<div tabindex="0"></div>
</Host>
);
}

View File

@@ -1,6 +1,40 @@
import { testAlert } from '../test.utils';
import { newE2EPage } from '@stencil/core/testing';
const DIRECTORY = 'basic';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.textContent, activeElement);
}
test('alert: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/alert/test/basic?ionic:_testing=true' });
await page.click('#multipleButtons');
await page.waitForSelector('#multipleButtons');
let alert = await page.find('ion-alert');
expect(alert).not.toBe(null);
await alert.waitForVisible();
await page.keyboard.press('Tab');
const activeElementText = await getActiveElementText(page);
expect(activeElementText).toEqual('Open Modal');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
const activeElementTextTwo = await getActiveElementText(page);
expect(activeElementTextTwo).toEqual('Cancel');
await page.keyboard.press('Tab');
const activeElementTextThree = await getActiveElementText(page);
expect(activeElementTextThree).toEqual('Open Modal');
});
test(`alert: basic`, async () => {
await testAlert(DIRECTORY, '#basic');

View File

@@ -5,8 +5,8 @@
// --------------------------------------------------
:host {
--background: #{$item-ios-background};
--color: #{$card-ios-text-color};
--background: #{$card-ios-background};
--color: #{$card-ios-color};
@include margin($card-ios-margin-top, $card-ios-margin-end, $card-ios-margin-bottom, $card-ios-margin-start);
@include border-radius($card-ios-border-radius);
@@ -22,4 +22,4 @@
:host(.ion-activated) {
transform: #{$card-ios-transform-activated};
}
}

View File

@@ -15,9 +15,6 @@ $card-ios-margin-bottom: $card-ios-margin-top !default;
/// @prop - Margin start of the card
$card-ios-margin-start: 16px !default;
/// @prop - Background color of the card
$card-ios-background-color: $item-ios-background !default;
/// @prop - Box shadow color of the card
$card-ios-box-shadow-color: rgba(0, 0, 0, .12) !default;
@@ -30,11 +27,8 @@ $card-ios-border-radius: 8px !default;
/// @prop - Font size of the card
$card-ios-font-size: 14px !default;
/// @prop - Color of the card text
$card-ios-text-color: $text-color-step-400 !default;
/// @prop - Transition timing function of the card
$card-ios-transition-timing-function: cubic-bezier(0.12, 0.72, 0.29, 1) !default;
/// @prop - Transform of the card on activate
$card-ios-transform-activated: scale3d(.97, .97, 1) !default;
$card-ios-transform-activated: scale3d(.97, .97, 1) !default;

View File

@@ -5,8 +5,8 @@
// --------------------------------------------------
:host {
--background: #{$item-md-background};
--color: #{$card-md-text-color};
--background: #{$card-md-background};
--color: #{$card-md-color};
@include margin($card-md-margin-top, $card-md-margin-end, $card-md-margin-bottom, $card-md-margin-start);
@include border-radius($card-md-border-radius);

View File

@@ -15,9 +15,6 @@ $card-md-margin-bottom: 10px !default;
/// @prop - Margin start of the card
$card-md-margin-start: 10px !default;
/// @prop - Background color of the card
$card-md-background-color: $item-md-background !default;
/// @prop - Box shadow of the card
$card-md-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12) !default;
@@ -29,6 +26,3 @@ $card-md-font-size: 14px !default;
/// @prop - Line height of the card
$card-md-line-height: 1.5 !default;
/// @prop - Color of the card text
$card-md-text-color: $text-color-step-450 !default;

View File

@@ -632,7 +632,6 @@ export class Datetime implements ComponentInterface {
return (
<Host
onClick={this.onClick}
role="combobox"
aria-disabled={disabled ? 'true' : null}
aria-expanded={`${isExpanded}`}
aria-haspopup="true"

View File

@@ -1,5 +1,42 @@
import { newE2EPage } from '@stencil/core/testing';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.textContent, activeElement);
}
test('datetime/picker: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/datetime/test/basic?ionic:_testing=true' });
await page.click('#datetime-part');
await page.waitForSelector('#datetime-part');
let datetime = await page.find('ion-datetime');
expect(datetime).not.toBe(null);
await datetime.waitForVisible();
// TODO fix
await page.waitFor(100);
await page.keyboard.press('Tab');
const activeElementText = await getActiveElementText(page);
expect(activeElementText).toEqual('Cancel');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
const activeElementTextTwo = await getActiveElementText(page);
expect(activeElementTextTwo).toEqual('1920');
await page.keyboard.press('Tab');
const activeElementTextThree = await getActiveElementText(page);
expect(activeElementTextThree).toEqual('Cancel');
});
test('datetime: basic', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/basic?ionic:_testing=true'

View File

@@ -150,7 +150,6 @@ export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, trans
const titleDiv = toolbar.innerTitleEl;
if (!ionTitle || ionTitle.size !== 'large') { return; }
titleDiv.style.transformOrigin = 'left center';
titleDiv.style.transition = (transition) ? TRANSITION : '';
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
});

View File

@@ -147,7 +147,11 @@
appearance: none;
}
:host(.has-focus.has-value) .input-clear-icon {
.input-clear-icon:focus {
opacity: 0.5;
}
:host(.has-value) .input-clear-icon {
visibility: visible;
}

View File

@@ -201,12 +201,12 @@ export class Input implements ComponentInterface {
/**
* Emitted when the input loses focus.
*/
@Event() ionBlur!: EventEmitter<void>;
@Event() ionBlur!: EventEmitter<FocusEvent>;
/**
* Emitted when the input has focus.
*/
@Event() ionFocus!: EventEmitter<void>;
@Event() ionFocus!: EventEmitter<FocusEvent>;
/**
* Emitted when the styles change.
@@ -293,20 +293,20 @@ export class Input implements ComponentInterface {
this.ionInput.emit(ev as KeyboardEvent);
}
private onBlur = () => {
private onBlur = (ev: FocusEvent) => {
this.hasFocus = false;
this.focusChanged();
this.emitStyle();
this.ionBlur.emit();
this.ionBlur.emit(ev);
}
private onFocus = () => {
private onFocus = (ev: FocusEvent) => {
this.hasFocus = true;
this.focusChanged();
this.emitStyle();
this.ionFocus.emit();
this.ionFocus.emit(ev);
}
private onKeydown = (ev: KeyboardEvent) => {
@@ -323,6 +323,10 @@ export class Input implements ComponentInterface {
}
}
private clearTextOnEnter = (ev: KeyboardEvent) => {
if (ev.key === 'Enter') { this.clearTextInput(ev); }
}
private clearTextInput = (ev?: Event) => {
if (this.clearInput && !this.readonly && !this.disabled && ev) {
ev.preventDefault();
@@ -408,9 +412,9 @@ export class Input implements ComponentInterface {
aria-label="reset"
type="button"
class="input-clear-icon"
tabindex="-1"
onTouchStart={this.clearTextInput}
onMouseDown={this.clearTextInput}
onKeyDown={this.clearTextOnEnter}
/>}
</Host>
);

View File

@@ -317,7 +317,7 @@ export class InputExample {
| `size` | `size` | The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored. | `number \| undefined` | `undefined` |
| `spellcheck` | `spellcheck` | If `true`, the element will have its spelling and grammar checked. | `boolean` | `false` |
| `step` | `step` | Works with the min and max attributes to limit the increments at which a value can be set. Possible values are: `"any"` or a positive floating point number. | `string \| undefined` | `undefined` |
| `type` | `type` | The type of control to display. The default type is text. | `"date" \| "email" \| "number" \| "password" \| "search" \| "tel" \| "text" \| "time" \| "url"` | `'text'` |
| `type` | `type` | The type of control to display. The default type is text. | `"date" \| "datetime-local" \| "email" \| "month" \| "number" \| "password" \| "search" \| "tel" \| "text" \| "time" \| "url" \| "week"` | `'text'` |
| `value` | `value` | The value of the input. | `null \| number \| string \| undefined` | `''` |
@@ -325,9 +325,9 @@ export class InputExample {
| Event | Description | Type |
| ----------- | --------------------------------------- | ------------------------------------- |
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<void>` |
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<FocusEvent>` |
| `ionChange` | Emitted when the value has changed. | `CustomEvent<InputChangeEventDetail>` |
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<void>` |
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<FocusEvent>` |
| `ionInput` | Emitted when a keyboard input occurred. | `CustomEvent<KeyboardEvent>` |

View File

@@ -136,6 +136,7 @@
</ion-content>
<script>
document.querySelector('ion-input').addEventListener('ionBlur', (ev) => { console.log(ev)})
function toggleBoolean(id, prop) {
var el = document.getElementById(id);

View File

@@ -27,6 +27,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
private durationTimeout: any;
presented = false;
lastFocus?: HTMLElement;
@Element() el!: HTMLIonLoadingElement;
@@ -111,7 +112,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
*/
@Event({ eventName: 'ionLoadingDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}
@@ -194,7 +195,10 @@ export class Loading implements ComponentInterface, OverlayInterface {
}}
>
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss} />
<div class="loading-wrapper" role="dialog">
<div tabindex="0"></div>
<div class="loading-wrapper ion-overlay-wrapper" role="dialog">
{spinner && (
<div class="loading-spinner">
<ion-spinner name={spinner} aria-hidden="true" />
@@ -203,6 +207,8 @@ export class Loading implements ComponentInterface, OverlayInterface {
{message && <div class="loading-content" innerHTML={sanitizeDOMString(message)}></div>}
</div>
<div tabindex="0"></div>
</Host>
);
}

View File

@@ -71,7 +71,6 @@ export class LoadingExample {
async presentLoadingWithOptions() {
const loading = await this.loadingController.create({
cssClass: 'my-custom-class',
spinner: null,
duration: 5000,
message: 'Click the backdrop to dismiss early...',
@@ -113,7 +112,6 @@ async function presentLoading() {
async function presentLoadingWithOptions() {
const loading = document.createElement('ion-loading');
loading.cssClass = 'my-custom-class';
loading.spinner = null;
loading.duration = 5000;
loading.message = 'Click the backdrop to dismiss early...';
@@ -185,7 +183,6 @@ export class LoadingExample {
async presentLoadingWithOptions() {
const loading = await loadingController.create({
cssClass: 'my-custom-class',
spinner: null,
duration: 5000,
message: 'Click the backdrop to dismiss early...',
@@ -245,7 +242,6 @@ export default {
presentLoadingWithOptions() {
return this.$ionic.loadingController
.create({
cssClass: 'my-custom-class',
spinner: null,
duration: this.timeout,
message: 'Click the backdrop to dismiss early...',

View File

@@ -1,6 +1,40 @@
import { testLoading } from '../test.utils';
import { newE2EPage } from '@stencil/core/testing';
const DIRECTORY = 'basic';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.textContent, activeElement);
}
test('loading: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/loading/test/basic?ionic:_testing=true' });
await page.click('#html-content-loading');
await page.waitForSelector('#html-content-loading');
let loading = await page.find('ion-loading');
expect(loading).not.toBe(null);
await loading.waitForVisible();
await page.keyboard.press('Tab');
const activeElementText = await getActiveElementText(page);
expect(activeElementText).toEqual('Click impatiently to load faster');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
const activeElementTextTwo = await getActiveElementText(page);
expect(activeElementTextTwo).toEqual('Click impatiently to load faster');
await page.keyboard.press('Tab');
const activeElementTextThree = await getActiveElementText(page);
expect(activeElementTextThree).toEqual('Click impatiently to load faster');
});
test('loading: basic', async () => {
await testLoading(DIRECTORY, '#basic-loading');

View File

@@ -24,7 +24,6 @@ export class LoadingExample {
async presentLoadingWithOptions() {
const loading = await this.loadingController.create({
cssClass: 'my-custom-class',
spinner: null,
duration: 5000,
message: 'Click the backdrop to dismiss early...',

View File

@@ -16,7 +16,6 @@ async function presentLoading() {
async function presentLoadingWithOptions() {
const loading = document.createElement('ion-loading');
loading.cssClass = 'my-custom-class';
loading.spinner = null;
loading.duration = 5000;
loading.message = 'Click the backdrop to dismiss early...';

View File

@@ -22,7 +22,6 @@ export class LoadingExample {
async presentLoadingWithOptions() {
const loading = await loadingController.create({
cssClass: 'my-custom-class',
spinner: null,
duration: 5000,
message: 'Click the backdrop to dismiss early...',

View File

@@ -30,7 +30,6 @@ export default {
presentLoadingWithOptions() {
return this.$ionic.loadingController
.create({
cssClass: 'my-custom-class',
spinner: null,
duration: this.timeout,
message: 'Click the backdrop to dismiss early...',

View File

@@ -34,6 +34,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
// Whether or not modal is being dismissed via gesture
private gestureAnimationDismissing = false;
presented = false;
lastFocus?: HTMLElement;
animation?: Animation;
@Element() el!: HTMLIonModalElement;
@@ -130,7 +131,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
}
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}
@@ -289,11 +290,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>
{mode === 'ios' && <div class="modal-shadow"></div>}
<div tabindex="0"></div>
<div
role="dialog"
class="modal-wrapper"
class="modal-wrapper ion-overlay-wrapper"
>
</div>
<div tabindex="0"></div>
</Host>
);
}

View File

@@ -353,18 +353,48 @@ Modals in iOS mode have the ability to be presented in a card-style and swiped t
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
```tsx
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={pageRef.current}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
const App: React.FC = () => {
const routerRef = useRef<HTMLIonRouterOutletElement | null>(null);
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet ref={routerRef}>
<Route path="/home" render={() => <Home router={routerRef.current.current} />} exact={true} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
...
interface HomePageProps {
router: HTMLIonRouterOutletElement | null;
}
const Home: React.FC<HomePageProps> = ({ router }) => {
const [showModal, setShowModal] = useState(false);
return (
...
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
...
);
};
```
In most scenarios, setting a ref on `IonPage` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
In most scenarios, setting a ref on `IonRouterOutlet` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
```tsx
<IonModal
@@ -372,7 +402,7 @@ In most scenarios, setting a ref on `IonPage` and passing that ref's `current` v
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={pageRef.current}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShow2ndModal(true)}>Show 2nd Modal</IonButton>

View File

@@ -1,6 +1,43 @@
import { testModal } from '../test.utils';
import { newE2EPage } from '@stencil/core/testing';
const DIRECTORY = 'basic';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.textContent, activeElement);
}
test('modal: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' });
await page.click('#basic-modal');
await page.waitForSelector('#basic-modal');
let modal = await page.find('ion-modal');
expect(modal).not.toBe(null);
await modal.waitForVisible();
// TODO fix
await page.waitFor(50);
await page.keyboard.press('Tab');
const activeElementText = await getActiveElementText(page);
expect(activeElementText).toEqual('Dismiss Modal');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
const activeElementTextTwo = await getActiveElementText(page);
expect(activeElementTextTwo).toEqual('Dismiss Modal');
await page.keyboard.press('Tab');
const activeElementTextThree = await getActiveElementText(page);
expect(activeElementTextThree).toEqual('Dismiss Modal');
});
test('modal: basic', async () => {
await testModal(DIRECTORY, '#basic-modal');

View File

@@ -24,18 +24,48 @@ Modals in iOS mode have the ability to be presented in a card-style and swiped t
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
```tsx
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={pageRef.current}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
const App: React.FC = () => {
const routerRef = useRef<HTMLIonRouterOutletElement | null>(null);
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet ref={routerRef}>
<Route path="/home" render={() => <Home router={routerRef.current.current} />} exact={true} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
...
interface HomePageProps {
router: HTMLIonRouterOutletElement | null;
}
const Home: React.FC<HomePageProps> = ({ router }) => {
const [showModal, setShowModal] = useState(false);
return (
...
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
...
);
};
```
In most scenarios, setting a ref on `IonPage` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
In most scenarios, setting a ref on `IonRouterOutlet` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
```tsx
<IonModal
@@ -43,7 +73,7 @@ In most scenarios, setting a ref on `IonPage` and passing that ref's `current` v
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={pageRef.current}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShow2ndModal(true)}>Show 2nd Modal</IonButton>

View File

@@ -1,10 +1,14 @@
import { Animation, AnimationBuilder, ComponentRef, FrameworkDelegate, Mode } from '../../interface';
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Mode } from '../../interface';
import { ViewController } from './view-controller';
export type NavDirection = 'back' | 'forward';
export type NavComponent = ComponentRef | ViewController;
export interface NavComponentWithProps<T = any> {
component: NavComponent;
componentProps?: ComponentProps<T> | null;
}
export interface NavResult {
hasCompleted: boolean;

View File

@@ -2,7 +2,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 { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, NavComponent, NavComponentWithProps, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
import { assert } from '../../utils/helpers';
import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition';
@@ -156,7 +156,7 @@ export class Nav implements NavOutlet {
return this.queueTrns(
{
insertStart: -1,
insertViews: [{ page: component, params: componentProps }],
insertViews: [{ component, componentProps }],
opts
},
done
@@ -184,7 +184,7 @@ export class Nav implements NavOutlet {
return this.queueTrns(
{
insertStart: insertIndex,
insertViews: [{ page: component, params: componentProps }],
insertViews: [{ component, componentProps }],
opts
},
done
@@ -204,7 +204,7 @@ export class Nav implements NavOutlet {
@Method()
insertPages(
insertIndex: number,
insertComponents: NavComponent[],
insertComponents: NavComponent[] | NavComponentWithProps[],
opts?: NavOptions | null,
done?: TransitionDoneFn
): Promise<boolean> {
@@ -326,7 +326,7 @@ export class Nav implements NavOutlet {
done?: TransitionDoneFn
): Promise<boolean> {
return this.setPages(
[{ page: component, params: componentProps }],
[{ component, componentProps }],
opts,
done
);
@@ -344,7 +344,7 @@ export class Nav implements NavOutlet {
*/
@Method()
setPages(
views: any[],
views: NavComponent[] | NavComponentWithProps[],
opts?: NavOptions | null,
done?: TransitionDoneFn
): Promise<boolean> {
@@ -513,7 +513,7 @@ export class Nav implements NavOutlet {
// 7. _transitionStart(): called once the transition actually starts, it initializes the Animation underneath.
// 8. _transitionFinish(): called once the transition finishes
// 9. _cleanup(): syncs the navigation internal state with the DOM. For example it removes the pages from the DOM or hides/show them.
private queueTrns(
private async queueTrns(
ti: TransitionInstruction,
done: TransitionDoneFn | undefined
): Promise<boolean> {
@@ -527,6 +527,25 @@ export class Nav implements NavOutlet {
});
ti.done = done;
/**
* If using router, check to see if navigation hooks
* will allow us to perform this transition. This
* is required in order for hooks to work with
* the ion-back-button or swipe to go back.
*/
if (ti.opts && ti.opts.updateURL !== false && this.useRouter) {
const router = document.querySelector('ion-router');
if (router) {
const canTransition = await router.canTransition();
if (canTransition === false) {
return Promise.resolve(false);
} else if (typeof canTransition === 'string') {
router.push(canTransition, ti.opts!.direction || 'back');
return Promise.resolve(false);
}
}
}
// Normalize empty
if (ti.insertViews && ti.insertViews.length === 0) {
ti.insertViews = undefined;
@@ -939,16 +958,27 @@ export class Nav implements NavOutlet {
for (let i = views.length - 1; i >= 0; i--) {
const view = views[i];
/**
* When inserting multiple views via insertPages
* the last page will be transitioned to, but the
* others will not be. As a result, a DOM element
* will only be created for the last page inserted.
* As a result, it is possible to have views in the
* stack that do not have `view.element` yet.
*/
const element = view.element;
if (i > activeViewIndex) {
// this view comes after the active view
// let's unload it
lifecycle(element, LIFECYCLE_WILL_UNLOAD);
this.destroyView(view);
} else if (i < activeViewIndex) {
// this view comes before the active view
// and it is not a portal then ensure it is hidden
setPageHidden(element!, true);
if (element) {
if (i > activeViewIndex) {
// this view comes after the active view
// let's unload it
lifecycle(element, LIFECYCLE_WILL_UNLOAD);
this.destroyView(view);
} else if (i < activeViewIndex) {
// this view comes before the active view
// and it is not a portal then ensure it is hidden
setPageHidden(element!, true);
}
}
}
}

View File

@@ -80,7 +80,7 @@ Type: `Promise<boolean>`
### `insertPages(insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
### `insertPages(insertIndex: number, insertComponents: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
Inserts an array of components into the navigation stack at the specified index.
The last component in the array will become instantiated as a view, and animate
@@ -145,7 +145,7 @@ Type: `Promise<boolean>`
### `setPages(views: any[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
### `setPages(views: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
Set the views of the current navigation stack and navigate to the last view.
By default animations are disabled, but they can be enabled by passing options

View File

@@ -119,9 +119,9 @@ describe('NavController', () => {
mockViews(nav, [view1]);
const view2 = mockView(MockView2);
await nav.push(view2, null, null, trnsDone);
const hasCompleted = true;
const requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
@@ -818,8 +818,8 @@ describe('NavController', () => {
const view5 = mockView(MockView5);
await nav.setPages([
{ page: view4 },
{ page: view5 }
{ component: view4 },
{ component: view5 }
], null, trnsDone);
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
@@ -924,10 +924,10 @@ describe('NavController', () => {
const MockView3 = 'mock-view3';
const MockView4 = 'mock-view4';
const MockView5 = 'mock-view5';
const mockWebAnimation = (el: HTMLElement) => {
Element.prototype.animate = () => {};
el.animate = () => {
const animation = {
stop: () => {},
@@ -935,13 +935,13 @@ describe('NavController', () => {
cancel: () => {},
onfinish: undefined
}
animation.play = () => {
if (animation.onfinish) {
animation.onfinish();
}
}
return animation;
}
}
@@ -953,9 +953,9 @@ describe('NavController', () => {
const view = new ViewController(component, params);
view.element = document.createElement(component) as HTMLElement;
mockWebAnimation(view.element);
return view;
}

View File

@@ -1,4 +1,4 @@
import { AnimationBuilder, ComponentProps, FrameworkDelegate } from '../../interface';
import { AnimationBuilder, ComponentProps, FrameworkDelegate, NavComponentWithProps } from '../../interface';
import { attachComponent } from '../../utils/framework-delegate';
import { assert } from '../../utils/helpers';
@@ -89,13 +89,20 @@ export const convertToView = (page: any, params: ComponentProps | undefined): Vi
return new ViewController(page, params);
};
export const convertToViews = (pages: any[]): ViewController[] => {
export const convertToViews = (pages: NavComponentWithProps[]): ViewController[] => {
return pages.map(page => {
if (page instanceof ViewController) {
return page;
}
if ('page' in page) {
return convertToView(page.page, page.params);
if ('component' in page) {
/**
* TODO Ionic 6:
* Consider switching to just using `undefined` here
* as well as on the public interfaces and on
* `NavComponentWithProps`. Previously `pages` was
* of type `any[]` so TypeScript did not catch this.
*/
return convertToView(page.component, (page.componentProps === null) ? undefined : page.componentProps);
}
return convertToView(page, undefined);
}).filter(v => v !== null) as ViewController[];

View File

@@ -21,6 +21,7 @@ import { iosLeaveAnimation } from './animations/ios.leave';
})
export class Picker implements ComponentInterface, OverlayInterface {
private durationTimeout: any;
lastFocus?: HTMLElement;
@Element() el!: HTMLIonPickerElement;
@@ -100,7 +101,7 @@ export class Picker implements ComponentInterface, OverlayInterface {
*/
@Event({ eventName: 'ionPickerDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}
@@ -236,7 +237,10 @@ export class Picker implements ComponentInterface, OverlayInterface {
tappable={this.backdropDismiss}
>
</ion-backdrop>
<div class="picker-wrapper" role="dialog">
<div tabindex="0"></div>
<div class="picker-wrapper ion-overlay-wrapper" role="dialog">
<div class="picker-toolbar">
{this.buttons.map(b => (
<div class={buttonWrapperClass(b)}>
@@ -259,6 +263,8 @@ export class Picker implements ComponentInterface, OverlayInterface {
<div class="picker-below-highlight"></div>
</div>
</div>
<div tabindex="0"></div>
</Host>
);
}

View File

@@ -28,6 +28,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
private usersElement?: HTMLElement;
presented = false;
lastFocus?: HTMLElement;
@Element() el!: HTMLIonPopoverElement;
@@ -115,7 +116,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
*/
@Event({ eventName: 'ionPopoverDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}
@@ -219,10 +220,15 @@ export class Popover implements ComponentInterface, OverlayInterface {
onIonBackdropTap={this.onBackdropTap}
>
<ion-backdrop tappable={this.backdropDismiss} visible={this.showBackdrop}/>
<div class="popover-wrapper">
<div tabindex="0"></div>
<div class="popover-wrapper ion-overlay-wrapper">
<div class="popover-arrow"></div>
<div class="popover-content"></div>
</div>
<div tabindex="0"></div>
</Host>
);
}

View File

@@ -1,7 +1,42 @@
import { testPopover } from '../test.utils';
import { newE2EPage } from '@stencil/core/testing';
const DIRECTORY = 'basic';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.textContent, activeElement);
}
test('popover: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
await page.click('#basic-popover');
await page.waitForSelector('#basic-popover');
let popover = await page.find('ion-popover');
expect(popover).not.toBe(null);
await popover.waitForVisible();
await page.keyboard.press('Tab');
const activeElementText = await getActiveElementText(page);
expect(activeElementText).toEqual('Item 0');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
const activeElementTextTwo = await getActiveElementText(page);
expect(activeElementTextTwo).toEqual('Item 3');
await page.keyboard.press('Tab');
const activeElementTextThree = await getActiveElementText(page);
expect(activeElementTextThree).toEqual('Item 0');
});
test('popover: basic', async () => {
await testPopover(DIRECTORY, '#basic-popover');
});

View File

@@ -75,10 +75,10 @@
<ion-content>
<ion-list>
<ion-list-header><ion-label>Ionic</ion-label></ion-list-header>
<ion-item><ion-label>Item 0</ion-label></ion-item>
<ion-item><ion-label>Item 1</ion-label></ion-item>
<ion-item><ion-label>Item 2</ion-label></ion-item>
<ion-item><ion-label>Item 3</ion-label></ion-item>
<ion-item button><ion-label>Item 0</ion-label></ion-item>
<ion-item button><ion-label>Item 1</ion-label></ion-item>
<ion-item button><ion-label>Item 2</ion-label></ion-item>
<ion-item button><ion-label>Item 3</ion-label></ion-item>
</ion-list>
</ion-content>
`;

View File

@@ -1,19 +1,233 @@
# ion-route
The route component takes a component and renders it when the Browser URl matches the url property.
The route component takes a component and renders it when the Browser URL matches the url property.
> Note: this component should only be used with vanilla and Stencil JavaScript projects. For Angular projects, use [`ion-router-outlet`](../router-outlet) and the Angular router.
## Navigation Hooks
Navigation hooks can be used to perform tasks or act as navigation guards. Hooks are used by providing functions to the `beforeEnter` and `beforeLeave` properties on each `ion-route`. Returning `true` allows navigation to proceed, while returning `false` causes it to be cancelled. Returning an object of type `NavigationHookOptions` allows you to redirect navigation to another page.
## Interfaces
```typescript
interface NavigationHookOptions {
/**
* A valid path to redirect navigation to.
*/
redirect: string;
}
```
<!-- Auto Generated Below -->
## Usage
### Javascript
```html
<ion-router>
<ion-route url="/home" component="page-home"></ion-route>
<ion-route url="/dashboard" component="page-dashboard"></ion-route>
<ion-route url="/new-message" component="page-new-message"></ion-route>
<ion-route url="/login" component="page-login"></ion-route>
</ion-router>
```
```javascript
const dashboardPage = document.querySelector('ion-route[url="/dashboard"]');
dashboardPage.beforeEnter = isLoggedInGuard;
const newMessagePage = document.querySelector('ion-route[url="/dashboard"]');
newMessagePage.beforeLeave = hasUnsavedDataGuard;
const isLoggedInGuard = async () => {
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
if (isLoggedIn) {
return true;
} else {
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
}
}
const hasUnsavedDataGuard = async () => {
const hasUnsavedData = await checkData(); // Replace this with actual validation
if (hasUnsavedData) {
return await confirmDiscardChanges();
} else {
return true;
}
}
const confirmDiscardChanges = async () => {
const alert = document.createElement('ion-alert');
alert.header = 'Discard Unsaved Changes?';
alert.message = 'Are you sure you want to leave? Any unsaved changed will be lost.';
alert.buttons = [
{
text: 'Cancel',
role: 'Cancel',
},
{
text: 'Discard',
role: 'destructive',
}
];
document.body.appendChild(alert);
await alert.present();
const { role } = await alert.onDidDismiss();
return (role === 'Cancel') ? false : true;
}
```
### Stencil
```typescript
import { Component, h } from '@stencil/core';
import { alertController } from '@ionic/core';
@Component({
tag: 'router-example',
styleUrl: 'router-example.css'
})
export class RouterExample {
render() {
return (
<ion-router>
<ion-route url="/home" component="page-home"></ion-route>
<ion-route url="/dashboard" component="page-dashboard" beforeEnter={isLoggedInGuard}></ion-route>
<ion-route url="/new-message" component="page-new-message" beforeLeave={hasUnsavedDataGuard}></ion-route>
<ion-route url="/login" component="page-login"></ion-route>
</ion-router>
)
}
}
const isLoggedInGuard = async () => {
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
if (isLoggedIn) {
return true;
} else {
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
}
}
const hasUnsavedDataGuard = async () => {
const hasUnsavedData = await checkData(); // Replace this with actual validation
if (hasUnsavedData) {
return await confirmDiscardChanges();
} else {
return true;
}
}
const confirmDiscardChanges = async () => {
const alert = await alertController.create({
header: 'Discard Unsaved Changes?',
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
buttons: [
{
text: 'Cancel',
role: 'Cancel',
},
{
text: 'Discard',
role: 'destructive',
}
]
});
await alert.present();
const { role } = await alert.onDidDismiss();
return (role === 'Cancel') ? false : true;
}
```
### Vue
```html
<template>
<ion-router>
<ion-route url="/home" component="page-home"></ion-route>
<ion-route url="/dashboard" component="page-dashboard" :beforeEnter="isLoggedInGuard"></ion-route>
<ion-route url="/new-message" component="page-new-message" :beforeLeave="hasUnsavedDataGuard"></ion-route>
<ion-route url="/login" component="page-login"></ion-route>
</ion-router>
</template>
<script>
import { alertController } from '@ionic/vue';
const isLoggedInGuard = async () => {
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
if (isLoggedIn) {
return true;
} else {
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
}
}
const hasUnsavedDataGuard = async () => {
const hasUnsavedData = await checkData(); // Replace this with actual validation
if (hasUnsavedData) {
return await confirmDiscardChanges();
} else {
return true;
}
}
const confirmDiscardChanges = async () => {
const alert = await alertController.create({
header: 'Discard Unsaved Changes?',
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
buttons: [
{
text: 'Cancel',
role: 'Cancel',
},
{
text: 'Discard',
role: 'destructive',
}
]
});
await alert.present();
const { role } = await alert.onDidDismiss();
return (role === 'Cancel') ? false : true;
}
</script>
```
## Properties
| Property | Attribute | Description | Type | Default |
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------- |
| `component` _(required)_ | `component` | Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. | `string` | `undefined` |
| `componentProps` | -- | A key value `{ 'red': true, 'blue': 'white'}` containing props that should be passed to the defined component when rendered. | `undefined \| { [key: string]: any; }` | `undefined` |
| `url` | `url` | Relative path that needs to match in order for this route to apply. Accepts paths similar to expressjs so that you can define parameters in the url /foo/:bar where bar would be available in incoming props. | `string` | `''` |
| Property | Attribute | Description | Type | Default |
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------- |
| `beforeEnter` | -- | A navigation hook that is fired when the route tries to enter. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified. | `(() => boolean \| NavigationHookOptions \| Promise<NavigationHookResult>) \| undefined` | `undefined` |
| `beforeLeave` | -- | A navigation hook that is fired when the route tries to leave. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified. | `(() => boolean \| NavigationHookOptions \| Promise<NavigationHookResult>) \| undefined` | `undefined` |
| `component` _(required)_ | `component` | Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. | `string` | `undefined` |
| `componentProps` | -- | A key value `{ 'red': true, 'blue': 'white'}` containing props that should be passed to the defined component when rendered. | `undefined \| { [key: string]: any; }` | `undefined` |
| `url` | `url` | Relative path that needs to match in order for this route to apply. Accepts paths similar to expressjs so that you can define parameters in the url /foo/:bar where bar would be available in incoming props. | `string` | `''` |
## Events

View File

@@ -0,0 +1,5 @@
export type NavigationHookCallback = () => NavigationHookResult | Promise<NavigationHookResult>;
export type NavigationHookResult = boolean | NavigationHookOptions;
export interface NavigationHookOptions {
redirect: string;
}

View File

@@ -1,5 +1,7 @@
import { Component, ComponentInterface, Event, EventEmitter, Prop, Watch } from '@stencil/core';
import { NavigationHookCallback } from './route-interface';
@Component({
tag: 'ion-route'
})
@@ -28,6 +30,22 @@ export class Route implements ComponentInterface {
*/
@Prop() componentProps?: {[key: string]: any};
/**
* A navigation hook that is fired when the route tries to leave.
* Returning `true` allows the navigation to proceed, while returning
* `false` causes it to be cancelled. Returning a `NavigationHookOptions`
* object causes the router to redirect to the path specified.
*/
@Prop() beforeLeave?: NavigationHookCallback;
/**
* A navigation hook that is fired when the route tries to enter.
* Returning `true` allows the navigation to proceed, while returning
* `false` causes it to be cancelled. Returning a `NavigationHookOptions`
* object causes the router to redirect to the path specified.
*/
@Prop() beforeEnter?: NavigationHookCallback;
/**
* Used internally by `ion-router` to know when this route did change.
*/

View File

@@ -0,0 +1,60 @@
```html
<ion-router>
<ion-route url="/home" component="page-home"></ion-route>
<ion-route url="/dashboard" component="page-dashboard"></ion-route>
<ion-route url="/new-message" component="page-new-message"></ion-route>
<ion-route url="/login" component="page-login"></ion-route>
</ion-router>
```
```javascript
const dashboardPage = document.querySelector('ion-route[url="/dashboard"]');
dashboardPage.beforeEnter = isLoggedInGuard;
const newMessagePage = document.querySelector('ion-route[url="/dashboard"]');
newMessagePage.beforeLeave = hasUnsavedDataGuard;
const isLoggedInGuard = async () => {
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
if (isLoggedIn) {
return true;
} else {
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
}
}
const hasUnsavedDataGuard = async () => {
const hasUnsavedData = await checkData(); // Replace this with actual validation
if (hasUnsavedData) {
return await confirmDiscardChanges();
} else {
return true;
}
}
const confirmDiscardChanges = async () => {
const alert = document.createElement('ion-alert');
alert.header = 'Discard Unsaved Changes?';
alert.message = 'Are you sure you want to leave? Any unsaved changed will be lost.';
alert.buttons = [
{
text: 'Cancel',
role: 'Cancel',
},
{
text: 'Discard',
role: 'destructive',
}
];
document.body.appendChild(alert);
await alert.present();
const { role } = await alert.onDidDismiss();
return (role === 'Cancel') ? false : true;
}
```

View File

@@ -0,0 +1,64 @@
```typescript
import { Component, h } from '@stencil/core';
import { alertController } from '@ionic/core';
@Component({
tag: 'router-example',
styleUrl: 'router-example.css'
})
export class RouterExample {
render() {
return (
<ion-router>
<ion-route url="/home" component="page-home"></ion-route>
<ion-route url="/dashboard" component="page-dashboard" beforeEnter={isLoggedInGuard}></ion-route>
<ion-route url="/new-message" component="page-new-message" beforeLeave={hasUnsavedDataGuard}></ion-route>
<ion-route url="/login" component="page-login"></ion-route>
</ion-router>
)
}
}
const isLoggedInGuard = async () => {
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
if (isLoggedIn) {
return true;
} else {
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
}
}
const hasUnsavedDataGuard = async () => {
const hasUnsavedData = await checkData(); // Replace this with actual validation
if (hasUnsavedData) {
return await confirmDiscardChanges();
} else {
return true;
}
}
const confirmDiscardChanges = async () => {
const alert = await alertController.create({
header: 'Discard Unsaved Changes?',
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
buttons: [
{
text: 'Cancel',
role: 'Cancel',
},
{
text: 'Discard',
role: 'destructive',
}
]
});
await alert.present();
const { role } = await alert.onDidDismiss();
return (role === 'Cancel') ? false : true;
}
```

View File

@@ -0,0 +1,57 @@
```html
<template>
<ion-router>
<ion-route url="/home" component="page-home"></ion-route>
<ion-route url="/dashboard" component="page-dashboard" :beforeEnter="isLoggedInGuard"></ion-route>
<ion-route url="/new-message" component="page-new-message" :beforeLeave="hasUnsavedDataGuard"></ion-route>
<ion-route url="/login" component="page-login"></ion-route>
</ion-router>
</template>
<script>
import { alertController } from '@ionic/vue';
const isLoggedInGuard = async () => {
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
if (isLoggedIn) {
return true;
} else {
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
}
}
const hasUnsavedDataGuard = async () => {
const hasUnsavedData = await checkData(); // Replace this with actual validation
if (hasUnsavedData) {
return await confirmDiscardChanges();
} else {
return true;
}
}
const confirmDiscardChanges = async () => {
const alert = await alertController.create({
header: 'Discard Unsaved Changes?',
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
buttons: [
{
text: 'Cancel',
role: 'Cancel',
},
{
text: 'Discard',
role: 'destructive',
}
]
});
await alert.present();
const { role } = await alert.onDidDismiss();
return (role === 'Cancel') ? false : true;
}
</script>
```

View File

@@ -70,9 +70,17 @@ export class Router implements ComponentInterface {
}
@Listen('popstate', { target: 'window' })
protected onPopState() {
protected async onPopState() {
const direction = this.historyDirection();
const path = this.getPath();
let path = this.getPath();
const canProceed = await this.runGuards(path);
if (canProceed !== true) {
if (typeof canProceed === 'object') {
path = parsePath(canProceed.redirect);
}
return false;
}
console.debug('[ion-router] URL changed -> update nav', path, direction);
return this.writeNavStateRoot(path, direction);
}
@@ -85,6 +93,21 @@ export class Router implements ComponentInterface {
});
}
/** @internal */
@Method()
async canTransition() {
const canProceed = await this.runGuards();
if (canProceed !== true) {
if (typeof canProceed === 'object') {
return canProceed.redirect;
} else {
return false;
}
}
return true;
}
/**
* Navigate to the specified URL.
*
@@ -92,14 +115,25 @@ export class Router implements ComponentInterface {
* @param direction The direction of the animation. Defaults to `"forward"`.
*/
@Method()
push(url: string, direction: RouterDirection = 'forward', animation?: AnimationBuilder) {
async push(url: string, direction: RouterDirection = 'forward', animation?: AnimationBuilder) {
if (url.startsWith('.')) {
url = (new URL(url, window.location.href)).pathname;
}
console.debug('[ion-router] URL pushed -> updating nav', url, direction);
const path = parsePath(url);
const queryString = url.split('?')[1];
let path = parsePath(url);
let queryString = url.split('?')[1];
const canProceed = await this.runGuards(path);
if (canProceed !== true) {
if (typeof canProceed === 'object') {
path = parsePath(canProceed.redirect);
queryString = canProceed.redirect.split('?')[1];
} else {
return false;
}
}
this.setPath(path, direction, queryString);
return this.writeNavStateRoot(path, direction, animation);
}
@@ -191,6 +225,7 @@ export class Router implements ComponentInterface {
// lookup redirect rule
const redirects = readRedirects(this.el);
const redirect = routeRedirect(path, redirects);
let redirectFrom: string[] | null = null;
if (redirect) {
this.setPath(redirect.to!, direction);
@@ -237,6 +272,25 @@ export class Router implements ComponentInterface {
}
return resolve;
}
private async runGuards(to: string[] | null = this.getPath(), from: string[] | null = parsePath(this.previousPath)) {
if (!to || !from) { return true; }
const routes = readRoutes(this.el);
const toChain = routerPathToChain(to, routes);
const fromChain = routerPathToChain(from, routes);
const beforeEnterHook = toChain && toChain[toChain.length - 1].beforeEnter;
const beforeLeaveHook = fromChain && fromChain[fromChain.length - 1].beforeLeave;
const canLeave = beforeLeaveHook ? await beforeLeaveHook() : true;
if (canLeave === false || typeof canLeave === 'object') { return canLeave; }
const canEnter = beforeEnterHook ? await beforeEnterHook() : true;
if (canEnter === false || typeof canEnter === 'object') { return canEnter; }
return true;
}
private async writeNavState(
node: HTMLElement | undefined, chain: RouteChain, direction: RouterDirection,

View File

@@ -0,0 +1,128 @@
import { newE2EPage } from '@stencil/core/testing';
test('router: guards - href - allow/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 1: beforeEnter: allow, beforeLeave: allow
await setBeforeEnterHook(page, 'allow');
const href = await page.$('#href');
await href.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - href - block/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 2: beforeEnter: block, beforeLeave: allow
await setBeforeEnterHook(page, 'block');
const href = await page.$('#href');
await href.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - href - redirect/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 3: beforeEnter: redirect, beforeLeave: allow
await setBeforeEnterHook(page, 'redirect');
const href = await page.$('#href');
await href.click();
await page.waitForChanges();
await checkUrl(page, '#/test');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - href - allow/block', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 4: beforeEnter: allow, beforeLeave: block
await setBeforeLeaveHook(page, 'block');
const href = await page.$('#href');
await href.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
});
// TODO this is an actual bug in the code.
test('router: guards - href - allow/redirect', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 5: beforeEnter: allow, beforeLeave: redirect
await setBeforeLeaveHook(page, 'redirect');
const href = await page.$('#href');
await href.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/test');
});
const checkUrl = async (page, url: string) => {
const getUrl = await page.url();
expect(getUrl).toContain(url);
}
const setBeforeEnterHook = async (page, type: string) => {
const button = await page.$(`ion-radio-group#beforeEnter ion-radio[value=${type}]`);
await button.click();
}
const setBeforeLeaveHook = async (page, type: string) => {
const button = await page.$(`ion-radio-group#beforeLeave ion-radio[value=${type}]`);
await button.click();
}

View File

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Navigation Guards</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<style>
.toolbar {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
width: 200px;
background: white;
box-shadow: 0px 1px 10px rgba(0,0,0,0.2);
}
</style>
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script> <script>
class HomePage extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Home Page</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list>
<ion-button id="router-push">router.push</ion-button><br>
<ion-router-link href="/child">
<ion-button id="router-link">ion-router-link</ion-button><br>
</ion-router-link>
<ion-button href="/child" id="href">href</ion-button>
</ion-list>
</ion-content>`;
const childButton = this.querySelector('#router-push');
childButton.addEventListener('click', () => {
const r = document.querySelector('ion-router');
r.push('/child');
});
}
}
class ChildPage extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-buttons>
<ion-back-button default-href="/test"></ion-back-button>
</ion-buttons>
<ion-title>Child Page</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>`;
}
}
class TestPage extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-buttons>
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Test Page</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>`;
}
}
customElements.define('home-page', HomePage);
customElements.define('child-page', ChildPage);
customElements.define('test-page', TestPage);
</script>
</head>
<body>
<div class="toolbar">
<ion-radio-group id="beforeEnter" value="allow">
<ion-list-header>
<ion-label>
beforeEnter Behavior
</ion-label>
</ion-list-header>
<ion-item>
<ion-label>Allow Navigation</ion-label>
<ion-radio value="allow"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Block Navigation</ion-label>
<ion-radio value="block"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Redirect</ion-label>
<ion-radio value="redirect"></ion-radio>
</ion-item>
</ion-radio-group>
<br><br>
<ion-radio-group id="beforeLeave" value="allow">
<ion-list-header>
<ion-label>
beforeLeave Behavior
</ion-label>
</ion-list-header>
<ion-item>
<ion-label>Allow Navigation</ion-label>
<ion-radio value="allow"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Block Navigation</ion-label>
<ion-radio value="block"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Redirect</ion-label>
<ion-radio value="redirect"></ion-radio>
</ion-item>
</ion-radio-group>
</div>
<ion-app>
<ion-router>
<ion-route-redirect from="/" to="/home"></ion-route-redirect>
<ion-route url="/home" component="home-page"></ion-route>
<ion-route url="/test" component="test-page"></ion-route>
<ion-route url="/child" component="child-page"></ion-route>
</ion-router>
<ion-nav></ion-nav>
<script>
const beforeEnterGroup = document.querySelector('ion-radio-group#beforeEnter');
const beforeLeaveGroup = document.querySelector('ion-radio-group#beforeLeave');
beforeEnterGroup.addEventListener('ionChange', (ev) => {
switch (ev.detail.value) {
case "redirect":
page.beforeEnter = redirect;
break;
case "block":
page.beforeEnter = block;
break;
default:
page.beforeEnter = allow;
break;
}
});
beforeLeaveGroup.addEventListener('ionChange', (ev) => {
switch (ev.detail.value) {
case "redirect":
page.beforeLeave = redirect;
break;
case "block":
page.beforeLeave = block;
break;
default:
page.beforeLeave = allow;
break;
}
});
const redirect = (to = '/test') => { return { redirect: to }};
const block = () => false;
const allow = () => true;
const page = document.querySelector('ion-route[component="child-page"]');
page.beforeEnter = allow;
page.beforeLeave = allow;
</script>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,128 @@
import { newE2EPage } from '@stencil/core/testing';
test('router: guards - router-link - allow/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 1: beforeEnter: allow, beforeLeave: allow
await setBeforeEnterHook(page, 'allow');
const routerLink = await page.$('#router-link');
await routerLink.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - router-link - block/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 2: beforeEnter: block, beforeLeave: allow
await setBeforeEnterHook(page, 'block');
const routerLink = await page.$('#router-link');
await routerLink.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - router-link - redirect/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 3: beforeEnter: redirect, beforeLeave: allow
await setBeforeEnterHook(page, 'redirect');
const routerLink = await page.$('#router-link');
await routerLink.click();
await page.waitForChanges();
await checkUrl(page, '#/test');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - router-link - allow/block', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 4: beforeEnter: allow, beforeLeave: block
await setBeforeLeaveHook(page, 'block');
const routerLink = await page.$('#router-link');
await routerLink.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
});
// TODO this is an actual bug in the code.
test('router: guards - router-link - allow/redirect', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 5: beforeEnter: allow, beforeLeave: redirect
await setBeforeLeaveHook(page, 'redirect');
const routerLink = await page.$('#router-link');
await routerLink.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/test');
});
const checkUrl = async (page, url: string) => {
const getUrl = await page.url();
expect(getUrl).toContain(url);
}
const setBeforeEnterHook = async (page, type: string) => {
const button = await page.$(`ion-radio-group#beforeEnter ion-radio[value=${type}]`);
await button.click();
}
const setBeforeLeaveHook = async (page, type: string) => {
const button = await page.$(`ion-radio-group#beforeLeave ion-radio[value=${type}]`);
await button.click();
}

View File

@@ -0,0 +1,128 @@
import { newE2EPage } from '@stencil/core/testing';
test('router: guards - router.push - allow/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 1: beforeEnter: allow, beforeLeave: allow
await setBeforeEnterHook(page, 'allow');
const routerPush = await page.$('#router-push');
await routerPush.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - router.push - block/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 2: beforeEnter: block, beforeLeave: allow
await setBeforeEnterHook(page, 'block');
const routerPush = await page.$('#router-push');
await routerPush.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - router.push - redirect/allow', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 3: beforeEnter: redirect, beforeLeave: allow
await setBeforeEnterHook(page, 'redirect');
const routerPush = await page.$('#router-push');
await routerPush.click();
await page.waitForChanges();
await checkUrl(page, '#/test');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/home');
});
test('router: guards - router.push - allow/block', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 4: beforeEnter: allow, beforeLeave: block
await setBeforeLeaveHook(page, 'block');
const routerPush = await page.$('#router-push');
await routerPush.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
});
// TODO this is an actual bug in the code.
test('router: guards - router.push - allow/redirect', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/guards?ionic:_testing=true'
});
// Test 5: beforeEnter: allow, beforeLeave: redirect
await setBeforeLeaveHook(page, 'redirect');
const routerPush = await page.$('#router-push');
await routerPush.click();
await page.waitForChanges();
await checkUrl(page, '#/child');
const backButton = await page.$('ion-back-button');
await backButton.click();
await page.waitForChanges();
await checkUrl(page, '#/test');
});
const checkUrl = async (page, url: string) => {
const getUrl = await page.url();
expect(getUrl).toContain(url);
}
const setBeforeEnterHook = async (page, type: string) => {
const button = await page.$(`ion-radio-group#beforeEnter ion-radio[value=${type}]`);
await button.click();
}
const setBeforeLeaveHook = async (page, type: string) => {
const button = await page.$(`ion-radio-group#beforeLeave ion-radio[value=${type}]`);
await button.click();
}

View File

@@ -1,4 +1,5 @@
import { AnimationBuilder, ComponentProps } from '../../../interface';
import { NavigationHookCallback } from '../../route/route-interface';
export interface HTMLStencilElement extends HTMLElement {
componentOnReady(): Promise<this>;
@@ -36,6 +37,8 @@ export interface RouteEntry {
id: string;
path: string[];
params: {[key: string]: any} | undefined;
beforeLeave?: NavigationHookCallback;
beforeEnter?: NavigationHookCallback;
}
export interface RouteNode extends RouteEntry {

View File

@@ -29,6 +29,8 @@ export const readRouteNodes = (root: Element, node = root): RouteTree => {
path: parsePath(readProp(el, 'url')),
id: component.toLowerCase(),
params: el.componentProps,
beforeLeave: el.beforeLeave,
beforeEnter: el.beforeEnter,
children: readRouteNodes(root, el)
};
});
@@ -57,7 +59,9 @@ const flattenNode = (chain: RouteChain, routes: RouteChain[], node: RouteNode) =
s.push({
id: node.id,
path: node.path,
params: node.params
params: node.params,
beforeLeave: node.beforeLeave,
beforeEnter: node.beforeEnter
});
if (node.children.length === 0) {

View File

@@ -796,10 +796,11 @@ export class SegmentButtonExample {
## Shadow Parts
| Part | Description |
| ------------- | ------------------------------------------------------------- |
| `"indicator"` | The indicator displayed on the checked segment button. |
| `"native"` | The native HTML button element that wraps all child elements. |
| Part | Description |
| ------------------------ | --------------------------------------------------------------------------------- |
| `"indicator"` | The indicator displayed on the checked segment button. |
| `"indicator-background"` | The background element for the indicator displayed on the checked segment button. |
| `"native"` | The native HTML button element that wraps all child elements. |
## CSS Custom Properties

View File

@@ -44,11 +44,6 @@
text-transform: uppercase;
}
.button-native {
min-width: $segment-button-md-min-width;
}
// Segment Button: Disabled
// --------------------------------------------------

View File

@@ -12,6 +12,7 @@ let ids = 0;
*
* @part native - The native HTML button element that wraps all child elements.
* @part indicator - The indicator displayed on the checked segment button.
* @part indicator-background - The background element for the indicator displayed on the checked segment button.
*/
@Component({
tag: 'ion-segment-button',

View File

@@ -159,6 +159,40 @@
</ion-segment-button>
</ion-segment>
<!-- Non Scrollable, Custom Min Width -->
<ion-segment color="tertiary" value="heart" id="min-width-custom">
<ion-segment-button value="home">
<ion-icon name="home"></ion-icon>
</ion-segment-button>
<ion-segment-button value="heart">
<ion-icon name="heart"></ion-icon>
</ion-segment-button>
<ion-segment-button value="pin">
<ion-icon name="pin"></ion-icon>
</ion-segment-button>
<ion-segment-button value="star">
<ion-icon name="star"></ion-icon>
</ion-segment-button>
<ion-segment-button value="call">
<ion-icon name="call"></ion-icon>
</ion-segment-button>
<ion-segment-button value="globe">
<ion-icon name="globe"></ion-icon>
</ion-segment-button>
<ion-segment-button value="basket">
<ion-icon name="basket"></ion-icon>
</ion-segment-button>
<ion-segment-button value="airplane">
<ion-icon name="airplane"></ion-icon>
</ion-segment-button>
<ion-segment-button value="boat">
<ion-icon name="boat"></ion-icon>
</ion-segment-button>
<ion-segment-button value="baseball">
<ion-icon name="baseball"></ion-icon>
</ion-segment-button>
</ion-segment>
<!-- Scrollable Icons -->
<ion-segment scrollable color="tertiary" value="heart">
<ion-segment-button value="home">
@@ -212,6 +246,10 @@
</ion-content>
<style>
ion-segment#min-width-custom ion-segment-button {
min-width: auto;
}
ion-content {
--padding-top: 16px;
}

View File

@@ -1225,12 +1225,12 @@ export class SelectExample {
## Events
| Event | Description | Type |
| ----------- | ---------------------------------------- | -------------------------------------- |
| `ionBlur` | Emitted when the select loses focus. | `CustomEvent<void>` |
| `ionCancel` | Emitted when the selection is cancelled. | `CustomEvent<void>` |
| `ionChange` | Emitted when the value has changed. | `CustomEvent<SelectChangeEventDetail>` |
| `ionFocus` | Emitted when the select has focus. | `CustomEvent<void>` |
| Event | Description | Type |
| ----------- | ---------------------------------------- | ------------------------------------------- |
| `ionBlur` | Emitted when the select loses focus. | `CustomEvent<void>` |
| `ionCancel` | Emitted when the selection is cancelled. | `CustomEvent<void>` |
| `ionChange` | Emitted when the value has changed. | `CustomEvent<SelectChangeEventDetail<any>>` |
| `ionFocus` | Emitted when the select has focus. | `CustomEvent<void>` |
## Methods

View File

@@ -2,6 +2,6 @@ export type SelectInterface = 'action-sheet' | 'popover' | 'alert';
export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean;
export interface SelectChangeEventDetail {
value: any | any[] | undefined | null;
export interface SelectChangeEventDetail<T = any> {
value: T;
}

View File

@@ -286,9 +286,9 @@ export class TextareaExample {
| Event | Description | Type |
| ----------- | ----------------------------------------- | ---------------------------------------- |
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<void>` |
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<FocusEvent>` |
| `ionChange` | Emitted when the input value has changed. | `CustomEvent<TextareaChangeEventDetail>` |
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<void>` |
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<FocusEvent>` |
| `ionInput` | Emitted when a keyboard input occurred. | `CustomEvent<KeyboardEvent>` |

View File

@@ -177,12 +177,12 @@ export class Textarea implements ComponentInterface {
/**
* Emitted when the input loses focus.
*/
@Event() ionBlur!: EventEmitter<void>;
@Event() ionBlur!: EventEmitter<FocusEvent>;
/**
* Emitted when the input has focus.
*/
@Event() ionFocus!: EventEmitter<void>;
@Event() ionFocus!: EventEmitter<FocusEvent>;
connectedCallback() {
this.emitStyle();
@@ -292,18 +292,18 @@ export class Textarea implements ComponentInterface {
this.ionInput.emit(ev as KeyboardEvent);
}
private onFocus = () => {
private onFocus = (ev: FocusEvent) => {
this.hasFocus = true;
this.focusChange();
this.ionFocus.emit();
this.ionFocus.emit(ev);
}
private onBlur = () => {
private onBlur = (ev: FocusEvent) => {
this.hasFocus = false;
this.focusChange();
this.ionBlur.emit();
this.ionBlur.emit(ev);
}
private onKeyDown = () => {

View File

@@ -41,6 +41,7 @@
:host(.title-large) {
@include padding(0, 16px);
@include transform-origin(start, center);
bottom: 0;
@@ -59,3 +60,7 @@
:host(.title-large.ion-cloned-element) {
--color: #{$text-color};
}
:host(.title-large) .toolbar-title {
@include transform-origin(inherit);
}

View File

@@ -288,6 +288,16 @@ Type: `Promise<void>`
## Shadow Parts
| Part | Description |
| ------------- | --------------------------------------------------------- |
| `"button"` | Any button element that is displayed inside of the toast. |
| `"container"` | The element that wraps all child elements. |
| `"header"` | The header text of the toast. |
| `"message"` | The body text of the toast. |
## CSS Custom Properties
| Name | Description |

View File

@@ -13,6 +13,11 @@ import { mdLeaveAnimation } from './animations/md.leave';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @part button - Any button element that is displayed inside of the toast.
* @part container - The element that wraps all child elements.
* @part header - The header text of the toast.
* @part message - The body text of the toast.
*/
@Component({
tag: 'ion-toast',
@@ -121,7 +126,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
*/
@Event({ eventName: 'ionToastDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
constructor() {
connectedCallback() {
prepareOverlay(this.el);
}

View File

@@ -266,7 +266,7 @@ export class VirtualScroll implements ComponentInterface {
let node: HTMLElement | null = el;
while (node && node !== contentEl) {
topOffset += node.offsetTop;
node = node.parentElement;
node = node.offsetParent as HTMLElement;
}
this.viewportOffset = topOffset;
if (scrollEl) {

View File

@@ -1,5 +1,5 @@
// Components interfaces
import {Components as IoniconsComponents} from 'ionicons';
import { Components as IoniconsComponents, JSX as IoniconsJSX } from 'ionicons';
export * from './components';
export * from './index';
export * from './components/alert/alert-interface';
@@ -47,7 +47,7 @@ export type AutocompleteTypes = (
| 'tel-extension' | 'impp' | 'url' | 'photo');
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time';
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time' | 'week' | 'month' | 'datetime-local';
export type Side = 'start' | 'end';
export type PredefinedColors = 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'light' | 'medium' | 'dark';
export type Color = PredefinedColors | string;
@@ -80,3 +80,9 @@ declare module "./components" {
export interface IonIcon extends IoniconsComponents.IonIcon{}
}
}
declare module "./components" {
export namespace JSX {
export interface IonIcon extends IoniconsJSX.IonIcon {}
}
}

View File

@@ -29,4 +29,9 @@ $toolbar-ios-color: var(--ion-toolbar-color, $text-color
// --------------------------------------------------
$item-ios-background: var(--ion-item-background, $background-color) !default;
$item-ios-border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, #c8c7cc))) !default;
$item-ios-color: var(--ion-item-color, $text-color) !default;
$item-ios-color: var(--ion-item-color, $text-color) !default;
// iOS Card
// --------------------------------------------------
$card-ios-background: var(--ion-card-background, $item-ios-background) !default;
$card-ios-color: var(--ion-card-color, var(--ion-item-color, $text-color-step-400)) !default;

View File

@@ -31,3 +31,8 @@ $toolbar-md-color: var(--ion-toolbar-color, var(--ion-t
$item-md-background: var(--ion-item-background, $background-color) !default;
$item-md-border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, .13)))) !default;
$item-md-color: var(--ion-item-color, $text-color) !default;
// Material Design Card
// --------------------------------------------------
$card-md-background: var(--ion-card-background, $item-md-background) !default;
$card-md-color: var(--ion-card-color, var(--ion-item-color, $text-color-step-450)) !default;

View File

@@ -74,6 +74,7 @@ const jsSetFocus = async (
clearTimeout(scrollContentTimeout);
}
window.removeEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
window.removeEventListener('ionKeyboardDidShow', scrollContent);
// scroll the input into place
@@ -89,6 +90,11 @@ const jsSetFocus = async (
inputEl.focus();
};
const doubleKeyboardEventListener = () => {
window.removeEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
window.addEventListener('ionKeyboardDidShow', scrollContent);
};
if (contentEl) {
const scrollEl = await contentEl.getScrollElement();
@@ -106,7 +112,20 @@ const jsSetFocus = async (
*/
const totalScrollAmount = scrollEl.scrollHeight - scrollEl.clientHeight;
if (scrollData.scrollAmount > (totalScrollAmount - scrollEl.scrollTop)) {
window.addEventListener('ionKeyboardDidShow', scrollContent);
/**
* On iOS devices, the system will show a "Passwords" bar above the keyboard
* after the initial keyboard is shown. This prevents the webview from resizing
* until the "Passwords" bar is shown, so we need to wait for that to happen first.
*/
if (inputEl.type === 'password') {
// Add 50px to account for the "Passwords" bar
scrollData.scrollAmount += 50;
window.addEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
} else {
window.addEventListener('ionKeyboardDidShow', scrollContent);
}
/**
* This should only fire in 2 instances:

View File

@@ -5,9 +5,6 @@ const KEYBOARD_THRESHOLD = 150;
let previousVisualViewport: any = {};
let currentVisualViewport: any = {};
let previousLayoutViewport: any = {};
let currentLayoutViewport: any = {};
let keyboardOpen = false;
/**
@@ -16,8 +13,6 @@ let keyboardOpen = false;
export const resetKeyboardAssist = () => {
previousVisualViewport = {};
currentVisualViewport = {};
previousLayoutViewport = {};
currentLayoutViewport = {};
keyboardOpen = false;
};
@@ -27,7 +22,6 @@ export const startKeyboardAssist = (win: Window) => {
if (!(win as any).visualViewport) { return; }
currentVisualViewport = copyVisualViewport((win as any).visualViewport);
currentLayoutViewport = copyLayoutViewport(win);
(win as any).visualViewport.onresize = () => {
trackViewportChanges(win);
@@ -67,7 +61,7 @@ export const setKeyboardClose = (win: Window) => {
* of the previous visual viewport height minus the current
* visual viewport height is greater than KEYBOARD_THRESHOLD
*
* We need to be able to accomodate users who have zooming
* We need to be able to accommodate users who have zooming
* enabled in their browser (or have zoomed in manually) which
* is why we take into account the current visual viewport's
* scale value.
@@ -77,8 +71,7 @@ export const keyboardDidOpen = (): boolean => {
return (
!keyboardOpen &&
previousVisualViewport.width === currentVisualViewport.width &&
scaledHeightDifference > KEYBOARD_THRESHOLD &&
!layoutViewportDidChange()
scaledHeightDifference > KEYBOARD_THRESHOLD
);
};
@@ -100,20 +93,6 @@ export const keyboardDidClose = (win: Window): boolean => {
return keyboardOpen && currentVisualViewport.height === win.innerHeight;
};
/**
* Determine if the layout viewport has
* changed since the last visual viewport change.
* It is rare that a layout viewport change is not
* associated with a visual viewport change so we
* want to make sure we don't get any false positives.
*/
const layoutViewportDidChange = (): boolean => {
return (
currentLayoutViewport.width !== previousLayoutViewport.width ||
currentLayoutViewport.height !== previousLayoutViewport.height
);
};
/**
* Dispatch a keyboard open event
*/
@@ -143,9 +122,6 @@ const fireKeyboardCloseEvent = (win: Window): void => {
export const trackViewportChanges = (win: Window) => {
previousVisualViewport = { ...currentVisualViewport };
currentVisualViewport = copyVisualViewport((win as any).visualViewport);
previousLayoutViewport = { ...currentLayoutViewport };
currentLayoutViewport = copyLayoutViewport(win);
};
/**
@@ -163,14 +139,3 @@ export const copyVisualViewport = (visualViewport: any): any => {
scale: visualViewport.scale
};
};
/**
* Creates a deep copy of the layout viewport
* at a given state
*/
export const copyLayoutViewport = (win: Window): any => {
return {
width: win.innerWidth,
height: win.innerHeight
};
};

View File

@@ -1,4 +1,4 @@
import { copyLayoutViewport, copyVisualViewport, setKeyboardClose, setKeyboardOpen, keyboardDidClose, keyboardDidOpen, keyboardDidResize, resetKeyboardAssist, startKeyboardAssist, trackViewportChanges, KEYBOARD_DID_OPEN, KEYBOARD_DID_CLOSE } from '../keyboard';
import { copyVisualViewport, setKeyboardClose, setKeyboardOpen, keyboardDidClose, keyboardDidOpen, keyboardDidResize, resetKeyboardAssist, startKeyboardAssist, trackViewportChanges, KEYBOARD_DID_OPEN, KEYBOARD_DID_CLOSE } from '../keyboard';
const mockVisualViewport = (win: Window, visualViewport: any = { width: 320, height: 568 }, layoutViewport = { innerWidth: 320, innerHeight: 568 }): any => {
win.visualViewport = {
@@ -32,23 +32,11 @@ const resizeVisualViewport = (win: Window, visualViewport: any = {}) => {
}
}
const resizeLayoutViewport = (win: Window, layoutViewport: any = {}) => {
win = Object.assign(win, { innerWidth: layoutViewport.width, innerHeight: layoutViewport.height });
}
describe('Keyboard Assist Tests', () => {
describe('copyLayoutViewport()', () => {
it('should properly copy the layout viewport', () => {
const win = {
innerWidth: 100,
innerHeight: 200
};
const copiedViewport = copyLayoutViewport(win);
win.innerWidth = 400;
win.innerHeight = 800;
expect(copiedViewport.width).toEqual(100);
expect(copiedViewport.height).toEqual(200);
});
});
describe('copyVisualViewport()', () => {
it('should properly copy the visual viewport', () => {
@@ -110,6 +98,13 @@ describe('Keyboard Assist Tests', () => {
expect(keyboardDidOpen(window)).toEqual(true);
});
it('should return true if the layout and visual viewports resize', () => {
resizeLayoutViewport(window, { width: 320, height: 300 });
resizeVisualViewport(window, { width: 320, height: 300 });
expect(keyboardDidOpen(window)).toEqual(true);
});
it('should return false when visual viewport height < layout viewport heigh but does not meet the keyboard threshold', () => {
resizeVisualViewport(window, { height: 500 });

View File

@@ -35,6 +35,7 @@ export interface OverlayController {
export interface HTMLIonOverlayElement extends HTMLStencilElement {
overlayIndex: number;
backdropDismiss?: boolean;
lastFocus?: HTMLElement;
dismiss(data?: any, role?: string): Promise<boolean>;
}

View File

@@ -3,6 +3,7 @@ import { getIonMode } from '../global/ionic-global';
import { ActionSheetOptions, AlertOptions, Animation, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface';
import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button';
import { getElementRoot } from './helpers';
let lastId = 0;
@@ -31,8 +32,10 @@ export const popoverController = /*@__PURE__*/createController<PopoverOptions, H
export const toastController = /*@__PURE__*/createController<ToastOptions, HTMLIonToastElement>('ion-toast');
export const prepareOverlay = <T extends HTMLIonOverlayElement>(el: T) => {
const doc = document;
connectListeners(doc);
/* tslint:disable-next-line */
if (typeof document !== 'undefined') {
connectListeners(document);
}
const overlayIndex = lastId++;
el.overlayIndex = overlayIndex;
if (!el.hasAttribute('id')) {
@@ -41,35 +44,150 @@ export const prepareOverlay = <T extends HTMLIonOverlayElement>(el: T) => {
};
export const createOverlay = <T extends HTMLIonOverlayElement>(tagName: string, opts: object | undefined): Promise<T> => {
return customElements.whenDefined(tagName).then(() => {
const doc = document;
const element = doc.createElement(tagName) as HTMLIonOverlayElement;
element.classList.add('overlay-hidden');
/* tslint:disable-next-line */
if (typeof customElements !== 'undefined') {
return customElements.whenDefined(tagName).then(() => {
const element = document.createElement(tagName) as HTMLIonOverlayElement;
element.classList.add('overlay-hidden');
// convert the passed in overlay options into props
// that get passed down into the new overlay
Object.assign(element, opts);
// convert the passed in overlay options into props
// that get passed down into the new overlay
Object.assign(element, opts);
// append the overlay element to the document body
getAppRoot(doc).appendChild(element);
// append the overlay element to the document body
getAppRoot(document).appendChild(element);
return element.componentOnReady() as any;
});
return element.componentOnReady() as any;
});
}
return Promise.resolve() as any;
};
const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input:not([type=hidden]), textarea, button, select, .ion-focusable';
const innerFocusableQueryString = 'input:not([type=hidden]), textarea, button, select';
const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
let firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null;
const shadowRoot = firstInput && firstInput.shadowRoot;
if (shadowRoot) {
// If there are no inner focusable elements, just focus the host element.
firstInput = shadowRoot.querySelector(innerFocusableQueryString) || firstInput;
}
if (firstInput) {
firstInput.focus();
} else {
// Focus overlay instead of letting focus escape
overlay.focus();
}
};
const focusLastDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
const inputs = Array.from(ref.querySelectorAll(focusableQueryString)) as HTMLElement[];
let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
const shadowRoot = lastInput && lastInput.shadowRoot;
if (shadowRoot) {
// If there are no inner focusable elements, just focus the host element.
lastInput = shadowRoot.querySelector(innerFocusableQueryString) || lastInput;
}
if (lastInput) {
lastInput.focus();
} else {
// Focus overlay instead of letting focus escape
overlay.focus();
}
};
/**
* Traps keyboard focus inside of overlay components.
* Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html
* This includes the following components: Action Sheet, Alert, Loading, Modal,
* Picker, and Popover.
* Should NOT include: Toast
*/
const trapKeyboardFocus = (ev: Event, doc: Document) => {
const lastOverlay = getOverlay(doc);
const target = ev.target as HTMLElement | null;
// If no active overlay, ignore this event
if (!lastOverlay || !target) { return; }
/**
* If we are focusing the overlay, clear
* the last focused element so that hitting
* tab activates the first focusable element
* in the overlay wrapper.
*/
if (lastOverlay === target) {
lastOverlay.lastFocus = undefined;
/**
* Otherwise, we must be focusing an element
* inside of the overlay. The two possible options
* here are an input/button/etc or the ion-focus-trap
* element. The focus trap element is used to prevent
* the keyboard focus from leaving the overlay when
* using Tab or screen assistants.
*/
} else {
/**
* We do not want to focus the traps, so get the overlay
* wrapper element as the traps live outside of the wrapper.
*/
const overlayRoot = getElementRoot(lastOverlay);
const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
if (!overlayWrapper) { return; }
/**
* If the target is inside the wrapper, let the browser
* focus as normal and keep a log of the last focused element.
*/
if (overlayWrapper.contains(target)) {
lastOverlay.lastFocus = target;
} else {
/**
* Otherwise, we must have focused one of the focus traps.
* We need to wrap the focus to either the first element
* or the last element.
*/
/**
* Once we call `focusFirstDescendant` and focus the first
* descendant, another focus event will fire which will
* cause `lastOverlay.lastFocus` to be updated before
* we can run the code after that. We will cache the value
* here to avoid that.
*/
const lastFocus = lastOverlay.lastFocus;
// Focus the first element in the overlay wrapper
focusFirstDescendant(overlayWrapper, lastOverlay);
/**
* If the cached last focused element is the
* same as the active element, then we need
* to wrap focus to the last descendant. This happens
* when the first descendant is focused, and the user
* presses Shift + Tab. The previous line will focus
* the same descendant again (the first one), causing
* last focus to equal the active element.
*/
if (lastFocus === doc.activeElement) {
focusLastDescendant(overlayWrapper, lastOverlay);
}
lastOverlay.lastFocus = doc.activeElement as HTMLElement;
}
}
};
export const connectListeners = (doc: Document) => {
if (lastId === 0) {
lastId = 1;
// trap focus inside overlays
doc.addEventListener('focusin', ev => {
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss && !isDescendant(lastOverlay, ev.target as HTMLElement)) {
const firstInput = lastOverlay.querySelector('input,button') as HTMLElement | null;
if (firstInput) {
firstInput.focus();
}
}
});
doc.addEventListener('focus', ev => trapKeyboardFocus(ev, doc), true);
// handle back-button click
doc.addEventListener('ionBackButton', ev => {
@@ -242,16 +360,6 @@ export const isCancel = (role: string | undefined): boolean => {
return role === 'cancel' || role === BACKDROP;
};
const isDescendant = (parent: HTMLElement, child: HTMLElement | null) => {
while (child) {
if (child === parent) {
return true;
}
child = child.parentElement;
}
return false;
};
const defaultGate = (h: any) => h();
export const safeCall = (handler: any, arg?: any) => {

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "5.2.3",
"version": "5.3.1",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "5.2.3",
"version": "5.3.1",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -49,7 +49,7 @@
"@angular/core": "8.2.13",
"@angular/platform-browser": "8.2.13",
"@angular/platform-server": "8.2.13",
"@ionic/core": "5.2.3",
"@ionic/core": "5.3.1",
"ng-packagr": "5.7.1",
"tslint": "^5.12.1",
"tslint-ionic-rules": "0.0.21",

1
packages/react-router/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.tgz

View File

@@ -0,0 +1 @@
package-lock=false

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react-router",
"version": "5.2.3",
"version": "5.3.1",
"description": "React Router wrapper for @ionic/react",
"keywords": [
"ionic",
@@ -39,16 +39,16 @@
"tslib": "*"
},
"peerDependencies": {
"@ionic/core": "5.2.3",
"@ionic/react": "5.2.3",
"@ionic/core": "5.3.1",
"@ionic/react": "5.3.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1"
},
"devDependencies": {
"@ionic/core": "5.2.3",
"@ionic/react": "5.2.3",
"@ionic/core": "5.3.1",
"@ionic/react": "5.3.1",
"@rollup/plugin-node-resolve": "^8.1.0",
"@testing-library/jest-dom": "^5.11.0",
"@testing-library/react": "^10.4.3",
@@ -81,7 +81,8 @@
"testPathIgnorePatterns": [
"node_modules",
"dist-transpiled",
"dist"
"dist",
"<rootDir>/test-app"
],
"globals": {
"ts-jest": {

View File

@@ -1,15 +1,41 @@
import { Action as HistoryAction, History, Location as HistoryLocation, createHashHistory as createHistory } from 'history';
import React from 'react';
import { HashRouter, HashRouterProps } from 'react-router-dom';
import { BrowserRouterProps, Router } from 'react-router-dom';
import { RouteManagerWithRouter } from './Router';
import { IonRouter } from './IonRouter';
interface IonReactHashRouterProps<THistoryLocationState = History.PoorMansUnknown> extends BrowserRouterProps {
history?: History<THistoryLocationState>;
}
export class IonReactHashRouter extends React.Component<IonReactHashRouterProps> {
history: History<History.PoorMansUnknown>;
historyListenHandler?: ((location: HistoryLocation, action: HistoryAction) => void);
constructor(props: IonReactHashRouterProps) {
super(props);
const { history, ...rest } = props;
this.history = history || createHistory(rest);
this.history.listen(this.handleHistoryChange.bind(this));
this.registerHistoryListener = this.registerHistoryListener.bind(this);
}
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
if (this.historyListenHandler) {
this.historyListenHandler(location, action);
}
}
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
this.historyListenHandler = cb;
}
export class IonReactHashRouter extends React.Component<HashRouterProps> {
render() {
const { children, ...props } = this.props;
return (
<HashRouter {...props}>
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
</HashRouter>
<Router history={this.history} {...props}>
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
</Router>
);
}
}

View File

@@ -1,21 +1,40 @@
import { MemoryHistory } from 'history';
import { Action as HistoryAction, Location as HistoryLocation, MemoryHistory } from 'history';
import React from 'react';
import { MemoryRouter, MemoryRouterProps, matchPath } from 'react-router';
import { MemoryRouterProps, Router } from 'react-router';
import { LocationState, RouteManager } from './Router';
import { IonRouter, LocationState } from './IonRouter';
interface IonReactMemoryRouterProps extends MemoryRouterProps {
history: MemoryHistory<LocationState>;
}
export class IonReactMemoryRouter extends React.Component<IonReactMemoryRouterProps> {
history: MemoryHistory<LocationState>;
historyListenHandler?: ((location: HistoryLocation, action: HistoryAction) => void);
constructor(props: IonReactMemoryRouterProps) {
super(props);
this.history = props.history;
this.history.listen(this.handleHistoryChange.bind(this));
this.registerHistoryListener = this.registerHistoryListener.bind(this);
}
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
if (this.historyListenHandler) {
this.historyListenHandler(location, action);
}
}
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
this.historyListenHandler = cb;
}
render() {
const { children, history, ...props } = this.props;
const match = matchPath(history.location.pathname, this.props);
const { children, ...props } = this.props;
return (
<MemoryRouter {...props}>
<RouteManager history={history} location={history.location} match={match!}>{children}</RouteManager>
</MemoryRouter>
<Router {...props}>
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
</Router>
);
}
}

View File

@@ -1,15 +1,42 @@
import { Action as HistoryAction, History, Location as HistoryLocation, createBrowserHistory as createHistory } from 'history';
import React from 'react';
import { BrowserRouter, BrowserRouterProps } from 'react-router-dom';
import { BrowserRouterProps, Router } from 'react-router-dom';
import { RouteManagerWithRouter } from './Router';
import { IonRouter } from './IonRouter';
interface IonReactRouterProps<THistoryLocationState = History.PoorMansUnknown> extends BrowserRouterProps {
history?: History<THistoryLocationState>;
}
export class IonReactRouter extends React.Component<IonReactRouterProps> {
historyListenHandler?: ((location: HistoryLocation, action: HistoryAction) => void);
history: History<History.PoorMansUnknown>;
constructor(props: IonReactRouterProps) {
super(props);
const { history, ...rest } = props;
this.history = history || createHistory(rest);
this.history.listen(this.handleHistoryChange.bind(this));
this.registerHistoryListener = this.registerHistoryListener.bind(this);
}
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
if (this.historyListenHandler) {
this.historyListenHandler(location, action);
}
}
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
this.historyListenHandler = cb;
}
export class IonReactRouter extends React.Component<BrowserRouterProps> {
render() {
const { children, ...props } = this.props;
return (
<BrowserRouter {...props}>
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
</BrowserRouter>
<Router history={this.history} {...props}>
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
</Router>
);
}
}

View File

@@ -1 +0,0 @@
export type IonRouteAction = 'push' | 'replace' | 'pop';

View File

@@ -1,6 +0,0 @@
import { RouteProps, match } from 'react-router-dom';
export interface IonRouteData {
match: match | null;
childProps: RouteProps;
}

View File

@@ -0,0 +1,11 @@
import { IonRouteProps } from '@ionic/react';
import React from 'react';
import { Route } from 'react-router';
export class IonRouteInner extends React.PureComponent<IonRouteProps> {
render() {
return (
<Route path={this.props.path} exact={this.props.exact} render={this.props.render} computedMatch={(this.props as any).computedMatch} />
);
}
}

View File

@@ -0,0 +1,273 @@
import { AnimationBuilder } from '@ionic/core';
import {
LocationHistory,
NavManager,
RouteAction,
RouteInfo,
RouteManagerContext,
RouteManagerContextState,
RouterDirection,
ViewItem,
generateId,
getConfig
} from '@ionic/react';
import { Action as HistoryAction, Location as HistoryLocation } from 'history';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { IonRouteInner } from './IonRouteInner';
import { ReactRouterViewStack } from './ReactRouterViewStack';
import StackManager from './StackManager';
export interface LocationState {
direction?: RouterDirection;
routerOptions?: { as?: string, unmount?: boolean; };
}
interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> {
registerHistoryListener: (cb: (location: HistoryLocation<any>, action: HistoryAction) => void) => void;
}
interface IonRouteState {
routeInfo: RouteInfo;
}
class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
currentTab?: string;
exitViewFromOtherOutletHandlers: ((pathname: string) => ViewItem | undefined)[] = [];
incomingRouteParams?: Partial<RouteInfo>;
locationHistory = new LocationHistory();
viewStack = new ReactRouterViewStack();
routeMangerContextState: RouteManagerContextState = {
canGoBack: () => this.locationHistory.canGoBack(),
clearOutlet: this.viewStack.clear,
getViewItemForTransition: this.viewStack.getViewItemForTransition,
getChildrenToRender: this.viewStack.getChildrenToRender,
goBack: () => this.handleNavigateBack(),
createViewItem: this.viewStack.createViewItem,
findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
addViewItem: this.viewStack.add,
unMountViewItem: this.viewStack.remove
};
constructor(props: IonRouteProps) {
super(props);
const routeInfo = {
id: generateId('routeInfo'),
pathname: this.props.location.pathname,
search: this.props.location.search
};
this.locationHistory.add(routeInfo);
this.handleChangeTab = this.handleChangeTab.bind(this);
this.handleResetTab = this.handleResetTab.bind(this);
this.handleNavigate = this.handleNavigate.bind(this);
this.handleNavigateBack = this.handleNavigateBack.bind(this);
this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
this.state = {
routeInfo
};
}
handleChangeTab(tab: string, path: string, routeOptions?: any) {
const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
const [pathname, search] = path.split('?');
if (routeInfo) {
this.incomingRouteParams = { ...routeInfo, routeAction: 'push', routeDirection: 'none' };
if (routeInfo.pathname === pathname) {
this.incomingRouteParams.routeOptions = routeOptions;
this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
} else {
this.incomingRouteParams.pathname = pathname;
this.incomingRouteParams.search = search ? '?' + search : undefined;
this.incomingRouteParams.routeOptions = routeOptions;
this.props.history.push(pathname + (search ? '?' + search : ''));
}
} else {
this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
}
}
handleHistoryChange(location: HistoryLocation<LocationState>, action: HistoryAction) {
let leavingLocationInfo: RouteInfo;
if (this.incomingRouteParams) {
if (this.incomingRouteParams.routeAction === 'replace') {
leavingLocationInfo = this.locationHistory.previous();
} else {
leavingLocationInfo = this.locationHistory.current();
}
} else if (action === 'REPLACE') {
leavingLocationInfo = this.locationHistory.previous();
} else {
leavingLocationInfo = this.locationHistory.current();
}
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
if (leavingUrl !== location.pathname) {
if (!this.incomingRouteParams) {
if (action === 'REPLACE') {
this.incomingRouteParams = {
routeAction: 'replace',
routeDirection: 'none',
tab: this.currentTab
};
}
if (action === 'POP') {
const ri = this.locationHistory.current();
if (ri && ri.pushedByRoute) {
const prevInfo = this.locationHistory.findLastLocation(ri);
this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
} else {
const direction = 'none';
this.incomingRouteParams = {
routeAction: 'pop',
routeDirection: direction,
tab: this.currentTab
};
}
}
if (!this.incomingRouteParams) {
this.incomingRouteParams = {
routeAction: 'push',
routeDirection: location.state?.direction || 'forward',
routeOptions: location.state?.routerOptions,
tab: this.currentTab
};
}
}
let routeInfo: RouteInfo;
if (this.incomingRouteParams?.id) {
routeInfo = {
...this.incomingRouteParams as RouteInfo,
lastPathname: leavingLocationInfo.pathname
};
this.locationHistory.add(routeInfo);
} else {
const isPushed = (this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward');
routeInfo = {
id: generateId('routeInfo'),
...this.incomingRouteParams,
lastPathname: leavingLocationInfo.pathname,
pathname: location.pathname,
search: location.search,
params: this.props.match.params
};
if (isPushed) {
routeInfo.tab = leavingLocationInfo.tab;
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
} else if (routeInfo.routeAction === 'pop') {
const r = this.locationHistory.findLastLocation(routeInfo);
routeInfo.pushedByRoute = r?.pushedByRoute;
} else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
// If we are switching tabs grab the last route info for the tab and use its pushedByRoute
const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
} else if (routeInfo.routeAction === 'replace') {
// Make sure to set the lastPathname, etc.. to the current route so the page transitions out
const currentRouteInfo = this.locationHistory.current();
routeInfo.lastPathname = currentRouteInfo?.pathname || routeInfo.lastPathname;
routeInfo.pushedByRoute = currentRouteInfo?.pushedByRoute || routeInfo.pushedByRoute;
routeInfo.routeDirection = currentRouteInfo?.routeDirection || routeInfo.routeDirection;
routeInfo.routeAnimation = currentRouteInfo?.routeAnimation || routeInfo.routeAnimation;
}
this.locationHistory.add(routeInfo);
}
this.setState({
routeInfo
});
}
this.incomingRouteParams = undefined;
}
handleNavigate(path: string, routeAction: RouteAction, routeDirection?: RouterDirection, routeAnimation?: AnimationBuilder, routeOptions?: any, tab?: string) {
this.incomingRouteParams = {
routeAction,
routeDirection,
routeOptions,
routeAnimation,
tab
};
if (routeAction === 'push') {
this.props.history.push(path);
} else {
this.props.history.replace(path);
}
}
handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) {
const config = getConfig();
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
const routeInfo = this.locationHistory.current();
if (routeInfo && routeInfo.pushedByRoute) {
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
if (prevInfo) {
this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back', routeAnimation: routeAnimation || routeInfo.routeAnimation };
if (routeInfo.lastPathname === routeInfo.pushedByRoute) {
this.props.history.goBack();
} else {
this.props.history.replace(prevInfo.pathname + (prevInfo.search || ''));
}
} else {
this.handleNavigate(defaultHref as string, 'pop', 'back');
}
} else {
this.handleNavigate(defaultHref as string, 'pop', 'back');
}
}
handleResetTab(tab: string, originalHref: string, originalRouteOptions: any) {
const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
if (routeInfo) {
const newRouteInfo = { ...routeInfo };
newRouteInfo.pathname = originalHref;
newRouteInfo.routeOptions = originalRouteOptions;
this.incomingRouteParams = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
}
}
handleSetCurrentTab(tab: string) {
this.currentTab = tab;
const ri = { ...this.locationHistory.current() };
if (ri.tab !== tab) {
ri.tab = tab;
this.locationHistory.update(ri);
}
}
render() {
return (
<RouteManagerContext.Provider
value={this.routeMangerContextState}
>
<NavManager
ionRoute={IonRouteInner}
ionRedirect={{}}
stackManager={StackManager}
routeInfo={this.state.routeInfo!}
onNavigateBack={this.handleNavigateBack}
onNavigate={this.handleNavigate}
onSetCurrentTab={this.handleSetCurrentTab}
onChangeTab={this.handleChangeTab}
onResetTab={this.handleResetTab}
locationHistory={this.locationHistory}
>
{this.props.children}
</NavManager>
</RouteManagerContext.Provider>
);
}
}
export const IonRouter = withRouter(IonRouterInner);
IonRouter.displayName = 'IonRouter';

View File

@@ -1,77 +0,0 @@
import { RouterDirection } from '@ionic/core';
import { NavContext, NavContextState } from '@ionic/react';
import { Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { IonRouteAction } from './IonRouteAction';
import { StackManager } from './StackManager';
interface NavManagerProps extends RouteComponentProps {
onNavigateBack: (defaultHref?: string) => void;
onNavigate: (ionRouteAction: IonRouteAction, path: string, state?: any) => void;
}
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
listenUnregisterCallback: UnregisterCallback | undefined;
constructor(props: NavManagerProps) {
super(props);
this.state = {
goBack: this.goBack.bind(this),
hasIonicRouter: () => true,
navigate: this.navigate.bind(this),
getStackManager: this.getStackManager.bind(this),
getPageManager: this.getPageManager.bind(this),
currentPath: this.props.location.pathname,
registerIonPage: () => { return; } // overridden in View for each IonPage
};
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
this.setState({
currentPath: location.pathname
});
});
if (document) {
document.addEventListener('ionBackButton', (e: any) => {
e.detail.register(0, (processNextHandler: () => void) => {
this.props.history.goBack();
processNextHandler();
});
});
}
}
componentWillUnmount() {
if (this.listenUnregisterCallback) {
this.listenUnregisterCallback();
}
}
goBack(defaultHref?: string) {
this.props.onNavigateBack(defaultHref);
}
navigate(path: string, direction?: RouterDirection | 'none', ionRouteAction: IonRouteAction = 'push') {
this.props.onNavigate(ionRouteAction, path, direction);
}
getPageManager() {
return (children: any) => children;
}
getStackManager() {
return StackManager;
}
render() {
return (
<NavContext.Provider value={this.state}>
{this.props.children}
</NavContext.Provider>
);
}
}

View File

@@ -0,0 +1,178 @@
import { IonRoute, RouteInfo, ViewItem, ViewLifeCycleManager, ViewStacks, generateId } from '@ionic/react';
import React from 'react';
import { matchPath } from 'react-router';
export class ReactRouterViewStack extends ViewStacks {
constructor() {
super();
this.createViewItem = this.createViewItem.bind(this);
this.findViewItemByRouteInfo = this.findViewItemByRouteInfo.bind(this);
this.findLeavingViewItemByRouteInfo = this.findLeavingViewItemByRouteInfo.bind(this);
this.getChildrenToRender = this.getChildrenToRender.bind(this);
this.getViewItemForTransition = this.getViewItemForTransition.bind(this);
}
createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) {
const viewItem: ViewItem = {
id: generateId('viewItem'),
outletId,
ionPageElement: page,
reactElement,
mount: true,
ionRoute: false
};
const matchProps = {
exact: reactElement.props.exact,
path: reactElement.props.path || reactElement.props.from,
component: reactElement.props.component
};
const match = matchPath(routeInfo.pathname, matchProps);
if (reactElement.type === IonRoute) {
viewItem.ionRoute = true;
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
}
viewItem.routeData = {
match,
childProps: reactElement.props
};
return viewItem;
}
getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) {
const viewItems = this.getViewItemsForOutlet(outletId);
// Sync latest routes with viewItems
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
const viewItem = viewItems.find(v => {
return matchComponent(child, v.routeData.childProps.path || v.routeData.childProps.from);
});
if (viewItem) {
viewItem.reactElement = child;
}
});
const children = viewItems.map(viewItem => {
let clonedChild;
if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
clonedChild = (
<ViewLifeCycleManager key={`view-${viewItem.id}`} mount={viewItem.mount} removeView={() => this.remove(viewItem)}>
{React.cloneElement(viewItem.reactElement, {
computedMatch: viewItem.routeData.match
})}
</ViewLifeCycleManager>
);
} else {
const match = matchComponent(viewItem.reactElement, routeInfo.pathname);
clonedChild = (
<ViewLifeCycleManager key={`view-${viewItem.id}`} mount={viewItem.mount} removeView={() => this.remove(viewItem)}>
{React.cloneElement(viewItem.reactElement, {
computedMatch: viewItem.routeData.match
})}
</ViewLifeCycleManager>
);
if (!match && viewItem.routeData.match) {
viewItem.routeData.match = undefined;
viewItem.mount = false;
}
}
return clonedChild;
});
return children;
}
findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string) {
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
if (viewItem && match) {
viewItem.routeData.match = match;
}
return viewItem;
}
findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string) {
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, false, true);
return viewItem;
}
getViewItemForTransition(pathname: string) {
const { viewItem } = this.findViewItemByPath(pathname, undefined, true, true);
return viewItem;
}
private findViewItemByPath(pathname: string, outletId?: string, forceExact?: boolean, mustBeIonRoute?: boolean) {
let viewItem: ViewItem | undefined;
let match: ReturnType<typeof matchPath> | undefined;
let viewStack: ViewItem[];
if (outletId) {
viewStack = this.getViewItemsForOutlet(outletId);
viewStack.some(matchView);
if (!viewItem) {
viewStack.some(matchDefaultRoute);
}
} else {
const viewItems = this.getAllViewItems();
viewItems.some(matchView);
if (!viewItem) {
viewItems.some(matchDefaultRoute);
}
}
return { viewItem, match };
function matchView(v: ViewItem) {
if (mustBeIonRoute && !v.ionRoute) {
return false;
}
const matchProps = {
exact: forceExact ? true : v.routeData.childProps.exact,
path: v.routeData.childProps.path || v.routeData.childProps.from,
component: v.routeData.childProps.component
};
const myMatch = matchPath(pathname, matchProps);
if (myMatch) {
viewItem = v;
match = myMatch;
return true;
}
return false;
}
function matchDefaultRoute(v: ViewItem) {
// try to find a route that doesn't have a path or from prop, that will be our default route
if (!v.routeData.childProps.path && !v.routeData.childProps.from) {
match = {
path: pathname,
url: pathname,
isExact: true,
params: {}
};
viewItem = v;
return true;
}
return false;
}
}
}
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
const matchProps = {
exact: forceExact ? true : node.props.exact,
path: node.props.path || node.props.from,
component: node.props.component
};
const match = matchPath(pathname, matchProps);
return match;
}

View File

@@ -1,27 +0,0 @@
import React, { ReactNode } from 'react';
import { ViewStacks } from './ViewStacks';
export interface RouteManagerContextState {
syncView: (page: HTMLElement, viewId: string) => void;
syncRoute: (route: any) => void;
hideView: (viewId: string) => void;
viewStacks: ViewStacks;
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
removeViewStack: (stack: string) => void;
getRoute: (id: string) => any;
}
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
viewStacks: new ViewStacks(),
syncView: () => { navContextNotFoundError(); },
syncRoute: () => { navContextNotFoundError(); },
hideView: () => { navContextNotFoundError(); },
setupIonRouter: () => Promise.reject(navContextNotFoundError()),
removeViewStack: () => { navContextNotFoundError(); },
getRoute: () => { navContextNotFoundError(); }
});
function navContextNotFoundError() {
console.error('IonReactRouter not found, did you add it to the app?');
}

View File

@@ -1,529 +0,0 @@
import { NavDirection } from '@ionic/core';
import { RouterDirection, getConfig } from '@ionic/react';
import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
import { generateId, isDevMode } from '../utils';
import { LocationHistory } from '../utils/LocationHistory';
import { IonRouteAction } from './IonRouteAction';
import { IonRouteData } from './IonRouteData';
import { NavManager } from './NavManager';
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
import { ViewItem } from './ViewItem';
import { ViewStack, ViewStacks } from './ViewStacks';
export interface LocationState {
direction?: RouterDirection;
action?: IonRouteAction;
}
interface RouteManagerProps extends RouteComponentProps<{}, {}, LocationState> {
location: HistoryLocation<LocationState>;
}
interface RouteManagerState extends RouteManagerContextState {
location?: HistoryLocation<LocationState>;
action?: IonRouteAction;
}
export class RouteManager extends React.Component<RouteManagerProps, RouteManagerState> {
listenUnregisterCallback: UnregisterCallback | undefined;
activeIonPageId?: string;
currentIonRouteAction?: IonRouteAction;
currentRouteDirection?: RouterDirection;
locationHistory = new LocationHistory();
routes: { [key: string]: React.ReactElement<any>; } = {};
ionPageElements: { [key: string]: HTMLElement; } = {};
routerOutlets: { [key: string]: HTMLIonRouterOutletElement; } = {};
firstRender = true;
constructor(props: RouteManagerProps) {
super(props);
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
this.handleNavigate = this.handleNavigate.bind(this);
this.navigateBack = this.navigateBack.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),
syncRoute: this.syncRoute.bind(this),
getRoute: this.getRoute.bind(this)
};
this.locationHistory.add({
hash: window.location.hash,
key: generateId(),
pathname: window.location.pathname,
search: window.location.search,
state: {}
});
}
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
// Trigger a page change if the location or action is different
if (this.state.location && prevState.location !== this.state.location || prevState.action !== this.state.action) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
this.setActiveView(this.state.location!, this.state.action!, viewStacks);
}
}
componentWillUnmount() {
if (this.listenUnregisterCallback) {
this.listenUnregisterCallback();
}
}
getRoute(id: string) {
return this.routes[id];
}
hideView(viewId: string) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
const { view } = viewStacks.findViewInfoById(viewId);
if (view) {
view.show = false;
view.isIonRoute = false;
view.prevId = undefined;
view.key = generateId();
delete this.ionPageElements[view.id];
this.setState({
viewStacks
});
}
}
historyChange(location: HistoryLocation<LocationState>, action: HistoryAction) {
const ionRouteAction = this.currentIonRouteAction === 'pop' ? 'pop' : action.toLowerCase() as IonRouteAction;
let direction = this.currentRouteDirection;
if (ionRouteAction === 'push') {
this.locationHistory.add(location);
} else if (ionRouteAction === 'pop') {
this.locationHistory.pop();
direction = direction || 'back';
} else if (ionRouteAction === 'replace') {
this.locationHistory.replace(location);
direction = 'none';
}
if (direction === 'root') {
this.locationHistory.clear();
this.locationHistory.add(location);
}
location.state = location.state || { direction };
this.setState({
location,
action: ionRouteAction as IonRouteAction
});
this.currentRouteDirection = undefined;
this.currentIonRouteAction = undefined;
}
setActiveView(location: HistoryLocation<LocationState>, action: IonRouteAction, viewStacks: ViewStacks) {
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
let leavingView: ViewItem | undefined;
const viewStackKeys = viewStacks.getKeys();
let shouldTransitionPage = false;
let leavingViewHtml: string | undefined;
viewStackKeys.forEach(key => {
const { view: enteringView, viewStack: enteringViewStack, match } = viewStacks.findViewInfoByLocation(location, key);
if (!enteringView || !enteringViewStack) {
return;
}
leavingView = viewStacks.findViewInfoById(this.activeIonPageId).view;
if (enteringView.isIonRoute) {
enteringView.show = true;
enteringView.mount = true;
enteringView.routeData.match = match!;
shouldTransitionPage = true;
this.activeIonPageId = enteringView.id;
if (leavingView) {
if (action === 'push' && direction === 'forward') {
/**
* If the page is being pushed into the stack by another view,
* record the view that originally directed to the new view for back button purposes.
*/
enteringView.prevId = leavingView.id;
} else if (direction !== 'none') {
leavingView.mount = false;
this.removeOrphanedViews(enteringView, enteringViewStack);
}
leavingViewHtml = enteringView.id === leavingView.id ? this.ionPageElements[leavingView.id].outerHTML : undefined;
} else {
// If there is not a leavingView, then we shouldn't provide a direction
direction = undefined;
}
} else {
enteringView.show = true;
enteringView.mount = true;
enteringView.routeData.match = match!;
}
});
if (leavingView) {
if (!leavingView.isIonRoute) {
leavingView.mount = false;
leavingView.show = false;
}
}
this.setState({
viewStacks
}, () => {
if (shouldTransitionPage) {
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (enteringView && viewStack) {
const enteringEl = this.ionPageElements[enteringView.id];
const leavingEl = leavingView && this.ionPageElements[leavingView.id];
if (enteringEl) {
let navDirection: NavDirection | undefined;
if (leavingEl && leavingEl.innerHTML === '') {
// Don't animate from an empty view
navDirection = undefined;
} else if (direction === 'none' || direction === 'root') {
navDirection = undefined;
} else {
navDirection = direction;
}
const shouldGoBack = !!enteringView.prevId;
const routerOutlet = this.routerOutlets[viewStack.id];
this.commitView(
enteringEl!,
leavingEl!,
routerOutlet,
navDirection,
shouldGoBack,
leavingViewHtml);
} else if (leavingEl) {
leavingEl.classList.add('ion-page-hidden');
leavingEl.setAttribute('aria-hidden', 'true');
}
}
// Warn if an IonPage was not eventually rendered in Dev Mode
if (isDevMode()) {
if (enteringView && enteringView.routeData.match!.url !== location.pathname) {
setTimeout(() => {
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (view!.routeData.match!.url !== location.pathname) {
console.warn('No IonPage was found to render. Make sure you wrap your page with an IonPage component.');
}
}, 100);
}
}
}
});
}
removeOrphanedViews(view: ViewItem, viewStack: ViewStack) {
// Note: This technique is a bit wonky for views that reference each other and get into a circular loop.
// It can still remove a view that probably shouldn't be.
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
const page = this.ionPageElements[v.id];
if (page.classList.contains('ion-page-hidden')) {
v.show = false;
v.isIonRoute = false;
v.prevId = undefined;
v.key = generateId();
delete this.ionPageElements[v.id];
}
v.mount = false;
}
});
}
setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
const views: ViewItem[] = [];
let activeId: string | undefined;
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
let foundMatch = false;
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
const routeId = generateId();
this.routes[routeId] = child;
views.push(createViewItem(child, routeId, this.props.history.location));
});
if (!foundMatch) {
const notFoundRoute = views.find(r => {
// try to find a route that doesn't have a path or from prop, that will be our not found route
return !r.routeData.childProps.path && !r.routeData.childProps.from;
});
if (notFoundRoute) {
notFoundRoute.show = true;
}
}
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
function createViewItem(child: React.ReactElement<any>, routeId: string, location: HistoryLocation) {
const viewId = generateId();
const key = generateId();
// const route = child;
const matchProps = {
exact: child.props.exact,
path: child.props.path || child.props.from,
component: child.props.component
};
const match: IonRouteData['match'] = matchPath(location.pathname, matchProps);
const view: ViewItem<IonRouteData> = {
id: viewId,
key,
routeData: {
match,
childProps: child.props
},
routeId,
mount: true,
show: !!match,
isIonRoute: false
};
if (match && view.isIonRoute) {
activeId = viewId;
}
if (!foundMatch && match) {
foundMatch = true;
}
return view;
}
}
registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) {
this.setState(prevState => {
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
const newStack: ViewStack = {
id: stack,
views: stackItems
};
this.routerOutlets[stack] = routerOutlet;
if (activeId) {
this.activeIonPageId = activeId;
}
prevViewStacks.set(stack, newStack);
return {
viewStacks: prevViewStacks
};
}, () => {
this.setupRouterOutlet(routerOutlet);
});
}
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const canStart = () => {
const config = getConfig();
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
if (swipeEnabled) {
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
return !!(view && view.prevId);
} else {
return false;
}
};
const onStart = () => {
this.navigateBack();
};
routerOutlet.swipeHandler = {
canStart,
onStart,
onEnd: _shouldContinue => true
};
}
removeViewStack(stack: string) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
viewStacks.delete(stack);
this.setState({
viewStacks
});
}
syncView(page: HTMLElement, viewId: string) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
const { view } = viewStacks.findViewInfoById(viewId);
if (view) {
view.isIonRoute = true;
this.ionPageElements[view.id] = page;
this.setActiveView(this.state.location || this.props.location, this.state.action!, viewStacks);
}
}
syncRoute(routerOutlet: any) {
const ionRouterOutlet = React.Children.only(routerOutlet) as React.ReactElement;
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
for (const routeKey in this.routes) {
const route = this.routes[routeKey];
if (
((route.props.path || route.props.from) === (child.props.path || child.props.from)) &&
(route.props.exact === child.props.exact) &&
(route.props.to === child.props.to)
) {
this.routes[routeKey] = child;
}
}
});
}
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean, leavingViewHtml?: string) {
if (!this.firstRender) {
if (!('componentOnReady' in ionRouterOutlet)) {
await waitUntilRouterOutletReady(ionRouterOutlet);
}
if ((enteringEl === leavingEl) && direction && leavingViewHtml) {
// If a page is transitioning to another version of itself
// we clone it so we can have an animation to show
const newLeavingElement = clonePageElement(leavingViewHtml);
ionRouterOutlet.appendChild(newLeavingElement);
await ionRouterOutlet.commit(enteringEl, newLeavingElement, {
deepWait: true,
duration: direction === undefined ? 0 : undefined,
direction,
showGoBack,
progressAnimation: false
});
ionRouterOutlet.removeChild(newLeavingElement);
} else {
await ionRouterOutlet.commit(enteringEl, leavingEl, {
deepWait: true,
duration: direction === undefined ? 0 : undefined,
direction,
showGoBack,
progressAnimation: false
});
}
if (leavingEl && (enteringEl !== leavingEl)) {
/** add hidden attributes */
leavingEl.classList.add('ion-page-hidden');
leavingEl.setAttribute('aria-hidden', 'true');
}
} else {
enteringEl.classList.remove('ion-page-invisible');
enteringEl.style.zIndex = '101';
enteringEl.dispatchEvent(new Event('ionViewWillEnter'));
enteringEl.dispatchEvent(new Event('ionViewDidEnter'));
this.firstRender = false;
}
}
handleNavigate(ionRouteAction: IonRouteAction, path: string, direction?: RouterDirection) {
this.currentIonRouteAction = ionRouteAction;
switch (ionRouteAction) {
case 'push':
this.currentRouteDirection = direction;
this.props.history.push(path);
break;
case 'pop':
this.currentRouteDirection = direction || 'back';
this.props.history.replace(path);
break;
case 'replace':
this.currentRouteDirection = 'none';
this.props.history.replace(path);
break;
}
}
navigateBack(defaultHref?: string) {
const config = getConfig();
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
const { view: leavingView } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (leavingView) {
if (leavingView.id === leavingView.prevId) {
const previousLocation = this.locationHistory.previous();
if (previousLocation) {
this.handleNavigate('pop', previousLocation.pathname + previousLocation.search);
} else {
defaultHref && this.handleNavigate('pop', defaultHref);
}
} else {
const { view: enteringView } = this.state.viewStacks.findViewInfoById(leavingView.prevId);
if (enteringView) {
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
if (lastLocation) {
this.handleNavigate('pop', lastLocation.pathname + lastLocation.search);
} else {
this.handleNavigate('pop', enteringView.routeData.match!.url);
}
} else {
const currentLocation = this.locationHistory.previous();
if (currentLocation) {
this.handleNavigate('pop', currentLocation.pathname + currentLocation.search);
} else {
if (defaultHref) {
this.handleNavigate('pop', defaultHref);
}
}
}
}
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
}
}
}
render() {
return (
<RouteManagerContext.Provider value={this.state}>
<NavManager
{...this.props}
onNavigateBack={this.navigateBack}
onNavigate={this.handleNavigate}
>
{this.props.children}
</NavManager>
</RouteManagerContext.Provider>
);
}
}
function clonePageElement(leavingViewHtml: string) {
const newEl = document.createElement('div');
newEl.innerHTML = leavingViewHtml;
newEl.classList.add('ion-page-hidden');
newEl.style.zIndex = '';
// Remove an existing back button so the new element doesn't get two of them
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
if (ionBackButton[0]) {
ionBackButton[0].innerHTML = '';
}
return newEl.firstChild as HTMLElement;
}
async function waitUntilRouterOutletReady(ionRouterOutlet: HTMLIonRouterElement) {
if ('componentOnReady' in ionRouterOutlet) {
return;
} else {
setTimeout(() => {
waitUntilRouterOutletReady(ionRouterOutlet);
}, 0);
}
}
export const RouteManagerWithRouter = withRouter(RouteManager);
RouteManagerWithRouter.displayName = 'RouteManager';

View File

@@ -1,109 +1,266 @@
import {
RouteInfo,
RouteManagerContext,
StackContext,
StackContextState,
ViewItem,
generateId,
getConfig
} from '@ionic/react';
import React from 'react';
import { matchPath } from 'react-router-dom';
import { generateId, isDevMode } from '../utils';
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
import { View } from './View';
import { ViewItem } from './ViewItem';
import { ViewTransitionManager } from './ViewTransitionManager';
import { clonePageElement } from './clonePageElement';
interface StackManagerProps {
id?: string;
routeManager: RouteManagerContextState;
children?: React.ReactNode;
routeInfo: RouteInfo;
}
interface StackManagerState { }
class StackManagerInner extends React.Component<StackManagerProps, StackManagerState> {
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
export class StackManager extends React.PureComponent<StackManagerProps, StackManagerState> {
id: string;
context!: React.ContextType<typeof RouteManagerContext>;
ionRouterOutlet?: React.ReactElement;
routerOutletElement: HTMLIonRouterOutletElement | undefined;
stackContextValue: StackContextState = {
registerIonPage: this.registerIonPage.bind(this),
isInOutlet: () => true
};
constructor(props: StackManagerProps) {
super(props);
this.id = this.props.id || generateId();
this.handleViewSync = this.handleViewSync.bind(this);
this.handleHideView = this.handleHideView.bind(this);
this.state = {};
this.registerIonPage = this.registerIonPage.bind(this);
this.transitionPage = this.transitionPage.bind(this);
this.handlePageTransition = this.handlePageTransition.bind(this);
this.id = generateId('routerOutlet');
}
componentDidMount() {
this.props.routeManager.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
if (this.routerOutletElement) {
this.setupRouterOutlet(this.routerOutletElement);
// console.log(`SM Mount - ${this.routerOutletElement.id} (${this.id})`);
this.handlePageTransition(this.props.routeInfo);
}
}
static getDerivedStateFromProps(props: StackManagerProps, state: StackManagerState) {
props.routeManager.syncRoute(props.children);
return state;
componentDidUpdate(prevProps: StackManagerProps) {
if (this.props.routeInfo.pathname !== prevProps.routeInfo.pathname) {
this.handlePageTransition(this.props.routeInfo);
}
}
componentWillUnmount() {
this.props.routeManager.removeViewStack(this.id);
// console.log(`SM UNMount - ${(this.routerOutletElement?.id as any).id} (${this.id})`);
this.context.clearOutlet(this.id);
}
handleViewSync(page: HTMLElement, viewId: string) {
this.props.routeManager.syncView(page, viewId);
async handlePageTransition(routeInfo: RouteInfo) {
// If routerOutlet isn't quite ready, give it another try in a moment
if (!this.routerOutletElement || !this.routerOutletElement.commit) {
setTimeout(() => this.handlePageTransition(routeInfo), 10);
} else {
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
const leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
if (!(routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward')) {
const shouldLeavingViewBeRemoved = routeInfo.routeDirection !== 'none' && leavingViewItem && (enteringViewItem !== leavingViewItem);
if (shouldLeavingViewBeRemoved) {
leavingViewItem!.mount = false;
}
}
if (leavingViewItem && routeInfo.routeOptions?.unmount) {
leavingViewItem.mount = false;
}
const enteringRoute = matchRoute(this.ionRouterOutlet?.props.children, routeInfo) as React.ReactElement;
if (enteringViewItem) {
enteringViewItem.reactElement = enteringRoute;
}
if (!enteringViewItem) {
if (enteringRoute) {
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
this.context.addViewItem(enteringViewItem);
}
}
if (enteringViewItem && enteringViewItem.ionPageElement) {
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
} else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
// If we have a leavingView but no entering view/route, we are probably leaving to
// another outlet, so hide this leavingView. We do it in a timeout to give time for a
// transition to finish.
setTimeout(() => {
if (leavingViewItem.ionPageElement) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
}, 250);
}
this.forceUpdate();
}
}
handleHideView(viewId: string) {
this.props.routeManager.hideView(viewId);
registerIonPage(page: HTMLElement, routeInfo: RouteInfo) {
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
if (foundView) {
foundView.ionPageElement = page;
foundView.ionRoute = true;
}
this.handlePageTransition(routeInfo);
}
renderChild(item: ViewItem, route: any) {
const component = React.cloneElement(route, {
computedMatch: item.routeData.match
});
return component;
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const canStart = () => {
const config = getConfig();
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
if (swipeEnabled) {
return this.context.canGoBack();
} else {
return false;
}
};
const onStart = () => {
this.context.goBack();
};
routerOutlet.swipeHandler = {
canStart,
onStart,
onEnd: _shouldContinue => true
};
}
async transitionPage(routeInfo: RouteInfo, enteringViewItem: ViewItem, leavingViewItem?: ViewItem) {
const routerOutlet = this.routerOutletElement!;
const direction = (routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root')
? undefined
: routeInfo.routeDirection;
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
if (leavingViewItem && leavingViewItem.ionPageElement && (enteringViewItem === leavingViewItem)) {
// If a page is transitioning to another version of itself
// we clone it so we can have an animation to show
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
if (match) {
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
if (newLeavingElement) {
this.routerOutletElement.appendChild(newLeavingElement);
await runCommit(enteringViewItem.ionPageElement, newLeavingElement);
this.routerOutletElement.removeChild(newLeavingElement);
}
} else {
await runCommit(enteringViewItem.ionPageElement, undefined);
}
} else {
await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
if (leavingViewItem && leavingViewItem.ionPageElement) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
}
}
async function runCommit(enteringEl: HTMLElement, leavingEl?: HTMLElement) {
enteringEl.classList.add('ion-page');
enteringEl.classList.add('ion-page-invisible');
await routerOutlet.commit(enteringEl, leavingEl, {
deepWait: true,
duration: direction === undefined ? 0 : undefined,
direction: direction as any,
showGoBack: direction === 'forward',
progressAnimation: false,
animationBuilder: routeInfo.routeAnimation
});
}
}
render() {
const routeManager = this.props.routeManager;
const viewStack = routeManager.viewStacks.get(this.id);
const views = (viewStack || { views: [] }).views.filter(x => x.show);
const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement;
const childElements = views.map(view => {
const route = routeManager.getRoute(view.routeId);
return (
<ViewTransitionManager
id={view.id}
key={view.key}
mount={view.mount}
>
<View
onViewSync={this.handleViewSync}
onHideView={this.handleHideView}
view={view}
route={route}
>
{this.renderChild(view, route)}
</View>
</ViewTransitionManager>
);
});
const { children } = this.props;
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
this.ionRouterOutlet = ionRouterOutlet;
const elementProps: any = {
ref: this.routerOutletEl
};
const components = this.context.getChildrenToRender(
this.id,
this.ionRouterOutlet,
this.props.routeInfo,
() => {
this.forceUpdate();
});
if (ionRouterOutlet.props.forwardedRef) {
ionRouterOutlet.props.forwardedRef.current = this.routerOutletEl;
}
return (
<StackContext.Provider value={this.stackContextValue}>
{React.cloneElement(ionRouterOutlet as any, {
ref: (node: HTMLIonRouterOutletElement) => {
if (ionRouterOutlet.props.setRef) {
ionRouterOutlet.props.setRef(node);
}
if (ionRouterOutlet.props.forwardedRef) {
ionRouterOutlet.props.forwardedRef.current = node;
}
this.routerOutletElement = node;
const { ref } = ionRouterOutlet as any;
if (typeof ref === 'function') {
ref(node);
}
}
},
components
)}
</StackContext.Provider>
);
}
if (isDevMode()) {
elementProps['data-stack-id'] = this.id;
}
const routerOutletChild = React.cloneElement(ionRouterOutlet, elementProps, childElements);
return routerOutletChild;
static get contextType() {
return RouteManagerContext;
}
}
const withContext = (Component: any) => {
return (props: any) => (
<RouteManagerContext.Consumer>
{context => <Component {...props} routeManager={context} />}
</RouteManagerContext.Consumer>
);
};
export default StackManager;
export const StackManager = withContext(StackManagerInner);
function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
let matchedNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
const matchProps = {
exact: child.props.exact,
path: child.props.path || child.props.from,
component: child.props.component
};
const match = matchPath(routeInfo.pathname, matchProps);
if (match) {
matchedNode = child;
}
});
if (matchedNode) {
return matchedNode;
}
// If we haven't found a node
// try to find one that doesn't have a path or from prop, that will be our not found route
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
if (!(child.props.path || child.props.from)) {
matchedNode = child;
}
});
return matchedNode;
}
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
const matchProps = {
exact: forceExact ? true : node.props.exact,
path: node.props.path || node.props.from,
component: node.props.component
};
const match = matchPath(pathname, matchProps);
return match;
}

View File

@@ -1,99 +0,0 @@
import { IonLifeCycleContext, NavContext } from '@ionic/react';
import React from 'react';
import { Redirect } from 'react-router';
import { isDevMode } from '../utils';
import { ViewItem } from './ViewItem';
interface ViewProps extends React.HTMLAttributes<HTMLElement> {
onViewSync: (page: HTMLElement, viewId: string) => void;
onHideView: (viewId: string) => void;
view: ViewItem;
route: any;
}
/**
* The View component helps manage the IonPage's lifecycle and registration
*/
export class View extends React.Component<ViewProps, {}> {
context!: React.ContextType<typeof IonLifeCycleContext>;
ionPage?: HTMLElement;
componentDidMount() {
/**
* If we can tell if view is a redirect, hide it so it will work again in future
*/
const { view, route } = this.props;
if (route.type === Redirect) {
this.props.onHideView(view.id);
} else if (route.props.render && !view.isIonRoute) {
// Test the render to see if it returns a redirect
if (route.props.render().type === Redirect) {
this.props.onHideView(view.id);
}
}
}
componentWillUnmount() {
if (this.ionPage) {
this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
this.ionPage.removeEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
this.ionPage.removeEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
this.ionPage.removeEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
}
}
ionViewWillEnterHandler() {
this.context.ionViewWillEnter();
}
ionViewDidEnterHandler() {
this.context.ionViewDidEnter();
}
ionViewWillLeaveHandler() {
this.context.ionViewWillLeave();
}
ionViewDidLeaveHandler() {
this.context.ionViewDidLeave();
}
registerIonPage(page: HTMLElement) {
this.ionPage = page;
this.ionPage.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
this.ionPage.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
this.ionPage.addEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
this.ionPage.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
this.ionPage.classList.add('ion-page-invisible');
if (isDevMode()) {
this.ionPage.setAttribute('data-view-id', this.props.view.id);
}
this.props.onViewSync(page, this.props.view.id);
}
render() {
return (
<NavContext.Consumer>
{value => {
const newProvider = {
...value,
registerIonPage: this.registerIonPage.bind(this)
};
return (
<NavContext.Provider value={newProvider}>
{this.props.children}
</NavContext.Provider>
);
}}
</NavContext.Consumer>
);
}
static get contextType() {
return IonLifeCycleContext;
}
}

View File

@@ -1,30 +0,0 @@
export interface ViewItem<RouteData = any> {
/** The generated id of the view */
id: string;
/** The key used by React. A new key is generated each time the view comes into the DOM so React thinks its a completely new element. */
key: string;
routeId: string;
/** The routeData for the view. */
routeData: RouteData;
/** Used to track which page pushed the page into view. Used for back button purposes. */
prevId?: string;
/**
* Mount is used for page transitions. If mount is false, it keeps the view in the DOM long enough to finish the transition.
*/
mount: boolean;
/**
* Show determines if the view will be in the DOM or not
*/
show: boolean;
/**
* An IonRoute is a Route that contains an IonPage. Only IonPages participate in transition and lifecycle events.
*/
isIonRoute: boolean;
/**
* location of the view
*/
location?: string;
}

Some files were not shown because too many files have changed in this diff Show More