Compare commits

..

44 Commits

Author SHA1 Message Date
Liam DeBeasi
7947a26ba4 5.9.4 2022-04-27 11:40:57 -04:00
Sean Perkins
ffb056d50e fix(core): inherit aria attributes on host elements (#25156) (#25169) 2022-04-27 10:59:58 -04:00
Liam DeBeasi
2d9724947d merge release-5.9.3
Release 5.9.3
2021-12-15 10:29:24 -05:00
Liam DeBeasi
02ef5ae179 5.9.3 2021-12-15 10:10:07 -05:00
Sean Perkins
4aab72b061 fix(vue): tabs no longer get unmounted when navigating back to a tabs context (#24337)
resolves #24332

Co-authored-by: Liam DeBeasi <liamdebeasi@icloud.com>
2021-12-15 09:55:29 -05:00
Liam DeBeasi
9c9e28ccc9 perf(content): remove global click listener to improve interaction performance (#24360)
resolves #24359
2021-12-15 09:55:22 -05:00
Liam DeBeasi
1c2875044a fix(vue): improve query params handling in tabs (#24355)
resolves #24353
2021-12-15 09:55:05 -05:00
Liam DeBeasi
d665ace5c4 chore(): create 5.9.x history
chore(): create 5.9.x history
2021-12-08 09:06:27 -05:00
Liam DeBeasi
672ab80807 merge release-5.9.2
5.9.2
2021-12-07 09:43:26 -05:00
Liam DeBeasi
da3b93b4a2 5.9.2 2021-12-07 09:21:00 -05:00
Sean Perkins
5e5054d369 fix(router): popping route now accounts for route params (#24315) 2021-12-06 17:25:40 -05:00
Anant Sharma
8f188eaae7 fix(react): properly check for custom elements to avoid errors in unit tests (#24156)
resolves #24149
2021-12-06 17:06:17 -05:00
Amanda Smith
f6a00ea954 fix(popover): handle scrolling in content so header can be sticky (#24294) 2021-12-06 10:45:37 -06:00
Djakson
b083ae4e58 docs(): fix typo in popover docs (#24318) 2021-12-06 10:26:03 -05:00
Sean Perkins
f7bd4c02c3 docs(back-button): update icon prop to include src and name approach (#24307) 2021-12-03 12:52:42 -05:00
Liam DeBeasi
047d3c7772 fix(vue): switching between tabs preserves query string (#24297)
resolves #23699
2021-12-02 10:04:26 -05:00
Sean Perkins
4b26feaa47 fix(react): present and dismiss hooks return promises (#24299) 2021-12-01 15:16:55 -05:00
Amanda Smith
e41b0e0cf2 fix(content): ensure fixed slot renders on top of content in iOS (#24300) 2021-12-01 13:47:39 -06:00
Will Martin
7f61b06895 build(commitizen): do not run on main, next, or release branches (#24258) 2021-11-25 09:17:23 -05:00
Sean Perkins
89e4bc56a1 fix(slides): update swiper instance after initialization (#24257) 2021-11-24 11:52:48 -05:00
Yuhongjie
fb96ab5a26 fix(vue): ionic lifecycle hooks now run when using vue 3.2 setup syntax (#24253)
resolves #23824

Co-authored-by: Liam DeBeasi <liamdebeasi@icloud.com>
2021-11-23 17:04:24 -05:00
Liam DeBeasi
6f01c3b73d chore(): update vue test app (#24262) 2021-11-23 16:35:54 -05:00
Ryan Waskiewicz
07d83ccd24 chore(ci): fix typo for test-vue-e2e step (#24256) 2021-11-23 14:28:58 -05:00
Sean Perkins
816096f897 fix(angular): strict type usage (#24221) 2021-11-22 15:59:12 -05:00
Liam DeBeasi
615dcc0461 merge release-5.9.1
5.9.1
2021-11-17 12:01:48 -05:00
Liam DeBeasi
351c30ce42 merge release-5.8.5
Release 5.8.5
2021-10-27 09:15:12 -04:00
Liam DeBeasi
3b9b9082b8 merge release-5.8.2
Release 5.8.2
2021-10-06 10:24:24 -04:00
Liam DeBeasi
0774cca2cd merge release-5.8.1
Release 5.8.1
2021-09-22 10:48:57 -04:00
Liam DeBeasi
6c366aaf87 merge release-5.8.0
Release 5.8.0
2021-09-15 11:37:12 -04:00
Liam DeBeasi
6876fd089f merge release-5.7.0
Release 5.7.0
2021-09-01 10:07:42 -04:00
Liam DeBeasi
22a8842ac2 merge release-5.6.14
Release 5.6.14
2021-08-18 09:33:30 -04:00
Liam DeBeasi
2d5faa75db merge release-5.6.13
Release 5.6.13
2021-08-04 10:25:37 -04:00
Liam DeBeasi
cab2a5103f merge release-5.6.12
Release 5.6.12
2021-07-21 09:37:04 -04:00
Liam DeBeasi
d36050918a merge release-5.6.11
Release 5.6.11
2021-07-01 12:03:38 -04:00
Liam DeBeasi
64f128be07 merge release-5.6.10
Release 5.6.10
2021-06-22 10:01:58 -04:00
Liam DeBeasi
87999e3c7a merge release-5.6.9
Release 5.6.9
2021-06-08 09:38:37 -04:00
Liam DeBeasi
bb4554211d merge release-5.6.8
Release 5.6.8
2021-05-27 16:01:36 -04:00
Liam DeBeasi
f71109b088 merge release-5.6.7
Release 5.6.7
2021-05-13 10:00:26 -04:00
Liam DeBeasi
44e18bd795 merge release-5.6.6
Release 5.6.6
2021-04-29 10:31:45 -04:00
Liam DeBeasi
f4d265eb60 merge release-5.6.5
Release 5.6.5
2021-04-22 13:37:44 -04:00
Liam DeBeasi
1e8dfb7d85 merge release-5.6.4
Release 5.6.4
2021-04-08 13:14:26 -04:00
Liam DeBeasi
9f023c92c4 merge release-5.6.3
Release 5.6.3
2021-03-23 11:21:16 -04:00
Liam DeBeasi
694d47b794 merge release-5.6.2
Release 5.6.2
2021-03-22 17:07:08 -04:00
Liam DeBeasi
b87c555a6e merge release-5.6.1
Release 5.6.1
2021-03-18 09:36:55 -04:00
57 changed files with 2681 additions and 2085 deletions

View File

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

View File

@@ -44,7 +44,7 @@ async function askNpmTag(version) {
type: 'list',
name: 'npmTag',
message: 'Select npm tag or specify a new tag',
choices: ['latest', 'next', 'v4-lts']
choices: ['latest', 'next', 'v4-lts', 'v5-lts']
.concat([
new inquirer.Separator(),
{

View File

@@ -1,3 +1,44 @@
## [5.9.4](https://github.com/ionic-team/ionic/compare/v5.9.3...v5.9.4) (2022-04-27)
### Bug Fixes
* **core:** inherit aria attributes on host elements ([#25156](https://github.com/ionic-team/ionic/issues/25156)) ([#25169](https://github.com/ionic-team/ionic/issues/25169)) ([ffb056d](https://github.com/ionic-team/ionic/commit/ffb056d50e126a1b89f5133de1e7516d0c29a61a))
## [5.9.3](https://github.com/ionic-team/ionic/compare/v5.9.2...v5.9.3) (2021-12-15)
### Bug Fixes
* **vue:** improve query params handling in tabs ([#24355](https://github.com/ionic-team/ionic/issues/24355)) ([1c28750](https://github.com/ionic-team/ionic/commit/1c2875044ad4d93fdca866017159a89f4dc8872d)), closes [#24353](https://github.com/ionic-team/ionic/issues/24353)
* **vue:** tabs no longer get unmounted when navigating back to a tabs context ([#24337](https://github.com/ionic-team/ionic/issues/24337)) ([4aab72b](https://github.com/ionic-team/ionic/commit/4aab72b06159729d2dcd18b2ef0b76f693e5a74e)), closes [#24332](https://github.com/ionic-team/ionic/issues/24332)
### Performance Improvements
* **content:** remove global click listener to improve interaction performance ([#24360](https://github.com/ionic-team/ionic/issues/24360)) ([9c9e28c](https://github.com/ionic-team/ionic/commit/9c9e28ccc9f899c403c757d911ac02d9099415af)), closes [#24359](https://github.com/ionic-team/ionic/issues/24359)
## [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)
## [5.9.1](https://github.com/ionic-team/ionic/compare/v5.9.0...v5.9.1) (2021-11-17)

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "5.9.1",
"version": "5.9.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "5.9.1",
"version": "5.9.4",
"license": "MIT",
"dependencies": {
"@ionic/core": "5.9.0",
"@ionic/core": "5.9.3",
"tslib": "^1.9.3"
},
"devDependencies": {
@@ -204,9 +204,9 @@
}
},
"node_modules/@ionic/core": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.0.tgz",
"integrity": "sha512-0mUnNPFzQK89/ZsuiKb9tQ1rRzILDSeNsp+4ASjf9z8FJuULeTqyDEHU3Pwnje7cLwl8lezGlvNpOXu7Xlz+/w==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.3.tgz",
"integrity": "sha512-WM50vVxAAw+MQYqWXKUK4usBgkr7iQ9UWSb6t59mG4ZSy/fPAb7ZIdAjxY0U5i1ykk6A7Ur4B9ZJMpC/a7nnug==",
"dependencies": {
"@stencil/core": "^2.4.0",
"ionicons": "^5.5.3",
@@ -5156,9 +5156,9 @@
}
},
"@ionic/core": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.0.tgz",
"integrity": "sha512-0mUnNPFzQK89/ZsuiKb9tQ1rRzILDSeNsp+4ASjf9z8FJuULeTqyDEHU3Pwnje7cLwl8lezGlvNpOXu7Xlz+/w==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.3.tgz",
"integrity": "sha512-WM50vVxAAw+MQYqWXKUK4usBgkr7iQ9UWSb6t59mG4ZSy/fPAb7ZIdAjxY0U5i1ykk6A7Ur4B9ZJMpC/a7nnug==",
"requires": {
"@stencil/core": "^2.4.0",
"ionicons": "^5.5.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "5.9.1",
"version": "5.9.4",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -42,7 +42,7 @@
"validate": "npm i && npm run lint && npm run test && npm run build"
},
"dependencies": {
"@ionic/core": "5.9.1",
"@ionic/core": "5.9.4",
"tslib": "^1.9.3"
},
"peerDependencies": {

View File

@@ -86,11 +86,11 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
*/
const formControl = ngControl.control;
if (formControl) {
const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine'];
const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine'] as const;
methodsToPatch.forEach(method => {
if (formControl[method]) {
const oldFn = formControl[method].bind(formControl);
formControl[method] = (...params) => {
formControl[method] = (...params: any[]) => {
oldFn(...params);
setIonicClasses(this.el);
};

View File

@@ -305,7 +305,7 @@ export class StackController {
const cleanupAsync = (activeRoute: RouteView, views: RouteView[], viewsSnapshot: RouteView[], location: Location) => {
if (typeof (requestAnimationFrame as any) === 'function') {
return new Promise<any>(resolve => {
return new Promise<void>(resolve => {
requestAnimationFrame(() => {
cleanup(activeRoute, views, viewsSnapshot, location);
resolve();

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/core",
"version": "5.9.1",
"version": "5.9.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "5.9.1",
"version": "5.9.4",
"license": "MIT",
"dependencies": {
"@stencil/core": "^2.4.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "5.9.1",
"version": "5.9.4",
"description": "Base components for Ionic",
"keywords": [
"ionic",

View File

@@ -176,7 +176,7 @@ export namespace Components {
*/
"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;
/**
@@ -3511,7 +3511,7 @@ declare namespace LocalJSX {
*/
"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;
/**

View File

@@ -2,9 +2,9 @@ import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, Color } from '../../interface';
import { ButtonInterface } from '../../utils/element-interface';
import { inheritAttributes } from '../../utils/helpers';
import type { AnimationBuilder, Color } from '../../interface';
import type { ButtonInterface } from '../../utils/element-interface';
import { inheritAriaAttributes } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -45,7 +45,8 @@ export class BackButton implements ComponentInterface, ButtonInterface {
@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;
@@ -66,7 +67,7 @@ export class BackButton implements ComponentInterface, ButtonInterface {
@Prop() routerAnimation: AnimationBuilder | undefined;
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.inheritedAttributes = inheritAriaAttributes(this.el);
if (this.defaultHref === undefined) {
this.defaultHref = config.get('backButtonDefaultHref');

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` |
| `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` |
| `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` |
| `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` |

View File

@@ -1,9 +1,9 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, Color, RouterDirection } from '../../interface';
import { AnchorInterface, ButtonInterface } from '../../utils/element-interface';
import { hasShadowDom, inheritAttributes } from '../../utils/helpers';
import type { AnimationBuilder, Color, RouterDirection } from '../../interface';
import type { AnchorInterface, ButtonInterface } from '../../utils/element-interface';
import { hasShadowDom, inheritAriaAttributes } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -135,7 +135,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
this.inToolbar = !!this.el.closest('ion-buttons');
this.inListHeader = !!this.el.closest('ion-list-header');
this.inItem = !!this.el.closest('ion-item') || !!this.el.closest('ion-item-divider');
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
private get hasIconOnly() {

View File

@@ -140,10 +140,37 @@
}
: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;
}
:host(.content-sizing) .inner-scroll {
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 {
@@ -199,4 +226,15 @@
::slotted([slot="fixed"]) {
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

@@ -119,14 +119,6 @@ export class Content implements ComponentInterface {
this.resize();
}
@Listen('click', { capture: true })
onClick(ev: Event) {
if (this.isScrolling) {
ev.preventDefault();
ev.stopPropagation();
}
}
private shouldForceOverscroll() {
const { forceOverscroll } = this;
const mode = getIonMode(this);
@@ -374,10 +366,17 @@ const getPageElement = (el: HTMLElement) => {
if (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) {
return page;
}
return getParentElement(el);
};

View File

@@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Host, Prop, h, writeTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { inheritAttributes } from '../../utils/helpers';
import { inheritAriaAttributes } from '../../utils/helpers';
import { hostContext } from '../../utils/theme';
import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
@@ -46,7 +46,7 @@ export class Header implements ComponentInterface {
@Prop() translucent = false;
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['role']);
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
async componentDidLoad() {

View File

@@ -1,8 +1,14 @@
import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { AutocompleteTypes, Color, InputChangeEventDetail, StyleEventDetail, TextFieldTypes } from '../../interface';
import { debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
import type {
AutocompleteTypes,
Color,
InputChangeEventDetail,
StyleEventDetail,
TextFieldTypes,
} from '../../interface';
import { debounceEvent, findItemLabel, inheritAriaAttributes, inheritAttributes } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme';
/**
@@ -234,7 +240,10 @@ export class Input implements ComponentInterface {
}
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label', 'tabindex', 'title']);
this.inheritedAttributes = {
...inheritAriaAttributes(this.el),
...inheritAttributes(this.el, ['tabindex', 'title']),
};
}
connectedCallback() {

View File

@@ -2,9 +2,9 @@ import { Component, ComponentInterface, Element, Host, Listen, Prop, State, h }
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { Color } from '../../interface';
import { ButtonInterface } from '../../utils/element-interface';
import { inheritAttributes } from '../../utils/helpers';
import type { Color } from '../../interface';
import type { ButtonInterface } from '../../utils/element-interface';
import { inheritAriaAttributes } from '../../utils/helpers';
import { menuController } from '../../utils/menu-controller';
import { createColorClasses, hostContext } from '../../utils/theme';
import { updateVisibility } from '../menu-toggle/menu-toggle-util';
@@ -58,7 +58,7 @@ export class MenuButton implements ComponentInterface, ButtonInterface {
@Prop() type: 'submit' | 'reset' | 'button' = 'button';
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
componentDidLoad() {

View File

@@ -5,7 +5,7 @@ import { getIonMode } from '../../global/ionic-global';
import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, Side } from '../../interface';
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
import { GESTURE_CONTROLLER } from '../../utils/gesture';
import { assert, clamp, inheritAttributes, isEndSide as isEnd } from '../../utils/helpers';
import { assert, clamp, inheritAriaAttributes, isEndSide as isEnd } from '../../utils/helpers';
import { menuController } from '../../utils/menu-controller';
const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
@@ -213,7 +213,7 @@ AFTER:
}
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
async componentDidLoad() {

View File

@@ -75,5 +75,9 @@
--ion-safe-area-right: 0px;
--ion-safe-area-bottom: 0px;
--ion-safe-area-left: 0px;
}
display: flex;
flex-direction: column;
overflow: hidden;
}

View File

@@ -4,7 +4,7 @@ A Popover is a dialog that appears on top of the current page. It can be used fo
## Presenting
To present a popover, call the `present` method on a popover instance. In order to position the popover relative to the element clicked, a click event needs to be passed into the options of the the `present` method. If the event is not passed, the popover will be positioned in the center of the viewport.
To present a popover, call the `present` method on a popover instance. In order to position the popover relative to the element clicked, a click event needs to be passed into the options of the `present` method. If the event is not passed, the popover will be positioned in the center of the viewport.
## Customization

View File

@@ -57,6 +57,14 @@ test('popover: custom class', async () => {
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
*/
@@ -81,6 +89,14 @@ test('popover:rtl: custom class', async () => {
await testPopover(DIRECTORY, '#custom-class-popover', 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 () => {
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="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="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-footer>
@@ -126,6 +128,56 @@
}
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>
</body>

View File

@@ -1,8 +1,16 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface';
import { clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers';
import type {
Color,
Gesture,
GestureDetail,
KnobName,
RangeChangeEventDetail,
RangeValue,
StyleEventDetail,
} from '../../interface';
import { clamp, debounceEvent, getAriaLabel, inheritAriaAttributes, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
/**
@@ -205,7 +213,7 @@ export class Range implements ComponentInterface {
*/
this.rangeId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-r-${rangeIds++}`;
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
componentDidLoad() {

View File

@@ -30,15 +30,24 @@ const CHAIN_3: RouteChain = [
describe('matchesIDs', () => {
it('should match simple set of ids', () => {
const chain: RouteChain = CHAIN_1;
expect(matchesIDs(['2'], chain)).toBe(1);
expect(matchesIDs(['2', '1'], chain)).toBe(2);
expect(matchesIDs(['2', '1', '3'], chain)).toBe(3);
expect(matchesIDs(['2', '1', '3', '4'], chain)).toBe(4);
expect(matchesIDs(['2', '1', '3', '4', '5'], chain)).toBe(4);
expect(matchesIDs([{ id: '2' }], chain)).toBe(1);
expect(matchesIDs([{ id: '2' }, { id: '1' }], chain)).toBe(2);
expect(matchesIDs([{ id: '2' }, { id: '1' }, { id: '3' }], chain)).toBe(3);
expect(matchesIDs([{ id: '2' }, { id: '1' }, { id: '3' }, { id: '4' }], 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(['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', () => {
@@ -227,7 +236,7 @@ describe('mergeParams', () => {
});
describe('RouterSegments', () => {
it ('should initialize with empty array', () => {
it('should initialize with empty array', () => {
const s = new RouterSegments([]);
expect(s.next()).toEqual('');
expect(s.next()).toEqual('');
@@ -236,7 +245,7 @@ describe('RouterSegments', () => {
expect(s.next()).toEqual('');
});
it ('should initialize with array', () => {
it('should initialize with array', () => {
const s = new RouterSegments(['', 'path', 'to', 'destination']);
expect(s.next()).toEqual('');
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));
};
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);
let i = 0;
for (; i < len; i++) {
if (ids[i].toLowerCase() !== chain[i].id) {
let score = 0;
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;
}
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 => {
const segments = new RouterSegments(inputPath);
@@ -90,16 +134,16 @@ export const matchesPath = (inputPath: string[], chain: RouteChain): RouteChain
// Merges the route parameter objects.
// 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;
};
export const routerIDsToChain = (ids: RouteID[], chains: RouteChain[]): RouteChain | null => {
let match: RouteChain | null = null;
let maxMatches = 0;
const plainIDs = ids.map(i => i.id);
for (const chain of chains) {
const score = matchesIDs(plainIDs, chain);
const score = matchesIDs(ids, chain);
if (score > maxMatches) {
match = chain;
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 { componentOnReady } from '../../utils/helpers'
@@ -24,8 +24,6 @@ export class Slides implements ComponentInterface {
private mutationO?: MutationObserver;
private readySwiper!: (swiper: SwiperInterface) => void;
private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; });
private syncSwiper?: SwiperInterface;
private didInit = false;
@Element() el!: HTMLIonSlidesElement;
@@ -141,8 +139,7 @@ export class Slides implements ComponentInterface {
}
connectedCallback() {
// tslint:disable-next-line: strict-type-predicates
if (typeof MutationObserver !== 'undefined') {
if (Build.isBrowser) {
const mut = this.mutationO = new MutationObserver(() => {
if (this.swiperReady) {
this.update();
@@ -154,10 +151,7 @@ export class Slides implements ComponentInterface {
});
componentOnReady(this.el, () => {
if (!this.didInit) {
this.didInit = true;
this.initSwiper();
}
})
}
}
@@ -167,23 +161,6 @@ export class Slides implements ComponentInterface {
this.mutationO.disconnect();
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);
const swiper = new Swiper(this.el, finalOptions);
this.swiperReady = true;
this.syncSwiper = swiper;
this.readySwiper(swiper);
}
@@ -483,6 +459,8 @@ export class Slides implements ComponentInterface {
init: () => {
setTimeout(() => {
this.ionSlidesDidLoad.emit();
// Forces the swiper instance to update after initializing.
this.update();
}, 20);
},
slideChangeTransitionStart: this.ionSlideWillChange.emit,

View File

@@ -1,8 +1,8 @@
import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, readTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface';
import { debounceEvent, findItemLabel, inheritAttributes, raf } from '../../utils/helpers';
import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface';
import { debounceEvent, findItemLabel, inheritAriaAttributes, inheritAttributes, raf } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme';
/**
@@ -214,7 +214,10 @@ export class Textarea implements ComponentInterface {
}
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['title']);
this.inheritedAttributes = {
...inheritAriaAttributes(this.el),
...inheritAttributes(this.el, ['title']),
};
}
componentDidLoad() {

View File

@@ -51,6 +51,74 @@ export const inheritAttributes = (el: HTMLElement, attributes: string[] = []) =>
return attributeObject;
}
/**
* List of available ARIA attributes + `role`.
* Removed deprecated attributes.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
*/
const ariaAttributes = [
'role',
'aria-activedescendant',
'aria-atomic',
'aria-autocomplete',
'aria-braillelabel',
'aria-brailleroledescription',
'aria-busy',
'aria-checked',
'aria-colcount',
'aria-colindex',
'aria-colindextext',
'aria-colspan',
'aria-controls',
'aria-current',
'aria-describedby',
'aria-description',
'aria-details',
'aria-disabled',
'aria-errormessage',
'aria-expanded',
'aria-flowto',
'aria-haspopup',
'aria-hidden',
'aria-invalid',
'aria-keyshortcuts',
'aria-label',
'aria-labelledby',
'aria-level',
'aria-live',
'aria-multiline',
'aria-multiselectable',
'aria-orientation',
'aria-owns',
'aria-placeholder',
'aria-posinset',
'aria-pressed',
'aria-readonly',
'aria-relevant',
'aria-required',
'aria-roledescription',
'aria-rowcount',
'aria-rowindex',
'aria-rowindextext',
'aria-rowspan',
'aria-selected',
'aria-setsize',
'aria-sort',
'aria-valuemax',
'aria-valuemin',
'aria-valuenow',
'aria-valuetext',
];
/**
* Returns an array of aria attributes that should be copied from
* the shadow host element to a target within the light DOM.
* @param el The element that the attributes should be copied from.
*/
export const inheritAriaAttributes = (el: HTMLElement) => {
return inheritAttributes(el, ariaAttributes);
};
export const addEventListener = (el: any, eventName: string, callback: any, opts?: any) => {
if (typeof (window as any) !== 'undefined') {
const win = window as any;
@@ -164,8 +232,8 @@ export const getAriaLabel = (componentEl: HTMLElement, inputId: string): { label
labelText = label.textContent;
label.setAttribute('aria-hidden', 'true');
// if there is no label, check to see if the user has provided
// one by setting an id on the component and using the label element
// if there is no label, check to see if the user has provided
// one by setting an id on the component and using the label element
} else if (componentId.trim() !== '') {
label = document.querySelector(`label[for="${componentId}"]`);

View File

@@ -1,4 +1,4 @@
import { inheritAttributes } from '../helpers';
import { inheritAttributes, inheritAriaAttributes } from '../helpers';
describe('inheritAttributes()', () => {
it('should create an attribute inheritance object', () => {
@@ -37,3 +37,29 @@ describe('inheritAttributes()', () => {
});
});
});
describe('inheritAriaAttributes()', () => {
it('should inherit ARIA attributes defined on the HTML element', () => {
const el = document.createElement('div');
el.setAttribute('aria-label', 'myLabel');
el.setAttribute('aria-describedby', 'myDescription');
const attributeObject = inheritAriaAttributes(el);
expect(attributeObject).toEqual({
'aria-label': 'myLabel',
'aria-describedby': 'myDescription',
});
});
it('should inherit the role attribute defined on the HTML element', () => {
const el = document.createElement('div');
el.setAttribute('role', 'button');
const attributeObject = inheritAriaAttributes(el);
expect(attributeObject).toEqual({
role: 'button',
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "5.9.1",
"version": "5.9.4",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -6,7 +6,8 @@
"release.dev": "node .scripts/release-dev.js",
"release.prepare": "node .scripts/prepare.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": {
"@commitlint/cli": "^13.1.0",
@@ -18,6 +19,7 @@
"cz-conventional-changelog": "^3.3.0",
"execa": "^0.10.0",
"fs-extra": "^7.0.0",
"git-branch-is": "^4.0.0",
"husky": "^4.3.8",
"inquirer": "^6.0.0",
"listr": "^0.14.0",
@@ -34,8 +36,8 @@
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true"
"commit-msg": "npm run commitizenBranches --silent && commitlint -E HUSKY_GIT_PARAMS || true",
"prepare-commit-msg": "npm run commitizenBranches --silent && exec < /dev/tty && git cz --hook || true"
}
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/angular-server",
"version": "5.9.1",
"version": "5.9.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "5.9.1",
"version": "5.9.4",
"license": "MIT",
"devDependencies": {
"@angular/animations": "8.2.13",
@@ -16,7 +16,7 @@
"@angular/core": "8.2.13",
"@angular/platform-browser": "8.2.13",
"@angular/platform-server": "8.2.13",
"@ionic/core": "5.9.0",
"@ionic/core": "5.9.3",
"ng-packagr": "5.7.1",
"tslint": "^5.12.1",
"tslint-ionic-rules": "0.0.21",
@@ -137,9 +137,9 @@
}
},
"node_modules/@ionic/core": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.0.tgz",
"integrity": "sha512-0mUnNPFzQK89/ZsuiKb9tQ1rRzILDSeNsp+4ASjf9z8FJuULeTqyDEHU3Pwnje7cLwl8lezGlvNpOXu7Xlz+/w==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.3.tgz",
"integrity": "sha512-WM50vVxAAw+MQYqWXKUK4usBgkr7iQ9UWSb6t59mG4ZSy/fPAb7ZIdAjxY0U5i1ykk6A7Ur4B9ZJMpC/a7nnug==",
"dev": true,
"dependencies": {
"@stencil/core": "^2.4.0",
@@ -5424,9 +5424,9 @@
}
},
"@ionic/core": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.0.tgz",
"integrity": "sha512-0mUnNPFzQK89/ZsuiKb9tQ1rRzILDSeNsp+4ASjf9z8FJuULeTqyDEHU3Pwnje7cLwl8lezGlvNpOXu7Xlz+/w==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.3.tgz",
"integrity": "sha512-WM50vVxAAw+MQYqWXKUK4usBgkr7iQ9UWSb6t59mG4ZSy/fPAb7ZIdAjxY0U5i1ykk6A7Ur4B9ZJMpC/a7nnug==",
"dev": true,
"requires": {
"@stencil/core": "^2.4.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "5.9.1",
"version": "5.9.4",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -49,7 +49,7 @@
"@angular/core": "8.2.13",
"@angular/platform-browser": "8.2.13",
"@angular/platform-server": "8.2.13",
"@ionic/core": "5.9.1",
"@ionic/core": "5.9.4",
"ng-packagr": "5.7.1",
"tslint": "^5.12.1",
"tslint-ionic-rules": "0.0.21",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react-router",
"version": "5.9.1",
"version": "5.9.4",
"description": "React Router wrapper for @ionic/react",
"keywords": [
"ionic",
@@ -40,14 +40,14 @@
"tslib": "*"
},
"peerDependencies": {
"@ionic/react": "5.9.1",
"@ionic/react": "5.9.4",
"react": ">=16.8.6",
"react-dom": ">=16.8.6",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1"
},
"devDependencies": {
"@ionic/react": "5.9.1",
"@ionic/react": "5.9.4",
"@rollup/plugin-node-resolve": "^8.1.0",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react",
"version": "5.9.1",
"version": "5.9.4",
"description": "React specific wrapper for @ionic/core",
"keywords": [
"ionic",
@@ -40,7 +40,7 @@
"css/"
],
"dependencies": {
"@ionic/core": "5.9.1",
"@ionic/core": "5.9.4",
"ionicons": "^5.1.2",
"tslib": "*"
},

View File

@@ -16,9 +16,9 @@ class IonTabsElement extends HTMLElementSSR {
}
if (typeof (window as any) !== 'undefined' && window.customElements) {
const element = customElements.get('ion-tabs');
const element = window.customElements.get('ion-tabs');
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
) => {
if (Array.isArray(buttonsOrOptions)) {
controller.present({
return controller.present({
buttons: buttonsOrOptions,
header,
});
} else {
controller.present(buttonsOrOptions);
return controller.present(buttonsOrOptions);
}
},
[controller.present]
@@ -41,15 +41,15 @@ export type UseIonActionSheetResult = [
* @param buttons An array of buttons 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
* @param options The options to pass to the IonActionSheet
*/
(options: ActionSheetOptions & HookOverlayOptions): void;
(options: ActionSheetOptions & HookOverlayOptions): Promise<void>;
},
/**
* Dismisses the action sheet
*/
() => void
() => Promise<void>
];

View File

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

View File

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

View File

@@ -19,12 +19,12 @@ export function useIonPicker(): UseIonPickerResult {
buttons?: PickerButton[]
) => {
if (Array.isArray(columnsOrOptions)) {
controller.present({
return controller.present({
columns: columnsOrOptions,
buttons: buttons ?? [{ text: 'Ok' }],
});
} else {
controller.present(columnsOrOptions);
return controller.present(columnsOrOptions);
}
}, [controller.present]);
@@ -38,15 +38,15 @@ export type UseIonPickerResult = [
* @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.
*/
(columns: PickerColumn[], buttons?: PickerButton[]): void;
(columns: PickerColumn[], buttons?: PickerButton[]): Promise<void>;
/**
* Presents the picker
* @param options The options to pass to the IonPicker
*/
(options: PickerOptions & HookOverlayOptions): void;
(options: PickerOptions & HookOverlayOptions): Promise<void>;
},
/**
* 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) => {
if (typeof messageOrOptions === 'string') {
controller.present({
return controller.present({
message: messageOrOptions,
duration
});
} else {
controller.present(messageOrOptions);
return controller.present(messageOrOptions);
}
}, [controller.present]);
@@ -38,15 +38,15 @@ export type UseIonToastResult = [
* @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.
*/
(message: string, duration?: number): void;
(message: string, duration?: number): Promise<void>;
/**
* Presents the Toast
* @param options The options to pass to the IonToast.
*/
(options: ToastOptions & HookOverlayOptions): void;
(options: ToastOptions & HookOverlayOptions): Promise<void>;
},
/**
* Dismisses the toast
*/
() => void
() => Promise<void>
];

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/vue-router",
"version": "5.9.1",
"version": "5.9.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue-router",
"version": "5.9.1",
"version": "5.9.4",
"license": "MIT",
"devDependencies": {
"@ionic/vue": "5.4.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue-router",
"version": "5.9.1",
"version": "5.9.4",
"description": "Vue Router integration for @ionic/vue",
"scripts": {
"test.spec": "jest",

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/vue",
"version": "5.9.1",
"version": "5.9.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue",
"version": "5.9.1",
"version": "5.9.4",
"license": "MIT",
"dependencies": {
"@ionic/core": "5.9.0",
"@ionic/core": "5.9.3",
"ionicons": "^5.1.2"
},
"devDependencies": {
@@ -53,9 +53,9 @@
}
},
"node_modules/@ionic/core": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.0.tgz",
"integrity": "sha512-0mUnNPFzQK89/ZsuiKb9tQ1rRzILDSeNsp+4ASjf9z8FJuULeTqyDEHU3Pwnje7cLwl8lezGlvNpOXu7Xlz+/w==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.3.tgz",
"integrity": "sha512-WM50vVxAAw+MQYqWXKUK4usBgkr7iQ9UWSb6t59mG4ZSy/fPAb7ZIdAjxY0U5i1ykk6A7Ur4B9ZJMpC/a7nnug==",
"dependencies": {
"@stencil/core": "^2.4.0",
"ionicons": "^5.5.3",
@@ -633,9 +633,9 @@
}
},
"@ionic/core": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.0.tgz",
"integrity": "sha512-0mUnNPFzQK89/ZsuiKb9tQ1rRzILDSeNsp+4ASjf9z8FJuULeTqyDEHU3Pwnje7cLwl8lezGlvNpOXu7Xlz+/w==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.3.tgz",
"integrity": "sha512-WM50vVxAAw+MQYqWXKUK4usBgkr7iQ9UWSb6t59mG4ZSy/fPAb7ZIdAjxY0U5i1ykk6A7Ur4B9ZJMpC/a7nnug==",
"requires": {
"@stencil/core": "^2.4.0",
"ionicons": "^5.5.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue",
"version": "5.9.1",
"version": "5.9.4",
"description": "Vue specific wrapper for @ionic/core",
"scripts": {
"lint": "echo add linter",
@@ -59,7 +59,7 @@
"vue-router": "^4.0.0-rc.4"
},
"dependencies": {
"@ionic/core": "5.9.1",
"@ionic/core": "5.9.4",
"ionicons": "^5.1.2"
},
"vetur": {

View File

@@ -14,6 +14,10 @@ import { AnimationBuilder, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_W
import { matchedRouteKey, routeLocationKey, useRoute } from 'vue-router';
import { fireLifecycle, generateId, getConfig } from '../utils';
const isViewVisible = (enteringEl: HTMLElement) => {
return !enteringEl.classList.contains('ion-page-hidden') && !enteringEl.classList.contains('ion-page-invisible');
}
let viewDepthKey: InjectionKey<0> = Symbol(0);
export const IonRouterOutlet = defineComponent({
name: 'IonRouterOutlet',
@@ -230,13 +234,35 @@ export const IonRouterOutlet = defineComponent({
See https://ionicframework.com/docs/vue/navigation#ionpage for more information.`);
}
if (enteringViewItem === leavingViewItem) return;
if (!leavingViewItem && prevRouteLastPathname) {
leavingViewItem = viewStacks.findViewItemByPathname(prevRouteLastPathname, id, usingDeprecatedRouteSetup);
}
/**
* If the entering view is already
* visible, then no transition is needed.
* This is most common when navigating
* from a tabs page to a non-tabs page
* and then back to the tabs page.
* Even when the tabs context navigated away,
* the inner tabs page was still active.
* This also avoids an issue where
* the previous tabs page is incorrectly
* unmounted since it would automatically
* unmount the previous view.
*
* This should also only apply to entering and
* leaving items in the same router outlet (i.e.
* Tab1 and Tab2), otherwise this will
* return early for swipe to go back when
* going from a non-tabs page to a tabs page.
*/
if (isViewVisible(enteringEl) && leavingViewItem !== undefined && !isViewVisible(leavingViewItem.ionPageElement)) {
return;
}
fireLifecycle(enteringViewItem.vueComponent, enteringViewItem.vueComponentRef, LIFECYCLE_WILL_ENTER);
if (leavingViewItem && enteringViewItem !== leavingViewItem) {

View File

@@ -113,9 +113,17 @@ export const IonTabBar = defineComponent({
* land on /tabs/tab1/child instead of /tabs/tab1.
*/
if (activeTab !== prevActiveTab || (prevHref !== currentRoute.pathname)) {
/**
* By default the search is `undefined` in Ionic Vue,
* but Vue Router can set the search to the empty string.
* We check for truthy here because empty string is falsy
* and currentRoute.search cannot ever be a boolean.
*/
const search = (currentRoute.search) ? `?${currentRoute.search}` : '';
tabs[activeTab] = {
...tabs[activeTab],
currentHref: currentRoute.pathname + (currentRoute.search || '')
currentHref: currentRoute.pathname + search
}
}

View File

@@ -80,6 +80,12 @@ const injectHook = (lifecycleType: LifecycleHooks, hook: Function, component: Co
// 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;

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,41 +2,41 @@
"name": "test-app",
"version": "0.0.1",
"private": true,
"description": "An Ionic project",
"scripts": {
"start": "npm run sync && vue-cli-service serve",
"build": "vue-cli-service build",
"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",
"lint": "vue-cli-service lint",
"cypress": "node_modules/.bin/cypress run --headless --browser chrome",
"start": "npm run sync && vue-cli-service serve",
"sync": "sh ./scripts/sync.sh"
},
"dependencies": {
"@ionic/vue": "5.6.3",
"@ionic/vue-router": "5.6.3",
"vue": "^3.0.0-0",
"vue-router": "^4.0.0-rc.4"
"vue": "^3.2.22",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^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-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-router": "~4.5.15",
"@vue/cli-plugin-typescript": "~4.5.15",
"@vue/cli-plugin-unit-jest": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/compiler-sfc": "^3.0.0-0",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^2.0.0-0",
"concurrently": "^6.0.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0-0",
"typescript": "~3.9.3",
"typescript": "~4.1.5",
"vue-jest": "^5.0.0-0",
"wait-on": "^5.3.0"
},
"description": "An Ionic project"
}
}

View File

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

View File

@@ -47,6 +47,9 @@
<ion-item button router-link="/lifecycle" id="lifecycle">
<ion-label>Lifecycle</ion-label>
</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-label>Delayed Redirect</ion-label>
</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-label>Go to Primary Tabs</ion-label>
</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-page>
</template>

View File

@@ -55,14 +55,62 @@ describe('Lifecycle', () => {
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 = {}) => {
cy.get(`[data-pageid=${selector}] #willEnter`).should('have.text', expected.ionViewWillEnter);
cy.get(`[data-pageid=${selector}] #didEnter`).should('have.text', expected.ionViewDidEnter);
cy.get(`[data-pageid=${selector}] #willLeave`).should('have.text', expected.ionViewWillLeave);
cy.get(`[data-pageid=${selector}] #didLeave`).should('have.text', expected.ionViewDidLeave);
if (expected.ionViewWillEnter) {
cy.get(`[data-pageid=${selector}] #willEnter`).should('have.text', expected.ionViewWillEnter);
}
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}] #onDidEnter`).should('have.text', expected.onIonViewDidEnter);
cy.get(`[data-pageid=${selector}] #onWillLeave`).should('have.text', expected.onIonViewWillLeave);

View File

@@ -285,6 +285,77 @@ describe('Tabs', () => {
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');
});
// 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');
});
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/24353
it('should handle clicking tab multiple times without query string', () => {
cy.visit('http://localhost:8080/tabs/tab1');
cy.ionPageVisible('tab1');
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageVisible('tab2');
cy.ionPageHidden('tab1');
cy.get('ion-tab-button#tab-button-tab1').click();
cy.ionPageVisible('tab1');
cy.ionPageHidden('tab2');
cy.get('ion-tab-button#tab-button-tab1').click();
cy.ionPageVisible('tab1');
cy.ionPageHidden('tab2');
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageVisible('tab2');
cy.ionPageHidden('tab1');
});
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/24332
it('should not unmount tab 1 when leaving tabs context', () => {
cy.visit('http://localhost:8080/tabs');
cy.ionPageVisible('tab1');
// Dynamically add tab 4 because tab 3 redirects to tab 1
cy.get('#add-tab').click();
cy.get('ion-tab-button#tab-button-tab4').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab4');
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageHidden('tab4');
cy.ionPageVisible('tab2');
cy.get('[data-pageid="tab2"] #routing').click();
cy.ionPageVisible('routing');
cy.ionPageHidden('tabs');
cy.ionBackClick('routing');
cy.ionPageDoesNotExist('routing');
cy.ionPageVisible('tabs');
cy.ionPageVisible('tab2');
cy.ionPageHidden('tab1');
});
})
describe('Tabs - Swipe to Go Back', () => {