Compare commits

...

34 Commits

Author SHA1 Message Date
Maria Hutt
a65886ba37 test(app, routing): use index 2025-06-20 17:41:22 -07:00
Maria Hutt
a926134ba5 chore(rr5, rr6): update package 2025-06-20 12:15:10 -07:00
Maria Hutt
49cbc46d4b test(dynamic-ionpage-classnames): use old version for rr5 2025-06-18 17:26:17 -07:00
Maria Hutt
9c5f55adae test(rr5): keep the old pages 2025-06-18 16:09:12 -07:00
Maria Hutt
9d69a69d08 Merge branch 'main' of github.com:ionic-team/ionic-framework into mh/react-router-6 2025-06-17 11:51:27 -07:00
Maria Hutt
7a20697849 fix(ReactRouterViewStack, StackManager): use correct value & add value check 2025-06-16 17:24:11 -07:00
Maria Hutt
10bb889dc3 docs(IonReactRouter): use updated param names 2025-06-16 11:36:40 -07:00
Maria Hutt
2656e98490 chore: update package and sync file 2025-06-13 16:29:34 -07:00
Maria Hutt
e6e17eb435 refactor(IonReactRouter): split component to use hooks correctly 2025-06-13 16:28:43 -07:00
Maria Hutt
5cccf0b297 refactor(many): update to prevent compile errors 2025-06-13 16:27:59 -07:00
Maria Hutt
366004e3f8 refactor(Tabs2): switch to navigate 2025-06-13 14:12:06 -07:00
Maria Hutt
c6f8dd4c54 refactor(many): replace render with element 2025-06-13 13:46:41 -07:00
Maria Hutt
71fb8cb8f9 refactor(many): update to use <Navigate> 2025-06-13 13:32:36 -07:00
Maria Hutt
12c49f5c51 refactor(many): remove exact from test pages 2025-06-13 12:05:18 -07:00
Maria Hutt
c45c0f9bfa docs(many): update comments 2025-06-13 11:35:29 -07:00
Maria Hutt
7ff8994d12 feat(IonReactHashRouter): migrate to a functional component and react router 6
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
2025-06-12 16:14:24 -07:00
Maria Hutt
205a7056d7 chore(IonReactMemoryRouter): remove unused type 2025-06-11 17:15:35 -07:00
Maria Hutt
331b39427b feat(IonReactMemoryRouter): migrate to a functional component and react router 6
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
2025-06-11 17:14:44 -07:00
Maria Hutt
0a0dcb6d7d feat(IonReactRouter): migrate to a functional component and react router 6
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
2025-06-10 17:14:14 -07:00
Maria Hutt
0008059f8c docs(IonRouter, ReactRouterViewStack, StackManager): update comments 2025-06-09 15:57:41 -07:00
Maria Hutt
93202c070e chore(IonRouteInner): update render 2025-06-09 15:57:13 -07:00
Maria Hutt
a78f6b3151 feat(IonRouter): migrate to functional component with react router 6
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
2025-06-09 13:07:24 -07:00
Maria Hutt
37f76c72ec chore(StackManager): run lint 2025-06-09 11:35:13 -07:00
Maria Hutt
ca27ed618b chore(IonRouteInner): upgrade to rr6 2025-05-19 17:01:48 -07:00
Maria Hutt
2436ba383e docs(matchPath): remove comment 2025-05-19 16:33:55 -07:00
Maria Hutt
e76c1e86f9 docs(StackManager): update comment 2025-05-19 14:04:06 -07:00
Maria Hutt
fd6baaceda chore(StackManager): more upgrades for rr6 2025-05-19 14:01:54 -07:00
Maria Hutt
4aad76a93a chore(StackManager): upgrade to rr6 and add comments 2025-05-19 13:09:11 -07:00
Maria Hutt
6a42e6959d chore(rr6): run lint 2025-05-19 13:08:31 -07:00
Maria Hutt
e364fea46b chore(matchPath): use the correct library 2025-05-19 13:07:05 -07:00
Maria Hutt
af7710b5a3 refactor(test): updated test pages 2025-05-12 10:45:54 -07:00
Maria Hutt
cccf290670 refactor(reactrouterviewstack): update for rr6 and improvements 2025-05-08 14:54:40 -07:00
Maria Hutt
f0127bd874 refactor(utils): update matchPath for rr6 2025-05-08 14:53:57 -07:00
Maria Hutt
426232456b chore(deps): update react router to v6 2025-05-08 14:49:05 -07:00
65 changed files with 41685 additions and 991 deletions

View File

@@ -27,8 +27,8 @@
"prettier": "^2.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-router": "^6.30.0",
"react-router-dom": "^6.30.0",
"rimraf": "^3.0.2",
"rollup": "^4.2.0",
"typescript": "^4.0.5"
@@ -36,8 +36,8 @@
"peerDependencies": {
"react": ">=16.8.6",
"react-dom": ">=16.8.6",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1"
"react-router": "^6.30.0",
"react-router-dom": "^6.30.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -152,18 +152,6 @@
"node": ">=4"
}
},
"node_modules/@babel/runtime": {
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
"integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -464,6 +452,15 @@
"node": ">= 8"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz",
@@ -2454,29 +2451,6 @@
"node": ">= 0.4"
}
},
"node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -2782,12 +2756,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3112,15 +3080,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dev": true,
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -3175,17 +3134,6 @@
"node": ">=0.4.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3240,56 +3188,38 @@
"react": "17.0.2"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"node_modules/react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"version": "6.30.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
"integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"version": "6.30.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz",
"integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.23.0",
"react-router": "6.30.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -3354,12 +3284,6 @@
"node": ">=4"
}
},
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
"dev": true
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -3754,18 +3678,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
"dev": true
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"dev": true
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3948,12 +3860,6 @@
"integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==",
"dev": true
},
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
"dev": true
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4108,15 +4014,6 @@
}
}
},
"@babel/runtime": {
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
"integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.14.0"
}
},
"@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -4316,6 +4213,12 @@
"fastq": "^1.6.0"
}
},
"@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"dev": true
},
"@rollup/plugin-typescript": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz",
@@ -5689,29 +5592,6 @@
"function-bind": "^1.1.2"
}
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dev": true,
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"requires": {
"react-is": "^16.7.0"
}
},
"ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -5920,12 +5800,6 @@
"call-bind": "^1.0.2"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -6184,15 +6058,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dev": true,
"requires": {
"isarray": "0.0.1"
}
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -6223,17 +6088,6 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6265,50 +6119,25 @@
"scheduler": "^0.20.2"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"version": "6.30.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
"integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.23.0"
}
},
"react-router-dom": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"version": "6.30.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz",
"integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.23.0",
"react-router": "6.30.0"
}
},
"regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
},
"regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -6349,12 +6178,6 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
"dev": true
},
"reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -6640,18 +6463,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
"dev": true
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6790,12 +6601,6 @@
"integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==",
"dev": true
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -42,8 +42,8 @@
"peerDependencies": {
"react": ">=16.8.6",
"react-dom": ">=16.8.6",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1"
"react-router": ">=6.0.0",
"react-router-dom": ">=6.0.0"
},
"devDependencies": {
"@ionic/eslint-config": "^0.3.0",
@@ -60,8 +60,8 @@
"prettier": "^2.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-router": "^6.30.0",
"react-router-dom": "^6.30.0",
"rimraf": "^3.0.2",
"rollup": "^4.2.0",
"typescript": "^4.0.5"

View File

@@ -2,14 +2,14 @@
set -e
# Copy ionic react dist
rm -rf node_modules/@ionic/react/dist node_modules/@ionic/react/css
cp -a ../react/dist node_modules/@ionic/react/dist
cp -a ../react/css node_modules/@ionic/react/css
cp -a ../react/package.json node_modules/@ionic/react/package.json
# Delete old packages
rm -f *.tgz
# Copy core dist
rm -rf node_modules/@ionic/core/dist node_modules/@ionic/core/components
cp -a ../../core/dist node_modules/@ionic/core/dist
cp -a ../../core/components node_modules/@ionic/core/components
cp -a ../../core/package.json node_modules/@ionic/core/package.json
# Pack @ionic/react
npm pack ../react
# Pack @ionic/core
npm pack ../../core
# Install Dependencies
npm install *.tgz --no-save

View File

@@ -1,53 +1,51 @@
import type { Action as HistoryAction, History, Location as HistoryLocation } from 'history';
import { createHashHistory as createHistory } from 'history';
import React from 'react';
import type { BrowserRouterProps } from 'react-router-dom';
import { Router } from 'react-router-dom';
/**
* `IonReactHashRouter` provides a way to use hash-based routing in Ionic
* React applications.
*/
import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useRef } from 'react';
import type { HashRouterProps } from 'react-router-dom';
import { HashRouter, useLocation, useNavigationType } from 'react-router-dom';
import { IonRouter } from './IonRouter';
interface IonReactHashRouterProps extends BrowserRouterProps {
history?: History;
}
export const IonReactHashRouter = ({ children }: PropsWithChildren<HashRouterProps>) => {
const location = useLocation();
const navigationType = useNavigationType();
export class IonReactHashRouter extends React.Component<IonReactHashRouterProps> {
history: History;
historyListenHandler?: (location: HistoryLocation, action: HistoryAction) => void;
const historyListenHandler = useRef<(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);
}
const registerHistoryListener = (cb: (location: HistoryLocation, action: HistoryAction) => void) => {
historyListenHandler.current = cb;
};
/**
* history@4.x passes separate location and action
* params. history@5.x passes location and action
* together as a single object.
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just assume
* a single object with both location and action.
* Processes navigation changes within the application.
*
* Its purpose is to relay the current `location` and the associated
* `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
* primarily for `IonRouter` to manage Ionic-specific UI updates and
* navigation stack behavior.
*
* @param location The current browser history location object.
* @param action The type of navigation action ('PUSH', 'POP', or
* 'REPLACE').
*/
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
const locationValue = (location as any).location || location;
const actionValue = (location as any).action || action;
if (this.historyListenHandler) {
this.historyListenHandler(locationValue, actionValue);
const handleHistoryChange = (location: HistoryLocation, action: HistoryAction) => {
if (historyListenHandler.current) {
historyListenHandler.current(location, action);
}
}
};
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
this.historyListenHandler = cb;
}
useEffect(() => {
handleHistoryChange(location, navigationType);
}, [location, navigationType]);
render() {
const { children, ...props } = this.props;
return (
<Router history={this.history} {...props}>
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
</Router>
);
}
}
return (
<HashRouter>
<IonRouter registerHistoryListener={registerHistoryListener}>{children}</IonRouter>
</HashRouter>
);
};

View File

@@ -1,51 +1,53 @@
import type { Action as HistoryAction, Location as HistoryLocation, MemoryHistory } from 'history';
import React from 'react';
/**
* `IonReactMemoryRouter` provides a way to use `react-router` in
* environments where a traditional browser history (like `BrowserRouter`)
* isn't available or desirable.
*/
import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useRef } from 'react';
import type { MemoryRouterProps } from 'react-router';
import { Router } from 'react-router';
import { MemoryRouter } from 'react-router';
import { useLocation, useNavigationType } from 'react-router-dom';
import { IonRouter } from './IonRouter';
interface IonReactMemoryRouterProps extends MemoryRouterProps {
history: MemoryHistory;
}
export const IonReactMemoryRouter = ({ children }: PropsWithChildren<MemoryRouterProps>) => {
const location = useLocation();
const navigationType = useNavigationType();
export class IonReactMemoryRouter extends React.Component<IonReactMemoryRouterProps> {
history: MemoryHistory;
historyListenHandler?: (location: HistoryLocation, action: HistoryAction) => void;
const historyListenHandler = useRef<(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);
}
const registerHistoryListener = (cb: (location: HistoryLocation, action: HistoryAction) => void) => {
historyListenHandler.current = cb;
};
/**
* history@4.x passes separate location and action
* params. history@5.x passes location and action
* together as a single object.
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just assume
* a single object with both location and action.
* Processes navigation changes within the application.
*
* Its purpose is to relay the current `location` and the associated
* `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
* primarily for `IonRouter` to manage Ionic-specific UI updates and
* navigation stack behavior.
*
* @param location The current browser history location object.
* @param action The type of navigation action ('PUSH', 'POP', or
* 'REPLACE').
*/
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
const locationValue = (location as any).location || location;
const actionValue = (location as any).action || action;
if (this.historyListenHandler) {
this.historyListenHandler(locationValue, actionValue);
const handleHistoryChange = (location: HistoryLocation, action: HistoryAction) => {
if (historyListenHandler.current) {
historyListenHandler.current(location, action);
}
}
};
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
this.historyListenHandler = cb;
}
useEffect(() => {
handleHistoryChange(location, navigationType);
}, [location, navigationType]);
render() {
const { children, ...props } = this.props;
return (
<Router {...props}>
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
</Router>
);
}
}
return (
<MemoryRouter>
<IonRouter registerHistoryListener={registerHistoryListener}>{children}</IonRouter>
</MemoryRouter>
);
};

View File

@@ -1,53 +1,65 @@
import type { Action as HistoryAction, History, Location as HistoryLocation } from 'history';
import { createBrowserHistory as createHistory } from 'history';
import React from 'react';
/**
* `IonReactRouter` facilitates the integration of Ionic's specific
* navigation and UI management with the standard React Router mechanisms,
* allowing an inner Ionic-specific router (`IonRouter`) to react to
* navigation events.
*/
import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useRef, useCallback } from 'react';
import type { BrowserRouterProps } from 'react-router-dom';
import { Router } from 'react-router-dom';
import { BrowserRouter, useLocation, useNavigationType } from 'react-router-dom';
import { IonRouter } from './IonRouter';
interface IonReactRouterProps extends BrowserRouterProps {
history?: History;
}
/**
* This component acts as a bridge to ensure React Router hooks like
* `useLocation` and `useNavigationType` are called within the valid
* context of a `<BrowserRouter>`.
*
* It was split from `IonReactRouter` because these hooks must be
* descendants of a `<Router>` component, which `BrowserRouter` provides.
*/
const RouterContent = ({ children }: PropsWithChildren<{}>) => {
const location = useLocation();
const navigationType = useNavigationType();
export class IonReactRouter extends React.Component<IonReactRouterProps> {
historyListenHandler?: (location: HistoryLocation, action: HistoryAction) => void;
history: History;
const historyListenHandler = useRef<(location: HistoryLocation, action: HistoryAction) => void>();
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);
}
const registerHistoryListener = useCallback((cb: (location: HistoryLocation, action: HistoryAction) => void) => {
historyListenHandler.current = cb;
}, []);
/**
* history@4.x passes separate location and action
* params. history@5.x passes location and action
* together as a single object.
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just assume
* a single object with both location and action.
* Processes navigation changes within the application.
*
* Its purpose is to relay the current `location` and the associated
* `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
* primarily for `IonRouter` to manage Ionic-specific UI updates and
* navigation stack behavior.
*
* @param loc The current browser history location object.
* @param act The type of navigation action ('PUSH', 'POP', or
* 'REPLACE').
*/
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
const locationValue = (location as any).location || location;
const actionValue = (location as any).action || action;
if (this.historyListenHandler) {
this.historyListenHandler(locationValue, actionValue);
const handleHistoryChange = useCallback((loc: HistoryLocation, act: HistoryAction) => {
if (historyListenHandler.current) {
historyListenHandler.current(loc, act);
}
}
}, []);
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
this.historyListenHandler = cb;
}
useEffect(() => {
handleHistoryChange(location, navigationType);
}, [location, navigationType, handleHistoryChange]);
render() {
const { children, ...props } = this.props;
return (
<Router history={this.history} {...props}>
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
</Router>
);
}
}
return <IonRouter registerHistoryListener={registerHistoryListener}>{children}</IonRouter>;
};
export const IonReactRouter = ({ children, ...browserRouterProps }: PropsWithChildren<BrowserRouterProps>) => {
return (
<BrowserRouter {...browserRouterProps}>
<RouterContent>{children}</RouterContent>
</BrowserRouter>
);
};

View File

@@ -2,29 +2,6 @@ import type { 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` is a private API in react-router v5 that
* has been removed in v6.
*
* This needs to be removed when we support v6.
*
* TODO: FW-647
*/
...((this.props as any).computedMatch !== undefined
? {
computedMatch: (this.props as any).computedMatch,
}
: {})
}
/>
);
}
}
export const IonRouteInner = ({ path, element }: IonRouteProps) => {
return <Route path={path} element={element} />;
};

View File

@@ -1,16 +1,17 @@
import type {
AnimationBuilder,
RouteAction,
RouteInfo,
RouteManagerContextState,
RouterDirection,
ViewItem,
} from '@ionic/react';
/**
* `IonRouter` is responsible for managing the application's navigation
* state, tracking the history of visited routes, and coordinating
* transitions between different views. It intercepts route changes from
* React Router and translates them into actions that Ionic can understand
* and animate.
*/
import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection } from '@ionic/react';
import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react';
import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
import React from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { IonRouteInner } from './IonRouteInner';
import { ReactRouterViewStack } from './ReactRouterViewStack';
@@ -21,162 +22,178 @@ export interface LocationState {
routerOptions?: { as?: string; unmount?: boolean };
}
interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> {
interface IonRouterProps {
registerHistoryListener: (cb: (location: HistoryLocation<any>, action: HistoryAction) => void) => void;
}
interface IonRouteState {
routeInfo: RouteInfo;
}
export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildren<IonRouterProps>) => {
const location = useLocation();
const params = useParams();
const navigate = useNavigate();
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,
findViewItemByPathname: this.viewStack.findViewItemByPathname,
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,
};
const didMountRef = useRef(false);
const locationHistory = useRef(new LocationHistory());
const currentTab = useRef<string | undefined>(undefined);
const viewStack = useRef(new ReactRouterViewStack());
const incomingRouteParams = useRef<Partial<RouteInfo> | null>(null);
constructor(props: IonRouteProps) {
super(props);
const [routeInfo, setRouteInfo] = useState({
id: generateId('routeInfo'),
pathname: location.pathname,
search: location.search,
});
const routeInfo = {
id: generateId('routeInfo'),
pathname: this.props.location.pathname,
search: this.props.location.search,
};
useEffect(() => {
didMountRef.current = true;
}, []);
this.locationHistory.add(routeInfo);
this.handleChangeTab = this.handleChangeTab.bind(this);
this.handleResetTab = this.handleResetTab.bind(this);
this.handleNativeBack = this.handleNativeBack.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) {
if (!path) {
return;
}
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) {
/**
* Triggered whenever the history changes, either through user navigation
* or programmatic changes. It transforms the raw browser history changes
* into `RouteInfo` objects, which are needed Ionic's animations and
* navigation patterns.
*
* @param location The current location object from the history.
* @param action The action that triggered the history change.
*/
const handleHistoryChange = (location: HistoryLocation<LocationState>, action: HistoryAction) => {
let leavingLocationInfo: RouteInfo;
if (this.incomingRouteParams) {
if (this.incomingRouteParams.routeAction === 'replace') {
leavingLocationInfo = this.locationHistory.previous();
/**
* A programmatic navigation was triggered.
* e.g., `<Redirect />`, `history.push()`, or `handleNavigate()`
*/
if (incomingRouteParams) {
/**
* The current history entry is overwritten, so the previous entry
* is the one we are leaving.
*/
if (incomingRouteParams.current?.routeAction === 'replace') {
leavingLocationInfo = locationHistory.current.previous();
} else {
leavingLocationInfo = this.locationHistory.current();
// If the action is 'push' or 'pop', we want to use the current route.
leavingLocationInfo = locationHistory.current.current();
}
} else {
leavingLocationInfo = this.locationHistory.current();
/**
* An external navigation was triggered
* e.g., browser back/forward button or direct link
*
* The leaving location is the current route.
*/
leavingLocationInfo = locationHistory.current.current();
}
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
// Check if the URL has changed.
if (leavingUrl !== location.pathname) {
if (!this.incomingRouteParams) {
// An external navigation was triggered.
if (!incomingRouteParams.current) {
/**
* A `REPLACE` action can be triggered by React Router's
* `<Redirect />` component.
*/
if (action === 'REPLACE') {
this.incomingRouteParams = {
incomingRouteParams.current = {
routeAction: 'replace',
routeDirection: 'none',
tab: this.currentTab,
tab: currentTab.current,
};
}
/**
* A `POP` action can be triggered by the browser's back/forward
* button.
*/
if (action === 'POP') {
const currentRoute = this.locationHistory.current();
const currentRoute = locationHistory.current.current();
/**
* Check if the current route was "pushed" by a previous route
* (indicates a linear history path).
*/
if (currentRoute && currentRoute.pushedByRoute) {
const prevInfo = this.locationHistory.findLastLocation(currentRoute);
this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
const prevInfo = locationHistory.current.findLastLocation(currentRoute);
incomingRouteParams.current = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
// It's a non-linear history path like a direct link.
} else {
this.incomingRouteParams = {
incomingRouteParams.current = {
routeAction: 'pop',
routeDirection: 'none',
tab: this.currentTab,
tab: currentTab.current,
};
}
}
if (!this.incomingRouteParams) {
this.incomingRouteParams = {
// Still found no params, set it to a default state of forward.
if (!incomingRouteParams.current) {
incomingRouteParams.current = {
routeAction: 'push',
routeDirection: location.state?.direction || 'forward',
routeOptions: location.state?.routerOptions,
tab: this.currentTab,
tab: currentTab.current,
};
}
}
let routeInfo: RouteInfo;
if (this.incomingRouteParams?.id) {
/**
* An existing id indicates that it's re-activating an existing route.
* e.g., tab switching or navigating back to a previous route
*/
if (incomingRouteParams.current?.id) {
routeInfo = {
...(this.incomingRouteParams as RouteInfo),
...(incomingRouteParams.current as RouteInfo),
lastPathname: leavingLocationInfo.pathname,
};
this.locationHistory.add(routeInfo);
locationHistory.current.add(routeInfo);
/**
* A new route is being created since it's not re-activating
* an existing route.
*/
} else {
const isPushed =
this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward';
incomingRouteParams.current?.routeAction === 'push' &&
incomingRouteParams.current.routeDirection === 'forward';
routeInfo = {
id: generateId('routeInfo'),
...this.incomingRouteParams,
lastPathname: leavingLocationInfo.pathname,
pathname: location.pathname,
...incomingRouteParams,
lastPathname: leavingLocationInfo.pathname, // The URL we just came from
pathname: location.pathname, // The current (destination) URL
search: location.search,
params: this.props.match.params,
prevRouteLastPathname: leavingLocationInfo.lastPathname,
params: params as { [key: string]: string | string[] },
prevRouteLastPathname: leavingLocationInfo.lastPathname, // The lastPathname of the route we are leaving
};
// It's a linear navigation.
if (isPushed) {
routeInfo.tab = leavingLocationInfo.tab;
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
// Triggered by a browser back button or handleNavigateBack.
} else if (routeInfo.routeAction === 'pop') {
const r = this.locationHistory.findLastLocation(routeInfo);
// Find the route that pushed this one.
const r = locationHistory.current.findLastLocation(routeInfo);
routeInfo.pushedByRoute = r?.pushedByRoute;
// Navigating to a new tab.
} 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);
/**
* If we are switching tabs grab the last route info for the
* tab and use its `pushedByRoute`.
*/
const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
// This helps maintain correct back stack behavior within tabs.
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
// Triggered by `history.replace()` or a `<Redirect />` component, etc.
} 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();
/**
* Make sure to set the `lastPathname`, etc.. to the current route
* so the page transitions out.
*/
const currentRouteInfo = locationHistory.current.current();
/**
* If going from /home to /child, then replacing from
* /child to /home, we don't want the route info to
* say that /home was pushed by /home which is not correct.
* Special handling for `replace` to ensure correct `pushedByRoute`
* and `lastPathname`.
*
* If going from `/home` to `/child`, then replacing from
* `/child` to `/home`, we don't want the route info to
* say that `/home` was pushed by `/home` which is not correct.
*/
const currentPushedBy = currentRouteInfo?.pushedByRoute;
const pushedByRoute =
@@ -198,58 +215,127 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
routeInfo.routeAnimation = routeInfo.routeAnimation || currentRouteInfo?.routeAnimation;
}
this.locationHistory.add(routeInfo);
locationHistory.current.add(routeInfo);
}
this.setState({
routeInfo,
});
setRouteInfo(routeInfo);
}
this.incomingRouteParams = undefined;
}
// Reset for the next navigation.
incomingRouteParams.current = null;
};
/**
* history@4.x uses goBack(), history@5.x uses back()
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just
* assume back() is available.
* Resets the specified tab to its initial, root route.
*
* @param tab The tab to reset.
* @param originalHref The original href for the tab.
* @param originalRouteOptions The original route options for the tab.
*/
handleNativeBack() {
const history = this.props.history as any;
const goBack = history.goBack || history.back;
goBack();
}
handleNavigate(
path: string,
routeAction: RouteAction,
routeDirection?: RouterDirection,
routeAnimation?: AnimationBuilder,
routeOptions?: any,
tab?: string
) {
this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, {
routeAction,
routeDirection,
routeOptions,
routeAnimation,
tab,
});
if (routeAction === 'push') {
this.props.history.push(path);
} else {
this.props.history.replace(path);
const handleResetTab = (tab: string, originalHref: string, originalRouteOptions: any) => {
const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
if (routeInfo) {
const newRouteInfo = { ...routeInfo };
newRouteInfo.pathname = originalHref;
newRouteInfo.routeOptions = originalRouteOptions;
incomingRouteParams.current = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
}
}
};
handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) {
/**
* Handles tab changes.
*
* @param tab The tab to switch to.
* @param path The new path for the tab.
* @param routeOptions Additional route options.
*/
const handleChangeTab = (tab: string, path?: string, routeOptions?: any) => {
if (!path) {
return;
}
const routeInfo = locationHistory.current.getCurrentRouteInfoForTab(tab);
const [pathname, search] = path.split('?');
// User has navigated to the current tab before.
if (routeInfo) {
const routeParams = {
...routeInfo,
routeAction: 'push' as RouteAction,
routeDirection: 'none' as RouterDirection,
};
/**
* User is navigating to the same tab.
* e.g., `/tabs/home` → `/tabs/home`
*/
if (routeInfo.pathname === pathname) {
incomingRouteParams.current = {
...routeParams,
routeOptions,
};
navigate(routeInfo.pathname + (routeInfo.search || ''));
/**
* User is navigating to a different tab.
* e.g., `/tabs/home` → `/tabs/settings`
*/
} else {
incomingRouteParams.current = {
...routeParams,
pathname,
search: search ? '?' + search : undefined,
routeOptions,
};
navigate(pathname + (search ? '?' + search : ''));
}
// User has not navigated to this tab before.
} else {
handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
}
};
/**
* Set the current active tab in `locationHistory`.
* This is crucial for maintaining tab history since each tab has
* its own navigation stack.
*
* @param tab The tab to set as active.
*/
const handleSetCurrentTab = (tab: string) => {
currentTab.current = tab;
const ri = { ...locationHistory.current.current() };
if (ri.tab !== tab) {
ri.tab = tab;
locationHistory.current.update(ri);
}
};
/**
* Handles the native back button press.
* It's usually called when a user presses the platform-native back action.
*/
const handleNativeBack = () => {
navigate(-1);
};
/**
* Used to manage the back navigation within the Ionic React's routing
* system. It's deeply integrated with Ionic's view lifecycle, animations,
* and its custom history tracking (`locationHistory`) to provide a
* native-like transition and maintain correct application state.
*
* @param defaultHref The fallback URL to navigate to if there's no
* previous entry in the `locationHistory` stack.
* @param routeAnimation A custom animation builder to override the
* default "back" animation.
*/
const handleNavigateBack = (defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) => {
const config = getConfig();
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
const routeInfo = this.locationHistory.current();
const routeInfo = locationHistory.current.current();
// It's a linear navigation.
if (routeInfo && routeInfo.pushedByRoute) {
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
const prevInfo = locationHistory.current.findLastLocation(routeInfo);
if (prevInfo) {
/**
* This needs to be passed to handleNavigate
@@ -257,12 +343,16 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
* will be overridden.
*/
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
this.incomingRouteParams = {
incomingRouteParams.current = {
...prevInfo,
routeAction: 'pop',
routeDirection: 'back',
routeAnimation: incomingAnimation,
};
/**
* Check if it's a simple linear back navigation (not tabbed).
* e.g., `/home` → `/settings` → back to `/home`
*/
if (
routeInfo.lastPathname === routeInfo.pushedByRoute ||
/**
@@ -273,68 +363,98 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
*/
(prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')
) {
/**
* history@4.x uses goBack(), history@5.x uses back()
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just
* assume back() is available.
*/
const history = this.props.history as any;
const goBack = history.goBack || history.back;
goBack();
navigate(-1);
} else {
this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
/**
* It's a non-linear back navigation.
* e.g., direct link or tab switch or nested navigation with redirects
*/
handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
}
/**
* `pushedByRoute` exists, but no corresponding previous entry in
* the history stack.
*/
} else {
this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
/**
* No `pushedByRoute`
* e.g., initial page load
*/
} else {
this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
};
/**
* Used to programmatically navigate through the app.
*
* @param path The path to navigate to.
* @param routeAction The action to take (push, replace, etc.).
* @param routeDirection The direction of the navigation (forward,
* back, etc.).
* @param routeAnimation The animation to use for the transition.
* @param routeOptions Additional options for the route.
* @param tab The tab to navigate to, if applicable.
*/
const handleNavigate = (
path: string,
routeAction: RouteAction,
routeDirection?: RouterDirection,
routeAnimation?: AnimationBuilder,
routeOptions?: any,
tab?: string
) => {
incomingRouteParams.current = Object.assign(incomingRouteParams.current || {}, {
routeAction,
routeDirection,
routeOptions,
routeAnimation,
tab,
});
navigate(path, { replace: routeAction !== 'push' });
};
if (!didMountRef.current) {
locationHistory.current.add(routeInfo);
registerHistoryListener(handleHistoryChange);
}
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 || ''));
}
}
const routeMangerContextValue: RouteManagerContextState = {
canGoBack: () => locationHistory.current.canGoBack(),
clearOutlet: viewStack.current.clear,
findViewItemByPathname: viewStack.current.findViewItemByPathname,
getChildrenToRender: viewStack.current.getChildrenToRender,
goBack: () => handleNavigateBack(),
createViewItem: viewStack.current.createViewItem,
findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo,
findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo,
addViewItem: viewStack.current.add,
unMountViewItem: viewStack.current.remove,
};
handleSetCurrentTab(tab: string) {
this.currentTab = tab;
const ri = { ...this.locationHistory.current() };
if (ri.tab !== tab) {
ri.tab = tab;
this.locationHistory.update(ri);
}
}
return (
<RouteManagerContext.Provider value={routeMangerContextValue}>
<NavManager
ionRoute={IonRouteInner}
ionRedirect={{}}
stackManager={StackManager}
routeInfo={routeInfo}
onNativeBack={handleNativeBack}
onNavigateBack={handleNavigateBack}
onNavigate={handleNavigate}
onSetCurrentTab={handleSetCurrentTab}
onChangeTab={handleChangeTab}
onResetTab={handleResetTab}
locationHistory={locationHistory.current}
>
{children}
</NavManager>
</RouteManagerContext.Provider>
);
};
render() {
return (
<RouteManagerContext.Provider value={this.routeMangerContextState}>
<NavManager
ionRoute={IonRouteInner}
ionRedirect={{}}
stackManager={StackManager}
routeInfo={this.state.routeInfo!}
onNativeBack={this.handleNativeBack}
onNavigateBack={this.handleNavigateBack}
onNavigate={this.handleNavigate}
onSetCurrentTab={this.handleSetCurrentTab}
onChangeTab={this.handleChangeTab}
onResetTab={this.handleResetTab}
locationHistory={this.locationHistory}
>
{this.props.children}
</NavManager>
</RouteManagerContext.Provider>
);
}
}
export const IonRouter = withRouter(IonRouterInner);
IonRouter.displayName = 'IonRouter';

View File

@@ -1,20 +1,28 @@
/**
* `ReactRouterViewStack` is a custom navigation manager used in Ionic React
* apps to map React Router route elements (such as `<IonRoute>`) to "view
* items" that Ionic can manage in a view stack. This is critical to maintain
* Ionics animation, lifecycle, and history behavior across views.
*/
import type { RouteInfo, ViewItem } from '@ionic/react';
import { IonRoute, ViewLifeCycleManager, ViewStacks, generateId } from '@ionic/react';
import React from 'react';
import type { PathMatch } from 'react-router';
import { Routes } from 'react-router';
import { matchPath } from './utils/matchPath';
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.findViewItemByPathname = this.findViewItemByPathname.bind(this);
}
createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) {
/**
* Creates a new view item for the given outlet and react route element.
* Associates route props with the matched route path for further lookups.
*/
createViewItem = (outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) => {
const viewItem: ViewItem = {
id: generateId('viewItem'),
outletId,
@@ -38,149 +46,196 @@ export class ReactRouterViewStack extends ViewStacks {
};
return viewItem;
}
};
getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) {
/**
* Renders a ViewLifeCycleManager for the given view item.
* Handles cleanup if the view no longer matches.
*
* - Deactivates view if it no longer matches the current route
* - Wraps the route element in <Routes> to support nested routing and ensure remounting
* - Adds a unique key to <Routes> so React Router remounts routes when switching
*/
private renderViewItem = (viewItem: ViewItem, routeInfo: RouteInfo) => {
const match = matchComponent(viewItem.reactElement, routeInfo.pathname);
if (!match && viewItem.routeData.match) {
this.deactivateView(viewItem);
}
return (
<ViewLifeCycleManager key={`view-${viewItem.id}`} mount={viewItem.mount} removeView={() => this.remove(viewItem)}>
{/**
* Wrapped in <Routes> to ensure React Router v6 correctly processes nested route elements
* `key` is provided to enforce remounting of Routes when switching between view items.
*/}
<Routes key={`routes-${viewItem.id}`}>{React.cloneElement(viewItem.reactElement)}</Routes>
</ViewLifeCycleManager>
);
};
/**
* Re-renders all active view items for the specified outlet.
* Ensures React elements are updated with the latest match.
*
* 1. Iterates through children of IonRouterOutlet
* 2. Updates each matching viewItem with the current child React element
* (important for updating props or changes to elements)
* 3. Returns a list of React components that will be rendered inside the outlet
* Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
*/
getChildrenToRender = (outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) => {
const viewItems = this.getViewItemsForOutlet(outletId);
// Sync latest routes with viewItems
// Sync child elements with stored viewItems (e.g. to reflect new props)
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>
// Ensure the child is a valid React element sincewe
// might have whitespace strings or other non-element children
if (React.isValidElement(child)) {
const viewItem = viewItems.find((v) =>
matchComponent(child, v.routeData.childProps.path || routeInfo.pathname)
);
} 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;
if (viewItem) {
viewItem.reactElement = child;
}
}
return clonedChild;
});
return children;
}
findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) {
// Render all view items using renderViewItem
return viewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo));
};
/**
* Finds a view item matching the current route, optionally updating its match state.
*/
findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) => {
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
if (shouldUpdateMatch && viewItem && match) {
viewItem.routeData.match = match;
}
return viewItem;
}
findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, mustBeIonRoute = true) {
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, mustBeIonRoute);
return viewItem;
}
findViewItemByPathname(pathname: string, outletId?: string) {
const { viewItem } = this.findViewItemByPath(pathname, outletId);
return viewItem;
}
};
/**
* Returns the matching view item and the match result for a given pathname.
* Finds the view item that was previously active before a route change.
*/
findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: string, mustBeIonRoute = true) => {
// If the lastPathname is not set, we cannot find a leaving view item
if (!routeInfo.lastPathname) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[ReactRouterViewStack] No matching leaving view item found for: ${routeInfo.pathname}`);
}
return undefined;
}
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
return viewItem;
};
/**
* Finds a view item by pathname only, used in simpler queries.
*/
findViewItemByPathname = (pathname: string, outletId?: string) => {
const { viewItem } = this.findViewItemByPath(pathname, outletId);
return viewItem;
};
/**
* Core function that matches a given pathname against all view items.
* Returns both the matched view item and match metadata.
*/
private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute?: boolean) {
let viewItem: ViewItem | undefined;
let match: ReturnType<typeof matchPath> | undefined;
let match: PathMatch<string> | null = null;
let viewStack: ViewItem[];
if (outletId) {
viewStack = this.getViewItemsForOutlet(outletId);
viewStack.some(matchView);
if (!viewItem) {
viewStack.some(matchDefaultRoute);
}
if (!viewItem) viewStack.some(matchDefaultRoute);
} else {
const viewItems = this.getAllViewItems();
viewItems.some(matchView);
if (!viewItem) {
viewItems.some(matchDefaultRoute);
}
if (!viewItem) viewItems.some(matchDefaultRoute);
}
if (!viewItem && process.env.NODE_ENV !== 'production') {
console.warn(`[ReactRouterViewStack] No matching view item found for: ${pathname}`);
}
return { viewItem, match };
/**
* Matches a route path with dynamic parameters (e.g. /tabs/:id)
*/
function matchView(v: ViewItem) {
if (mustBeIonRoute && !v.ionRoute) {
return false;
}
if (mustBeIonRoute && !v.ionRoute) return false;
match = matchPath({
const result = matchPath({
pathname,
componentProps: v.routeData.childProps,
});
if (match) {
/**
* Even though we have a match from react-router, we do not know if the match
* is for this specific view item.
*
* To validate this, we need to check if the path and url match the view item's route data.
*/
const hasParameter = match.path.includes(':');
if (!hasParameter || (hasParameter && match.url === v.routeData?.match?.url)) {
if (result) {
const hasParams = result.params && Object.keys(result.params).length > 0;
const previousMatch = v.routeData?.match;
const isSamePath = result.pathname === previousMatch?.pathname;
if (!hasParams || isSamePath) {
match = result;
viewItem = v;
return true;
}
}
return false;
}
/**
* Matches a view with no path prop (default fallback route).
*/
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: {},
};
if (!v.routeData.childProps.path) {
match = createDefaultMatch(pathname);
viewItem = v;
return true;
}
return false;
}
}
/**
* Unmounts a view by clearing its match and setting mount to false.
*/
private deactivateView = (viewItem: ViewItem) => {
viewItem.routeData.match = undefined; // clear it so it's no longer active
viewItem.mount = false; // do not display the view anymore
};
}
/**
* Utility to apply matchPath to a React element and return its match state.
*/
function matchComponent(node: React.ReactElement, pathname: string) {
return matchPath({
pathname,
componentProps: node.props,
});
}
/**
* Creates a default match object for a fallback route.
*/
function createDefaultMatch(pathname: string): PathMatch<string> {
return {
params: {},
pathname,
pathnameBase: pathname,
pattern: {
path: pathname,
caseSensitive: false,
end: true,
},
};
}

View File

@@ -1,8 +1,16 @@
/**
* `StackManager` is responsible for managing page transitions, keeping track
* of views (pages), and ensuring that navigation behaves like native apps —
* particularly with animations and swipe gestures.
*/
import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react';
import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import React from 'react';
import { Route } from 'react-router';
import { clonePageElement } from './clonePageElement';
import { findRoutesNode } from './utils/findRoutesNode';
import { matchPath } from './utils/matchPath';
// TODO(FW-2959): types
@@ -18,7 +26,7 @@ const isViewVisible = (el: HTMLElement) =>
!el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
export class StackManager extends React.PureComponent<StackManagerProps, StackManagerState> {
id: string;
id: string; // Unique id for the router outlet aka outletId
context!: React.ContextType<typeof RouteManagerContext>;
ionRouterOutlet?: React.ReactElement;
routerOutletElement: HTMLIonRouterOutletElement | undefined;
@@ -79,25 +87,49 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.clearOutletTimeout = this.context.clearOutlet(this.id);
}
/**
* Sets the transition between pages within this router outlet.
* This function determines the entering and leaving views based on the
* provided route information and triggers the appropriate animation.
* It also handles scenarios like initial loads, back navigation, and
* navigation to the same view with different parameters.
*
* @param routeInfo It contains info about the current route,
* the previous route, and the action taken (e.g., push, replace).
*
* @returns A promise that resolves when the transition is complete.
* If no transition is needed or if the router outlet isn't ready,
* the Promise may resolve immediately.
*/
async handlePageTransition(routeInfo: RouteInfo) {
if (!this.routerOutletElement || !this.routerOutletElement.commit) {
/**
* The route outlet has not mounted yet. We need to wait for it to render
* before we can transition the page.
* The route outlet has not mounted yet (i.e., not in the DOM yet).
* We need to wait for it to render before we can transition the page.
*
* Set a flag to indicate that we should transition the page after
* the component has updated.
* the component has updated (i.e., in `componentDidUpdate`).
*/
this.pendingPageTransition = true;
} else {
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
/**
* If we don't have a leaving view item, but the route info indicates
* that the user has routed from a previous path, then the leaving view
* can be found by the last known pathname.
*/
if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
}
// Check if leavingViewItem should be unmounted
/**
* The leaving view item should be unmounted in the following cases:
* - Navigating with `replace`
* - Navigating forward but not pushing a new view (e.g., back navigation or non-animated transition) and the leaving view is not the same as the entering view
* - The routeOptions explicitly says unmount
*/
if (leavingViewItem) {
if (routeInfo.routeAction === 'replace') {
leavingViewItem.mount = false;
@@ -110,8 +142,13 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
}
}
const enteringRoute = matchRoute(this.ionRouterOutlet?.props.children, routeInfo) as React.ReactElement;
// Match the route element to render
const enteringRoute = findRouteByRouteInfo(this.ionRouterOutlet?.props.children, routeInfo) as React.ReactElement;
/**
* If we already have a view item for this route, update its element.
* Otherwise, create a new view item for the route.
*/
if (enteringViewItem) {
enteringViewItem.reactElement = enteringRoute;
} else if (enteringRoute) {
@@ -119,6 +156,9 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.context.addViewItem(enteringViewItem);
}
/**
* Begin transition only if we have an ionPageElement (i.e., the page has rendered).
*/
if (enteringViewItem && enteringViewItem.ionPageElement) {
/**
* If the entering view item is the same as the leaving view item,
@@ -127,21 +167,27 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
if (enteringViewItem === leavingViewItem) {
/**
* If the entering view item is the same as the leaving view item,
* we are either transitioning using parameterized routes to the same view
* or a parent router outlet is re-rendering as a result of React props changing.
* we are either transitioning using parameterized routes to the same
* view (e.g., `/user/1` → `/user/2`)
* or a parent router outlet is re-rendering as a result of React props
* changing (e.g., tab navigation).
*
* If the route data does not match the current path, the parent router outlet
* is attempting to transition and we cancel the operation.
* If the route data does not match the current path, it indicates a
* situation where the view within this nested outlet might already be
* visible due to the parent's re-render. In such cases
* (like tab navigation), we prevent a redundant transition in this
* outlet to avoid flickering.
*/
if (enteringViewItem.routeData.match.url !== routeInfo.pathname) {
if (enteringViewItem.routeData.match.pathname !== routeInfo.pathname) {
return;
}
}
/**
* If there isn't a leaving view item, but the route info indicates
* that the user has routed from a previous path, then we need
* to find the leaving view item to transition between.
* If the leaving view is still not found, especially during a
* 'pop' (back navigation) operation, try to retrieve it using the
* previous route information that was available as a prop on the
* component.
*/
if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
@@ -175,21 +221,29 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
*/
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 we have a leavingView but no entering view/route, we are probably
* leaving to another outlet, so hide this leavingView.
* (e.g., /tabs/tab1 → /settings)
*/
if (leavingViewItem.ionPageElement) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
// }, 250);
}
// Force re-render so views update according to their new mount/visible status
this.forceUpdate();
}
}
/**
* Registers an `<IonPage>` DOM element with the `StackManager`.
* This is called when `<IonPage>` has been mounted.
*
* @param page The element of the rendered `<IonPage>`.
* @param routeInfo The route information that associates with `<IonPage>`.
*/
registerIonPage(page: HTMLElement, routeInfo: RouteInfo) {
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
if (foundView) {
@@ -209,9 +263,15 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.handlePageTransition(routeInfo);
}
/**
* Configures the router outlet for the swipe-to-go-back gesture.
*
* @param routerOutlet The Ionic router outlet component: `<IonRouterOutlet>`.
*/
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const canStart = () => {
const config = getConfig();
// Check if swipe back is enabled in config (default to true for iOS mode)
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
if (!swipeEnabled) {
return false;
@@ -219,10 +279,12 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
const { routeInfo } = this.props;
// Determine the route to use for finding the view we would be navigating back to
const propsToUse =
this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: ({ pathname: routeInfo.pushedByRoute || '' } as any);
// Find the view item for the route we are going back to
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
return (
@@ -242,18 +304,21 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
* Make sure that we are not swiping back to the same
* instances of a view.
*/
enteringViewItem.routeData.match.path !== routeInfo.pathname
enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname
);
};
const onStart = async () => {
const { routeInfo } = this.props;
// Determine the route to use for finding the view we would be navigating back to
const propsToUse =
this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: ({ pathname: routeInfo.pushedByRoute || '' } as any);
// Find the view item for the route we are going back to
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
// Find the view item for the route we are going back from
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
/**
@@ -267,8 +332,10 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
return Promise.resolve();
};
const onEnd = (shouldContinue: boolean) => {
if (shouldContinue) {
// User finished the swipe gesture, so complete the back navigation
this.skipTransition = true;
this.context.goBack();
@@ -280,11 +347,14 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
*/
const { routeInfo } = this.props;
// Determine the route to use for finding the view we would be navigating back to
const propsToUse =
this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: ({ pathname: routeInfo.pushedByRoute || '' } as any);
// Find the view item for the route we are going back to
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
// Find the view item for the route we are going back from
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
/**
@@ -311,6 +381,18 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
};
}
/**
* Animates the transition between the entering and leaving pages within the
* router outlet.
*
* @param routeInfo Info about the current route.
* @param enteringViewItem The view item that is entering.
* @param leavingViewItem The view item that is leaving.
* @param direction The direction of the transition.
* @param progressAnimation Indicates if the transition is part of a
* gesture controlled animation (e.g., swipe to go back).
* Defaults to `false`.
*/
async transitionPage(
routeInfo: RouteInfo,
enteringViewItem: ViewItem,
@@ -367,8 +449,8 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
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);
// (e.g., `/user/1` → `/user/2`)
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname);
if (match) {
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
if (newLeavingElement) {
@@ -377,11 +459,23 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.routerOutletElement.removeChild(newLeavingElement);
}
} else {
/**
* The route no longer matches the component type of the leaving view.
* (e.g., `/user/1` → `/settings`)
*
* This can also occur in edge cases like rapid navigation
* or during parent component re-renders that briefly cause
* the view items to be the same instance before the final
* route component is determined.
*/
await runCommit(enteringViewItem.ionPageElement, undefined);
}
} else {
// The leaving view is not the same as the entering view
// (e.g., `/home` → `/settings` or initial load `/`)
await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
// An initiial load will not have a leaving view.
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
@@ -392,10 +486,10 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
render() {
const { children } = this.props;
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
this.ionRouterOutlet = ionRouterOutlet;
this.ionRouterOutlet = ionRouterOutlet; // TODO: check if we can use a ref instead of storing this in the class
const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
this.forceUpdate();
this.forceUpdate(); // TODO: investigate why this is needed
});
return (
@@ -405,13 +499,16 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
{
ref: (node: HTMLIonRouterOutletElement) => {
if (ionRouterOutlet.props.setRef) {
// Needed to handle external refs from devs.
ionRouterOutlet.props.setRef(node);
}
if (ionRouterOutlet.props.forwardedRef) {
// Needed to handle external refs from devs.
ionRouterOutlet.props.forwardedRef.current = node;
}
this.routerOutletElement = node;
const { ref } = ionRouterOutlet as any;
// Check for legacy refs.
if (typeof ref === 'function') {
ref(node);
}
@@ -430,30 +527,51 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
export default StackManager;
function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
/**
* Finds the `<Route />` node matching the current route info.
* If no `<Route />` can be matched, a fallback node is returned.
*
* @param node The root node to search for `<Route />` nodes.
* @param routeInfo The route information to match against.
*/
function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo) {
let matchedNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
const match = matchPath({
pathname: routeInfo.pathname,
componentProps: child.props,
});
if (match) {
matchedNode = child;
let fallbackNode: React.ReactNode;
// `<Route />` nodes are rendered inside of a <Routes /> node
const routesNode = findRoutesNode(node) ?? node;
for (const child of React.Children.toArray(routesNode) as React.ReactElement[]) {
// Check if the child is a `<Route />` node
if (child.type === Route) {
const match = matchPath({
pathname: routeInfo.pathname,
componentProps: child.props,
});
if (match) {
matchedNode = child;
break;
}
}
});
}
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;
// If we haven't found a node
// try to find one that doesn't have a path prop, that will be our not found route
for (const child of React.Children.toArray(routesNode) as React.ReactElement[]) {
if (child.type === Route) {
if (!child.props.path) {
fallbackNode = child;
break;
}
}
}
return matchedNode ?? fallbackNode;
}
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
@@ -461,7 +579,7 @@ function matchComponent(node: React.ReactElement, pathname: string, forceExact?:
pathname,
componentProps: {
...node.props,
exact: forceExact,
end: forceExact,
},
});
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Routes } from 'react-router';
export const findRoutesNode = (node: React.ReactNode) => {
// The use of `<Routes />` is encouraged with React Router v6.
let routesNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
if (child.type === Routes) {
routesNode = child;
}
});
if (routesNode) {
// The childern of the `<Routes />` component are most likely
// (and should be) the `<Route />` components.
return (routesNode as React.ReactElement).props.children;
}
return undefined;
};

View File

@@ -1,4 +1,5 @@
import { matchPath as reactRouterMatchPath } from 'react-router';
import type { PathMatch } from 'react-router';
import { matchPath as reactRouterMatchPath } from 'react-router-dom';
interface MatchPathOptions {
/**
@@ -10,37 +11,30 @@ interface MatchPathOptions {
*/
componentProps: {
path?: string;
from?: string;
component?: any;
exact?: boolean;
caseSensitive?: boolean;
end?: boolean;
index?: boolean;
};
}
/**
* @see https://v5.reactrouter.com/web/api/matchPath
* The matchPath function is used only for matching paths, not rendering components or elements.
* @see https://reactrouter.com/v6/utils/match-path
*/
export const matchPath = ({
pathname,
componentProps,
}: MatchPathOptions): false | ReturnType<typeof reactRouterMatchPath> => {
const { exact, component } = componentProps;
export const matchPath = ({ pathname, componentProps }: MatchPathOptions): PathMatch<string> | null => {
const { path, ...restProps } = componentProps;
const path = componentProps.path || componentProps.from;
/***
* The props to match against, they are identical
* to the matching props `Route` accepts. It could also be a string
* or an array of strings as shortcut for `{ path }`.
*/
const matchProps = {
exact,
path,
component,
};
if (!path) {
console.warn('[Ionic] matchPath: No path prop provided. This will always return null.', {
componentProps,
});
return null;
}
const match = reactRouterMatchPath(pathname, matchProps);
const match = reactRouterMatchPath({ path, ...restProps }, pathname);
if (!match) {
return false;
return null;
}
return match;

View File

@@ -8,13 +8,13 @@
"name": "react-router-new",
"version": "0.0.1",
"dependencies": {
"@ionic/react": "^6.6.1",
"@ionic/react-router": "^6.6.1",
"@ionic/react": "^8.6.1",
"@ionic/react-router": "^8.6.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"ionicons": "^6.0.4",
@@ -2295,22 +2295,52 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"node_modules/@ionic/core": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.6.1.tgz",
"integrity": "sha512-+LMBk7kUX55rvYQ35AiAXPNzbNm3zNx9ginvuCzByguMjl+N63lpdPzIEfeRURkmq7NByD1VqpodMj5c6Oq2KQ==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.2.tgz",
"integrity": "sha512-CGZ9CDp/XHtm9WrK3wt0ZtR2f2B76qEvJIaF/juCqmpza9Al6u2L9R/NTEwInDRCWfbkAIF22nHNH54/VvN78Q==",
"dependencies": {
"@stencil/core": "^2.18.0",
"ionicons": "^6.1.3",
"@stencil/core": "4.33.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
},
"node_modules/@ionic/react": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-6.6.1.tgz",
"integrity": "sha512-gq8FzC0CAPt6MpOFethe9+zIU7jg1JyWPWRANJ/UudlF05f2eFOzLgqe/EH0uIIsuDjeoM50hrqfuvg6x2j3UQ==",
"node_modules/@ionic/core/node_modules/@stencil/core": {
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"bin": {
"stencil": "bin/stencil"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.10.0"
},
"optionalDependencies": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.9"
}
},
"node_modules/@ionic/core/node_modules/ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"dependencies": {
"@ionic/core": "6.6.1",
"ionicons": "^6.1.3",
"@stencil/core": "^4.0.3"
}
},
"node_modules/@ionic/react": {
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.6.2.tgz",
"integrity": "sha512-SXE1RnzGqj0MGKGs6D4UCk4rOghbLYI5qwANdZJuBxlIcrcBJuAySjneuTGt+Y3UHS8W3YZHFujRv2Gvb+zvqQ==",
"dependencies": {
"@ionic/core": "8.6.2",
"ionicons": "^7.0.0",
"tslib": "*"
},
"peerDependencies": {
@@ -2319,11 +2349,11 @@
}
},
"node_modules/@ionic/react-router": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-6.6.1.tgz",
"integrity": "sha512-9bHlz3MdzvkUyZ9QfxzcAGDtbRhZ7R5uMjm3UHvGhYS1Rdx4KIc8E5q31IQf7H6j2ULU9YcB7UeyW5ORxBX18Q==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-8.6.2.tgz",
"integrity": "sha512-wNVYZHEHkRkNimiK24bJ8KsWjuQyug7C+J/rNER7BKtZDzU3kWKVjvzD3P7kaiOf/DtVo+OrZNvYQJOuoIEhWg==",
"dependencies": {
"@ionic/react": "6.6.1",
"@ionic/react": "8.6.2",
"tslib": "*"
},
"peerDependencies": {
@@ -2333,6 +2363,36 @@
"react-router-dom": "^5.0.1"
}
},
"node_modules/@ionic/react/node_modules/@stencil/core": {
"version": "4.35.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.1.tgz",
"integrity": "sha512-u65m3TbzOtpn679gUV4Yvi8YpInhRJ62js30a7YtXief9Ej/vzrhwDE22U0w4DMWJOYwAsJl133BUaZkWwnmzg==",
"bin": {
"stencil": "bin/stencil"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.10.0"
},
"optionalDependencies": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.9"
}
},
"node_modules/@ionic/react/node_modules/ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"dependencies": {
"@stencil/core": "^4.0.3"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -3775,6 +3835,102 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz",
@@ -23882,31 +24038,81 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"@ionic/core": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.6.1.tgz",
"integrity": "sha512-+LMBk7kUX55rvYQ35AiAXPNzbNm3zNx9ginvuCzByguMjl+N63lpdPzIEfeRURkmq7NByD1VqpodMj5c6Oq2KQ==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.2.tgz",
"integrity": "sha512-CGZ9CDp/XHtm9WrK3wt0ZtR2f2B76qEvJIaF/juCqmpza9Al6u2L9R/NTEwInDRCWfbkAIF22nHNH54/VvN78Q==",
"requires": {
"@stencil/core": "^2.18.0",
"ionicons": "^6.1.3",
"@stencil/core": "4.33.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
},
"dependencies": {
"@stencil/core": {
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.9"
}
},
"ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"requires": {
"@stencil/core": "^4.0.3"
}
}
}
},
"@ionic/react": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-6.6.1.tgz",
"integrity": "sha512-gq8FzC0CAPt6MpOFethe9+zIU7jg1JyWPWRANJ/UudlF05f2eFOzLgqe/EH0uIIsuDjeoM50hrqfuvg6x2j3UQ==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.6.2.tgz",
"integrity": "sha512-SXE1RnzGqj0MGKGs6D4UCk4rOghbLYI5qwANdZJuBxlIcrcBJuAySjneuTGt+Y3UHS8W3YZHFujRv2Gvb+zvqQ==",
"requires": {
"@ionic/core": "6.6.1",
"ionicons": "^6.1.3",
"@ionic/core": "8.6.2",
"ionicons": "^7.0.0",
"tslib": "*"
},
"dependencies": {
"@stencil/core": {
"version": "4.35.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.1.tgz",
"integrity": "sha512-u65m3TbzOtpn679gUV4Yvi8YpInhRJ62js30a7YtXief9Ej/vzrhwDE22U0w4DMWJOYwAsJl133BUaZkWwnmzg==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.9"
}
},
"ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"requires": {
"@stencil/core": "^4.0.3"
}
}
}
},
"@ionic/react-router": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-6.6.1.tgz",
"integrity": "sha512-9bHlz3MdzvkUyZ9QfxzcAGDtbRhZ7R5uMjm3UHvGhYS1Rdx4KIc8E5q31IQf7H6j2ULU9YcB7UeyW5ORxBX18Q==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-8.6.2.tgz",
"integrity": "sha512-wNVYZHEHkRkNimiK24bJ8KsWjuQyug7C+J/rNER7BKtZDzU3kWKVjvzD3P7kaiOf/DtVo+OrZNvYQJOuoIEhWg==",
"requires": {
"@ionic/react": "6.6.1",
"@ionic/react": "8.6.2",
"tslib": "*"
}
},
@@ -24962,6 +25168,54 @@
}
}
},
"@rollup/rollup-darwin-arm64": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
"optional": true
},
"@rollup/rollup-darwin-x64": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
"optional": true
},
"@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
"optional": true
},
"@rollup/rollup-linux-arm64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
"optional": true
},
"@rollup/rollup-linux-x64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
"optional": true
},
"@rollup/rollup-linux-x64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
"optional": true
},
"@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
"optional": true
},
"@rollup/rollup-win32-x64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
"optional": true
},
"@rushstack/eslint-patch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz",

View File

@@ -3,8 +3,8 @@
"version": "0.0.1",
"private": true,
"dependencies": {
"@ionic/react": "^6.6.1",
"@ionic/react-router": "^6.6.1",
"@ionic/react": "^8.6.1",
"@ionic/react-router": "^8.6.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",

View File

@@ -0,0 +1,72 @@
import { IonApp, setupIonicReact, IonRouterOutlet } from '@ionic/react';
import React from 'react';
import { Route } from 'react-router-dom';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/display.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/padding.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
/* Theme variables */
import './theme/variables.css';
import Main from './pages/Main';
import { IonReactRouter } from '@ionic/react-router';
import DynamicRoutes from './pages/dynamic-routes/DynamicRoutes';
import Routing from './pages/routing/Routing';
import MultipleTabs from './pages/muiltiple-tabs/MultipleTabs';
import DynamicTabs from './pages/dynamic-tabs/DynamicTabs';
import NestedOutlet from './pages/nested-outlet/NestedOutlet';
import NestedOutlet2 from './pages/nested-outlet/NestedOutlet2';
import ReplaceAction from './pages/replace-action/Replace';
import TabsContext from './pages/tab-context/TabContext';
import { OutletRef } from './pages/outlet-ref/OutletRef';
import { SwipeToGoBack } from './pages/swipe-to-go-back/SwipToGoBack';
import Refs from './pages/refs/Refs';
import DynamicIonpageClassnames from './pages/dynamic-ionpage-classnames/DynamicIonpageClassnames';
import Tabs from './pages/tabs/Tabs';
import TabsSecondary from './pages/tabs/TabsSecondary';
import Params from './pages/params/Params';
import Overlays from './pages/overlays/Overlays';
setupIonicReact();
const App: React.FC = () => {
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/" component={Main} exact />
<Route path="/routing" component={Routing} />
<Route path="/dynamic-routes" component={DynamicRoutes} />
<Route path="/multiple-tabs" component={MultipleTabs} />
<Route path="/dynamic-tabs" component={DynamicTabs} />
<Route path="/nested-outlet" component={NestedOutlet} />
<Route path="/nested-outlet2" component={NestedOutlet2} />
<Route path="/replace-action" component={ReplaceAction} />
<Route path="/tab-context" component={TabsContext} />
<Route path="/outlet-ref" component={OutletRef} />
<Route path="/swipe-to-go-back" component={SwipeToGoBack} />
<Route path="/dynamic-ionpage-classnames" component={DynamicIonpageClassnames} />
<Route path="/tabs" component={Tabs} />
<Route path="/tabs-secondary" component={TabsSecondary} />
<Route path="/refs" component={Refs} />
<Route path="/overlays" component={Overlays} />
<Route path="/params/:id" component={Params} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
};
export default App;

View File

@@ -0,0 +1,60 @@
import React, { useEffect, useRef, useState } from 'react';
import {
IonButton,
IonContent,
IonHeader,
IonPage,
IonRouterOutlet,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { Route } from 'react-router';
interface DynamicIonpageClassnamesProps {}
const DynamicIonpageClassnames: React.FC<DynamicIonpageClassnamesProps> = () => {
return (
<IonRouterOutlet>
<Route path="/dynamic-ionpage-classnames" component={Page} />
</IonRouterOutlet>
);
};
export default DynamicIonpageClassnames;
const Page: React.FC = (props) => {
const [styleClass, setStyleClass] = useState('initial-class');
const [divClasses, setDivClasses] = useState<string>();
const ref = useRef<HTMLDivElement>();
useEffect(() => {
if(ref.current) {
var observer = new MutationObserver(function (event) {
setDivClasses(ref.current?.className)
})
observer.observe(ref.current, {
attributes: true,
attributeFilter: ['class'],
childList: false,
characterData: false
})
}
return () => observer.disconnect()
}, [])
return (
<IonPage className={styleClass} ref={ref} data-pageid="dynamic-ionpage-classnames">
<IonHeader>
<IonToolbar>
<IonTitle>Dynamic Ionpage Classnames</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton onClick={() => setStyleClass('other-class')}>Add Class</IonButton>
<br />
Div classes: {divClasses}
</IonContent>
</IonPage>
);
};

View File

@@ -0,0 +1,112 @@
import React, { useState, ReactElement } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonRouterOutlet,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Link } from 'react-router-dom';
const DynamicRoutes: React.FC = () => {
const [routes, setRoutes] = useState<ReactElement[]>([
<Route
key="sldjflsdj"
path="/dynamic-routes/home"
render={() => <Home update={addRoute} />}
exact={true}
/>,
]);
const addRoute = () => {
const newRoute = (
<Route key="lsdjldj" path="/dynamic-routes/newRoute" component={NewRoute} exact={true} />
);
setRoutes([...routes, newRoute]);
};
return (
<IonRouterOutlet>
{routes}
{/* <Route exact path="/home" render={() => <Home update={addRoute} />} /> */}
<Route exact path="/dynamic-routes" render={() => <Redirect to="/dynamic-routes/home" />} />
<Route render={() => <Failed />} />
</IonRouterOutlet>
);
};
export default DynamicRoutes;
const Home: React.FC<{
update: Function;
}> = (props) => {
const updateRoute = () => {
props.update();
};
return (
<IonPage data-pageid="dynamic-routes-home">
<IonHeader>
<IonToolbar>
<IonTitle>HOME</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">HOME</IonTitle>
</IonToolbar>
</IonHeader>
<div className="container">
<strong>Click Add Route Button</strong>
<br />
<button className="" onClick={() => updateRoute()}>
Add Route
</button>
<br />
<Link to="/dynamic-routes/newRoute">Take me to the newRoute</Link>
</div>
</IonContent>
</IonPage>
);
};
const NewRoute: React.FC = () => {
return (
<IonPage data-pageid="dynamic-routes-newroute">
<IonHeader>
<IonToolbar>
<IonTitle>New Route</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">New Route</IonTitle>
</IonToolbar>
</IonHeader>
</IonContent>
</IonPage>
);
};
const Failed: React.FC = () => {
return (
<IonPage data-pageid="dynamic-routes-failed">
<IonHeader>
<IonToolbar>
<IonTitle>New Route Failed</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">New Route Failed</IonTitle>
</IonToolbar>
</IonHeader>
</IonContent>
</IonPage>
);
};

View File

@@ -0,0 +1,107 @@
import React, { useState, useCallback } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonApp,
IonTabs,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonButton,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { IonReactRouter } from '@ionic/react-router';
import { triangle, square } from 'ionicons/icons';
const DynamicTabs: React.FC = () => {
const [display2ndTab, setDisplayThirdTab] = useState<boolean>(false);
const renderFirstTab = useCallback(() => {
return <Tab1 setDisplayThirdTab={() => setDisplayThirdTab(!display2ndTab)} />;
}, [display2ndTab]);
const render2ndTabRoute = useCallback(() => {
if (display2ndTab) {
return <Route path="/dynamic-tabs/tab2" component={Tab2} />;
} else {
// This is weird, if I return null or undefined then I get all sorts of errors, seemingly
// because the router is mad about a child not being a route.
return <Route path="/dynamic-tabs/tab200" component={Tab1} />;
}
}, [display2ndTab]);
return (
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route path="/dynamic-tabs/tab1" render={renderFirstTab} exact={true} />
{render2ndTabRoute()}
<Route
path="/dynamic-tabs/"
render={() => <Redirect to="/dynamic-tabs/tab1" />}
exact={true}
/>
<Route render={() => <Redirect to="/dynamic-tabs/tab1" />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/dynamic-tabs/tab1">
<IonIcon icon={triangle} />
<IonLabel>Tab 1</IonLabel>
</IonTabButton>
{display2ndTab && (
<IonTabButton tab="tab2" href="/dynamic-tabs/tab2">
<IonIcon icon={square} />
<IonLabel>Tab 2</IonLabel>
</IonTabButton>
)}
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
};
export default DynamicTabs;
const Tab1: React.FC<{
setDisplayThirdTab: (value: boolean) => void;
}> = ({ setDisplayThirdTab }) => {
const doIt = useCallback(() => {
setDisplayThirdTab(true);
}, [setDisplayThirdTab]);
return (
<IonPage data-pageid="Tab1">
<IonHeader>
<IonToolbar>
<IonTitle>Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div>Tab 1 Page</div>
<IonButton onClick={doIt}>Add Tab 2</IonButton>
</IonContent>
</IonPage>
);
};
const Tab2 = () => {
return (
<IonPage data-pageid="Tab2">
<IonHeader>
<IonToolbar>
<IonTitle>Tab 2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div>Tab 2 Page</div>
</IonContent>
</IonPage>
);
};

View File

@@ -0,0 +1,123 @@
import React from 'react';
import {
IonSplitPane,
IonRouterOutlet,
IonTabs,
IonTabBar,
IonTabButton,
IonLabel,
IonPage,
IonContent,
IonHeader,
IonMenuButton,
IonButtons,
IonIcon,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Menu } from './Menu';
import { triangle, ellipse, square, rocket } from 'ionicons/icons';
const MultipleTabs: React.FC = () => {
return (
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route
path="/multiple-tabs/tab1"
render={() => {
return <Tab1 />;
}}
exact={false}
/>
<Route
path="/multiple-tabs/tab2"
render={() => {
return <Tab2 />;
}}
exact={false}
/>
<Route
path="/multiple-tabs"
render={() => <Redirect to="/multiple-tabs/tab1" />}
exact={true}
/>
</IonRouterOutlet>
</IonSplitPane>
);
};
export default MultipleTabs;
const Tab1: React.FC = () => {
return (
<IonTabs>
<IonTabBar slot="bottom">
<IonTabButton tab="pagea" href="/multiple-tabs/tab1/pagea">
<IonIcon icon={triangle} />
<IonLabel>Page A</IonLabel>
</IonTabButton>
<IonTabButton tab="pageb" href="/multiple-tabs/tab1/pageb">
<IonIcon icon={ellipse} />
<IonLabel>Page B</IonLabel>
</IonTabButton>
</IonTabBar>
<IonRouterOutlet id="tab1">
<Route
path="/multiple-tabs/tab1"
render={() => <Redirect to="/multiple-tabs/tab1/pagea" />}
exact={true}
/>
{/* <Redirect path="/multiple-tabs/event" to="/multiple-tabs/tab1/pagea" exact={true} /> */}
<Route path="/multiple-tabs/tab1/pagea" render={() => <Page name="PageA" />} exact={true} />
<Route path="/multiple-tabs/tab1/pageb" render={() => <Page name="PageB" />} exact={true} />
</IonRouterOutlet>
</IonTabs>
);
};
const Tab2: React.FC = () => {
return (
<IonTabs>
<IonTabBar slot="bottom">
<IonTabButton tab="pagec" href="/multiple-tabs/tab2/pagec">
<IonIcon icon={square} />
<IonLabel>Page C</IonLabel>
</IonTabButton>
<IonTabButton tab="paged" href="/multiple-tabs/tab2/paged">
<IonIcon icon={rocket} />
<IonLabel>Page D</IonLabel>
</IonTabButton>
</IonTabBar>
<IonRouterOutlet id="tab2">
<Route
path="/multiple-tabs/tab2"
render={() => <Redirect to="/multiple-tabs/tab2/pagec" />}
exact={true}
/>
{/* <Redirect path="/multiple-tabs/tab2" to="/multiple-tabs/tab2/pagec" exact={true} /> */}
<Route path="/multiple-tabs/tab2/pagec" render={() => <Page name="PageC" />} exact={true} />
<Route path="/multiple-tabs/tab2/paged" render={() => <Page name="PageD" />} exact={true} />
</IonRouterOutlet>
</IonTabs>
);
};
const Page: React.FC<{ name: string }> = ({ name }) => {
return (
<IonPage data-pageid={name}>
<IonHeader>
<IonToolbar>
<IonButtons>
<IonMenuButton />
<IonTitle>{name}</IonTitle>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>{name}</IonContent>
</IonPage>
);
};

View File

@@ -0,0 +1,89 @@
import {
IonButton,
IonContent,
IonHeader,
IonPage,
IonRouterOutlet,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { useEffect } from 'react';
import React from 'react';
import { Route, Redirect } from 'react-router';
const Page: React.FC = () => {
useEffect(() => {
console.log('mount MySubPage');
return () => {
console.log('unmount MySubPage');
};
}, []);
return (
<IonPage data-pageid="secondpage">
<IonHeader>
<IonToolbar>
<IonTitle>Second Page</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton routerLink="/nested-outlet" routerDirection="root">
Back with direction "root"
</IonButton>
<IonButton routerLink="/nested-outlet" routerDirection="back">
Back with direction "back"
</IonButton>
</IonContent>
</IonPage>
);
};
const SecondPage: React.FC = () => {
useEffect(() => {
console.log('mount secondpage');
return () => {
console.log('unmount secondpage'); // Never called.
};
}, []);
return (
<IonRouterOutlet ionPage>
<Route
path="/nested-outlet/secondpage"
exact={true}
render={() => <Redirect to="/nested-outlet/secondpage/page" />}
/>
<Route path="/nested-outlet/secondpage/page" component={Page} exact={true} />
</IonRouterOutlet>
);
};
const FirstPage: React.FC = () => {
useEffect(() => {
console.log('mount FirstPage');
return () => {
console.log('unmount FirstPage');
};
}, []);
return (
<IonPage data-pageid="firstpage">
<IonHeader>
<IonToolbar>
<IonTitle>FirstPage</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton routerLink="/nested-outlet/secondpage/page" routerDirection="forward">
Go to second page
</IonButton>
</IonContent>
</IonPage>
);
};
const NestedOutlet: React.FC = () => (
<IonRouterOutlet>
<Route path="/nested-outlet" component={FirstPage} exact={true} />
<Route path="/nested-outlet/secondpage" component={SecondPage} />
</IonRouterOutlet>
);
export default NestedOutlet;

View File

@@ -0,0 +1,135 @@
import React from 'react';
import { Redirect, Route, RouteComponentProps } from 'react-router-dom';
import {
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonList,
IonPage,
IonRouterOutlet,
IonTitle,
IonToolbar,
} from '@ionic/react';
const ListPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<IonRouterOutlet ionPage id="listpage">
<Route exact path="/nested-outlet2/list" component={List} />
<Route path={`${match.url}/:id`} component={Item} />
</IonRouterOutlet>
);
};
const List: React.FC = () => {
return (
<IonPage data-pageid="list">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref={'/nested-outlet2/home'} text={'Home'} />
</IonButtons>
<IonTitle>List</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem detail routerLink="/nested-outlet2/list/1">
Item #1
</IonItem>
<IonItem detail routerLink="/nested-outlet2/list/2">
Item #2
</IonItem>
<IonItem detail routerLink="/nested-outlet2/list/3">
Item #3
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
const Item: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
return (
<IonPage data-pageid="item">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Item</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>Detail of item #{match.params.id}</IonContent>
</IonPage>
);
};
const HomePage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<IonRouterOutlet ionPage id="homepage">
<Route exact path="/nested-outlet2/home" component={Home} />
<Route path="/nested-outlet2/home/welcome" component={Welcome} />
</IonRouterOutlet>
);
};
const Welcome: React.FC = () => {
return (
<IonPage data-pageid="welcome">
<IonHeader>
<IonToolbar>
<IonTitle>Welcome</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem routerLink="/nested-outlet2/list" detail>
<IonLabel>Go to list from Welcome</IonLabel>
</IonItem>
<IonItem routerLink="/nested-outlet2/list/1" detail>
<IonLabel>Go to first item</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
const Home: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
return (
<IonPage data-pageid="home">
<IonHeader>
<IonToolbar>
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem routerLink="/nested-outlet2/home/welcome" target="_blank" detail>
<IonLabel>Go to Welcome</IonLabel>
</IonItem>
<IonItem routerLink="/nested-outlet2/list" detail>
<IonLabel>Go to list from Home</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
const NestedOutlet2: React.FC = () => (
<IonRouterOutlet id="main">
<Route path="/nested-outlet2/list" component={ListPage} />
<Route path="/nested-outlet2/home" component={HomePage} />
<Route
path="/nested-outlet2"
render={() => <Redirect to="/nested-outlet2/home" />}
exact={true}
/>
</IonRouterOutlet>
);
export default NestedOutlet2;

View File

@@ -0,0 +1,46 @@
import React, { useRef, useEffect } from 'react';
import {
IonRouterOutlet,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
} from '@ionic/react';
import { Route } from 'react-router';
interface OutletRefProps {}
export const OutletRef: React.FC<OutletRefProps> = () => {
const ref = useRef<HTMLIonRouterOutletElement>(null);
useEffect(() => {
console.log(ref);
}, []);
return (
<IonRouterOutlet id="main-outlet" ref={ref}>
<Route
path="/outlet-ref"
render={() => {
return <Main outletId={ref.current?.id} />;
}}
/>
</IonRouterOutlet>
);
};
const Main: React.FC<{ outletId?: string }> = ({ outletId }) => {
return (
<IonPage data-pageid="main">
<IonHeader>
<IonToolbar>
<IonTitle>Main</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div>{outletId}</div>
</IonContent>
</IonPage>
);
};

View File

@@ -0,0 +1,41 @@
import { IonButton, IonContent, IonModal } from '@ionic/react';
import { useState } from 'react';
import { useHistory } from 'react-router';
const Overlays: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const history = useHistory();
const goBack = () => history.goBack();
const replace = () => history.replace('/');
const push = () => history.push('/');
return (
<>
<IonButton id="openModal" onClick={() => setIsOpen(true)}>
Open Modal
</IonButton>
<IonModal
isOpen={isOpen}
onDidDismiss={() => {
setIsOpen(false);
}}
>
<IonContent>
<IonButton id="goBack" onClick={goBack}>
Go Back
</IonButton>
<IonButton id="replace" onClick={replace}>
Replace
</IonButton>
<IonButton id="push" onClick={push}>
Push
</IonButton>
</IonContent>
</IonModal>
</>
);
};
export default Overlays;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import {
IonButtons,
IonBackButton,
IonButton,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { RouteComponentProps } from 'react-router';
interface PageProps
extends RouteComponentProps<{
id: string;
}> {}
const Page: React.FC<PageProps> = ({ match }) => {
const parseID = parseInt(match.params.id);
return (
<IonPage data-pageid={'params-' + match.params.id }>
<IonHeader>
<IonToolbar>
<IonTitle>Params { match.params.id }</IonTitle>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton id="next-page" routerLink={'/params/' + (parseID + 1) } >Go to next param</IonButton>
<br />
Page ID: { match.params.id }
</IonContent>
</IonPage>
);
};
export default Page;

View File

@@ -0,0 +1,54 @@
import React, { useRef } from "react";
import {
IonContent,
IonHeader,
IonPage,
IonRouterOutlet,
IonTitle,
IonToolbar,
} from "@ionic/react";
import { Route } from "react-router";
interface RefsProps {}
const Refs: React.FC = () => {
return (
<IonRouterOutlet>
{/* <Route exact path="/home" render={() => <Home update={addRoute} />} /> */}
<Route exact path="/refs" component={RefsFC} />
<Route exact path="/refs/class" component={RefsClass} />
</IonRouterOutlet>
);
};
const RefsFC: React.FC<RefsProps> = () => {
const contentRef = useRef<HTMLIonContentElement>(null);
return (
<IonPage data-pageid="refs-fc">
<IonHeader>
<IonToolbar>
<IonTitle>Refs FC</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent ref={contentRef} className="ref-test"></IonContent>
</IonPage>
);
};
class RefsClass extends React.Component {
ref = React.createRef<HTMLIonContentElement>();
render() {
return (
<IonPage data-pageid="refs-class">
<IonHeader>
<IonToolbar>
<IonTitle>Refs Class</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent ref={this.ref} className="ref-test"></IonContent>
</IonPage>
);
}
}
export default Refs;

View File

@@ -0,0 +1,86 @@
import React from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButton,
IonRouterOutlet,
IonButtons,
IonBackButton,
} from '@ionic/react';
import { Route, Redirect, useHistory } from 'react-router';
interface TopPageProps {}
const ReplaceAction: React.FC<TopPageProps> = () => {
return (
<IonRouterOutlet>
<Route path="/replace-action/page1" component={Page1} exact />
<Route path="/replace-action/page2" component={Page2} exact />
<Route path="/replace-action/page3" component={Page3} exact />
<Route exact path="/replace-action" render={() => <Redirect to="/replace-action/page1" />} />
</IonRouterOutlet>
);
};
const Page1: React.FC = () => (
<IonPage data-pageid="page1">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Page one</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton routerLink={'/replace-action/page2'}>Goto Page2</IonButton>
</IonContent>
</IonPage>
);
const Page2: React.FC = () => {
const history = useHistory();
const clickButton = () => {
history.replace('/replace-action/page3');
};
return (
<IonPage data-pageid="page2">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Page two</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton onClick={() => clickButton()}>Goto Page3</IonButton>
</IonContent>
</IonPage>
);
};
const Page3: React.FC = () => {
return (
<IonPage data-pageid="page3">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/replace-action/page1" />
</IonButtons>
<IonTitle>Page three</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<p>Page 3</p>
</IonContent>
</IonPage>
);
};
export default ReplaceAction;

View File

@@ -0,0 +1,65 @@
import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonBackButton,
IonLabel,
IonButton,
} from '@ionic/react';
import { useParams, useLocation } from 'react-router';
interface DetailsProps {}
const Details: React.FC<DetailsProps> = () => {
const { id } = useParams<{ id: string }>();
const location = useLocation();
useEffect(() => {
console.log('Home Details mount');
return () => console.log('Home Details unmount');
}, []);
const nextId = parseInt(id, 10) + 1;
return (
<IonPage data-pageid={`home-details-page-${id}`}>
<IonHeader>
<IonToolbar>
<IonButtons>
<IonBackButton></IonBackButton>
</IonButtons>
<IonTitle>Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonLabel data-testid="details-label">Details {id}</IonLabel>
<br />
<br />
{location.search && (
<>
<IonLabel data-testid="query-label">Query Params: {location.search}</IonLabel>
<br />
<br />
</>
)}
<IonButton routerLink={`/routing/tabs/home/details/${nextId}`}>
<IonLabel>Go to Details {nextId}</IonLabel>
</IonButton>
<br />
<IonButton routerLink={`/routing/tabs/settings/details/1`}>
<IonLabel>Go to Settings Details 1</IonLabel>
</IonButton>
<br />
<br />
<input data-testid="details-input" />
</IonContent>
</IonPage>
);
};
export default Details;

View File

@@ -0,0 +1,47 @@
import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonBackButton,
useIonViewWillEnter,
IonButton,
} from '@ionic/react';
interface OtherPageProps {}
const OtherPage: React.FC<OtherPageProps> = () => {
useIonViewWillEnter(() => {
console.log('IVWE on otherpage');
});
useEffect(() => {
console.log('Other Page mount');
return () => console.log('Other Page unmount');
}, []);
return (
// <IonRouterOutlet id="other" ionPageContainer>
// <Route path="/otherpage" render={() => (
<IonPage data-pageid="other-page">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>OtherPage</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton routerLink="/routing/tabs/tab3">Go to tab3</IonButton>
</IonContent>
</IonPage>
// )}></Route>
// </IonRouterOutlet>
);
};
export default OtherPage;

View File

@@ -0,0 +1,46 @@
import React, { useState, useEffect } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButton,
IonRouterOutlet,
} from '@ionic/react';
import { Route } from 'react-router';
interface PropsTestProps {}
const PropsTest: React.FC<PropsTestProps> = () => {
const [count, setCount] = useState(1);
useEffect(() => {
console.log(count);
}, [count]);
return (
<IonRouterOutlet>
<Route
path="/routing/propstest"
render={() => <InnerPropsTest count={count} setCount={setCount} />}
/>
</IonRouterOutlet>
);
};
const InnerPropsTest: React.FC<{ count: number; setCount: any }> = ({ count, setCount }) => {
return (
<IonPage data-pageid="props-test">
<IonHeader>
<IonToolbar>
<IonTitle>PropsTest</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="count-label">Count: {count}</div>
<IonButton onClick={() => setCount(count + 1)}>Increment</IonButton>
</IonContent>
</IonPage>
);
};
export default PropsTest;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import {
IonContent,
IonPage,
IonRouterOutlet,
IonSplitPane,
} from '@ionic/react';
import Menu from './Menu';
import { Route, Redirect } from 'react-router';
import Tabs from './Tabs';
import Favorites from './Favorites';
import OtherPage from './OtherPage';
import PropsTest from './PropsTest';
import RedirectRouting from './RedirectRouting';
interface RoutingProps {}
const Routing: React.FC<RoutingProps> = () => {
return (
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/routing/tabs" render={() => <Tabs />} />
{/* <Route path="/routing/tabs" component={Tabs} /> */}
<Route path="/routing/" render={() => <Redirect to="/routing/tabs" />} exact />
<Route path="/routing/favorites" component={Favorites} />
{/* <Route path="/routing/favorites" render={() => {
return (
<IonRouterOutlet id="favorites">
<Route path="/routing/favorites" component={Favorites} />
</IonRouterOutlet>
);
}} /> */}
{/* <Route path="/routing/otherpage" render={() => {
return (
<IonRouterOutlet id="otherpage">
<Route path="/routing/otherpage" component={OtherPage} />
</IonRouterOutlet>
);
}} /> */}
<Route path="/routing/otherpage" component={OtherPage} />
<Route path="/routing/propstest" component={PropsTest} />
<Route path="/routing/redirect" render={() => <Redirect to="/routing/tabs" />} />
<Route path="/routing/redirect-routing" render={() => <RedirectRouting />} />
<Route
render={() => (
<IonPage data-pageid="not-found">
<IonContent>
<div>Not found</div>
</IonContent>
</IonPage>
)}
/>
{/* <Route render={() => <Redirect to="/tabs" />} /> */}
</IonRouterOutlet>
</IonSplitPane>
);
};
export default Routing;

View File

@@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonBackButton,
IonLabel,
IonButton,
} from '@ionic/react';
import { useParams } from 'react-router';
interface DetailsProps {}
const SettingsDetails: React.FC<DetailsProps> = () => {
const { id } = useParams<{ id: string }>();
useEffect(() => {
console.log('Settings Details mount');
return () => console.log('Settings Details unmount');
}, []);
const nextId = parseInt(id, 10) + 1;
// LEFT OFF - why is back button not working for multiple entries?
return (
<IonPage data-pageid={`settings-details-page-${id}`}>
<IonHeader>
<IonToolbar>
<IonButtons>
<IonBackButton defaultHref="/routing/tabs/settings"></IonBackButton>
</IonButtons>
<IonTitle>Settings Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonLabel data-testid="details-label">Details {id}</IonLabel>
<br />
<br />
<IonButton routerLink={`/routing/tabs/settings/details/${nextId}`}>
<IonLabel>Go to Settings Details {nextId}</IonLabel>
</IonButton>
</IonContent>
</IonPage>
);
};
export default SettingsDetails;

View File

@@ -0,0 +1,64 @@
import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonList,
IonItem,
IonLabel,
IonButtons,
IonMenuButton,
IonButton,
} from '@ionic/react';
import './Tab2.css';
import { useHistory } from 'react-router';
const Tab2: React.FC = () => {
const history = useHistory();
useEffect(() => {
console.log('Settings mount');
return () => console.log('Settings unmount');
}, []);
return (
<IonPage data-pageid="settings-page">
<IonHeader translucent={true}>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Settings</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Settings</IonTitle>
</IonToolbar>
</IonHeader>
<IonList>
<IonItem routerLink="/routing/tabs/settings/details/1">
<IonLabel>Settings Details 1</IonLabel>
</IonItem>
<IonItem routerLink="/routing/tabs/settings/details/2">
<IonLabel>Settings Details 2</IonLabel>
</IonItem>
</IonList>
<br />
<br />
<IonButton
onClick={() => {
history.push('/routing/tabs/settings/details/1', { routerOptions: { unmount: true } });
}}
>
Details with Unmount via history.push
</IonButton>
</IonContent>
</IonPage>
);
};
export default Tab2;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router';
import Tab1 from './Tab1';
import Details from './Details';
import Tab2 from './Tab2';
import Tab3 from './Tab3';
import { triangle, ellipse, square } from 'ionicons/icons';
import SettingsDetails from './SettingsDetails';
interface TabsProps {}
const Tabs: React.FC<TabsProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs">
<Route path="/routing/tabs/home" component={Tab1} exact />
<Route path="/routing/tabs/home/details/:id" component={Details} exact={true} />
{/* <Route path="/routing/tabs/home/details/:id" render={(props) => {
return <Details />
}} exact={true} /> */}
<Route path="/routing/tabs/settings" component={Tab2} exact={true} />
<Route path="/routing/tabs/settings/details/:id" component={SettingsDetails} exact={true} />
<Route path="/routing/tabs/tab3" component={Tab3} />
<Route
path="/routing/tabs"
render={() => <Redirect to="/routing/tabs/home" />}
exact={true}
/>
<Route
path="/routing/tabs/redirect"
render={() => <Redirect to="/routing/tabs/settings" />}
exact={true}
/>
{/* <Route path="/routing/tabs" render={() => <Route render={() => <Redirect to="/tabs/home" />} />} /> */}
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/routing/tabs/home" routerOptions={{ unmount: true }}>
<IonIcon icon={triangle} />
<IonLabel>Home</IonLabel>
</IonTabButton>
<IonTabButton tab="settings" href="/routing/tabs/settings">
<IonIcon icon={ellipse} />
<IonLabel>Settings</IonLabel>
</IonTabButton>
<IonTabButton tab="tab3" href="/routing/tabs/tab3">
<IonIcon icon={square} />
<IonLabel>Tab 3</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default Tabs;

View File

@@ -0,0 +1,57 @@
import React from 'react';
import {
IonRouterOutlet,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonButtons,
IonBackButton,
} from '@ionic/react';
import { Route } from 'react-router';
interface SwipeToGoBackProps {}
export const SwipeToGoBack: React.FC<SwipeToGoBackProps> = () => {
return (
<IonRouterOutlet id="swipe-to-go-back">
<Route path="/swipe-to-go-back" component={Main} exact />
<Route path="/swipe-to-go-back/details" component={Details} />
</IonRouterOutlet>
);
};
const Main: React.FC = () => {
return (
<IonPage data-pageid="main">
<IonHeader>
<IonToolbar>
<IonTitle>Main</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonItem routerLink="/swipe-to-go-back/details">Details</IonItem>
</IonContent>
</IonPage>
);
};
const Details: React.FC = () => {
return (
<IonPage data-pageid="details">
<IonHeader>
<IonToolbar>
<IonButtons>
<IonBackButton></IonBackButton>
</IonButtons>
<IonTitle>Details</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div>Details</div>
</IonContent>
</IonPage>
);
};

View File

@@ -0,0 +1,88 @@
import React, { useContext } from 'react';
import {
IonTabs,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonMenuButton,
IonTitle,
IonContent,
IonTabsContext,
IonButton,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { triangle, square } from 'ionicons/icons';
interface TabsContextProps {}
const TabsContext: React.FC<TabsContextProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs">
<Route path="/tab-context/tab1" component={Tab1} exact />
<Route path="/tab-context/tab2" component={Tab2} exact />
<Redirect from="/tab-context" to="/tab-context/tab1" exact />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tab-context/tab1" routerOptions={{ unmount: true }}>
<IonIcon icon={triangle} />
<IonLabel>Tab1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tab-context/tab2">
<IonIcon icon={square} />
<IonLabel>Tab2</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
const Tab1 = () => {
const tabContext = useContext(IonTabsContext);
return (
<IonPage id="home" data-pageid="tab1">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Tab1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div>Page: {tabContext.activeTab}</div>
<IonButton onClick={() => tabContext.selectTab('tab2')}>Go to tab2</IonButton>
</IonContent>
</IonPage>
);
};
const Tab2 = () => {
const tabContext = useContext(IonTabsContext);
return (
<IonPage id="home" data-pageid="tab2">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Tab2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div>Page: {tabContext.activeTab}</div>
<IonButton onClick={() => tabContext.selectTab('tab1')}>Go to tab1</IonButton>
</IonContent>
</IonPage>
);
};
export default TabsContext;

View File

@@ -0,0 +1,115 @@
import React from 'react';
import {
IonTabs,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonBackButton,
IonTitle,
IonContent,
IonButton,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { triangle, square } from 'ionicons/icons';
interface TabsProps {}
const Tabs: React.FC<TabsProps> = () => {
return (
<IonTabs data-pageid="tabs">
<IonRouterOutlet id="tabs">
<Route path="/tabs/tab1" component={Tab1} exact />
<Route path="/tabs/tab2" component={Tab2} exact />
<Route path="/tabs/tab1/child" component={Tab1Child1} exact />
<Route path="/tabs/tab1/child2" component={Tab1Child2} exact />
<Redirect from="/tabs" to="/tabs/tab1" exact />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tabs/tab1">
<IonIcon icon={triangle} />
<IonLabel>Tab1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tabs/tab2">
<IonIcon icon={square} />
<IonLabel>Tab2</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
const Tab1 = () => {
return (
<IonPage data-pageid="tab1">
<IonHeader>
<IonToolbar>
<IonTitle>Tab1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton routerLink="/tabs/tab1/child" id="child-one">Go to Tab1Child1</IonButton>
<IonButton routerLink="/tabs-secondary/tab1" id="tabs-secondary">Go to Secondary Tabs</IonButton>
</IonContent>
</IonPage>
);
};
const Tab1Child1 = () => {
return (
<IonPage data-pageid="tab1child1">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab 1 Child 1
<IonButton routerLink="/tabs/tab1/child2" id="child-two">Go to Tab1Child2</IonButton>
</IonContent>
</IonPage>
);
};
const Tab1Child2 = () => {
return (
<IonPage data-pageid="tab1child2">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>Tab1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab 1 Child 2
</IonContent>
</IonPage>
);
};
const Tab2 = () => {
return (
<IonPage data-pageid="tab2">
<IonHeader>
<IonToolbar>
<IonTitle>Tab2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab 2
</IonContent>
</IonPage>
);
};
export default Tabs;

View File

@@ -0,0 +1,77 @@
import React from 'react';
import {
IonTabs,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonBackButton,
IonTitle,
IonContent,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { triangle, square } from 'ionicons/icons';
interface TabsSecondaryProps {}
const TabsSecondary: React.FC<TabsSecondaryProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs-secondary">
<Route path="/tabs-secondary/tab1" component={Tab1} exact />
<Route path="/tabs-secondary/tab2" component={Tab2} exact />
<Redirect from="/tabs-secondary" to="/tabs-secondary/tab1" exact />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1-secondary" href="/tabs-secondary/tab1">
<IonIcon icon={triangle} />
<IonLabel>Tab1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2-secondary" href="/tabs-secondary/tab2">
<IonIcon icon={square} />
<IonLabel>Tab2</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
const Tab1 = () => {
return (
<IonPage data-pageid="tab1-secondary">
<IonHeader>
<IonToolbar>
<IonTitle>Tab1</IonTitle>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
Tab 1
</IonContent>
</IonPage>
);
};
const Tab2 = () => {
return (
<IonPage data-pageid="tab2-secondary">
<IonHeader>
<IonToolbar>
<IonTitle>Tab2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
Tab 2
</IonContent>
</IonPage>
);
};
export default TabsSecondary;

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
{
"name": "react-router-new",
"version": "0.0.1",
"private": true,
"dependencies": {
"@ionic/react": "^8.6.1",
"@ionic/react-router": "^8.6.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"ionicons": "^6.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.0.0",
"react-router-dom": "^6.0.0",
"react-scripts": "^5.0.1",
"sass-loader": "8.0.2",
"typescript": "^4.4.2",
"wait-on": "^5.3.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "cypress open",
"cypress": "node_modules/.bin/cypress run --headless --browser chrome",
"cypress.open": "cypress open",
"e2e": "concurrently \"serve -s build -l 3000\" \"wait-on http-get://localhost:3000 && npm run cypress\" --kill-others --success first",
"sync": "sh ./scripts/sync.sh"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"concurrently": "^6.3.0",
"cypress": "^13.2.0",
"serve": "^14.0.1",
"wait-on": "^6.0.0",
"webpack-cli": "^4.9.1"
},
"description": "An Ionic project",
"engines": {
"node": ">= 16"
}
}

View File

@@ -46,23 +46,23 @@ const App: React.FC = () => {
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/" component={Main} exact />
<Route path="/routing" component={Routing} />
<Route path="/dynamic-routes" component={DynamicRoutes} />
<Route path="/multiple-tabs" component={MultipleTabs} />
<Route path="/dynamic-tabs" component={DynamicTabs} />
<Route path="/nested-outlet" component={NestedOutlet} />
<Route path="/nested-outlet2" component={NestedOutlet2} />
<Route path="/replace-action" component={ReplaceAction} />
<Route path="/tab-context" component={TabsContext} />
<Route path="/outlet-ref" component={OutletRef} />
<Route path="/swipe-to-go-back" component={SwipeToGoBack} />
<Route path="/dynamic-ionpage-classnames" component={DynamicIonpageClassnames} />
<Route path="/tabs" component={Tabs} />
<Route path="/tabs-secondary" component={TabsSecondary} />
<Route path="/refs" component={Refs} />
<Route path="/overlays" component={Overlays} />
<Route path="/params/:id" component={Params} />
<Route path="/" element={<Main />} />
<Route path="/routing/*" element={<Routing />} />
<Route path="/dynamic-routes" element={<DynamicRoutes />} />
<Route path="/multiple-tabs" element={<MultipleTabs />} />
<Route path="/dynamic-tabs" element={<DynamicTabs />} />
<Route path="/nested-outlet" element={<NestedOutlet />} />
<Route path="/nested-outlet2" element={<NestedOutlet2 />} />
<Route path="/replace-action" element={<ReplaceAction />} />
<Route path="/tab-context" element={<TabsContext />} />
<Route path="/outlet-ref" element={<OutletRef />} />
<Route path="/swipe-to-go-back" element={<SwipeToGoBack />} />
<Route path="/dynamic-ionpage-classnames" element={<DynamicIonpageClassnames/ >} />
<Route path="/tabs" element={<Tabs />} />
<Route path="/tabs-secondary" element={<TabsSecondary />} />
<Route path="/refs" element={<Refs />} />
<Route path="/overlays" element={<Overlays />} />
<Route path="/params/:id" element={<Params />} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>

View File

@@ -10,14 +10,20 @@ import {
IonLabel,
} from '@ionic/react';
import packageJson from '../../package.json';
interface MainProps {}
const Main: React.FC<MainProps> = () => {
const majorVersion = packageJson.dependencies['react-router'].match(
/(\d+)\.(\d+)\.(\d+)/
)?.[1];
return (
<IonPage data-pageid="home">
<IonHeader>
<IonToolbar>
<IonTitle>Main</IonTitle>
<IonTitle>Test App - React Router v{majorVersion}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>

View File

@@ -15,7 +15,7 @@ interface DynamicIonpageClassnamesProps {}
const DynamicIonpageClassnames: React.FC<DynamicIonpageClassnamesProps> = () => {
return (
<IonRouterOutlet>
<Route path="/dynamic-ionpage-classnames" component={Page} />
<Route path="/dynamic-ionpage-classnames" element={<Page />} />
</IonRouterOutlet>
);
};

View File

@@ -7,32 +7,31 @@ import {
IonToolbar,
IonRouterOutlet,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import { Link } from 'react-router-dom';
const DynamicRoutes: React.FC = () => {
const [routes, setRoutes] = useState<ReactElement[]>([
<Route
key="sldjflsdj"
path="/dynamic-routes/home"
render={() => <Home update={addRoute} />}
exact={true}
/>,
]);
const addRoute = () => {
const newRoute = (
<Route key="lsdjldj" path="/dynamic-routes/newRoute" component={NewRoute} exact={true} />
<Route key="lsdjldj" path="/dynamic-routes/newRoute" element={<NewRoute />} />
);
setRoutes([...routes, newRoute]);
};
const [routes, setRoutes] = useState<ReactElement[]>([
<Route
key="sldjflsdj"
path="/dynamic-routes/home"
element={<Home update={addRoute} />}
/>,
]);
return (
<IonRouterOutlet>
{routes}
{/* <Route exact path="/home" render={() => <Home update={addRoute} />} /> */}
<Route exact path="/dynamic-routes" render={() => <Redirect to="/dynamic-routes/home" />} />
<Route render={() => <Failed />} />
{/* <Route path="/home" render={() => <Home update={addRoute} />} /> */}
<Route path="/dynamic-routes" element={<Navigate to="/dynamic-routes/home" replace />} />
<Route element={<Failed />} />
</IonRouterOutlet>
);
};

View File

@@ -14,7 +14,7 @@ import {
IonLabel,
IonButton,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import { IonReactRouter } from '@ionic/react-router';
import { triangle, square } from 'ionicons/icons';
@@ -22,16 +22,16 @@ const DynamicTabs: React.FC = () => {
const [display2ndTab, setDisplayThirdTab] = useState<boolean>(false);
const renderFirstTab = useCallback(() => {
return <Tab1 setDisplayThirdTab={() => setDisplayThirdTab(!display2ndTab)} />;
}, [display2ndTab]);
return <Tab1 setDisplayThirdTab={() => setDisplayThirdTab(true)} />;
}, []);
const render2ndTabRoute = useCallback(() => {
if (display2ndTab) {
return <Route path="/dynamic-tabs/tab2" component={Tab2} />;
return <Route path="/dynamic-tabs/tab2" element={<Tab2 />} />;
} else {
// This is weird, if I return null or undefined then I get all sorts of errors, seemingly
// because the router is mad about a child not being a route.
return <Route path="/dynamic-tabs/tab200" component={Tab1} />;
return <Route path="/dynamic-tabs/tab200" element={<Tab1 setDisplayThirdTab={setDisplayThirdTab} />} />;
}
}, [display2ndTab]);
@@ -40,14 +40,10 @@ const DynamicTabs: React.FC = () => {
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route path="/dynamic-tabs/tab1" render={renderFirstTab} exact={true} />
<Route path="/dynamic-tabs/tab1" element={renderFirstTab()} />
{render2ndTabRoute()}
<Route
path="/dynamic-tabs/"
render={() => <Redirect to="/dynamic-tabs/tab1" />}
exact={true}
/>
<Route render={() => <Redirect to="/dynamic-tabs/tab1" />} />
<Route path="/dynamic-tabs" element={<Navigate to="/dynamic-tabs/tab1" replace />} />
<Route path="*" element={<Navigate to="/dynamic-tabs/tab1" replace />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/dynamic-tabs/tab1">

View File

@@ -16,7 +16,7 @@ import {
IonToolbar,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import { Menu } from './Menu';
import { triangle, ellipse, square, rocket } from 'ionicons/icons';
@@ -26,24 +26,14 @@ const MultipleTabs: React.FC = () => {
<Menu />
<IonRouterOutlet id="main">
<Route
path="/multiple-tabs/tab1"
render={() => {
return <Tab1 />;
}}
exact={false}
path="/multiple-tabs/tab1/*"
element={<Tab1 />}
/>
<Route
path="/multiple-tabs/tab2"
render={() => {
return <Tab2 />;
}}
exact={false}
/>
<Route
path="/multiple-tabs"
render={() => <Redirect to="/multiple-tabs/tab1" />}
exact={true}
path="/multiple-tabs/tab2/*"
element={<Tab2 />}
/>
<Route path="/multiple-tabs" element={<Navigate to="/multiple-tabs/tab1" replace />} />
</IonRouterOutlet>
</IonSplitPane>
);
@@ -68,12 +58,11 @@ const Tab1: React.FC = () => {
<IonRouterOutlet id="tab1">
<Route
path="/multiple-tabs/tab1"
render={() => <Redirect to="/multiple-tabs/tab1/pagea" />}
exact={true}
element={<Navigate to="/multiple-tabs/tab1/pagea" replace />}
/>
{/* <Redirect path="/multiple-tabs/event" to="/multiple-tabs/tab1/pagea" exact={true} /> */}
<Route path="/multiple-tabs/tab1/pagea" render={() => <Page name="PageA" />} exact={true} />
<Route path="/multiple-tabs/tab1/pageb" render={() => <Page name="PageB" />} exact={true} />
{/* <Route path="/multiple-tabs/event" element={<Navigate to="/multiple-tabs/tab1/pagea" replace />} /> */}
<Route path="/multiple-tabs/tab1/pagea" element={<Page name="PageA" />} />
<Route path="/multiple-tabs/tab1/pageb" element={<Page name="PageB" />} />
</IonRouterOutlet>
</IonTabs>
);
@@ -95,12 +84,11 @@ const Tab2: React.FC = () => {
<IonRouterOutlet id="tab2">
<Route
path="/multiple-tabs/tab2"
render={() => <Redirect to="/multiple-tabs/tab2/pagec" />}
exact={true}
element={<Navigate to="/multiple-tabs/tab2/pagec" replace />}
/>
{/* <Redirect path="/multiple-tabs/tab2" to="/multiple-tabs/tab2/pagec" exact={true} /> */}
<Route path="/multiple-tabs/tab2/pagec" render={() => <Page name="PageC" />} exact={true} />
<Route path="/multiple-tabs/tab2/paged" render={() => <Page name="PageD" />} exact={true} />
{/* <Route path="/multiple-tabs/tab2" element={<Navigate to="/multiple-tabs/tab2/pagec" replace />} /> */}
<Route path="/multiple-tabs/tab2/pagec" element={<Page name="PageC" />} />
<Route path="/multiple-tabs/tab2/paged" element={<Page name="PageD" />} />
</IonRouterOutlet>
</IonTabs>
);

View File

@@ -9,7 +9,7 @@ import {
} from '@ionic/react';
import { useEffect } from 'react';
import React from 'react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
const Page: React.FC = () => {
useEffect(() => {
@@ -48,10 +48,9 @@ const SecondPage: React.FC = () => {
<IonRouterOutlet ionPage>
<Route
path="/nested-outlet/secondpage"
exact={true}
render={() => <Redirect to="/nested-outlet/secondpage/page" />}
element={<Navigate to="/nested-outlet/secondpage/page" replace />}
/>
<Route path="/nested-outlet/secondpage/page" component={Page} exact={true} />
<Route path="/nested-outlet/secondpage/page" element={<Page />} />
</IonRouterOutlet>
);
};
@@ -81,8 +80,8 @@ const FirstPage: React.FC = () => {
const NestedOutlet: React.FC = () => (
<IonRouterOutlet>
<Route path="/nested-outlet" component={FirstPage} exact={true} />
<Route path="/nested-outlet/secondpage" component={SecondPage} />
<Route path="/nested-outlet" element={<FirstPage />} />
<Route path="/nested-outlet/secondpage" element={<SecondPage />} />
</IonRouterOutlet>
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Redirect, Route, RouteComponentProps } from 'react-router-dom';
import { Navigate, Route, useParams } from 'react-router-dom';
import {
IonBackButton,
IonButtons,
@@ -14,11 +14,11 @@ import {
IonToolbar,
} from '@ionic/react';
const ListPage: React.FC<RouteComponentProps> = ({ match }) => {
const ListPage: React.FC = () => {
return (
<IonRouterOutlet ionPage id="listpage">
<Route exact path="/nested-outlet2/list" component={List} />
<Route path={`${match.url}/:id`} component={Item} />
<Route path="/nested-outlet2/list" element={<List />} />
<Route path="/nested-outlet2/list/:id" element={<Item />} />
</IonRouterOutlet>
);
};
@@ -51,7 +51,9 @@ const List: React.FC = () => {
);
};
const Item: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
const Item: React.FC = () => {
const { id } = useParams<{ id: string }>();
return (
<IonPage data-pageid="item">
<IonHeader>
@@ -62,16 +64,16 @@ const Item: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
<IonTitle>Item</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>Detail of item #{match.params.id}</IonContent>
<IonContent>Detail of item #{id}</IonContent>
</IonPage>
);
};
const HomePage: React.FC<RouteComponentProps> = ({ match }) => {
const HomePage: React.FC = () => {
return (
<IonRouterOutlet ionPage id="homepage">
<Route exact path="/nested-outlet2/home" component={Home} />
<Route path="/nested-outlet2/home/welcome" component={Welcome} />
<Route path="/nested-outlet2/home" element={<Home />} />
<Route path="/nested-outlet2/home/welcome" element={<Welcome />} />
</IonRouterOutlet>
);
};
@@ -98,7 +100,7 @@ const Welcome: React.FC = () => {
);
};
const Home: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
const Home: React.FC = () => {
return (
<IonPage data-pageid="home">
<IonHeader>
@@ -122,12 +124,11 @@ const Home: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
const NestedOutlet2: React.FC = () => (
<IonRouterOutlet id="main">
<Route path="/nested-outlet2/list" component={ListPage} />
<Route path="/nested-outlet2/home" component={HomePage} />
<Route path="/nested-outlet2/list" element={<ListPage />} />
<Route path="/nested-outlet2/home" element={<HomePage />} />
<Route
path="/nested-outlet2"
render={() => <Redirect to="/nested-outlet2/home" />}
exact={true}
element={<Navigate to="/nested-outlet2/home" replace />}
/>
</IonRouterOutlet>
);

View File

@@ -22,9 +22,7 @@ export const OutletRef: React.FC<OutletRefProps> = () => {
<IonRouterOutlet id="main-outlet" ref={ref}>
<Route
path="/outlet-ref"
render={() => {
return <Main outletId={ref.current?.id} />;
}}
element={<Main outletId={ref.current?.id} />}
/>
</IonRouterOutlet>
);

View File

@@ -1,15 +1,15 @@
import { IonButton, IonContent, IonModal } from '@ionic/react';
import { useState } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
const Overlays: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const history = useHistory();
const navigate = useNavigate();
const goBack = () => history.goBack();
const replace = () => history.replace('/');
const push = () => history.push('/');
const goBack = () => navigate(-1);
const replace = () => navigate('/', { replace: true });
const push = () => navigate('/');
return (
<>

View File

@@ -9,30 +9,28 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react';
import { RouteComponentProps } from 'react-router';
import { useParams } from 'react-router';
interface PageProps
extends RouteComponentProps<{
id: string;
}> {}
const Page: React.FC = () => {
const { id } = useParams<{ id: string }>();
const parseID = id ? parseInt(id) : NaN;
const displayId = id || 'N/A';
const nextParamLink = !isNaN(parseID) ? `/params/${parseID + 1}` : '/params/1';
const Page: React.FC<PageProps> = ({ match }) => {
const parseID = parseInt(match.params.id);
return (
<IonPage data-pageid={'params-' + match.params.id }>
<IonPage data-pageid={'params-' + displayId }>
<IonHeader>
<IonToolbar>
<IonTitle>Params { match.params.id }</IonTitle>
<IonTitle>Params { displayId }</IonTitle>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton id="next-page" routerLink={'/params/' + (parseID + 1) } >Go to next param</IonButton>
<IonButton id="next-page" routerLink={nextParamLink} >Go to next param</IonButton>
<br />
Page ID: { match.params.id }
Page ID: { displayId }
</IonContent>
</IonPage>
);

View File

@@ -14,9 +14,9 @@ interface RefsProps {}
const Refs: React.FC = () => {
return (
<IonRouterOutlet>
{/* <Route exact path="/home" render={() => <Home update={addRoute} />} /> */}
<Route exact path="/refs" component={RefsFC} />
<Route exact path="/refs/class" component={RefsClass} />
{/* <Route path="/home" element={<Home update={addRoute} />} /> */}
<Route path="/refs" element={<RefsFC />} />
<Route path="/refs/class" element={<RefsClass />} />
</IonRouterOutlet>
);
};

View File

@@ -10,17 +10,17 @@ import {
IonButtons,
IonBackButton,
} from '@ionic/react';
import { Route, Redirect, useHistory } from 'react-router';
import { Route, Navigate, useNavigate } from 'react-router';
interface TopPageProps {}
const ReplaceAction: React.FC<TopPageProps> = () => {
return (
<IonRouterOutlet>
<Route path="/replace-action/page1" component={Page1} exact />
<Route path="/replace-action/page2" component={Page2} exact />
<Route path="/replace-action/page3" component={Page3} exact />
<Route exact path="/replace-action" render={() => <Redirect to="/replace-action/page1" />} />
<Route path="/replace-action/page1" element={<Page1 />} />
<Route path="/replace-action/page2" element={<Page2 />} />
<Route path="/replace-action/page3" element={<Page3 />} />
<Route path="/replace-action" element={<Navigate to="/replace-action/page1" replace />} />
</IonRouterOutlet>
);
};
@@ -42,10 +42,10 @@ const Page1: React.FC = () => (
);
const Page2: React.FC = () => {
const history = useHistory();
const navigate = useNavigate();
const clickButton = () => {
history.replace('/replace-action/page3');
navigate('/replace-action/page3', { replace: true });
};
return (

View File

@@ -24,7 +24,7 @@ const Details: React.FC<DetailsProps> = () => {
return () => console.log('Home Details unmount');
}, []);
const nextId = parseInt(id, 10) + 1;
const nextId = parseInt(id ?? '0', 10) + 1;
return (
<IonPage data-pageid={`home-details-page-${id}`}>

View File

@@ -25,7 +25,7 @@ const OtherPage: React.FC<OtherPageProps> = () => {
return (
// <IonRouterOutlet id="other" ionPageContainer>
// <Route path="/otherpage" render={() => (
// <Route path="/otherpage" element={
<IonPage data-pageid="other-page">
<IonHeader>
<IonToolbar>
@@ -39,7 +39,7 @@ const OtherPage: React.FC<OtherPageProps> = () => {
<IonButton routerLink="/routing/tabs/tab3">Go to tab3</IonButton>
</IonContent>
</IonPage>
// )}></Route>
// }></Route>
// </IonRouterOutlet>
);
};

View File

@@ -21,7 +21,7 @@ const PropsTest: React.FC<PropsTestProps> = () => {
<IonRouterOutlet>
<Route
path="/routing/propstest"
render={() => <InnerPropsTest count={count} setCount={setCount} />}
element={<InnerPropsTest count={count} setCount={setCount} />}
/>
</IonRouterOutlet>
);

View File

@@ -6,7 +6,7 @@ import {
IonSplitPane,
} from '@ionic/react';
import Menu from './Menu';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import Tabs from './Tabs';
import Favorites from './Favorites';
import OtherPage from './OtherPage';
@@ -20,38 +20,25 @@ const Routing: React.FC<RoutingProps> = () => {
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/routing/tabs" render={() => <Tabs />} />
{/* <Route path="/routing/tabs" component={Tabs} /> */}
<Route path="/routing/" render={() => <Redirect to="/routing/tabs" />} exact />
<Route path="/routing/favorites" component={Favorites} />
{/* <Route path="/routing/favorites" render={() => {
return (
<IonRouterOutlet id="favorites">
<Route path="/routing/favorites" component={Favorites} />
</IonRouterOutlet>
);
}} /> */}
{/* <Route path="/routing/otherpage" render={() => {
return (
<IonRouterOutlet id="otherpage">
<Route path="/routing/otherpage" component={OtherPage} />
</IonRouterOutlet>
);
}} /> */}
<Route path="/routing/otherpage" component={OtherPage} />
<Route path="/routing/propstest" component={PropsTest} />
<Route path="/routing/redirect" render={() => <Redirect to="/routing/tabs" />} />
<Route path="/routing/redirect-routing" render={() => <RedirectRouting />} />
<Route index element={<Navigate to="/routing/tabs" replace />} />
<Route path="tabs" element={<Tabs />} />
<Route path="favorites" element={<Favorites />} />
<Route path="otherpage" element={<OtherPage />} />
<Route path="propstest" element={<PropsTest />} />
<Route path="redirect" element={<Navigate to="/routing/tabs" replace />} />
<Route path="redirect-routing" element={<RedirectRouting />} />
<Route
render={() => (
path="*"
element={
<IonPage data-pageid="not-found">
<IonContent>
<div>Not found</div>
</IonContent>
</IonPage>
)}
}
/>
{/* <Route render={() => <Redirect to="/tabs" />} /> */}
</IonRouterOutlet>
</IonSplitPane>
);

View File

@@ -22,7 +22,7 @@ const SettingsDetails: React.FC<DetailsProps> = () => {
return () => console.log('Settings Details unmount');
}, []);
const nextId = parseInt(id, 10) + 1;
const nextId = parseInt(id ?? '0', 10) + 1;
// LEFT OFF - why is back button not working for multiple entries?
return (

View File

@@ -13,10 +13,10 @@ import {
IonButton,
} from '@ionic/react';
import './Tab2.css';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
const Tab2: React.FC = () => {
const history = useHistory();
const navigate = useNavigate();
useEffect(() => {
console.log('Settings mount');
@@ -51,10 +51,10 @@ const Tab2: React.FC = () => {
<br />
<IonButton
onClick={() => {
history.push('/routing/tabs/settings/details/1', { routerOptions: { unmount: true } });
navigate('/routing/tabs/settings/details/1');
}}
>
Details with Unmount via history.push
Details with Unmount via navigate
</IonButton>
</IonContent>
</IonPage>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import Tab1 from './Tab1';
import Details from './Details';
import Tab2 from './Tab2';
@@ -14,25 +14,21 @@ const Tabs: React.FC<TabsProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs">
<Route path="/routing/tabs/home" component={Tab1} exact />
<Route path="/routing/tabs/home/details/:id" component={Details} exact={true} />
{/* <Route path="/routing/tabs/home/details/:id" render={(props) => {
return <Details />
}} exact={true} /> */}
<Route path="/routing/tabs/settings" component={Tab2} exact={true} />
<Route path="/routing/tabs/settings/details/:id" component={SettingsDetails} exact={true} />
<Route path="/routing/tabs/tab3" component={Tab3} />
<Route path="/routing/tabs/home" element={<Tab1 />} />
<Route path="/routing/tabs/home/details/:id" element={<Details />} />
{/* <Route path="/routing/tabs/home/details/:id" element={<Details />} /> */}
<Route path="/routing/tabs/settings" element={<Tab2 />} />
<Route path="/routing/tabs/settings/details/:id" element={<SettingsDetails />} />
<Route path="/routing/tabs/tab3" element={<Tab3 />} />
<Route
path="/routing/tabs"
render={() => <Redirect to="/routing/tabs/home" />}
exact={true}
element={<Navigate to="/routing/tabs/home" replace />}
/>
<Route
path="/routing/tabs/redirect"
render={() => <Redirect to="/routing/tabs/settings" />}
exact={true}
element={<Navigate to="/routing/tabs/settings" replace />}
/>
{/* <Route path="/routing/tabs" render={() => <Route render={() => <Redirect to="/tabs/home" />} />} /> */}
{/* <Route path="/routing/tabs" element={<Navigate to="/tabs/home" replace />} /> */}
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/routing/tabs/home" routerOptions={{ unmount: true }}>

View File

@@ -17,8 +17,8 @@ interface SwipeToGoBackProps {}
export const SwipeToGoBack: React.FC<SwipeToGoBackProps> = () => {
return (
<IonRouterOutlet id="swipe-to-go-back">
<Route path="/swipe-to-go-back" component={Main} exact />
<Route path="/swipe-to-go-back/details" component={Details} />
<Route path="/swipe-to-go-back" element={<Main />} />
<Route path="/swipe-to-go-back/details" element={<Details />} />
</IonRouterOutlet>
);
};

View File

@@ -16,7 +16,7 @@ import {
IonTabsContext,
IonButton,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import { triangle, square } from 'ionicons/icons';
interface TabsContextProps {}
@@ -25,9 +25,9 @@ const TabsContext: React.FC<TabsContextProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs">
<Route path="/tab-context/tab1" component={Tab1} exact />
<Route path="/tab-context/tab2" component={Tab2} exact />
<Redirect from="/tab-context" to="/tab-context/tab1" exact />
<Route path="/tab-context/tab1" element={<Tab1 />} />
<Route path="/tab-context/tab2" element={<Tab2 />} />
<Route path="/tab-context" element={<Navigate to="/tab-context/tab1" replace />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tab-context/tab1" routerOptions={{ unmount: true }}>

View File

@@ -15,7 +15,7 @@ import {
IonContent,
IonButton,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import { triangle, square } from 'ionicons/icons';
interface TabsProps {}
@@ -24,11 +24,11 @@ const Tabs: React.FC<TabsProps> = () => {
return (
<IonTabs data-pageid="tabs">
<IonRouterOutlet id="tabs">
<Route path="/tabs/tab1" component={Tab1} exact />
<Route path="/tabs/tab2" component={Tab2} exact />
<Route path="/tabs/tab1/child" component={Tab1Child1} exact />
<Route path="/tabs/tab1/child2" component={Tab1Child2} exact />
<Redirect from="/tabs" to="/tabs/tab1" exact />
<Route path="/tabs/tab1" element={<Tab1 />} />
<Route path="/tabs/tab2" element={<Tab2 />} />
<Route path="/tabs/tab1/child" element={<Tab1Child1 />} />
<Route path="/tabs/tab1/child2" element={<Tab1Child2 />} />
<Route path="/tabs" element={<Navigate to="/tabs/tab1" replace />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tabs/tab1">

View File

@@ -14,7 +14,7 @@ import {
IonTitle,
IonContent,
} from '@ionic/react';
import { Route, Redirect } from 'react-router';
import { Route, Navigate } from 'react-router';
import { triangle, square } from 'ionicons/icons';
interface TabsSecondaryProps {}
@@ -23,9 +23,9 @@ const TabsSecondary: React.FC<TabsSecondaryProps> = () => {
return (
<IonTabs>
<IonRouterOutlet id="tabs-secondary">
<Route path="/tabs-secondary/tab1" component={Tab1} exact />
<Route path="/tabs-secondary/tab2" component={Tab2} exact />
<Redirect from="/tabs-secondary" to="/tabs-secondary/tab1" exact />
<Route path="/tabs-secondary/tab1" element={<Tab1 />} />
<Route path="/tabs-secondary/tab2" element={<Tab2 />} />
<Route path="/tabs-secondary" element={<Navigate to="/tabs-secondary/tab1" replace />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1-secondary" href="/tabs-secondary/tab1">

View File

@@ -1,4 +1,4 @@
import { Location as HistoryLocation } from 'history';
import type { Location as HistoryLocation } from 'history';
const RESTRICT_SIZE = 25;

View File

@@ -4,9 +4,8 @@ import { NavContext } from '../contexts/NavContext';
export interface IonRouteProps {
path?: string;
exact?: boolean;
show?: boolean;
render: (props?: any) => JSX.Element; // TODO(FW-2959): type
element: React.ReactElement;
disableIonPageManagement?: boolean;
}