chore(): sync with main

This commit is contained in:
Liam DeBeasi
2021-12-07 14:57:29 -05:00
31 changed files with 2487 additions and 2056 deletions

View File

@ -41,7 +41,7 @@ runs:
run: npm run test:unit run: npm run test:unit
shell: bash shell: bash
working-directory: ./packages/vue/test-app working-directory: ./packages/vue/test-app
- name: Run E2E ests - name: Run E2E Tests
run: npm run test:e2e run: npm run test:e2e
shell: bash shell: bash
working-directory: ./packages/vue/test-app working-directory: ./packages/vue/test-app

View File

@ -1,3 +1,20 @@
## [5.9.2](https://github.com/ionic-team/ionic/compare/v5.9.1...v5.9.2) (2021-12-07)
### Bug Fixes
* **angular:** improve typing when compiling with legacy View Engine ([#24221](https://github.com/ionic-team/ionic/issues/24221)) ([816096f](https://github.com/ionic-team/ionic/commit/816096f89747e943a4a273175d384189f25e4628))
* **content:** ensure fixed slot renders on top of content in iOS ([#24300](https://github.com/ionic-team/ionic/issues/24300)) ([e41b0e0](https://github.com/ionic-team/ionic/commit/e41b0e0cf2a794972d7f4d8943a0bec3d1e08016)), closes [#24286](https://github.com/ionic-team/ionic-framework/issues/24286)
* **popover:** improve scrolling in popover when using header and footer ([#24294](https://github.com/ionic-team/ionic/issues/24294)) ([f6a00ea](https://github.com/ionic-team/ionic/commit/f6a00ea9544aa70620b5f8f65a7702fa3bedd974))
* **react:** present and dismiss hooks return promises ([#24299](https://github.com/ionic-team/ionic/issues/24299)) ([4b26fea](https://github.com/ionic-team/ionic/commit/4b26feaa47efed4806aba565a52554db232b99e2)), closes [#24293](https://github.com/ionic-team/ionic-framework/issues/24293)
* **react:** properly check for custom elements to avoid errors in unit tests ([#24156](https://github.com/ionic-team/ionic/issues/24156)) ([8f188ea](https://github.com/ionic-team/ionic/commit/8f188eaae7422c9e81053868b9dd93b4ac738e98)), closes [#24149](https://github.com/ionic-team/ionic/issues/24149)
* **router:** popping route now accounts for route params ([#24315](https://github.com/ionic-team/ionic/issues/24315)) ([5e5054d](https://github.com/ionic-team/ionic/commit/5e5054d369ad68c9ac43e12439d71fb42d6ca26b)), closes [#24223](https://github.com/ionic-team/ionic-framework/issues/24223)
* **slides:** update swiper instance after initialization ([#24257](https://github.com/ionic-team/ionic/issues/24257)) ([89e4bc5](https://github.com/ionic-team/ionic/commit/89e4bc56a1c3cd4fb26fc5514f38c6a01f047297)), closes [#19638](https://github.com/ionic-team/ionic-framework/issues/19638)
* **vue:** ionic lifecycle hooks now run when using vue 3.2 setup syntax ([#24253](https://github.com/ionic-team/ionic/issues/24253)) ([fb96ab5](https://github.com/ionic-team/ionic/commit/fb96ab5a26d87818a8b64ee82df0020355054183)), closes [#23824](https://github.com/ionic-team/ionic/issues/23824)
* **vue:** switching between tabs preserves query string ([#24297](https://github.com/ionic-team/ionic/issues/24297)) ([047d3c7](https://github.com/ionic-team/ionic/commit/047d3c77729db08e4fd84f426f6c5c2af0eacc52)), closes [#23699](https://github.com/ionic-team/ionic/issues/23699)
# [6.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v6.0.0-rc.2...v6.0.0-rc.3) (2021-11-17) # [6.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v6.0.0-rc.2...v6.0.0-rc.3) (2021-11-17)

View File

@ -94,7 +94,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
*/ */
const formControl = ngControl.control as any; const formControl = ngControl.control as any;
if (formControl) { if (formControl) {
const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine']; const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine'] as const;
methodsToPatch.forEach((method) => { methodsToPatch.forEach((method) => {
if (formControl.get(method)) { if (formControl.get(method)) {
const oldFn = formControl[method].bind(formControl); const oldFn = formControl[method].bind(formControl);

View File

@ -238,7 +238,7 @@ export namespace Components {
*/ */
"disabled": boolean; "disabled": boolean;
/** /**
* The icon name to use for the back button. * The built-in named SVG icon name or the exact `src` of an SVG file to use for the back button.
*/ */
"icon"?: string | null; "icon"?: string | null;
/** /**
@ -3891,7 +3891,7 @@ declare namespace LocalJSX {
*/ */
"disabled"?: boolean; "disabled"?: boolean;
/** /**
* The icon name to use for the back button. * The built-in named SVG icon name or the exact `src` of an SVG file to use for the back button.
*/ */
"icon"?: string | null; "icon"?: string | null;
/** /**

View File

@ -45,7 +45,8 @@ export class BackButton implements ComponentInterface, ButtonInterface {
@Prop({ reflect: true }) disabled = false; @Prop({ reflect: true }) disabled = false;
/** /**
* The icon name to use for the back button. * The built-in named SVG icon name or the exact `src` of an SVG file
* to use for the back button.
*/ */
@Prop() icon?: string | null; @Prop() icon?: string | null;

View File

@ -315,7 +315,7 @@ export default defineComponent({
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` | | `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `defaultHref` | `default-href` | The url to navigate back to by default when there is no history. | `string \| undefined` | `undefined` | | `defaultHref` | `default-href` | The url to navigate back to by default when there is no history. | `string \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the button. | `boolean` | `false` | | `disabled` | `disabled` | If `true`, the user cannot interact with the button. | `boolean` | `false` |
| `icon` | `icon` | The icon name to use for the back button. | `null \| string \| undefined` | `undefined` | | `icon` | `icon` | The built-in named SVG icon name or the exact `src` of an SVG file to use for the back button. | `null \| string \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | | `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` | | `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `text` | `text` | The text to display in the back button. | `null \| string \| undefined` | `undefined` | | `text` | `text` | The text to display in the back button. | `null \| string \| undefined` | `undefined` |

View File

@ -136,10 +136,37 @@
} }
:host(.content-sizing) { :host(.content-sizing) {
display: flex;
flex-direction: column;
/**
* This resolves a sizing issue in popovers where extra long content
* would overflow the popover's height, preventing scrolling. It's a
* quirk of flexbox that forces the content to shrink to fit.
*
* overflow: hidden can't be used here because it prevents the visual
* effect from showing on translucent headers.
*/
min-height: 0;
contain: none; contain: none;
} }
:host(.content-sizing) .inner-scroll { :host(.content-sizing) .inner-scroll {
position: relative; position: relative;
/**
* Because the outer content has display: flex here (to help enable
* scrolling in a popover), offsetting via `top` (such as when using
* a translucent header) creates white space under the content. Use
* a negative margin instead to keep the bottom in place. (A similar
* thing happens with `bottom` and footers.)
*/
top: 0;
bottom: 0;
margin-top: calc(var(--offset-top) * -1);
margin-bottom: calc(var(--offset-bottom) * -1);
} }
.transition-effect { .transition-effect {
@ -195,4 +222,15 @@
::slotted([slot="fixed"]) { ::slotted([slot="fixed"]) {
position: absolute; position: absolute;
/**
* When presenting ion-content inside of an ion-modal, the .inner-scroll
* element is composited. In WebKit, the fixed content is not composited
* causing it to appear under the main scrollable content as a result.
* The fixed content is correctly composited in other browsers. Adding
* the translateZ forces the fixed content to be composited so it correctly
* shows on top of the scrollable content. Setting a negative z-index will
* still allow the fixed content to appear under the scroll content if specified.
*/
transform: translateZ(0);
} }

View File

@ -384,10 +384,17 @@ const getPageElement = (el: HTMLElement) => {
if (tabs) { if (tabs) {
return tabs; return tabs;
} }
const page = el.closest('ion-app,ion-page,.ion-page,page-inner');
/**
* If we're in a popover, we need to use its wrapper so we can account for space
* between the popover and the edges of the screen. But if the popover contains
* its own page element, we should use that instead.
*/
const page = el.closest('ion-app, ion-page, .ion-page, page-inner, .popover-content');
if (page) { if (page) {
return page; return page;
} }
return getParentElement(el); return getParentElement(el);
}; };

View File

@ -87,6 +87,11 @@
--ion-safe-area-right: 0px; --ion-safe-area-right: 0px;
--ion-safe-area-bottom: 0px; --ion-safe-area-bottom: 0px;
--ion-safe-area-left: 0px; --ion-safe-area-left: 0px;
display: flex;
flex-direction: column;
overflow: hidden;
} }
// Nested Popovers // Nested Popovers
@ -114,4 +119,3 @@
--offset-x: 5px; --offset-x: 5px;
} }
} }

View File

@ -73,6 +73,14 @@ test('popover: custom class', async () => {
await testPopover(DIRECTORY, '#custom-class-popover'); await testPopover(DIRECTORY, '#custom-class-popover');
}); });
test('popover: header', async () => {
await testPopover(DIRECTORY, '#header-popover');
});
test('popover: translucent header', async () => {
await testPopover(DIRECTORY, '#translucent-header-popover');
});
/** /**
* RTL Tests * RTL Tests
*/ */
@ -97,6 +105,14 @@ test('popover:rtl: custom class', async () => {
await testPopover(DIRECTORY, '#custom-class-popover', true, true); await testPopover(DIRECTORY, '#custom-class-popover', true, true);
}); });
test('popover:rtl: header', async () => {
await testPopover(DIRECTORY, '#header-popover', true);
});
test('popover:rtl: translucent header', async () => {
await testPopover(DIRECTORY, '#translucent-header-popover', true);
});
test('popover: htmlAttributes', async () => { test('popover: htmlAttributes', async () => {
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' }); const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });

View File

@ -34,6 +34,8 @@
<ion-button id="long-list-popover" expand="block" color="secondary" onclick="presentPopover({ component: 'list-page', event: event })">Show Long List Popover</ion-button> <ion-button id="long-list-popover" expand="block" color="secondary" onclick="presentPopover({ component: 'list-page', event: event })">Show Long List Popover</ion-button>
<ion-button id="no-event-popover" expand="block" color="danger" onclick="presentPopover({ component: 'profile-page' })">No Event Popover</ion-button> <ion-button id="no-event-popover" expand="block" color="danger" onclick="presentPopover({ component: 'profile-page' })">No Event Popover</ion-button>
<ion-button id="custom-class-popover" expand="block" color="tertiary" onclick="presentPopover({ component: 'translucent-page', event: event, cssClass: 'my-custom-class' })">Custom Class Popover</ion-button> <ion-button id="custom-class-popover" expand="block" color="tertiary" onclick="presentPopover({ component: 'translucent-page', event: event, cssClass: 'my-custom-class' })">Custom Class Popover</ion-button>
<ion-button id="header-popover" expand="block" onclick="presentPopover({ component: 'header-page' })">Popover With Header</ion-button>
<ion-button id="translucent-header-popover" expand="block" onclick="presentPopover({ component: 'translucent-header-page' })">Popover With Translucent Header</ion-button>
</ion-content> </ion-content>
<ion-footer> <ion-footer>
@ -126,6 +128,56 @@
} }
customElements.define('translucent-page', TranslucentPage); customElements.define('translucent-page', TranslucentPage);
class HeaderPage extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" color="primary">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.In rutrum tortor lacus, ac interdum ipsum bibendum vel.Aenean non nibh gravida, ullamcorper mi at, tempor nulla.Proin malesuada tellus ut ullamcorper accumsan.Donec semper justo vulputate neque tempus ultricies.Proin non aliquet ipsum.Praesent mauris sem, facilisis eu justo nec, euismod imperdiet tellus.Duis eget justo congue, lacinia orci sed, fermentum urna.Quisque sed massa faucibus, interdum dolor rhoncus, molestie erat.Proin suscipit ante non mauris volutpat egestas.Donec a ultrices ligula.Mauris in felis vel dui consectetur viverra.Nam vitae quam in arcu aliquam aliquam.Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Cras non velit nisl.Donec viverra, magna quis vestibulum volutpat, metus ante tincidunt augue, non porta nisi mi sit amet neque.Proin dapibus eros vitae nibh tincidunt, blandit rhoncus est porttitor.
</ion-content>
`;
}
}
customElements.define('header-page', HeaderPage);
class TranslucentHeaderPage extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<ion-header translucent>
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" fullscreen color="primary">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.In rutrum tortor lacus, ac interdum ipsum bibendum vel.Aenean non nibh gravida, ullamcorper mi at, tempor nulla.Proin malesuada tellus ut ullamcorper accumsan.Donec semper justo vulputate neque tempus ultricies.Proin non aliquet ipsum.Praesent mauris sem, facilisis eu justo nec, euismod imperdiet tellus.Duis eget justo congue, lacinia orci sed, fermentum urna.Quisque sed massa faucibus, interdum dolor rhoncus, molestie erat.Proin suscipit ante non mauris volutpat egestas.Donec a ultrices ligula.Mauris in felis vel dui consectetur viverra.Nam vitae quam in arcu aliquam aliquam.Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Cras non velit nisl.Donec viverra, magna quis vestibulum volutpat, metus ante tincidunt augue, non porta nisi mi sit amet neque.Proin dapibus eros vitae nibh tincidunt, blandit rhoncus est porttitor.
</ion-content>
<ion-footer translucent>
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
`;
}
}
customElements.define('translucent-header-page', TranslucentHeaderPage);
</script> </script>
</body> </body>

View File

@ -30,15 +30,24 @@ const CHAIN_3: RouteChain = [
describe('matchesIDs', () => { describe('matchesIDs', () => {
it('should match simple set of ids', () => { it('should match simple set of ids', () => {
const chain: RouteChain = CHAIN_1; const chain: RouteChain = CHAIN_1;
expect(matchesIDs(['2'], chain)).toBe(1); expect(matchesIDs([{ id: '2' }], chain)).toBe(1);
expect(matchesIDs(['2', '1'], chain)).toBe(2); expect(matchesIDs([{ id: '2' }, { id: '1' }], chain)).toBe(2);
expect(matchesIDs(['2', '1', '3'], chain)).toBe(3); expect(matchesIDs([{ id: '2' }, { id: '1' }, { id: '3' }], chain)).toBe(3);
expect(matchesIDs(['2', '1', '3', '4'], chain)).toBe(4); expect(matchesIDs([{ id: '2' }, { id: '1' }, { id: '3' }, { id: '4' }], chain)).toBe(4);
expect(matchesIDs(['2', '1', '3', '4', '5'], chain)).toBe(4); expect(matchesIDs([{ id: '2' }, { id: '1' }, { id: '3' }, { id: '4' }, { id: '5' }], chain)).toBe(4);
expect(matchesIDs([], chain)).toBe(0); expect(matchesIDs([], chain)).toBe(0);
expect(matchesIDs(['1'], chain)).toBe(0); expect(matchesIDs([{ id: '1' }], chain)).toBe(0);
}); });
it('should match path with params', () => {
const ids = [{ id: 'my-page', params: { s1: 'a', s2: 'b' } }];
expect(matchesIDs(ids, [{ id: 'my-page', path: [''], params: {} }])).toBe(1);
expect(matchesIDs(ids, [{ id: 'my-page', path: [':s1'], params: {} }])).toBe(1);
expect(matchesIDs(ids, [{ id: 'my-page', path: [':s1', ':s2'], params: {} }])).toBe(3);
expect(matchesIDs(ids, [{ id: 'my-page', path: [':s1', ':s2', ':s3'], params: {} }])).toBe(1);
})
}); });
describe('matchesPath', () => { describe('matchesPath', () => {
@ -227,7 +236,7 @@ describe('mergeParams', () => {
}); });
describe('RouterSegments', () => { describe('RouterSegments', () => {
it ('should initialize with empty array', () => { it('should initialize with empty array', () => {
const s = new RouterSegments([]); const s = new RouterSegments([]);
expect(s.next()).toEqual(''); expect(s.next()).toEqual('');
expect(s.next()).toEqual(''); expect(s.next()).toEqual('');
@ -236,7 +245,7 @@ describe('RouterSegments', () => {
expect(s.next()).toEqual(''); expect(s.next()).toEqual('');
}); });
it ('should initialize with array', () => { it('should initialize with array', () => {
const s = new RouterSegments(['', 'path', 'to', 'destination']); const s = new RouterSegments(['', 'path', 'to', 'destination']);
expect(s.next()).toEqual(''); expect(s.next()).toEqual('');
expect(s.next()).toEqual('path'); expect(s.next()).toEqual('path');

View File

@ -32,16 +32,60 @@ export const findRouteRedirect = (path: string[], redirects: RouteRedirect[]) =>
return redirects.find(redirect => matchesRedirect(path, redirect)); return redirects.find(redirect => matchesRedirect(path, redirect));
}; };
export const matchesIDs = (ids: string[], chain: RouteChain): number => { export const matchesIDs = (ids: Pick<RouteID, 'id' | 'params'>[], chain: RouteChain): number => {
const len = Math.min(ids.length, chain.length); const len = Math.min(ids.length, chain.length);
let i = 0;
for (; i < len; i++) { let score = 0;
if (ids[i].toLowerCase() !== chain[i].id) {
for (let i = 0; i < len; i++) {
const routeId = ids[i];
const routeChain = chain[i];
// Skip results where the route id does not match the chain at the same index
if (routeId.id.toLowerCase() !== routeChain.id) {
break; break;
} }
if (routeId.params) {
const routeIdParams = Object.keys(routeId.params);
/**
* Only compare routes with the chain that have the same number of parameters.
*/
if (routeIdParams.length === routeChain.path.length) {
/**
* Maps the route's params into a path based on the path variable names,
* to compare against the route chain format.
*
* Before:
* ```ts
* {
* params: {
* s1: 'a',
* s2: 'b'
* }
* }
* ```
*
* After:
* ```ts
* [':s1',':s2']
* ```
*/
const pathWithParams = routeIdParams.map(key => `:${key}`);
for (let j = 0; j < pathWithParams.length; j++) {
// Skip results where the path variable is not a match
if (pathWithParams[j].toLowerCase() !== routeChain.path[j]) {
break;
}
// Weight path matches for the same index higher.
score++;
}
}
}
// Weight id matches
score++;
} }
return i; return score;
}; }
export const matchesPath = (inputPath: string[], chain: RouteChain): RouteChain | null => { export const matchesPath = (inputPath: string[], chain: RouteChain): RouteChain | null => {
const segments = new RouterSegments(inputPath); const segments = new RouterSegments(inputPath);
@ -90,16 +134,16 @@ export const matchesPath = (inputPath: string[], chain: RouteChain): RouteChain
// Merges the route parameter objects. // Merges the route parameter objects.
// Returns undefined when both parameters are undefined. // Returns undefined when both parameters are undefined.
export const mergeParams = (a: {[key: string]: any} | undefined, b: {[key: string]: any} | undefined): {[key: string]: any} | undefined => { export const mergeParams = (a: { [key: string]: any } | undefined, b: { [key: string]: any } | undefined): { [key: string]: any } | undefined => {
return a || b ? { ...a, ...b } : undefined; return a || b ? { ...a, ...b } : undefined;
}; };
export const routerIDsToChain = (ids: RouteID[], chains: RouteChain[]): RouteChain | null => { export const routerIDsToChain = (ids: RouteID[], chains: RouteChain[]): RouteChain | null => {
let match: RouteChain | null = null; let match: RouteChain | null = null;
let maxMatches = 0; let maxMatches = 0;
const plainIDs = ids.map(i => i.id);
for (const chain of chains) { for (const chain of chains) {
const score = matchesIDs(plainIDs, chain); const score = matchesIDs(ids, chain);
if (score > maxMatches) { if (score > maxMatches) {
match = chain; match = chain;
maxMatches = score; maxMatches = score;

View File

@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h } from '@stencil/core'; import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { componentOnReady } from '../../utils/helpers' import { componentOnReady } from '../../utils/helpers'
@ -24,8 +24,6 @@ export class Slides implements ComponentInterface {
private mutationO?: MutationObserver; private mutationO?: MutationObserver;
private readySwiper!: (swiper: SwiperInterface) => void; private readySwiper!: (swiper: SwiperInterface) => void;
private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; }); private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; });
private syncSwiper?: SwiperInterface;
private didInit = false;
@Element() el!: HTMLIonSlidesElement; @Element() el!: HTMLIonSlidesElement;
@ -141,8 +139,7 @@ export class Slides implements ComponentInterface {
} }
connectedCallback() { connectedCallback() {
// tslint:disable-next-line: strict-type-predicates if (Build.isBrowser) {
if (typeof MutationObserver !== 'undefined') {
const mut = this.mutationO = new MutationObserver(() => { const mut = this.mutationO = new MutationObserver(() => {
if (this.swiperReady) { if (this.swiperReady) {
this.update(); this.update();
@ -154,10 +151,7 @@ export class Slides implements ComponentInterface {
}); });
componentOnReady(this.el, () => { componentOnReady(this.el, () => {
if (!this.didInit) {
this.didInit = true;
this.initSwiper(); this.initSwiper();
}
}) })
} }
} }
@ -167,23 +161,6 @@ export class Slides implements ComponentInterface {
this.mutationO.disconnect(); this.mutationO.disconnect();
this.mutationO = undefined; this.mutationO = undefined;
} }
/**
* We need to synchronously destroy
* swiper otherwise it is possible
* that it will be left in a
* destroyed state if connectedCallback
* is called multiple times
*/
const swiper = this.syncSwiper;
if (swiper !== undefined) {
swiper.destroy(true, true);
this.swiper = new Promise(resolve => { this.readySwiper = resolve; });
this.swiperReady = false;
this.syncSwiper = undefined;
}
this.didInit = false;
} }
/** /**
@ -369,7 +346,6 @@ export class Slides implements ComponentInterface {
await waitForSlides(this.el); await waitForSlides(this.el);
const swiper = new Swiper(this.el, finalOptions); const swiper = new Swiper(this.el, finalOptions);
this.swiperReady = true; this.swiperReady = true;
this.syncSwiper = swiper;
this.readySwiper(swiper); this.readySwiper(swiper);
} }
@ -483,6 +459,8 @@ export class Slides implements ComponentInterface {
init: () => { init: () => {
setTimeout(() => { setTimeout(() => {
this.ionSlidesDidLoad.emit(); this.ionSlidesDidLoad.emit();
// Forces the swiper instance to update after initializing.
this.update();
}, 20); }, 20);
}, },
slideChangeTransitionStart: this.ionSlideWillChange.emit, slideChangeTransitionStart: this.ionSlideWillChange.emit,

View File

@ -6,7 +6,8 @@
"release.dev": "node .scripts/release-dev.js", "release.dev": "node .scripts/release-dev.js",
"release.prepare": "node .scripts/prepare.js", "release.prepare": "node .scripts/prepare.js",
"release": "node .scripts/release.js", "release": "node .scripts/release.js",
"changelog": "conventional-changelog -p angular -i ./CHANGELOG.md -k core -s" "changelog": "conventional-changelog -p angular -i ./CHANGELOG.md -k core -s",
"commitizenBranches": "git-branch-is -q --not -r \"^(main|next|release-)\""
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^13.1.0", "@commitlint/cli": "^13.1.0",
@ -18,6 +19,7 @@
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"execa": "^0.10.0", "execa": "^0.10.0",
"fs-extra": "^7.0.0", "fs-extra": "^7.0.0",
"git-branch-is": "^4.0.0",
"husky": "^4.3.8", "husky": "^4.3.8",
"inquirer": "^6.0.0", "inquirer": "^6.0.0",
"listr": "^0.14.0", "listr": "^0.14.0",
@ -34,8 +36,8 @@
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS", "commit-msg": "npm run commitizenBranches --silent && commitlint -E HUSKY_GIT_PARAMS || true",
"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true" "prepare-commit-msg": "npm run commitizenBranches --silent && exec < /dev/tty && git cz --hook || true"
} }
} }
} }

View File

@ -16,9 +16,9 @@ class IonTabsElement extends HTMLElementSSR {
} }
if (typeof (window as any) !== 'undefined' && window.customElements) { if (typeof (window as any) !== 'undefined' && window.customElements) {
const element = customElements.get('ion-tabs'); const element = window.customElements.get('ion-tabs');
if (!element) { if (!element) {
customElements.define('ion-tabs', IonTabsElement); window.customElements.define('ion-tabs', IonTabsElement);
} }
} }

View File

@ -20,12 +20,12 @@ export function useIonActionSheet(): UseIonActionSheetResult {
header?: string header?: string
) => { ) => {
if (Array.isArray(buttonsOrOptions)) { if (Array.isArray(buttonsOrOptions)) {
controller.present({ return controller.present({
buttons: buttonsOrOptions, buttons: buttonsOrOptions,
header, header,
}); });
} else { } else {
controller.present(buttonsOrOptions); return controller.present(buttonsOrOptions);
} }
}, },
[controller.present] [controller.present]
@ -41,15 +41,15 @@ export type UseIonActionSheetResult = [
* @param buttons An array of buttons for the action sheet * @param buttons An array of buttons for the action sheet
* @param header Optional - Title for the action sheet * @param header Optional - Title for the action sheet
*/ */
(buttons: ActionSheetButton[], header?: string | undefined): void; (buttons: ActionSheetButton[], header?: string | undefined): Promise<void>;
/** /**
* Presents the action sheet * Presents the action sheet
* @param options The options to pass to the IonActionSheet * @param options The options to pass to the IonActionSheet
*/ */
(options: ActionSheetOptions & HookOverlayOptions): void; (options: ActionSheetOptions & HookOverlayOptions): Promise<void>;
}, },
/** /**
* Dismisses the action sheet * Dismisses the action sheet
*/ */
() => void () => Promise<void>
]; ];

View File

@ -14,12 +14,12 @@ export function useIonAlert(): UseIonAlertResult {
const present = useCallback( const present = useCallback(
(messageOrOptions: string | (AlertOptions & HookOverlayOptions), buttons?: AlertButton[]) => { (messageOrOptions: string | (AlertOptions & HookOverlayOptions), buttons?: AlertButton[]) => {
if (typeof messageOrOptions === 'string') { if (typeof messageOrOptions === 'string') {
controller.present({ return controller.present({
message: messageOrOptions, message: messageOrOptions,
buttons: buttons ?? [{ text: 'Ok' }], buttons: buttons ?? [{ text: 'Ok' }],
}); });
} else { } else {
controller.present(messageOrOptions); return controller.present(messageOrOptions);
} }
}, },
[controller.present] [controller.present]
@ -35,15 +35,15 @@ export type UseIonAlertResult = [
* @param message The main message to be displayed in the alert * @param message The main message to be displayed in the alert
* @param buttons Optional - Array of buttons to be added to the alert * @param buttons Optional - Array of buttons to be added to the alert
*/ */
(message: string, buttons?: AlertButton[]): void; (message: string, buttons?: AlertButton[]): Promise<void>;
/** /**
* Presents the alert * Presents the alert
* @param options The options to pass to the IonAlert * @param options The options to pass to the IonAlert
*/ */
(options: AlertOptions & HookOverlayOptions): void; (options: AlertOptions & HookOverlayOptions): Promise<void>;
}, },
/** /**
* Dismisses the alert * Dismisses the alert
*/ */
() => void () => Promise<void>
]; ];

View File

@ -21,13 +21,13 @@ export function useIonLoading(): UseIonLoadingResult {
spinner?: SpinnerTypes spinner?: SpinnerTypes
) => { ) => {
if (typeof messageOrOptions === 'string') { if (typeof messageOrOptions === 'string') {
controller.present({ return controller.present({
message: messageOrOptions, message: messageOrOptions,
duration, duration,
spinner: spinner ?? 'lines', spinner: spinner ?? 'lines',
}); });
} else { } else {
controller.present(messageOrOptions); return controller.present(messageOrOptions);
} }
}, },
[controller.present] [controller.present]
@ -44,15 +44,15 @@ export type UseIonLoadingResult = [
* @param duration Optional - Number of milliseconds to wait before dismissing the loading indicator * @param duration Optional - Number of milliseconds to wait before dismissing the loading indicator
* @param spinner Optional - The name of the spinner to display, defaults to "lines" * @param spinner Optional - The name of the spinner to display, defaults to "lines"
*/ */
(message?: string, duration?: number, spinner?: SpinnerTypes): void; (message?: string, duration?: number, spinner?: SpinnerTypes): Promise<void>;
/** /**
* Presents the loading indicator * Presents the loading indicator
* @param options The options to pass to the IonLoading * @param options The options to pass to the IonLoading
*/ */
(options: LoadingOptions & HookOverlayOptions): void; (options: LoadingOptions & HookOverlayOptions): Promise<void>;
}, },
/** /**
* Dismisses the loading indicator * Dismisses the loading indicator
*/ */
() => void () => Promise<void>
]; ];

View File

@ -24,12 +24,12 @@ export function useIonPicker(): UseIonPickerResult {
buttons?: PickerButton[] buttons?: PickerButton[]
) => { ) => {
if (Array.isArray(columnsOrOptions)) { if (Array.isArray(columnsOrOptions)) {
controller.present({ return controller.present({
columns: columnsOrOptions, columns: columnsOrOptions,
buttons: buttons ?? [{ text: 'Ok' }], buttons: buttons ?? [{ text: 'Ok' }],
}); });
} else { } else {
controller.present(columnsOrOptions); return controller.present(columnsOrOptions);
} }
}, [controller.present]); }, [controller.present]);
@ -43,15 +43,15 @@ export type UseIonPickerResult = [
* @param columns Array of columns to be displayed in the picker. * @param columns Array of columns to be displayed in the picker.
* @param buttons Optional - Array of buttons to be displayed at the top of the picker. * @param buttons Optional - Array of buttons to be displayed at the top of the picker.
*/ */
(columns: PickerColumn[], buttons?: PickerButton[]): void; (columns: PickerColumn[], buttons?: PickerButton[]): Promise<void>;
/** /**
* Presents the picker * Presents the picker
* @param options The options to pass to the IonPicker * @param options The options to pass to the IonPicker
*/ */
(options: PickerOptions & HookOverlayOptions): void; (options: PickerOptions & HookOverlayOptions): Promise<void>;
}, },
/** /**
* Dismisses the picker * Dismisses the picker
*/ */
() => void () => Promise<void>
]; ];

View File

@ -16,12 +16,12 @@ export function useIonToast(): UseIonToastResult {
const present = useCallback((messageOrOptions: string | ToastOptions & HookOverlayOptions, duration?: number) => { const present = useCallback((messageOrOptions: string | ToastOptions & HookOverlayOptions, duration?: number) => {
if (typeof messageOrOptions === 'string') { if (typeof messageOrOptions === 'string') {
controller.present({ return controller.present({
message: messageOrOptions, message: messageOrOptions,
duration duration
}); });
} else { } else {
controller.present(messageOrOptions); return controller.present(messageOrOptions);
} }
}, [controller.present]); }, [controller.present]);
@ -38,15 +38,15 @@ export type UseIonToastResult = [
* @param message Message to be shown in the toast. * @param message Message to be shown in the toast.
* @param duration Optional - How many milliseconds to wait before hiding the toast. By default, it will show until dismissToast() is called. * @param duration Optional - How many milliseconds to wait before hiding the toast. By default, it will show until dismissToast() is called.
*/ */
(message: string, duration?: number): void; (message: string, duration?: number): Promise<void>;
/** /**
* Presents the Toast * Presents the Toast
* @param options The options to pass to the IonToast. * @param options The options to pass to the IonToast.
*/ */
(options: ToastOptions & HookOverlayOptions): void; (options: ToastOptions & HookOverlayOptions): Promise<void>;
}, },
/** /**
* Dismisses the toast * Dismisses the toast
*/ */
() => void () => Promise<void>
]; ];

View File

@ -115,9 +115,10 @@ export const IonTabBar = defineComponent({
* land on /tabs/tab1/child instead of /tabs/tab1. * land on /tabs/tab1/child instead of /tabs/tab1.
*/ */
if (activeTab !== prevActiveTab || (prevHref !== currentRoute.pathname)) { if (activeTab !== prevActiveTab || (prevHref !== currentRoute.pathname)) {
const search = (currentRoute.search !== undefined) ? `?${currentRoute.search}` : '';
tabs[activeTab] = { tabs[activeTab] = {
...tabs[activeTab], ...tabs[activeTab],
currentHref: currentRoute.pathname + (currentRoute.search || '') currentHref: currentRoute.pathname + search
} }
} }

112
packages/vue/src/hooks.ts Normal file
View File

@ -0,0 +1,112 @@
import { BackButtonEvent } from '@ionic/core';
import {
inject,
ref,
Ref,
ComponentInternalInstance,
getCurrentInstance
} from 'vue';
import { LifecycleHooks } from './utils';
type Handler = (processNextHandler: () => void) => Promise<any> | void | null;
export interface IonRouter {
canGoBack: (deep?: number) => boolean;
}
export interface IonKeyboardRef {
isOpen: Ref<boolean>;
keyboardHeight: Ref<number>;
unregister: () => void
}
export const useBackButton = (priority: number, handler: Handler) => {
const callback = (ev: BackButtonEvent) => ev.detail.register(priority, handler);
const unregister = () => document.removeEventListener('ionBackButton', callback);
document.addEventListener('ionBackButton', callback);
return { unregister };
}
export const useIonRouter = (): IonRouter => {
const { canGoBack } = inject('navManager') as any;
return {
canGoBack
} as IonRouter
}
export const useKeyboard = (): IonKeyboardRef => {
let isOpen = ref(false);
let keyboardHeight = ref(0);
const showCallback = (ev: CustomEvent) => {
isOpen.value = true;
keyboardHeight.value = ev.detail.keyboardHeight;
}
const hideCallback = () => {
isOpen.value = false;
keyboardHeight.value = 0;
}
const unregister = () => {
if (typeof (window as any) !== 'undefined') {
window.removeEventListener('ionKeyboardDidShow', showCallback);
window.removeEventListener('ionKeyboardDidHide', hideCallback);
}
}
if (typeof (window as any) !== 'undefined') {
window.addEventListener('ionKeyboardDidShow', showCallback);
window.addEventListener('ionKeyboardDidHide', hideCallback);
}
return {
isOpen,
keyboardHeight,
unregister
}
}
/**
* Creates an returns a function that
* can be used to provide a lifecycle hook.
*/
const injectHook = (lifecycleType: LifecycleHooks, hook: Function, component: ComponentInternalInstance | null): Function | undefined => {
if (component) {
// Add to public instance so it is accessible to IonRouterOutlet
const target = component as any;
const hooks = target.proxy[lifecycleType] || (target.proxy[lifecycleType] = []);
/**
* Define property on public instances using `setup` syntax in Vue 3.x
*/
if (target.exposed) {
target.exposed[lifecycleType] = hooks;
}
const wrappedHook = (...args: unknown[]) => {
if (target.isUnmounted) {
return;
}
return args ? hook(...args) : hook();
};
hooks.push(wrappedHook);
return wrappedHook;
} else {
console.warn('[@ionic/vue]: Ionic Lifecycle Hooks can only be used during execution of setup().');
}
}
const createHook = <T extends Function = () => any>(lifecycle: LifecycleHooks) => {
return (hook: T, target: ComponentInternalInstance | null = getCurrentInstance()) => injectHook(lifecycle, hook, target);
}
export const onIonViewWillEnter = createHook(LifecycleHooks.WillEnter);
export const onIonViewDidEnter = createHook(LifecycleHooks.DidEnter);
export const onIonViewWillLeave = createHook(LifecycleHooks.WillLeave);
export const onIonViewDidLeave = createHook(LifecycleHooks.DidLeave);

File diff suppressed because it is too large Load Diff

View File

@ -2,41 +2,41 @@
"name": "test-app", "name": "test-app",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"description": "An Ionic project",
"scripts": { "scripts": {
"start": "npm run sync && vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit",
"test:e2e": "concurrently \"npm run start\" \"wait-on http-get://localhost:8080 && npm run cypress\" --kill-others --success first", "test:e2e": "concurrently \"npm run start\" \"wait-on http-get://localhost:8080 && npm run cypress\" --kill-others --success first",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"cypress": "node_modules/.bin/cypress run --headless --browser chrome", "cypress": "node_modules/.bin/cypress run --headless --browser chrome",
"start": "npm run sync && vue-cli-service serve",
"sync": "sh ./scripts/sync.sh" "sync": "sh ./scripts/sync.sh"
}, },
"dependencies": { "dependencies": {
"@ionic/vue": "5.6.3", "@ionic/vue": "5.6.3",
"@ionic/vue-router": "5.6.3", "@ionic/vue-router": "5.6.3",
"vue": "^3.1.5", "vue": "^3.2.22",
"vue-router": "^4.0.0-rc.4" "vue-router": "^4.0.12"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0", "@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "^4.5.12", "@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-e2e-cypress": "^5.0.0-alpha.7", "@vue/cli-plugin-e2e-cypress": "^5.0.0-alpha.7",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-router": "~4.5.15",
"@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-plugin-typescript": "~4.5.15",
"@vue/cli-plugin-unit-jest": "~4.5.0", "@vue/cli-plugin-unit-jest": "~4.5.15",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.15",
"@vue/compiler-sfc": "^3.0.0-0", "@vue/compiler-sfc": "^3.0.0-0",
"@vue/eslint-config-typescript": "^5.0.2", "@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^2.0.0-0", "@vue/test-utils": "^2.0.0-0",
"concurrently": "^6.0.0", "concurrently": "^6.0.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0-0", "eslint-plugin-vue": "^7.0.0-0",
"typescript": "~3.9.3", "typescript": "~4.1.5",
"vue-jest": "^5.0.0-0", "vue-jest": "^5.0.0-0",
"wait-on": "^5.3.0" "wait-on": "^5.3.0"
}, }
"description": "An Ionic project"
} }

View File

@ -17,6 +17,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/lifecycle', path: '/lifecycle',
component: () => import('@/views/Lifecycle.vue') component: () => import('@/views/Lifecycle.vue')
}, },
{
path: '/lifecycle-setup',
component: () => import('@/views/LifecycleSetup.vue')
},
{ {
path: '/overlays', path: '/overlays',
name: 'Overlays', name: 'Overlays',

View File

@ -47,6 +47,9 @@
<ion-item button router-link="/lifecycle" id="lifecycle"> <ion-item button router-link="/lifecycle" id="lifecycle">
<ion-label>Lifecycle</ion-label> <ion-label>Lifecycle</ion-label>
</ion-item> </ion-item>
<ion-item button router-link="/lifecycle-setup" id="lifecycle-setup">
<ion-label>Lifecycle (Setup)</ion-label>
</ion-item>
<ion-item button router-link="/delayed-redirect" id="delayed-redirect"> <ion-item button router-link="/delayed-redirect" id="delayed-redirect">
<ion-label>Delayed Redirect</ion-label> <ion-label>Delayed Redirect</ion-label>
</ion-item> </ion-item>

View File

@ -0,0 +1,56 @@
<template>
<ion-page data-pageid="lifecycle-setup">
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons>
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Lifecycle (Setup)</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Lifecycle (Setup)</ion-title>
</ion-toolbar>
</ion-header>
<div class="ion-padding">
onIonViewWillEnter: <div id="onWillEnter">{{ onWillEnter }}</div><br />
onIonViewDidEnter: <div id="onDidEnter">{{ onDidEnter }}</div><br />
onIonViewWillLeave: <div id="onWillLeave">{{ onWillLeave }}</div><br />
onIonViewDidLeave: <div id="onDidLeave">{{ onDidLeave }}</div><br />
<ion-button router-link="/navigation" id="lifecycle-navigation">Go to another page</ion-button>
</div>
</ion-content>
</ion-page>
</template>
<script lang="ts" setup>
import {
IonButton,
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
onIonViewWillEnter,
onIonViewDidEnter,
onIonViewWillLeave,
onIonViewDidLeave
} from '@ionic/vue';
import { ref } from 'vue';
const onWillEnter = ref(0);
const onDidEnter = ref(0);
const onWillLeave = ref(0);
const onDidLeave = ref(0);
onIonViewWillEnter(() => onWillEnter.value += 1);
onIonViewDidEnter(() => onDidEnter.value += 1);
onIonViewWillLeave(() => onWillLeave.value += 1);
onIonViewDidLeave(() => onDidLeave.value += 1);
</script>

View File

@ -31,6 +31,10 @@
<ion-item button router-link="/tabs" id="tabs-primary"> <ion-item button router-link="/tabs" id="tabs-primary">
<ion-label>Go to Primary Tabs</ion-label> <ion-label>Go to Primary Tabs</ion-label>
</ion-item> </ion-item>
<ion-item router-link="/tabs/tab1/child-one?key=value" id="child-one-query-string">
<ion-label>Go to Tab 1 Child 1 with Query Params</ion-label>
</ion-item>
</ion-content> </ion-content>
</ion-page> </ion-page>
</template> </template>

View File

@ -55,14 +55,62 @@ describe('Lifecycle', () => {
onIonViewDidLeave: 0 onIonViewDidLeave: 0
}); });
}); });
it('should fire lifecycle events when navigating to and from a page - setup', () => {
cy.visit('http://localhost:8080');
cy.get('#lifecycle-setup').click();
testLifecycle('lifecycle-setup', {
onIonViewWillEnter: 1,
onIonViewDidEnter: 1,
onIonViewWillLeave: 0,
onIonViewDidLeave: 0
});
cy.get('#lifecycle-navigation').click();
testLifecycle('lifecycle-setup', {
onIonViewWillEnter: 1,
onIonViewDidEnter: 1,
onIonViewWillLeave: 1,
onIonViewDidLeave: 1
});
cy.ionBackClick('navigation');
testLifecycle('lifecycle-setup', {
onIonViewWillEnter: 2,
onIonViewDidEnter: 2,
onIonViewWillLeave: 1,
onIonViewDidLeave: 1
});
});
it('should fire lifecycle events when landed on directly - setup', () => {
cy.visit('http://localhost:8080/lifecycle-setup');
testLifecycle('lifecycle-setup', {
onIonViewWillEnter: 1,
onIonViewDidEnter: 1,
onIonViewWillLeave: 0,
onIonViewDidLeave: 0
});
});
}) })
const testLifecycle = (selector, expected = {}) => { const testLifecycle = (selector, expected = {}) => {
cy.get(`[data-pageid=${selector}] #willEnter`).should('have.text', expected.ionViewWillEnter); if (expected.ionViewWillEnter) {
cy.get(`[data-pageid=${selector}] #didEnter`).should('have.text', expected.ionViewDidEnter); cy.get(`[data-pageid=${selector}] #willEnter`).should('have.text', expected.ionViewWillEnter);
cy.get(`[data-pageid=${selector}] #willLeave`).should('have.text', expected.ionViewWillLeave); }
cy.get(`[data-pageid=${selector}] #didLeave`).should('have.text', expected.ionViewDidLeave); if (expected.ionViewDidEnter) {
cy.get(`[data-pageid=${selector}] #didEnter`).should('have.text', expected.ionViewDidEnter);
}
if (expected.ionViewWillLeave) {
cy.get(`[data-pageid=${selector}] #willLeave`).should('have.text', expected.ionViewWillLeave);
}
if (expected.ionViewDidLeave) {
cy.get(`[data-pageid=${selector}] #didLeave`).should('have.text', expected.ionViewDidLeave);
}
cy.get(`[data-pageid=${selector}] #onWillEnter`).should('have.text', expected.onIonViewWillEnter); cy.get(`[data-pageid=${selector}] #onWillEnter`).should('have.text', expected.onIonViewWillEnter);
cy.get(`[data-pageid=${selector}] #onDidEnter`).should('have.text', expected.onIonViewDidEnter); cy.get(`[data-pageid=${selector}] #onDidEnter`).should('have.text', expected.onIonViewDidEnter);
cy.get(`[data-pageid=${selector}] #onWillLeave`).should('have.text', expected.onIonViewWillLeave); cy.get(`[data-pageid=${selector}] #onWillLeave`).should('have.text', expected.onIonViewWillLeave);

View File

@ -268,6 +268,27 @@ describe('Tabs', () => {
cy.get('ion-tab-button#tab-button-tab1').should('not.have.class', 'tab-selected'); cy.get('ion-tab-button#tab-button-tab1').should('not.have.class', 'tab-selected');
cy.get('ion-tab-button#tab-button-tab4').should('have.class', 'tab-selected'); cy.get('ion-tab-button#tab-button-tab4').should('have.class', 'tab-selected');
}); });
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23699
it('should preserve query string when switching tabs', () => {
cy.visit('http://localhost:8080/tabs/tab1');
cy.ionPageVisible('tab1');
cy.get('#child-one-query-string').click();
cy.ionPageVisible('tab1childone');
cy.ionPageHidden('tab1');
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageVisible('tab2');
cy.ionPageHidden('tab1childone');
cy.get('ion-tab-button#tab-button-tab1').click();
cy.ionPageVisible('tab1childone');
cy.ionPageHidden('tab2');
cy.url().should('include', '/tabs/tab1/child-one?key=value');
})
}) })
describe('Tabs - Swipe to Go Back', () => { describe('Tabs - Swipe to Go Back', () => {