mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4476817825 | ||
|
|
97e32f3b0b | ||
|
|
bfddb17065 | ||
|
|
3c9d6ea5f5 | ||
|
|
8e1178b98b | ||
|
|
470478d387 | ||
|
|
cc73a1063e | ||
|
|
349e8cd5b0 | ||
|
|
bcbe8cbb8d | ||
|
|
03ca0c5968 | ||
|
|
fff4aec6cf | ||
|
|
eb592b8917 | ||
|
|
a15cd01bc3 | ||
|
|
4199762d3e | ||
|
|
79518468dd | ||
|
|
f4a08b7ed4 | ||
|
|
dbe6853884 | ||
|
|
096eef4a79 | ||
|
|
d4a5fbd955 | ||
|
|
591c133344 | ||
|
|
d297ecb87a | ||
|
|
a625c837a6 | ||
|
|
77464ef21a | ||
|
|
fa93dffdb4 | ||
|
|
6f200ac751 | ||
|
|
7c2d0c981a | ||
|
|
1351b2eafc | ||
|
|
88f1828bd8 | ||
|
|
020f3cc56c | ||
|
|
3cbf9e7c4c | ||
|
|
2664587749 | ||
|
|
f00ad8a835 | ||
|
|
81ef3f1ecd | ||
|
|
c171ccbd37 | ||
|
|
a0735b97bf | ||
|
|
b4423a816f | ||
|
|
ff23e4f267 | ||
|
|
1dcd9de50a | ||
|
|
c458523b0d | ||
|
|
a5e4669c4b |
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
23
core/api.txt
23
core/api.txt
@@ -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
360
core/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
32
core/src/components.d.ts
vendored
32
core/src/components.d.ts
vendored
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)`;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>` |
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
|
||||
5
core/src/components/route/route-interface.ts
Normal file
5
core/src/components/route/route-interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type NavigationHookCallback = () => NavigationHookResult | Promise<NavigationHookResult>;
|
||||
export type NavigationHookResult = boolean | NavigationHookOptions;
|
||||
export interface NavigationHookOptions {
|
||||
redirect: string;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
60
core/src/components/route/usage/javascript.md
Normal file
60
core/src/components/route/usage/javascript.md
Normal 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;
|
||||
}
|
||||
```
|
||||
64
core/src/components/route/usage/stencil.md
Normal file
64
core/src/components/route/usage/stencil.md
Normal 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;
|
||||
}
|
||||
```
|
||||
57
core/src/components/route/usage/vue.md
Normal file
57
core/src/components/route/usage/vue.md
Normal 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>
|
||||
```
|
||||
@@ -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,
|
||||
|
||||
128
core/src/components/router/test/guards/href.e2e.ts
Normal file
128
core/src/components/router/test/guards/href.e2e.ts
Normal 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();
|
||||
}
|
||||
194
core/src/components/router/test/guards/index.html
Normal file
194
core/src/components/router/test/guards/index.html
Normal 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>
|
||||
128
core/src/components/router/test/guards/router-link.e2e.ts
Normal file
128
core/src/components/router/test/guards/router-link.e2e.ts
Normal 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();
|
||||
}
|
||||
128
core/src/components/router/test/guards/router-push.e2e.ts
Normal file
128
core/src/components/router/test/guards/router-push.e2e.ts
Normal 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();
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,11 +44,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.button-native {
|
||||
min-width: $segment-button-md-min-width;
|
||||
}
|
||||
|
||||
|
||||
// Segment Button: Disabled
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>` |
|
||||
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
10
core/src/interface.d.ts
vendored
10
core/src/interface.d.ts
vendored
@@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
1
packages/react-router/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.tgz
|
||||
1
packages/react-router/.npmrc
Normal file
1
packages/react-router/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
package-lock=false
|
||||
9348
packages/react-router/package-lock.json
generated
9348
packages/react-router/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type IonRouteAction = 'push' | 'replace' | 'pop';
|
||||
@@ -1,6 +0,0 @@
|
||||
import { RouteProps, match } from 'react-router-dom';
|
||||
|
||||
export interface IonRouteData {
|
||||
match: match | null;
|
||||
childProps: RouteProps;
|
||||
}
|
||||
11
packages/react-router/src/ReactRouter/IonRouteInner.tsx
Normal file
11
packages/react-router/src/ReactRouter/IonRouteInner.tsx
Normal 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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
273
packages/react-router/src/ReactRouter/IonRouter.tsx
Normal file
273
packages/react-router/src/ReactRouter/IonRouter.tsx
Normal 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';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
178
packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
Normal file
178
packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
Normal 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;
|
||||
}
|
||||
@@ -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?');
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user