Compare commits

..

6 Commits

Author SHA1 Message Date
Sean Perkins
e8e075172b chore: set permissions 2023-12-04 15:01:10 -05:00
Liam DeBeasi
60d79103fd Update label.yml 2023-12-04 13:58:29 -05:00
Liam DeBeasi
27b4f86619 Update label.yml 2023-12-04 13:55:10 -05:00
Shawn Taylor
fe3c3d500a docs(input, searchbar, textarea): improve docs for managing focus (#28614)
Issue number: Related to #18132 

---------

<!-- 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. -->
The documentation about the `autofocus` prop is unclear and does not
accurately reflect how it actually works across browsers and devices.

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

- The documentation for `autofocus` and `setFocus` are more detailed.
- The documentation links to the relevant page in the docs.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-12-04 13:24:34 +00:00
Sean Perkins
1705d064cc fix(react): router creates new view instances of parameterized routes (#28616)
Issue number: Resolves #26524 

---------

<!-- 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. -->

## Definitions

**Parameterized routes**: A route that includes one or more variables in
the path segments, such as `/form/:index`.

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

When an application routes from a parameterized route, to an
intermediary route, to the same parameterized route, but with a
different value/url, Ionic's routing logic is incorrectly reusing the
view item from the first instance of the parameterized route instead of
calculating that the matched path is different. This results in the
wrong view item being recycled and rendered.

Another way of representing it:
- User navigates to `/form/0` which resolves `FormPage`
- User enters `0` into the form and submits the form
- User navigates to `/link`, which resolves `LinkPage`
- User navigates to `/form/1`, which resolves `FormPage`
- However, instead of creating a new instance of `FormPage` it is
reusing the instance of `FormPage` from `/form/0` which includes the
form having `0` in the input.
  - The user now sees a "new view", but with cached data in the form.

This is not expected or desired. 


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

- Ionic's routing logic will validate if the entering view item matches
the match route data before reusing it. This results in new instances of
the view item being constructed when using parameterized routes.


https://github.com/ionic-team/ionic-framework/assets/13732623/e7e3d03f-2848-4429-9f60-9074d0761e45


## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

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

Dev-build: `7.5.8-dev.11701383555.17254408`
2023-12-01 22:11:41 +00:00
Éric Le Maître
60303aad23 fix(vue): nav component accepts kebab-case component properties (#28615)
Issue number: resolves #28611

---------

<!-- 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. -->
It's not possible to pass props that are not camelCase to the `IonNav`
component.

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

- It is now possible to set a props with kebab-case instead of camelCase
(for example, `root-params` instead of `rootParams`)

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
⚠️ This is my first PR for ionic so I hope I didn't miss important steps
into the process. I also checked on my project that the fix is working
well. Thank you! 🙂

---------

Co-authored-by: Sean Perkins <sean@ionic.io>
2023-12-01 15:30:13 +00:00
14 changed files with 195 additions and 103 deletions

View File

@@ -11,9 +11,13 @@ on:
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
- uses: actions/checkout@v4
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true

View File

@@ -1162,7 +1162,7 @@ export namespace Components {
*/
"autocorrect": 'on' | 'off';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus": boolean;
/**
@@ -1274,7 +1274,7 @@ export namespace Components {
*/
"required": boolean;
/**
* Sets focus on the native `input` in `ion-input`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved.
* Sets focus on the native `input` in `ion-input`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"setFocus": () => Promise<void>;
/**
@@ -2601,7 +2601,7 @@ export namespace Components {
*/
"searchIcon"?: string;
/**
* Sets focus on the native `input` in `ion-searchbar`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved.
* Sets focus on the native `input` in `ion-searchbar`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"setFocus": () => Promise<void>;
/**
@@ -2950,7 +2950,7 @@ export namespace Components {
*/
"autocapitalize": string;
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus": boolean;
/**
@@ -3050,7 +3050,7 @@ export namespace Components {
*/
"rows"?: number;
/**
* Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global `textarea.focus()`.
* Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global `textarea.focus()`. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"setFocus": () => Promise<void>;
/**
@@ -5854,7 +5854,7 @@ declare namespace LocalJSX {
*/
"autocorrect"?: 'on' | 'off';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus"?: boolean;
/**
@@ -7699,7 +7699,7 @@ declare namespace LocalJSX {
*/
"autocapitalize"?: string;
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus"?: boolean;
/**

View File

@@ -95,7 +95,9 @@ export class Input implements ComponentInterface {
@Prop() autocorrect: 'on' | 'off' = 'off';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element.
*
* This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Prop() autofocus = false;
@@ -424,6 +426,8 @@ export class Input implements ComponentInterface {
*
* Developers who wish to focus an input when an overlay is presented
* should call `setFocus` after `didPresent` has resolved.
*
* See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Method()
async setFocus() {

View File

@@ -257,6 +257,8 @@ export class Searchbar implements ComponentInterface {
*
* Developers who wish to focus an input when an overlay is presented
* should call `setFocus` after `didPresent` has resolved.
*
* See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Method()
async setFocus() {

View File

@@ -93,7 +93,9 @@ export class Textarea implements ComponentInterface {
@Prop() autocapitalize = 'none';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element.
*
* This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Prop() autofocus = false;
@@ -372,6 +374,8 @@ export class Textarea implements ComponentInterface {
/**
* Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global
* `textarea.focus()`.
*
* See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Method()
async setFocus() {

View File

@@ -1,7 +1,8 @@
import type { RouteInfo, ViewItem } from '@ionic/react';
import { IonRoute, ViewLifeCycleManager, ViewStacks, generateId } from '@ionic/react';
import React from 'react';
import { matchPath } from 'react-router';
import { matchPath } from './utils/matchPath';
export class ReactRouterViewStack extends ViewStacks {
constructor() {
@@ -23,21 +24,16 @@ export class ReactRouterViewStack extends ViewStacks {
ionRoute: false,
};
const matchProps = {
exact: reactElement.props.exact,
path: reactElement.props.path || reactElement.props.from,
component: reactElement.props.component,
};
const match = matchPath(routeInfo.pathname, matchProps);
if (reactElement.type === IonRoute) {
viewItem.ionRoute = true;
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
}
viewItem.routeData = {
match,
match: matchPath({
pathname: routeInfo.pathname,
componentProps: reactElement.props,
}),
childProps: reactElement.props,
};
@@ -106,7 +102,7 @@ export class ReactRouterViewStack extends ViewStacks {
}
findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, mustBeIonRoute = true) {
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, false, mustBeIonRoute);
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, mustBeIonRoute);
return viewItem;
}
@@ -115,7 +111,10 @@ export class ReactRouterViewStack extends ViewStacks {
return viewItem;
}
private findViewItemByPath(pathname: string, outletId?: string, forceExact?: boolean, mustBeIonRoute?: boolean) {
/**
* Returns the matching view item and the match result for a given pathname.
*/
private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute?: boolean) {
let viewItem: ViewItem | undefined;
let match: ReturnType<typeof matchPath> | undefined;
let viewStack: ViewItem[];
@@ -140,16 +139,24 @@ export class ReactRouterViewStack extends ViewStacks {
if (mustBeIonRoute && !v.ionRoute) {
return false;
}
const matchProps = {
exact: forceExact ? true : v.routeData.childProps.exact,
path: v.routeData.childProps.path || v.routeData.childProps.from,
component: v.routeData.childProps.component,
};
const myMatch = matchPath(pathname, matchProps);
if (myMatch) {
viewItem = v;
match = myMatch;
return true;
match = 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)) {
viewItem = v;
return true;
}
}
return false;
}
@@ -171,13 +178,9 @@ export class ReactRouterViewStack extends ViewStacks {
}
}
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
const matchProps = {
exact: forceExact ? true : node.props.exact,
path: node.props.path || node.props.from,
component: node.props.component,
};
const match = matchPath(pathname, matchProps);
return match;
function matchComponent(node: React.ReactElement, pathname: string) {
return matchPath({
pathname,
componentProps: node.props,
});
}

View File

@@ -1,9 +1,9 @@
import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react';
import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import React from 'react';
import { matchPath } from 'react-router-dom';
import { clonePageElement } from './clonePageElement';
import { matchPath } from './utils/matchPath';
// TODO(FW-2959): types
@@ -433,12 +433,10 @@ export default StackManager;
function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
let matchedNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
const matchProps = {
exact: child.props.exact,
path: child.props.path || child.props.from,
component: child.props.component,
};
const match = matchPath(routeInfo.pathname, matchProps);
const match = matchPath({
pathname: routeInfo.pathname,
componentProps: child.props,
});
if (match) {
matchedNode = child;
}
@@ -459,12 +457,11 @@ function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
}
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
const matchProps = {
exact: forceExact ? true : node.props.exact,
path: node.props.path || node.props.from,
component: node.props.component,
};
const match = matchPath(pathname, matchProps);
return match;
return matchPath({
pathname,
componentProps: {
...node.props,
exact: forceExact,
},
});
}

View File

@@ -0,0 +1,47 @@
import { matchPath as reactRouterMatchPath } from 'react-router';
interface MatchPathOptions {
/**
* The pathname to match against.
*/
pathname: string;
/**
* The props to match against, they are identical to the matching props `Route` accepts.
*/
componentProps: {
path?: string;
from?: string;
component?: any;
exact?: boolean;
};
}
/**
* @see https://v5.reactrouter.com/web/api/matchPath
*/
export const matchPath = ({
pathname,
componentProps,
}: MatchPathOptions): false | ReturnType<typeof reactRouterMatchPath> => {
const { exact, component } = 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,
};
const match = reactRouterMatchPath(pathname, matchProps);
if (!match) {
return false;
}
return match;
};

View File

@@ -24,10 +24,6 @@ const Details: React.FC<DetailsProps> = () => {
return () => console.log('Home Details unmount');
}, []);
// useIonViewWillEnter(() => {
// console.log('IVWE Details')
// })
const nextId = parseInt(id, 10) + 1;
return (
@@ -58,6 +54,9 @@ const Details: React.FC<DetailsProps> = () => {
<IonButton routerLink={`/routing/tabs/settings/details/1`}>
<IonLabel>Go to Settings Details 1</IonLabel>
</IonButton>
<br />
<br />
<input data-testid="details-input" />
</IonContent>
</IonPage>
);

View File

@@ -309,6 +309,41 @@ describe('Routing Tests', () => {
cy.ionPageDoesNotExist('home-details-page-1');
cy.ionPageVisible('home-page');
});
it('should mount new view item instances of parameterized routes', () => {
cy.visit(`http://localhost:${port}/routing/tabs/home/details/1`);
cy.get('div.ion-page[data-pageid=home-details-page-1]')
.get('[data-testid="details-input"]')
.should('have.value', '');
cy.get('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]').type('1');
cy.ionNav('ion-button', 'Go to Details 2');
cy.ionPageVisible('home-details-page-2');
cy.get('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]').should('have.value', '');
cy.get('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]').type('2');
cy.ionNav('ion-button', 'Go to Details 3');
cy.ionPageVisible('home-details-page-3');
cy.get('div.ion-page[data-pageid=home-details-page-3] [data-testid="details-input"]').should('have.value', '');
cy.get('div.ion-page[data-pageid=home-details-page-3] [data-testid="details-input"]').type('3');
cy.ionBackClick('home-details-page-3');
cy.ionPageVisible('home-details-page-2');
cy.get('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]').should('have.value', '2');
cy.ionBackClick('home-details-page-2');
cy.ionPageVisible('home-details-page-1');
cy.get('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]').should('have.value', '1');
});
/*
Tests to add:
Test that lifecycle events fire

View File

@@ -36,22 +36,22 @@ IonNav.name = "IonNav";
IonNav.props = {
animated: {
type: Boolean,
default: true
default: true,
},
animation: {
type: Function,
default: undefined
default: undefined,
},
root: {
type: [Function, Object, String],
default: undefined
default: undefined,
},
rootParams: {
type: Object,
default: undefined
default: undefined,
},
swipeGesture: {
type: Boolean,
default: undefined
}
}
default: undefined,
},
};

View File

@@ -1,16 +1,12 @@
<template>
<ion-nav :root="NavRoot"></ion-nav>
<ion-nav :root="NavRoot" :root-params="rootParams"></ion-nav>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { IonNav } from '@ionic/vue';
import NavRoot from '@/components/NavRoot.vue';
<script setup lang="ts">
import { IonNav } from "@ionic/vue";
import NavRoot from "@/components/NavRoot.vue";
export default defineComponent({
components: { IonNav },
setup() {
return { NavRoot }
}
});
const rootParams = {
message: "Hello World!",
};
</script>

View File

@@ -8,11 +8,14 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button expand="block" @click="pushPage" id="push-nav-child">Go to Nav Child</ion-button>
<div id="nav-root-params">Message: {{ message }}</div>
<ion-button expand="block" @click="pushPage" id="push-nav-child"
>Go to Nav Child</ion-button
>
</ion-content>
</template>
<script lang="ts">
<script setup lang="ts">
import {
IonButtons,
IonButton,
@@ -20,28 +23,20 @@ import {
IonHeader,
IonTitle,
IonToolbar,
modalController
} from '@ionic/vue';
import { defineComponent } from 'vue';
import NavChild from '@/components/NavChild.vue';
modalController,
} from "@ionic/vue";
import NavChild from "@/components/NavChild.vue";
export default defineComponent({
components: {
IonButtons,
IonButton,
IonContent,
IonHeader,
IonTitle,
IonToolbar
},
methods: {
pushPage: function() {
const ionNav = document.querySelector('ion-nav') as any;
ionNav.push(NavChild, { title: 'Custom Title' });
},
dismiss: async function() {
await modalController.dismiss();
}
}
})
defineProps<{
message: string;
}>();
function pushPage() {
const ionNav = document.querySelector("ion-nav") as any;
ionNav.push(NavChild, { title: "Custom Title" });
}
async function dismiss() {
await modalController.dismiss();
}
</script>

View File

@@ -10,4 +10,10 @@ describe('Navigation', () => {
cy.get('#nav-child-content').should('have.text', 'Custom Title');
});
it('nav should support kebab-case root-params', () => {
cy.get('#open-nav-modal').click();
cy.get('#nav-root-params').should('have.text', 'Message: Hello World!');
});
});