Compare commits

...

35 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
Shane
6811fe5cc8 fix(range): improve focus and blur handling for dual knobs (#30482)
Issue number: resolves #internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Currently, if you use keyboard navigation to move between dual range
slider knobs, only the first knob you navigate to is highlighted. This
is because both elements in the same component are marked as focusable
and the code that manages focusable doesn't take into account multiple
elements in the same component.


https://github.com/user-attachments/assets/36d84eed-6928-446e-becd-ffa2a97e3cc2

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

After these changes, we manage focusing on dual knob range sliders
manually, so using tab navigation through dual knob range sliders
focuses knobs as expected.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

[Test - Range - Basic
screen](https://ionic-framework-git-fw-6401-ionic1.vercel.app/src/components/range/test/basic)
2025-06-13 16:43:48 +00: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
68 changed files with 42001 additions and 992 deletions

View File

@@ -639,6 +639,51 @@ export class Range implements ComponentInterface {
}
};
private onKnobFocus = (knob: KnobName) => {
if (!this.hasFocus) {
this.hasFocus = true;
this.ionFocus.emit();
}
// Manually manage ion-focused class for dual knobs
if (this.dualKnobs && this.el.shadowRoot) {
const knobA = this.el.shadowRoot.querySelector('.range-knob-a');
const knobB = this.el.shadowRoot.querySelector('.range-knob-b');
// Remove ion-focused from both knobs first
knobA?.classList.remove('ion-focused');
knobB?.classList.remove('ion-focused');
// Add ion-focused only to the focused knob
const focusedKnobEl = knob === 'A' ? knobA : knobB;
focusedKnobEl?.classList.add('ion-focused');
}
};
private onKnobBlur = () => {
// Check if focus is moving to another knob within the same range
// by delaying the reset to allow the new focus to register
setTimeout(() => {
const activeElement = this.el.shadowRoot?.activeElement;
const isStillFocusedOnKnob = activeElement && activeElement.classList.contains('range-knob-handle');
if (!isStillFocusedOnKnob) {
if (this.hasFocus) {
this.hasFocus = false;
this.ionBlur.emit();
}
// Remove ion-focused from both knobs when focus leaves the range
if (this.dualKnobs && this.el.shadowRoot) {
const knobA = this.el.shadowRoot.querySelector('.range-knob-a');
const knobB = this.el.shadowRoot.querySelector('.range-knob-b');
knobA?.classList.remove('ion-focused');
knobB?.classList.remove('ion-focused');
}
}
}, 0);
};
/**
* Returns true if content was passed to the "start" slot
*/
@@ -813,6 +858,8 @@ export class Range implements ComponentInterface {
min,
max,
inheritedAttributes,
onKnobFocus: this.onKnobFocus,
onKnobBlur: this.onKnobBlur,
})}
{this.dualKnobs &&
@@ -828,6 +875,8 @@ export class Range implements ComponentInterface {
min,
max,
inheritedAttributes,
onKnobFocus: this.onKnobFocus,
onKnobBlur: this.onKnobBlur,
})}
</div>
);
@@ -908,11 +957,27 @@ interface RangeKnob {
pinFormatter: PinFormatter;
inheritedAttributes: Attributes;
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
onKnobFocus: (knob: KnobName) => void;
onKnobBlur: () => void;
}
const renderKnob = (
rtl: boolean,
{ knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, pinFormatter, inheritedAttributes }: RangeKnob
{
knob,
value,
ratio,
min,
max,
disabled,
pressed,
pin,
handleKeyboard,
pinFormatter,
inheritedAttributes,
onKnobFocus,
onKnobBlur,
}: RangeKnob
) => {
const start = rtl ? 'right' : 'left';
@@ -941,6 +1006,8 @@ const renderKnob = (
ev.stopPropagation();
}
}}
onFocus={() => onKnobFocus(knob)}
onBlur={onKnobBlur}
class={{
'range-knob-handle': true,
'range-knob-a': knob === 'A',

View File

@@ -80,6 +80,10 @@
lower: '10',
upper: '90',
};
dualKnobs.addEventListener('ionFocus', () => {
console.log('Dual Knob ionFocus', dualKnobs.value);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,244 @@
import { newSpecPage } from '@stencil/core/testing';
import { Range } from '../../range';
describe('range: dual knobs focus management', () => {
it('should properly manage initial focus with dual knobs', async () => {
const page = await newSpecPage({
components: [Range],
html: `
<ion-range dual-knobs="true" min="0" max="100" value='{"lower": 25, "upper": 75}' aria-label="Dual range">
</ion-range>
`,
});
const range = page.body.querySelector('ion-range');
expect(range).not.toBeNull();
await page.waitForChanges();
// Get the knob elements
const knobA = range!.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
const knobB = range!.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
expect(knobA).not.toBeNull();
expect(knobB).not.toBeNull();
// Initially, neither knob should have the ion-focused class
expect(knobA.classList.contains('ion-focused')).toBe(false);
expect(knobB.classList.contains('ion-focused')).toBe(false);
});
it('should show focus on the correct knob when focused via keyboard navigation', async () => {
const page = await newSpecPage({
components: [Range],
html: `
<ion-range dual-knobs="true" min="0" max="100" value='{"lower": 25, "upper": 75}' aria-label="Dual range">
</ion-range>
`,
});
const range = page.body.querySelector('ion-range');
await page.waitForChanges();
const knobA = range!.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
const knobB = range!.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
// Focus knob A
knobA.dispatchEvent(new Event('focus'));
await page.waitForChanges();
// Only knob A should have the ion-focused class
expect(knobA.classList.contains('ion-focused')).toBe(true);
expect(knobB.classList.contains('ion-focused')).toBe(false);
// Focus knob B
knobB.dispatchEvent(new Event('focus'));
await page.waitForChanges();
// Only knob B should have the ion-focused class
expect(knobA.classList.contains('ion-focused')).toBe(false);
expect(knobB.classList.contains('ion-focused')).toBe(true);
});
it('should remove focus from all knobs when focus leaves the range', async () => {
const page = await newSpecPage({
components: [Range],
html: `
<ion-range dual-knobs="true" min="0" max="100" value='{"lower": 25, "upper": 75}' aria-label="Dual range">
</ion-range>
`,
});
const range = page.body.querySelector('ion-range');
await page.waitForChanges();
const knobA = range!.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
const knobB = range!.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
// Focus knob A
knobA.dispatchEvent(new Event('focus'));
await page.waitForChanges();
expect(knobA.classList.contains('ion-focused')).toBe(true);
// Blur the knob (focus leaves the range)
knobA.dispatchEvent(new Event('blur'));
await page.waitForChanges();
// Wait for the timeout in onKnobBlur to complete
await new Promise((resolve) => setTimeout(resolve, 10));
await page.waitForChanges();
// Neither knob should have the ion-focused class
expect(knobA.classList.contains('ion-focused')).toBe(false);
expect(knobB.classList.contains('ion-focused')).toBe(false);
});
it('should emit ionFocus when any knob receives focus but only once until blur', async () => {
const page = await newSpecPage({
components: [Range],
html: `
<ion-range dual-knobs="true" min="0" max="100" value='{"lower": 25, "upper": 75}' aria-label="Dual range">
</ion-range>
`,
});
const range = page.body.querySelector('ion-range')!;
await page.waitForChanges();
let focusEventFiredCount = 0;
range.addEventListener('ionFocus', () => {
focusEventFiredCount++;
});
const knobA = range.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
const knobB = range.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
// Focus knob A
knobA.dispatchEvent(new Event('focus'));
knobB.dispatchEvent(new Event('focus'));
await page.waitForChanges();
expect(focusEventFiredCount).toBe(1);
});
it('should emit ionBlur when focus leaves the range completely', async () => {
const page = await newSpecPage({
components: [Range],
html: `
<ion-range dual-knobs="true" min="0" max="100" value='{"lower": 25, "upper": 75}' aria-label="Dual range">
</ion-range>
`,
});
const range = page.body.querySelector('ion-range')!;
await page.waitForChanges();
let blurEventFired = false;
range.addEventListener('ionBlur', () => {
blurEventFired = true;
});
const knobA = range.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
// Focus and then blur knob A
knobA.dispatchEvent(new Event('focus'));
await page.waitForChanges();
knobA.dispatchEvent(new Event('blur'));
await page.waitForChanges();
// Wait for the timeout in onKnobBlur to complete
await new Promise((resolve) => setTimeout(resolve, 10));
await page.waitForChanges();
expect(blurEventFired).toBe(true);
});
it('should correctly handle Tab navigation between knobs using KeyboardEvent', async () => {
// Using KeyboardEvent to simulate Tab key is more realistic than just firing focus events
// because it tests the actual keyboard navigation behavior users would experience
const page = await newSpecPage({
components: [Range],
html: `
<button id="before">Before</button>
<ion-range dual-knobs="true" min="0" max="100" value='{"lower": 25, "upper": 75}' aria-label="Dual range">
</ion-range>
<button id="after">After</button>
`,
});
const range = page.body.querySelector('ion-range')!;
const beforeButton = page.body.querySelector('#before') as HTMLElement;
await page.waitForChanges();
const knobA = range.shadowRoot!.querySelector('.range-knob-a') as HTMLElement;
const knobB = range.shadowRoot!.querySelector('.range-knob-b') as HTMLElement;
// Start with focus on element before the range
beforeButton.focus();
// Simulate Tab key press - this would move focus to first knob
let tabEvent = new KeyboardEvent('keydown', {
key: 'Tab',
code: 'Tab',
bubbles: true,
cancelable: true,
});
beforeButton.dispatchEvent(tabEvent);
knobA.focus(); // Browser would focus next tabindex element
await page.waitForChanges();
// First knob should be focused
expect(knobA.classList.contains('ion-focused')).toBe(true);
expect(knobB.classList.contains('ion-focused')).toBe(false);
// Simulate another Tab key press - this would move focus to second knob
tabEvent = new KeyboardEvent('keydown', {
key: 'Tab',
code: 'Tab',
bubbles: true,
cancelable: true,
});
knobA.dispatchEvent(tabEvent);
knobB.focus(); // Browser would focus next tabindex element
await page.waitForChanges();
// Second knob should be focused, first should not
expect(knobA.classList.contains('ion-focused')).toBe(false);
expect(knobB.classList.contains('ion-focused')).toBe(true);
// Simulate Shift+Tab (reverse tab) - should go back to first knob
const shiftTabEvent = new KeyboardEvent('keydown', {
key: 'Tab',
code: 'Tab',
shiftKey: true,
bubbles: true,
cancelable: true,
});
knobB.dispatchEvent(shiftTabEvent);
knobA.focus(); // Browser would focus previous tabindex element
await page.waitForChanges();
// First knob should be focused again
expect(knobA.classList.contains('ion-focused')).toBe(true);
expect(knobB.classList.contains('ion-focused')).toBe(false);
// Verify Arrow key navigation still works on focused knob
const arrowEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
code: 'ArrowRight',
bubbles: true,
cancelable: true,
});
knobA.dispatchEvent(arrowEvent);
await page.waitForChanges();
// The knob that visually appears focused should be the one that responds to keyboard input
expect(knobA.classList.contains('ion-focused')).toBe(true);
});
});

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;
}