mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
3 Commits
ionic-modu
...
ROU-10896
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac17dd6a44 | ||
|
|
7fc02b4251 | ||
|
|
e97ccd8370 |
@@ -46,7 +46,17 @@ export class Tabs implements NavOutlet {
|
||||
|
||||
async componentWillLoad() {
|
||||
if (!this.useRouter) {
|
||||
this.useRouter = !!document.querySelector('ion-router') && !this.el.closest('[no-router]');
|
||||
/**
|
||||
* JavaScript and StencilJS use `ion-router`, while
|
||||
* the other frameworks use `ion-router-outlet`.
|
||||
*
|
||||
* If either component is present then tabs will not use
|
||||
* a basic tab-based navigation. It will use the history
|
||||
* stack or URL updates associated with the router.
|
||||
*/
|
||||
this.useRouter =
|
||||
(!!this.el.querySelector('ion-router-outlet') || !!document.querySelector('ion-router')) &&
|
||||
!this.el.closest('[no-router]');
|
||||
}
|
||||
if (!this.useRouter) {
|
||||
const tabs = this.tabs;
|
||||
|
||||
@@ -26,7 +26,6 @@ const getAngularOutputTargets = () => {
|
||||
|
||||
// tabs
|
||||
'ion-tabs',
|
||||
'ion-tab',
|
||||
|
||||
// auxiliar
|
||||
'ion-picker-legacy-column',
|
||||
@@ -173,7 +172,6 @@ export const config: Config = {
|
||||
'ion-back-button',
|
||||
'ion-tab-button',
|
||||
'ion-tabs',
|
||||
'ion-tab',
|
||||
'ion-tab-bar',
|
||||
|
||||
// Overlays
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
HostListener,
|
||||
Output,
|
||||
ViewChild,
|
||||
AfterViewInit,
|
||||
QueryList,
|
||||
} from '@angular/core';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
@@ -17,14 +19,15 @@ import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
|
||||
selector: 'ion-tabs',
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
export abstract class IonTabs implements AfterViewInit, AfterContentInit, AfterContentChecked {
|
||||
/**
|
||||
* Note: These must be redeclared on each child class since it needs
|
||||
* access to generated components such as IonRouterOutlet and IonTabBar.
|
||||
*/
|
||||
abstract outlet: any;
|
||||
abstract tabBar: any;
|
||||
abstract tabBars: any;
|
||||
abstract tabBars: QueryList<any>;
|
||||
abstract tabs: QueryList<any>;
|
||||
|
||||
@ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef<HTMLDivElement>;
|
||||
|
||||
@@ -39,8 +42,29 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
|
||||
private tabBarSlot = 'bottom';
|
||||
|
||||
private hasTab = false;
|
||||
private selectedTab?: { tab: string };
|
||||
private leavingTab?: any;
|
||||
|
||||
constructor(private navCtrl: NavController) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
/**
|
||||
* Developers must pass at least one ion-tab
|
||||
* inside of ion-tabs if they want to use a
|
||||
* basic tab-based navigation without the
|
||||
* history stack or URL updates associated
|
||||
* with the router.
|
||||
*/
|
||||
const firstTab = this.tabs.length > 0 ? this.tabs.first : undefined;
|
||||
|
||||
if (firstTab) {
|
||||
this.hasTab = true;
|
||||
this.setActiveTab(firstTab.tab);
|
||||
this.tabSwitch();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
@@ -96,6 +120,19 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
|
||||
const isTabString = typeof tabOrEvent === 'string';
|
||||
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;
|
||||
|
||||
/**
|
||||
* If the tabs are not using the router, then
|
||||
* the tab switch logic is handled by the tabs
|
||||
* component itself.
|
||||
*/
|
||||
if (this.hasTab) {
|
||||
this.setActiveTab(tab);
|
||||
this.tabSwitch();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadySelected = this.outlet.getActiveStackId() === tab;
|
||||
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
|
||||
|
||||
@@ -142,7 +179,46 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
}
|
||||
}
|
||||
|
||||
private setActiveTab(tab: string): void {
|
||||
const tabs = this.tabs;
|
||||
const selectedTab = tabs.find((t: any) => t.tab === tab);
|
||||
|
||||
if (!selectedTab) {
|
||||
console.error(`[Ionic Error]: Tab with id: "${tab}" does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.leavingTab = this.selectedTab;
|
||||
this.selectedTab = selectedTab;
|
||||
|
||||
this.ionTabsWillChange.emit({ tab });
|
||||
|
||||
selectedTab.el.active = true;
|
||||
}
|
||||
|
||||
private tabSwitch(): void {
|
||||
const { selectedTab, leavingTab } = this;
|
||||
|
||||
if (this.tabBar && selectedTab) {
|
||||
this.tabBar.selectedTab = selectedTab.tab;
|
||||
}
|
||||
|
||||
if (leavingTab?.tab !== selectedTab?.tab) {
|
||||
if (leavingTab?.el) {
|
||||
leavingTab.el.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab) {
|
||||
this.ionTabsDidChange.emit({ tab: selectedTab.tab });
|
||||
}
|
||||
}
|
||||
|
||||
getSelected(): string | undefined {
|
||||
if (this.hasTab) {
|
||||
return this.selectedTab?.tab;
|
||||
}
|
||||
|
||||
return this.outlet.getActiveStackId();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const appInitialize = (config: Config, doc: Document, zone: NgZone) => {
|
||||
|
||||
return applyPolyfills().then(() => {
|
||||
return defineCustomElements(win, {
|
||||
exclude: ['ion-tabs', 'ion-tab'],
|
||||
exclude: ['ion-tabs'],
|
||||
syncQueue: true,
|
||||
raf,
|
||||
jmp: (h: any) => zone.runOutsideAngular(h),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
|
||||
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
|
||||
|
||||
import { IonTabBar } from '../proxies';
|
||||
import { IonTabBar, IonTab } from '../proxies';
|
||||
|
||||
import { IonRouterOutlet } from './ion-router-outlet';
|
||||
|
||||
@@ -11,11 +11,13 @@ import { IonRouterOutlet } from './ion-router-outlet';
|
||||
<ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner" #tabsInner>
|
||||
<ion-router-outlet
|
||||
*ngIf="tabs.length === 0"
|
||||
#outlet
|
||||
tabs="true"
|
||||
(stackWillChange)="onStackWillChange($event)"
|
||||
(stackDidChange)="onStackDidChange($event)"
|
||||
></ion-router-outlet>
|
||||
<ng-content *ngIf="tabs.length > 0" select="ion-tab"></ng-content>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
@@ -52,4 +54,5 @@ export class IonTabs extends IonTabsBase {
|
||||
|
||||
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
|
||||
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
|
||||
@ContentChildren(IonTab) tabs: QueryList<IonTab>;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export const DIRECTIVES = [
|
||||
d.IonSkeletonText,
|
||||
d.IonSpinner,
|
||||
d.IonSplitPane,
|
||||
d.IonTab,
|
||||
d.IonTabBar,
|
||||
d.IonTabButton,
|
||||
d.IonText,
|
||||
|
||||
@@ -2154,6 +2154,29 @@ export declare interface IonSplitPane extends Components.IonSplitPane {
|
||||
}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['component', 'mode', 'tab', 'theme'],
|
||||
methods: ['setActive']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-tab',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['component', 'mode', 'tab', 'theme'],
|
||||
})
|
||||
export class IonTab {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export declare interface IonTab extends Components.IonTab {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['color', 'mode', 'selectedTab', 'theme', 'translucent']
|
||||
})
|
||||
|
||||
@@ -69,6 +69,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon
|
||||
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
|
||||
import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js';
|
||||
import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js';
|
||||
import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js';
|
||||
import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js';
|
||||
import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js';
|
||||
import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js';
|
||||
@@ -1944,6 +1945,31 @@ export declare interface IonSplitPane extends Components.IonSplitPane {
|
||||
}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonTab,
|
||||
inputs: ['component', 'mode', 'tab', 'theme'],
|
||||
methods: ['setActive']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-tab',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['component', 'mode', 'tab', 'theme'],
|
||||
standalone: true
|
||||
})
|
||||
export class IonTab {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export declare interface IonTab extends Components.IonTab {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonTabBar,
|
||||
inputs: ['color', 'mode', 'selectedTab', 'theme', 'translucent']
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
|
||||
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
|
||||
|
||||
import { IonTabBar } from '../directives/proxies';
|
||||
import { IonTabBar, IonTab } from '../directives/proxies';
|
||||
|
||||
import { IonRouterOutlet } from './router-outlet';
|
||||
|
||||
@@ -11,11 +12,13 @@ import { IonRouterOutlet } from './router-outlet';
|
||||
<ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner" #tabsInner>
|
||||
<ion-router-outlet
|
||||
*ngIf="tabs.length === 0"
|
||||
#outlet
|
||||
tabs="true"
|
||||
(stackWillChange)="onStackWillChange($event)"
|
||||
(stackDidChange)="onStackDidChange($event)"
|
||||
></ion-router-outlet>
|
||||
<ng-content *ngIf="tabs.length > 0" select="ion-tab"></ng-content>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
@@ -46,7 +49,7 @@ import { IonRouterOutlet } from './router-outlet';
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [IonRouterOutlet],
|
||||
imports: [IonRouterOutlet, NgIf],
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/component-class-suffix
|
||||
export class IonTabs extends IonTabsBase {
|
||||
@@ -54,4 +57,5 @@ export class IonTabs extends IonTabsBase {
|
||||
|
||||
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
|
||||
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
|
||||
@ContentChildren(IonTab) tabs: QueryList<IonTab>;
|
||||
}
|
||||
|
||||
@@ -1,436 +1,462 @@
|
||||
describe('Tabs', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs');
|
||||
})
|
||||
|
||||
describe('entry url - /tabs', () => {
|
||||
it('should redirect and load tab-account', () => {
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
|
||||
testState(1, 'account');
|
||||
});
|
||||
|
||||
it('should navigate between tabs and ionChange events should be dispatched', () => {
|
||||
let tab = testTabTitle('Tab 1 - Page 1');
|
||||
tab.find('.segment-changed').should('have.text', 'false');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
tab = testTabTitle('Tab 2 - Page 1');
|
||||
tab.find('.segment-changed').should('have.text', 'false');
|
||||
});
|
||||
|
||||
describe('when navigating between tabs', () => {
|
||||
|
||||
it('should emit ionTabsWillChange before setting the selected tab', () => {
|
||||
cy.get('#ionTabsWillChangeCounter').should('have.text', '1');
|
||||
cy.get('#ionTabsWillChangeEvent').should('have.text', 'account');
|
||||
cy.get('#ionTabsWillChangeSelectedTab').should('have.text', '');
|
||||
|
||||
cy.get('#ionTabsDidChangeCounter').should('have.text', '1');
|
||||
cy.get('#ionTabsDidChangeEvent').should('have.text', 'account');
|
||||
cy.get('#ionTabsDidChangeSelectedTab').should('have.text', 'account');
|
||||
|
||||
describe('With IonRouterOutlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs');
|
||||
})
|
||||
|
||||
describe('entry url - /tabs', () => {
|
||||
it('should redirect and load tab-account', () => {
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
|
||||
testState(1, 'account');
|
||||
});
|
||||
|
||||
it('should navigate between tabs and ionChange events should be dispatched', () => {
|
||||
let tab = testTabTitle('Tab 1 - Page 1');
|
||||
tab.find('.segment-changed').should('have.text', 'false');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
|
||||
cy.get('#ionTabsWillChangeCounter').should('have.text', '2');
|
||||
cy.get('#ionTabsWillChangeEvent').should('have.text', 'contact');
|
||||
cy.get('#ionTabsWillChangeSelectedTab').should('have.text', 'account');
|
||||
|
||||
cy.get('#ionTabsDidChangeCounter').should('have.text', '2');
|
||||
cy.get('#ionTabsDidChangeEvent').should('have.text', 'contact');
|
||||
cy.get('#ionTabsDidChangeSelectedTab').should('have.text', 'contact');
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
it('should simulate stack + double tab click', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested']);
|
||||
testState(1, 'account');
|
||||
|
||||
// When you call find on tab above it changes the value of tab
|
||||
// so we need to redefine it
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').should('be.visible');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
testState(2, 'contact');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
testState(3, 'account');
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').should('be.visible');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
testState(3, 'account');
|
||||
});
|
||||
|
||||
it('should simulate stack + back button click', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testState(1, 'account');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
testState(2, 'contact');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testState(3, 'account');
|
||||
|
||||
cy.get('ion-back-button').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
testState(3, 'account');
|
||||
});
|
||||
|
||||
it('should navigate deep then go home', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested');
|
||||
cy.ionPageHidden('app-tabs-tab1');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-next').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:first-of-type');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
cy.ionPageVisible('app-tabs-tab2');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:last-of-type');
|
||||
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab2');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', [
|
||||
'app-tabs-tab1',
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab2'
|
||||
]);
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
|
||||
/**
|
||||
* Wait for the leaving view to
|
||||
* be unmounted otherwise testTabTitle
|
||||
* may get the leaving view before it
|
||||
* is unmounted.
|
||||
*/
|
||||
cy.ionPageVisible('app-tabs-tab1');
|
||||
cy.ionPageDoesNotExist('app-tabs-tab1-nested');
|
||||
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', [
|
||||
'app-tabs-tab1',
|
||||
'app-tabs-tab2'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should switch tabs and go back', () => {
|
||||
cy.get('#tab-button-contact').click();
|
||||
const tab = testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
tab.find('#goto-tab1-page1').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
});
|
||||
|
||||
it('should switch tabs and go to nested', () => {
|
||||
cy.get('#tab-button-contact').click();
|
||||
const tab = testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab2', 'app-tabs-tab1-nested']);
|
||||
});
|
||||
|
||||
it('should load lazy loaded tab', () => {
|
||||
cy.get('#tab-button-lazy').click();
|
||||
cy.ionPageVisible('app-tabs-tab3');
|
||||
testTabTitle('Tab 3 - Page 1');
|
||||
});
|
||||
|
||||
it('should use ion-back-button defaultHref', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-tab3-page2').click();
|
||||
testTabTitle('Tab 3 - Page 2');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3-nested']);
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').click();
|
||||
testTabTitle('Tab 3 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3']);
|
||||
});
|
||||
|
||||
it('should preserve navigation extras when switching tabs', () => {
|
||||
const expectUrlToContain = 'search=hello#fragment';
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1-with-query-params').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectUrlToContain);
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectUrlToContain);
|
||||
});
|
||||
|
||||
it('should set root when clicking on an active tab to navigate to the root', () => {
|
||||
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
||||
cy.url().then(url => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1-with-query-params').click();
|
||||
tab = testTabTitle('Tab 2 - Page 1');
|
||||
tab.find('.segment-changed').should('have.text', 'false');
|
||||
});
|
||||
|
||||
describe('when navigating between tabs', () => {
|
||||
|
||||
it('should emit ionTabsWillChange before setting the selected tab', () => {
|
||||
cy.get('#ionTabsWillChangeCounter').should('have.text', '1');
|
||||
cy.get('#ionTabsWillChangeEvent').should('have.text', 'account');
|
||||
cy.get('#ionTabsWillChangeSelectedTab').should('have.text', '');
|
||||
|
||||
cy.get('#ionTabsDidChangeCounter').should('have.text', '1');
|
||||
cy.get('#ionTabsDidChangeEvent').should('have.text', 'account');
|
||||
cy.get('#ionTabsDidChangeSelectedTab').should('have.text', 'account');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
|
||||
cy.get('#ionTabsWillChangeCounter').should('have.text', '2');
|
||||
cy.get('#ionTabsWillChangeEvent').should('have.text', 'contact');
|
||||
cy.get('#ionTabsWillChangeSelectedTab').should('have.text', 'account');
|
||||
|
||||
cy.get('#ionTabsDidChangeCounter').should('have.text', '2');
|
||||
cy.get('#ionTabsDidChangeEvent').should('have.text', 'contact');
|
||||
cy.get('#ionTabsDidChangeSelectedTab').should('have.text', 'contact');
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
it('should simulate stack + double tab click', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectNestedTabUrlToContain);
|
||||
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested']);
|
||||
testState(1, 'account');
|
||||
|
||||
// When you call find on tab above it changes the value of tab
|
||||
// so we need to redefine it
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').should('be.visible');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
testState(2, 'contact');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
testState(3, 'account');
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').should('be.visible');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlEquals(url);
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
testState(3, 'account');
|
||||
});
|
||||
|
||||
it('should simulate stack + back button click', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testState(1, 'account');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
testState(2, 'contact');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testState(3, 'account');
|
||||
|
||||
cy.get('ion-back-button').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
testState(3, 'account');
|
||||
});
|
||||
|
||||
it('should navigate deep then go home', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested');
|
||||
cy.ionPageHidden('app-tabs-tab1');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-next').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:first-of-type');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
cy.ionPageVisible('app-tabs-tab2');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:last-of-type');
|
||||
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab2');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', [
|
||||
'app-tabs-tab1',
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab2'
|
||||
]);
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
|
||||
/**
|
||||
* Wait for the leaving view to
|
||||
* be unmounted otherwise testTabTitle
|
||||
* may get the leaving view before it
|
||||
* is unmounted.
|
||||
*/
|
||||
cy.ionPageVisible('app-tabs-tab1');
|
||||
cy.ionPageDoesNotExist('app-tabs-tab1-nested');
|
||||
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', [
|
||||
'app-tabs-tab1',
|
||||
'app-tabs-tab2'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should switch tabs and go back', () => {
|
||||
cy.get('#tab-button-contact').click();
|
||||
const tab = testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
tab.find('#goto-tab1-page1').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
});
|
||||
|
||||
it('should switch tabs and go to nested', () => {
|
||||
cy.get('#tab-button-contact').click();
|
||||
const tab = testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab2', 'app-tabs-tab1-nested']);
|
||||
});
|
||||
|
||||
it('should load lazy loaded tab', () => {
|
||||
cy.get('#tab-button-lazy').click();
|
||||
cy.ionPageVisible('app-tabs-tab3');
|
||||
testTabTitle('Tab 3 - Page 1');
|
||||
});
|
||||
|
||||
it('should use ion-back-button defaultHref', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-tab3-page2').click();
|
||||
testTabTitle('Tab 3 - Page 2');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3-nested']);
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').click();
|
||||
testTabTitle('Tab 3 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3']);
|
||||
});
|
||||
|
||||
it('should preserve navigation extras when switching tabs', () => {
|
||||
const expectUrlToContain = 'search=hello#fragment';
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1-with-query-params').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectUrlToContain);
|
||||
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectUrlToContain);
|
||||
});
|
||||
|
||||
it('should set root when clicking on an active tab to navigate to the root', () => {
|
||||
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
||||
cy.url().then(url => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1-with-query-params').click();
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectNestedTabUrlToContain);
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlEquals(url);
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
describe('entry tab contains navigation extras', () => {
|
||||
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
||||
const rootUrlParams = 'test=123#rootFragment';
|
||||
const rootUrl = `/lazy/tabs/account?${rootUrlParams}`;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit(rootUrl);
|
||||
})
|
||||
|
||||
it('should preserve root url navigation extras when clicking on an active tab to navigate to the root', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1-with-query-params').click();
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectNestedTabUrlToContain);
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlContains(rootUrl);
|
||||
});
|
||||
|
||||
it('should preserve root url navigation extras when changing tabs', () => {
|
||||
getSelectedTab();
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlContains(rootUrl);
|
||||
});
|
||||
|
||||
it('should navigate deep then go home and preserve navigation extras', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested');
|
||||
cy.ionPageHidden('app-tabs-tab1');
|
||||
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
|
||||
tab.find('#goto-next').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:first-of-type');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
cy.ionTabClick('Tab Two');
|
||||
cy.ionPageVisible('app-tabs-tab2');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:last-of-type');
|
||||
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.ionTabClick('Tab One');
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab2');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
cy.ionTabClick('Tab One');
|
||||
/**
|
||||
* Wait for the leaving view to
|
||||
* be unmounted otherwise testTabTitle
|
||||
* may get the leaving view before it
|
||||
* is unmounted.
|
||||
*/
|
||||
cy.ionPageVisible('app-tabs-tab1');
|
||||
cy.ionPageDoesNotExist('app-tabs-tab1-nested');
|
||||
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlContains(rootUrl);
|
||||
});
|
||||
})
|
||||
|
||||
describe('entry url - /tabs/account', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/account');
|
||||
});
|
||||
it('should pop to previous view when leaving tabs outlet', () => {
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
cy.get('#goto-tab1-page2').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-global').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Global Page');
|
||||
|
||||
cy.get('#goto-prev-pop').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-prev').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
/**
|
||||
* Verifies that when entering the tabs outlet directly,
|
||||
* the navController.pop() method does not pop the previous view,
|
||||
* when you are at the root of the tabs outlet.
|
||||
*/
|
||||
cy.get('#goto-previous-page').click();
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('entry url - /', () => {
|
||||
it('should pop to the root outlet from the tabs outlet', () => {
|
||||
cy.visit('/lazy/');
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Test App');
|
||||
|
||||
cy.get('ion-item').contains('Tabs test').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
cy.get('#goto-tab1-page2').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-global').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Global Page');
|
||||
|
||||
cy.get('#goto-prev-pop').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-prev').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
cy.get('#goto-previous-page').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Test App');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('entry url - /tabs/account/nested/1', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/account/nested/1');
|
||||
})
|
||||
|
||||
it('should only display the back-button when there is a page in the stack', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('ion-back-button').should('not.be.visible');
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
tab = testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
tab.find('ion-back-button').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not reuse the same page', () => {
|
||||
let tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
tab.find('#goto-next').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
tab.find('#goto-next').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (3)');
|
||||
|
||||
cy.testStack('ion-tabs ion-router-outlet', [
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab1-nested'
|
||||
]);
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (2)');
|
||||
tab.find('ion-back-button').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
|
||||
tab.find('ion-back-button').should('not.be.visible');
|
||||
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
||||
});
|
||||
})
|
||||
|
||||
describe('entry url - /tabs/lazy', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/lazy');
|
||||
});
|
||||
|
||||
it('should not display the back-button if coming from a different stack', () => {
|
||||
let tab = testTabTitle('Tab 3 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3']);
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3', 'app-tabs-tab1-nested']);
|
||||
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
tab.find('ion-back-button').should('not.be.visible');
|
||||
});
|
||||
})
|
||||
|
||||
describe('enter url - /tabs/contact/one', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/contact/one');
|
||||
});
|
||||
|
||||
it('should return to correct tab after going to page in different outlet', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1').click();
|
||||
cy.testStack('app-nested-outlet ion-router-outlet', ['app-nested-outlet-page']);
|
||||
|
||||
const nestedOutlet = cy.get('app-nested-outlet');
|
||||
nestedOutlet.find('ion-back-button').click();
|
||||
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
describe('entry tab contains navigation extras', () => {
|
||||
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
||||
const rootUrlParams = 'test=123#rootFragment';
|
||||
const rootUrl = `/lazy/tabs/account?${rootUrlParams}`;
|
||||
|
||||
describe('Without IonRouterOutlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(rootUrl);
|
||||
cy.visit('/lazy/tabs-basic');
|
||||
})
|
||||
|
||||
it('should preserve root url navigation extras when clicking on an active tab to navigate to the root', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1-with-query-params').click();
|
||||
it('should show correct tab when clicking the tab button', () => {
|
||||
cy.get('ion-tab[tab="tab1"]').should('be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('not.be.visible');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
testUrlContains(expectNestedTabUrlToContain);
|
||||
cy.get('ion-tab-button[tab="tab2"]').click();
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlContains(rootUrl);
|
||||
cy.get('ion-tab[tab="tab1"]').should('not.be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should preserve root url navigation extras when changing tabs', () => {
|
||||
getSelectedTab();
|
||||
cy.get('#tab-button-contact').click();
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
it('should not change the URL when clicking the tab button', () => {
|
||||
cy.url().should('include', '/tabs-basic');
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
cy.get('ion-tab-button[tab="tab2"]').click();
|
||||
|
||||
testUrlContains(rootUrl);
|
||||
});
|
||||
|
||||
it('should navigate deep then go home and preserve navigation extras', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested');
|
||||
cy.ionPageHidden('app-tabs-tab1');
|
||||
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
|
||||
tab.find('#goto-next').click();
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:first-of-type');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
cy.ionTabClick('Tab Two');
|
||||
cy.ionPageVisible('app-tabs-tab2');
|
||||
cy.ionPageHidden('app-tabs-tab1-nested:last-of-type');
|
||||
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
cy.ionTabClick('Tab One');
|
||||
cy.ionPageVisible('app-tabs-tab1-nested:last-of-type');
|
||||
cy.ionPageHidden('app-tabs-tab2');
|
||||
|
||||
testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
cy.ionTabClick('Tab One');
|
||||
/**
|
||||
* Wait for the leaving view to
|
||||
* be unmounted otherwise testTabTitle
|
||||
* may get the leaving view before it
|
||||
* is unmounted.
|
||||
*/
|
||||
cy.ionPageVisible('app-tabs-tab1');
|
||||
cy.ionPageDoesNotExist('app-tabs-tab1-nested');
|
||||
|
||||
testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
testUrlContains(rootUrl);
|
||||
});
|
||||
})
|
||||
|
||||
describe('entry url - /tabs/account', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/account');
|
||||
});
|
||||
it('should pop to previous view when leaving tabs outlet', () => {
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
cy.get('#goto-tab1-page2').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-global').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Global Page');
|
||||
|
||||
cy.get('#goto-prev-pop').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-prev').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
/**
|
||||
* Verifies that when entering the tabs outlet directly,
|
||||
* the navController.pop() method does not pop the previous view,
|
||||
* when you are at the root of the tabs outlet.
|
||||
*/
|
||||
cy.get('#goto-previous-page').click();
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('entry url - /', () => {
|
||||
it('should pop to the root outlet from the tabs outlet', () => {
|
||||
cy.visit('/lazy/');
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Test App');
|
||||
|
||||
cy.get('ion-item').contains('Tabs test').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
cy.get('#goto-tab1-page2').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-global').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Global Page');
|
||||
|
||||
cy.get('#goto-prev-pop').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)');
|
||||
|
||||
cy.get('#goto-prev').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1');
|
||||
|
||||
cy.get('#goto-previous-page').click();
|
||||
|
||||
cy.get('ion-title').should('contain.text', 'Test App');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('entry url - /tabs/account/nested/1', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/account/nested/1');
|
||||
})
|
||||
|
||||
it('should only display the back-button when there is a page in the stack', () => {
|
||||
let tab = getSelectedTab();
|
||||
tab.find('ion-back-button').should('not.be.visible');
|
||||
testTabTitle('Tab 1 - Page 2 (1)');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
||||
|
||||
cy.get('#tab-button-account').click();
|
||||
tab = testTabTitle('Tab 1 - Page 1');
|
||||
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
tab.find('ion-back-button').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not reuse the same page', () => {
|
||||
let tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
tab.find('#goto-next').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (2)');
|
||||
|
||||
tab.find('#goto-next').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (3)');
|
||||
|
||||
cy.testStack('ion-tabs ion-router-outlet', [
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab1-nested',
|
||||
'app-tabs-tab1-nested'
|
||||
]);
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('ion-back-button').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (2)');
|
||||
tab.find('ion-back-button').click();
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
|
||||
tab.find('ion-back-button').should('not.be.visible');
|
||||
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
||||
});
|
||||
})
|
||||
|
||||
describe('entry url - /tabs/lazy', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/lazy');
|
||||
});
|
||||
|
||||
it('should not display the back-button if coming from a different stack', () => {
|
||||
let tab = testTabTitle('Tab 3 - Page 1');
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3']);
|
||||
|
||||
tab = getSelectedTab();
|
||||
tab.find('#goto-tab1-page2').click();
|
||||
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3', 'app-tabs-tab1-nested']);
|
||||
|
||||
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||
tab.find('ion-back-button').should('not.be.visible');
|
||||
});
|
||||
})
|
||||
|
||||
describe('enter url - /tabs/contact/one', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/lazy/tabs/contact/one');
|
||||
});
|
||||
|
||||
it('should return to correct tab after going to page in different outlet', () => {
|
||||
const tab = getSelectedTab();
|
||||
tab.find('#goto-nested-page1').click();
|
||||
cy.testStack('app-nested-outlet ion-router-outlet', ['app-nested-outlet-page']);
|
||||
|
||||
const nestedOutlet = cy.get('app-nested-outlet');
|
||||
nestedOutlet.find('ion-back-button').click();
|
||||
|
||||
testTabTitle('Tab 2 - Page 1');
|
||||
cy.url().should('include', '/tabs-basic');
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
describe('Tabs', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/standalone/tabs');
|
||||
describe('Without IonRouterOutlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/standalone/tabs');
|
||||
});
|
||||
|
||||
it('should redirect to the default tab', () => {
|
||||
cy.get('app-tab-one').should('be.visible');
|
||||
cy.contains('Tab 1');
|
||||
});
|
||||
|
||||
it('should render new content when switching tabs', () => {
|
||||
cy.get('#tab-button-tab-two').click();
|
||||
cy.get('app-tab-two').should('be.visible');
|
||||
cy.contains('Tab 2');
|
||||
});
|
||||
|
||||
// Issue: https://github.com/ionic-team/ionic-framework/issues/28417
|
||||
it('parentOutlet should be defined', () => {
|
||||
cy.get('#parent-outlet span').should('have.text', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to the default tab', () => {
|
||||
cy.get('app-tab-one').should('be.visible');
|
||||
cy.contains('Tab 1');
|
||||
});
|
||||
describe('Without IonRouterOutlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/standalone/tabs-basic');
|
||||
})
|
||||
|
||||
it('should render new content when switching tabs', () => {
|
||||
cy.get('#tab-button-tab-two').click();
|
||||
cy.get('app-tab-two').should('be.visible');
|
||||
cy.contains('Tab 2');
|
||||
});
|
||||
it('should show correct tab when clicking the tab button', () => {
|
||||
cy.get('ion-tab[tab="tab1"]').should('be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('not.be.visible');
|
||||
|
||||
// Fixes https://github.com/ionic-team/ionic-framework/issues/28417
|
||||
it('parentOutlet should be defined', () => {
|
||||
cy.get('#parent-outlet span').should('have.text', 'true');
|
||||
cy.get('ion-tab-button[tab="tab2"]').click();
|
||||
|
||||
cy.get('ion-tab[tab="tab1"]').should('not.be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not change the URL when clicking the tab button', () => {
|
||||
cy.url().should('include', '/tabs-basic');
|
||||
|
||||
cy.get('ion-tab-button[tab="tab2"]').click();
|
||||
|
||||
cy.url().should('include', '/tabs-basic');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import { NavigationPage3Component } from '../navigation-page3/navigation-page3.c
|
||||
import { AlertComponent } from '../alert/alert.component';
|
||||
import { AccordionComponent } from '../accordion/accordion.component';
|
||||
import { AccordionModalComponent } from '../accordion/accordion-modal/accordion-modal.component';
|
||||
import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -51,7 +52,8 @@ import { AccordionModalComponent } from '../accordion/accordion-modal/accordion-
|
||||
NavigationPage3Component,
|
||||
AlertComponent,
|
||||
AccordionComponent,
|
||||
AccordionModalComponent
|
||||
AccordionModalComponent,
|
||||
TabsBasicComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { NavigationPage2Component } from '../navigation-page2/navigation-page2.c
|
||||
import { NavigationPage3Component } from '../navigation-page3/navigation-page3.component';
|
||||
import { AlertComponent } from '../alert/alert.component';
|
||||
import { AccordionComponent } from '../accordion/accordion.component';
|
||||
import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -65,6 +66,7 @@ export const routes: Routes = [
|
||||
path: 'tabs-slots',
|
||||
loadComponent: () => import('../tabs-slots.component').then(c => c.TabsSlotsComponent)
|
||||
},
|
||||
{ path: 'tabs-basic', component: TabsBasicComponent },
|
||||
{
|
||||
path: 'nested-outlet',
|
||||
component: NestedOutletComponent,
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
Tabs test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/lazy/tabs-basic">
|
||||
<ion-label>
|
||||
Basic Tabs test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/lazy/nested-outlet/page">
|
||||
<ion-label>
|
||||
Nested ion-router-outlet
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#test {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<ion-tabs (ionTabsWillChange)="onTabWillChange($event)" (ionTabsDidChange)="onTabDidChange($event)">
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="tab1">
|
||||
<ion-label>Tab One</ion-label>
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab2">
|
||||
<ion-label>Tab Two</ion-label>
|
||||
<ion-icon name="logo-ionic"></ion-icon>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab3">
|
||||
<ion-label>Tab Three</ion-label>
|
||||
<ion-icon name="save"></ion-icon>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
|
||||
<ion-tab tab="tab1">
|
||||
<ion-label>Tab 1 Content</ion-label>
|
||||
</ion-tab>
|
||||
<ion-tab tab="tab2">
|
||||
<ion-label>Tab 2 Content</ion-label>
|
||||
</ion-tab>
|
||||
<ion-tab tab="tab3">
|
||||
<ion-label>Tab 3 Content</ion-label>
|
||||
</ion-tab>
|
||||
</ion-tabs>
|
||||
|
||||
<div id="test">
|
||||
<ul>
|
||||
<li>
|
||||
ionTabsWillChange counter: <span id="ionTabsWillChangeCounter">{{ tabsWillChangeCounter }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsWillChange event: <span id="ionTabsWillChangeEvent">{{ tabsWillChangeEvent }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsWillChange selectedTab: <span id="ionTabsWillChangeSelectedTab">{{ tabsWillChangeSelectedTab }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
ionTabsDidChange counter: <span id="ionTabsDidChangeCounter">{{ tabsDidChangeCounter }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsDidChange event: <span id="ionTabsDidChangeEvent">{{ tabsDidChangeEvent }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsDidChange selectedTab: <span id="ionTabsDidChangeSelectedTab">{{ tabsDidChangeSelectedTab }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonTabBar } from '@ionic/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tabs-basic',
|
||||
templateUrl: './tabs-basic.component.html',
|
||||
styleUrls: ['./tabs-basic.component.css']
|
||||
})
|
||||
export class TabsBasicComponent {
|
||||
constructor() { }
|
||||
|
||||
tabsWillChangeCounter = 0;
|
||||
tabsWillChangeEvent = '';
|
||||
tabsWillChangeSelectedTab? = '';
|
||||
|
||||
tabsDidChangeCounter = 0;
|
||||
tabsDidChangeEvent = '';
|
||||
tabsDidChangeSelectedTab? = '';
|
||||
|
||||
@ViewChild(IonTabBar) tabBar!: IonTabBar;
|
||||
|
||||
onTabWillChange(ev: { tab: string }) {
|
||||
console.log('ionTabsWillChange', this.tabBar.selectedTab);
|
||||
this.tabsWillChangeCounter++;
|
||||
this.tabsWillChangeEvent = ev.tab;
|
||||
this.tabsWillChangeSelectedTab = this.tabBar.selectedTab;
|
||||
}
|
||||
|
||||
onTabDidChange(ev: { tab: string }) {
|
||||
console.log('ionTabsDidChange', this.tabBar.selectedTab);
|
||||
this.tabsDidChangeCounter++;
|
||||
this.tabsDidChangeEvent = ev.tab;
|
||||
this.tabsDidChangeSelectedTab = this.tabBar.selectedTab;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export const routes: Routes = [
|
||||
{ path: 'tab-three', loadComponent: () => import('../tabs/tab3.component').then(c => c.TabThreeComponent) }
|
||||
]
|
||||
},
|
||||
{ path: 'tabs-basic', loadComponent: () => import('../tabs-basic/tabs-basic.component').then(c => c.TabsBasicComponent) },
|
||||
{
|
||||
path: 'value-accessors',
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#test {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<ion-tabs (ionTabsWillChange)="onTabWillChange($event)" (ionTabsDidChange)="onTabDidChange($event)">
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="tab1">
|
||||
<ion-label>Tab One</ion-label>
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab2">
|
||||
<ion-label>Tab Two</ion-label>
|
||||
<ion-icon name="logo-ionic"></ion-icon>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab3">
|
||||
<ion-label>Tab Three</ion-label>
|
||||
<ion-icon name="save"></ion-icon>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
|
||||
<ion-tab tab="tab1">
|
||||
<ion-label>Tab 1 Content</ion-label>
|
||||
</ion-tab>
|
||||
<ion-tab tab="tab2">
|
||||
<ion-label>Tab 2 Content</ion-label>
|
||||
</ion-tab>
|
||||
<ion-tab tab="tab3">
|
||||
<ion-label>Tab 3 Content</ion-label>
|
||||
</ion-tab>
|
||||
</ion-tabs>
|
||||
|
||||
<div id="test">
|
||||
<ul>
|
||||
<li>
|
||||
ionTabsWillChange counter: <span id="ionTabsWillChangeCounter">{{ tabsWillChangeCounter }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsWillChange event: <span id="ionTabsWillChangeEvent">{{ tabsWillChangeEvent }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsWillChange selectedTab: <span id="ionTabsWillChangeSelectedTab">{{ tabsWillChangeSelectedTab }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
ionTabsDidChange counter: <span id="ionTabsDidChangeCounter">{{ tabsDidChangeCounter }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsDidChange event: <span id="ionTabsDidChangeEvent">{{ tabsDidChangeEvent }}</span>
|
||||
</li>
|
||||
<li>
|
||||
ionTabsDidChange selectedTab: <span id="ionTabsDidChangeSelectedTab">{{ tabsDidChangeSelectedTab }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonTabBar, IonTabButton, IonIcon, IonLabel, IonTabs, IonTab } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { add, logoIonic, save } from 'ionicons/icons';
|
||||
|
||||
addIcons({ add, logoIonic, save });
|
||||
|
||||
@Component({
|
||||
selector: 'app-tabs-basic',
|
||||
templateUrl: './tabs-basic.component.html',
|
||||
styleUrls: ['./tabs-basic.component.css'],
|
||||
standalone: true,
|
||||
imports: [IonTabBar, IonTabButton, IonIcon, IonLabel, IonTabs, IonTab]
|
||||
})
|
||||
export class TabsBasicComponent {
|
||||
tabsDidChangeCounter = 0;
|
||||
tabsDidChangeEvent = '';
|
||||
tabsDidChangeSelectedTab? = '';
|
||||
|
||||
tabsWillChangeCounter = 0;
|
||||
tabsWillChangeEvent = '';
|
||||
tabsWillChangeSelectedTab? = '';
|
||||
|
||||
@ViewChild(IonTabBar) tabBar!: IonTabBar;
|
||||
|
||||
onTabWillChange(ev: { tab: string }) {
|
||||
console.log('ionTabsWillChange', this.tabBar.selectedTab);
|
||||
this.tabsWillChangeCounter++;
|
||||
this.tabsWillChangeEvent = ev.tab;
|
||||
this.tabsWillChangeSelectedTab = this.tabBar.selectedTab;
|
||||
}
|
||||
|
||||
onTabDidChange(ev: { tab: string }) {
|
||||
console.log('ionTabsDidChange', this.tabBar.selectedTab);
|
||||
this.tabsDidChangeCounter++;
|
||||
this.tabsDidChangeEvent = ev.tab;
|
||||
this.tabsDidChangeSelectedTab = this.tabBar.selectedTab;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { defineCustomElement as defineIonBackButton } from '@ionic/core/componen
|
||||
import { defineCustomElement as defineIonRouterOutlet } from '@ionic/core/components/ion-router-outlet.js';
|
||||
import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js';
|
||||
import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js';
|
||||
import { defineCustomElement as defineIonTabs } from '@ionic/core/components/ion-tabs.js';
|
||||
import type { JSX as IoniconsJSX } from 'ionicons';
|
||||
import { defineCustomElement as defineIonIcon } from 'ionicons/components/ion-icon.js';
|
||||
|
||||
@@ -19,6 +20,12 @@ export const IonTabBarInner = /*@__PURE__*/ createReactComponent<JSX.IonTabBar,
|
||||
undefined,
|
||||
defineIonTabBar
|
||||
);
|
||||
export const IonTabsInner = /*@__PURE__*/ createReactComponent<JSX.IonTabs, HTMLIonTabsElement>(
|
||||
'ion-tabs',
|
||||
undefined,
|
||||
undefined,
|
||||
defineIonTabs
|
||||
);
|
||||
export const IonBackButtonInner = /*@__PURE__*/ createReactComponent<
|
||||
Omit<JSX.IonBackButton, 'icon'>,
|
||||
HTMLIonBackButtonElement
|
||||
|
||||
@@ -21,6 +21,7 @@ interface InternalProps extends IonTabBarProps {
|
||||
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
|
||||
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||
routeInfo: RouteInfo;
|
||||
routerOutletRef?: React.RefObject<HTMLIonRouterOutletElement> | undefined;
|
||||
}
|
||||
|
||||
interface TabUrls {
|
||||
@@ -182,7 +183,12 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
) {
|
||||
const tappedTab = this.state.tabs[e.detail.tab];
|
||||
const originalHref = tappedTab.originalHref;
|
||||
const currentHref = e.detail.href;
|
||||
/**
|
||||
* If the router outlet is not defined, then the tabs is being used
|
||||
* as a basic tab navigation without the router. In this case, we
|
||||
* don't want to update the href else the URL will change.
|
||||
*/
|
||||
const currentHref = this.props.routerOutletRef?.current ? e.detail.href : '';
|
||||
const { activeTab: prevActiveTab } = this.state;
|
||||
|
||||
if (onClickFn) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { NavContext } from '../../contexts/NavContext';
|
||||
import PageManager from '../../routing/PageManager';
|
||||
import { HTMLElementSSR } from '../../utils/HTMLElementSSR';
|
||||
import { IonRouterOutlet } from '../IonRouterOutlet';
|
||||
import { IonTab } from '../components';
|
||||
import { IonTabsInner } from '../inner-proxies';
|
||||
|
||||
import { IonTabBar } from './IonTabBar';
|
||||
import type { IonTabsContextState } from './IonTabsContext';
|
||||
@@ -91,6 +93,8 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
||||
render() {
|
||||
let outlet: React.ReactElement<{}> | undefined;
|
||||
let tabBar: React.ReactElement | undefined;
|
||||
// Check if IonTabs has any IonTab children
|
||||
let hasTab = false;
|
||||
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
|
||||
|
||||
const children =
|
||||
@@ -98,19 +102,30 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
||||
? (this.props.children as ChildFunction)(this.ionTabContextState)
|
||||
: this.props.children;
|
||||
|
||||
const outletProps = {
|
||||
ref: this.routerOutletRef,
|
||||
};
|
||||
|
||||
React.Children.forEach(children, (child: any) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
|
||||
return;
|
||||
}
|
||||
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
|
||||
outlet = React.cloneElement(child);
|
||||
outlet = React.cloneElement(child, outletProps);
|
||||
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
||||
outlet = child.props.children[0];
|
||||
outlet = React.cloneElement(child.props.children[0], outletProps);
|
||||
} else if (child.type === IonTab) {
|
||||
/**
|
||||
* This indicates that IonTabs will be using a basic tab-based navigation
|
||||
* without the history stack or URL updates associated with the router.
|
||||
*/
|
||||
hasTab = true;
|
||||
}
|
||||
|
||||
let childProps: any = {
|
||||
ref: this.tabBarRef,
|
||||
routerOutletRef: this.routerOutletRef,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -144,24 +159,67 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
||||
}
|
||||
});
|
||||
|
||||
if (!outlet) {
|
||||
throw new Error('IonTabs must contain an IonRouterOutlet');
|
||||
if (!outlet && !hasTab) {
|
||||
throw new Error('IonTabs must contain an IonRouterOutlet or an IonTab');
|
||||
}
|
||||
if (outlet && hasTab) {
|
||||
throw new Error('IonTabs cannot contain an IonRouterOutlet and an IonTab at the same time');
|
||||
}
|
||||
if (!tabBar) {
|
||||
throw new Error('IonTabs needs a IonTabBar');
|
||||
}
|
||||
|
||||
if (hasTab) {
|
||||
return <IonTabsInner {...this.props}></IonTabsInner>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(ROU-11051)
|
||||
*
|
||||
* There is no error handling for the case where there
|
||||
* is no associated Route for the given IonTabButton.
|
||||
*
|
||||
* More investigation is needed to determine how to
|
||||
* handle this to prevent any overwriting of the
|
||||
* IonTabButton's onClick handler and how the routing
|
||||
* is handled.
|
||||
*/
|
||||
|
||||
return (
|
||||
<IonTabsContext.Provider value={this.ionTabContextState}>
|
||||
{this.context.hasIonicRouter() ? (
|
||||
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
||||
<ion-tabs className="ion-tabs" style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</ion-tabs>
|
||||
<IonTabsInner {...this.props}>
|
||||
{React.Children.map(children, (child: React.ReactNode) => {
|
||||
if (React.isValidElement(child)) {
|
||||
const isTabBar =
|
||||
child.type === IonTabBar ||
|
||||
(child.type as any).isTabBar ||
|
||||
(child.type === Fragment &&
|
||||
(child.props.children[1].type === IonTabBar || child.props.children[1].type.isTabBar));
|
||||
const isRouterOutlet =
|
||||
child.type === IonRouterOutlet ||
|
||||
(child.type as any).isRouterOutlet ||
|
||||
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
|
||||
|
||||
if (isTabBar) {
|
||||
/**
|
||||
* The modified tabBar needs to be returned to include
|
||||
* the context and the overridden methods.
|
||||
*/
|
||||
return tabBar;
|
||||
}
|
||||
if (isRouterOutlet) {
|
||||
/**
|
||||
* The modified outlet needs to be returned to include
|
||||
* the ref.
|
||||
*/
|
||||
return outlet;
|
||||
}
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</IonTabsInner>
|
||||
</PageManager>
|
||||
) : (
|
||||
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>
|
||||
|
||||
@@ -26,6 +26,7 @@ import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
|
||||
import OverlayComponents from './pages/overlay-components/OverlayComponents';
|
||||
import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted';
|
||||
import Tabs from './pages/Tabs';
|
||||
import TabsBasic from './pages/TabsBasic';
|
||||
import Icons from './pages/Icons';
|
||||
import NavComponent from './pages/navigation/NavComponent';
|
||||
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
|
||||
@@ -60,6 +61,7 @@ const App: React.FC = () => (
|
||||
<Route path="/keep-contents-mounted" component={KeepContentsMounted} />
|
||||
<Route path="/navigation" component={NavComponent} />
|
||||
<Route path="/tabs" component={Tabs} />
|
||||
<Route path="/tabs-basic" component={TabsBasic} />
|
||||
<Route path="/icons" component={Icons} />
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
|
||||
@@ -37,6 +37,9 @@ const Main: React.FC<MainProps> = () => {
|
||||
<IonItem routerLink="/tabs">
|
||||
<IonLabel>Tabs</IonLabel>
|
||||
</IonItem>
|
||||
<IonItem routerLink="/tabs-basic">
|
||||
<IonLabel>Tabs with Basic Navigation</IonLabel>
|
||||
</IonItem>
|
||||
<IonItem routerLink="/icons">
|
||||
<IonLabel>Icons</IonLabel>
|
||||
</IonItem>
|
||||
|
||||
35
packages/react/test/base/src/pages/TabsBasic.tsx
Normal file
35
packages/react/test/base/src/pages/TabsBasic.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { IonLabel, IonTabBar, IonTabButton, IonTabs, IonTab } from '@ionic/react';
|
||||
|
||||
interface TabsProps {}
|
||||
|
||||
const TabsBasic: React.FC<TabsProps> = () => {
|
||||
const onTabWillChange = (event: CustomEvent) => {
|
||||
console.log('onIonTabsWillChange', event.detail.tab);
|
||||
};
|
||||
|
||||
const onTabDidChange = (event: CustomEvent) => {
|
||||
console.log('onIonTabsDidChange:', event.detail.tab);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonTabs onIonTabsWillChange={onTabWillChange} onIonTabsDidChange={onTabDidChange}>
|
||||
<IonTab tab="tab1">
|
||||
<IonLabel>Tab 1 Content</IonLabel>
|
||||
</IonTab>
|
||||
<IonTab tab="tab2">
|
||||
<IonLabel>Tab 2 Content</IonLabel>
|
||||
</IonTab>
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="tab1">
|
||||
<IonLabel>Tab 1</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab2">
|
||||
<IonLabel>Tab 2</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabsBasic;
|
||||
@@ -1,15 +1,46 @@
|
||||
describe('IonTabs', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tabs/tab1');
|
||||
describe('With IonRouterOutlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tabs/tab1');
|
||||
});
|
||||
|
||||
it('should handle onClick handlers on IonTabButton', () => {
|
||||
const stub = cy.stub();
|
||||
|
||||
cy.on('window:alert', stub);
|
||||
cy.get('ion-tab-button[tab="tab1"]').click().then(() => {
|
||||
expect(stub.getCall(0)).to.be.calledWith('Tab was clicked')
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle onClick handlers on IonTabButton', () => {
|
||||
const stub = cy.stub();
|
||||
|
||||
cy.on('window:alert', stub);
|
||||
cy.get('ion-tab-button[tab="tab1"]').click().then(() => {
|
||||
expect(stub.getCall(0)).to.be.calledWith('Tab was clicked')
|
||||
describe('Without IonRouterOutlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tabs-basic');
|
||||
});
|
||||
|
||||
it('should show correct tab when clicking the tab button', () => {
|
||||
cy.get('ion-tab[tab="tab1"]').should('be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('not.be.visible');
|
||||
|
||||
cy.get('ion-tab-button[tab="tab2"]').click();
|
||||
|
||||
cy.get('ion-tab[tab="tab1"]').should('not.be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('be.visible');
|
||||
|
||||
cy.get('ion-tab-button[tab="tab1"]').click();
|
||||
|
||||
cy.get('ion-tab[tab="tab1"]').should('be.visible');
|
||||
cy.get('ion-tab[tab="tab2"]').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should not change the URL when clicking the tab button', () => {
|
||||
cy.url().should('include', '/tabs-basic');
|
||||
|
||||
cy.get('ion-tab-button[tab="tab2"]').click();
|
||||
|
||||
cy.url().should('include', '/tabs-basic');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { h, defineComponent, getCurrentInstance, inject } from "vue";
|
||||
interface TabState {
|
||||
activeTab?: string;
|
||||
tabs: { [k: string]: Tab };
|
||||
hasRouterOutlet?: boolean;
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
@@ -37,6 +38,7 @@ export const IonTabBar = defineComponent({
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
_tabsWillChange: { type: Function, default: () => {} },
|
||||
_tabsDidChange: { type: Function, default: () => {} },
|
||||
_hasRouterOutlet: { type: Boolean, default: false },
|
||||
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||
},
|
||||
data() {
|
||||
@@ -53,6 +55,7 @@ export const IonTabBar = defineComponent({
|
||||
},
|
||||
methods: {
|
||||
setupTabState(ionRouter: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
/**
|
||||
* For each tab, we need to keep track of its
|
||||
* base href as well as any child page that
|
||||
@@ -72,27 +75,76 @@ export const IonTabBar = defineComponent({
|
||||
ref: child,
|
||||
};
|
||||
|
||||
/**
|
||||
* Passing this prop to each tab button
|
||||
* lets it be aware of the presence of
|
||||
* the router outlet.
|
||||
*/
|
||||
tabState.hasRouterOutlet = hasRouterOutlet;
|
||||
|
||||
/**
|
||||
* Passing this prop to each tab button
|
||||
* lets it be aware of the state that
|
||||
* ion-tab-bar is managing for it.
|
||||
*/
|
||||
child.component.props._getTabState = () => tabState;
|
||||
|
||||
/**
|
||||
* If the router outlet is not defined, then the tabs are being used
|
||||
* as a basic tab navigation without the router. In this case, the
|
||||
* tabs will not emit the `ionTabsDidChange` and `ionTabsWillChange`
|
||||
* events through the `checkActiveTab` method. Instead, we need to
|
||||
* handle those events through the tab buttons.
|
||||
*/
|
||||
if (!hasRouterOutlet) {
|
||||
child.component.props._onClick = (
|
||||
event: CustomEvent<{
|
||||
href: string;
|
||||
selected: boolean;
|
||||
tab: string;
|
||||
}>
|
||||
) => {
|
||||
this.handleIonTabButtonClick(event);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.checkActiveTab(ionRouter);
|
||||
},
|
||||
/**
|
||||
* This method is called upon setup and when the
|
||||
* history changes. It checks the current route
|
||||
* and updates the active tab accordingly.
|
||||
*
|
||||
* History changes only occur when the router
|
||||
* outlet is present. Due to this, the
|
||||
* `ionTabsDidChange` and `ionTabsWillChange`
|
||||
* events are only emitted when the router
|
||||
* outlet is present. A different approach must
|
||||
* be taken for tabs without a router outlet.
|
||||
*
|
||||
* @param ionRouter
|
||||
*/
|
||||
checkActiveTab(ionRouter: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
const currentRoute = ionRouter.getCurrentRouteInfo();
|
||||
const childNodes = this.$data.tabVnodes;
|
||||
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
|
||||
const tabState = this.$data.tabState;
|
||||
const tabKeys = Object.keys(tabs);
|
||||
const activeTab = tabKeys.find((key) => {
|
||||
let activeTab = tabKeys.find((key) => {
|
||||
const href = tabs[key].originalHref;
|
||||
return currentRoute.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tabs is being used as a basic tab navigation,
|
||||
* so we need to set the first tab as active since
|
||||
* `checkActiveTab` will not be called after setup.
|
||||
*/
|
||||
if (!activeTab && !hasRouterOutlet) {
|
||||
activeTab = tabKeys[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* For each tab, check to see if the
|
||||
* base href has changed. If so, update
|
||||
@@ -147,6 +199,24 @@ export const IonTabBar = defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
this.tabSwitch(activeTab, ionRouter);
|
||||
},
|
||||
handleIonTabButtonClick(
|
||||
event: CustomEvent<{
|
||||
href: string;
|
||||
selected: boolean;
|
||||
tab: string;
|
||||
}>
|
||||
) {
|
||||
const activeTab = event.detail.tab;
|
||||
|
||||
this.tabSwitch(activeTab);
|
||||
},
|
||||
tabSwitch(activeTab: string, ionRouter?: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
const childNodes = this.$data.tabVnodes;
|
||||
const { activeTab: prevActiveTab } = this.$data.tabState;
|
||||
const tabState = this.$data.tabState;
|
||||
const activeChild = childNodes.find(
|
||||
(child: VNode) => isTabButton(child) && child.props?.tab === activeTab
|
||||
);
|
||||
@@ -156,17 +226,20 @@ export const IonTabBar = defineComponent({
|
||||
if (activeChild) {
|
||||
tabDidChange && this.$props._tabsWillChange(activeTab);
|
||||
|
||||
ionRouter.handleSetCurrentTab(activeTab);
|
||||
if (hasRouterOutlet && ionRouter) {
|
||||
ionRouter.handleSetCurrentTab(activeTab);
|
||||
}
|
||||
|
||||
tabBar.selectedTab = tabState.activeTab = activeTab;
|
||||
|
||||
tabDidChange && this.$props._tabsDidChange(activeTab);
|
||||
} else {
|
||||
/**
|
||||
* When going to a tab that does
|
||||
* not have an associated ion-tab-button
|
||||
* we need to remove the selected state from
|
||||
* the old tab.
|
||||
*/
|
||||
} else {
|
||||
tabBar.selectedTab = tabState.activeTab = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ export const IonTabButton = /*@__PURE__*/ defineComponent({
|
||||
selected: Boolean,
|
||||
tab: String,
|
||||
target: String,
|
||||
_onClick: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
defineCustomElement();
|
||||
@@ -37,11 +41,29 @@ export const IonTabButton = /*@__PURE__*/ defineComponent({
|
||||
*/
|
||||
const { tab, href, _getTabState } = props;
|
||||
const tabState = _getTabState();
|
||||
const hasRouterOutlet = tabState.hasRouterOutlet;
|
||||
const tappedTab = tabState.tabs[tab] || {};
|
||||
const originalHref = tappedTab.originalHref || href;
|
||||
const currentHref = tappedTab.currentHref || href;
|
||||
/**
|
||||
* If the router outlet is not defined, then the tabs is being used
|
||||
* as a basic tab navigation without the router. In this case, we
|
||||
* don't want to update the href else the URL will change.
|
||||
*/
|
||||
const currentHref = hasRouterOutlet ? tappedTab.currentHref || href : "";
|
||||
const prevActiveTab = tabState.activeTab;
|
||||
|
||||
if (!hasRouterOutlet && props._onClick) {
|
||||
props._onClick(
|
||||
new CustomEvent("ionTabButtonClick", {
|
||||
detail: {
|
||||
href: currentHref,
|
||||
selected: tab === prevActiveTab,
|
||||
tab,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are still on the same
|
||||
* tab as before, but the base href
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
|
||||
import type { VNode } from "vue";
|
||||
import { h, defineComponent } from "vue";
|
||||
import { h, defineComponent, Fragment, isVNode } from "vue";
|
||||
|
||||
import { IonTab } from "../proxies";
|
||||
|
||||
const WILL_CHANGE = "ionTabsWillChange";
|
||||
const DID_CHANGE = "ionTabsDidChange";
|
||||
@@ -28,64 +31,74 @@ const isTabBar = (node: VNode) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isTab = (node: VNode): boolean => {
|
||||
// The `ion-tab` component was created with the `v-for` directive.
|
||||
if (node.type === Fragment) {
|
||||
if (Array.isArray(node.children)) {
|
||||
return node.children.some((child) => isVNode(child) && isTab(child));
|
||||
}
|
||||
|
||||
return false; // In case the fragment has no children.
|
||||
}
|
||||
|
||||
return (
|
||||
node.type && ((node.type as any).name === "ion-tab" || node.type === IonTab)
|
||||
);
|
||||
};
|
||||
|
||||
export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
name: "IonTabs",
|
||||
emits: [WILL_CHANGE, DID_CHANGE],
|
||||
setup(props, { slots, emit }) {
|
||||
// Define the custom element
|
||||
defineCustomElement();
|
||||
|
||||
return {
|
||||
props,
|
||||
slots,
|
||||
emit,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const { $slots: slots, $emit } = this;
|
||||
const { slots, emit, props } = this;
|
||||
const slottedContent = slots.default && slots.default();
|
||||
let routerOutlet;
|
||||
let hasTab = false;
|
||||
|
||||
/**
|
||||
* Developers must pass an ion-router-outlet
|
||||
* inside of ion-tabs.
|
||||
*/
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
/**
|
||||
* Developers must pass an ion-router-outlet
|
||||
* inside of ion-tabs if they want to use
|
||||
* the history stack or URL updates associated
|
||||
* wit the router.
|
||||
*/
|
||||
routerOutlet = slottedContent.find((child: VNode) =>
|
||||
isRouterOutlet(child)
|
||||
);
|
||||
}
|
||||
|
||||
if (!routerOutlet) {
|
||||
throw new Error(
|
||||
"IonTabs must contain an IonRouterOutlet. See https://ionicframework.com/docs/vue/navigation#working-with-tabs for more information."
|
||||
);
|
||||
}
|
||||
|
||||
let childrenToRender = [
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
class: "tabs-inner",
|
||||
style: {
|
||||
position: "relative",
|
||||
flex: "1",
|
||||
contain: "layout size style",
|
||||
},
|
||||
},
|
||||
routerOutlet
|
||||
),
|
||||
];
|
||||
|
||||
/**
|
||||
* If ion-tab-bar has slot="top" it needs to be
|
||||
* rendered before `.tabs-inner` otherwise it will
|
||||
* not show above the tab content.
|
||||
*/
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
/**
|
||||
* Render all content except for router outlet
|
||||
* since that needs to be inside of `.tabs-inner`.
|
||||
* Developers must pass at least one ion-tab
|
||||
* inside of ion-tabs if they want to use a
|
||||
* basic tab-based navigation without the
|
||||
* history stack or URL updates associated
|
||||
* with the router.
|
||||
*/
|
||||
const filteredContent = slottedContent.filter(
|
||||
(child: VNode) => !child.type || !isRouterOutlet(child)
|
||||
);
|
||||
hasTab = slottedContent.some((child: VNode) => isTab(child));
|
||||
}
|
||||
|
||||
const slottedTabBar = filteredContent.find((child: VNode) =>
|
||||
if (!routerOutlet && !hasTab) {
|
||||
throw new Error("IonTabs must contain an IonRouterOutlet or an IonTab.");
|
||||
}
|
||||
if (routerOutlet && hasTab) {
|
||||
throw new Error(
|
||||
"IonTabs cannot contain an IonRouterOutlet and IonTab at the same time."
|
||||
);
|
||||
}
|
||||
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
const slottedTabBar = slottedContent.find((child: VNode) =>
|
||||
isTabBar(child)
|
||||
);
|
||||
const hasTopSlotTabBar =
|
||||
slottedTabBar && slottedTabBar.props?.slot === "top";
|
||||
|
||||
if (slottedTabBar) {
|
||||
if (!slottedTabBar.props) {
|
||||
@@ -99,18 +112,34 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
* so we do not have code split across two components.
|
||||
*/
|
||||
slottedTabBar.props._tabsWillChange = (tab: string) =>
|
||||
$emit(WILL_CHANGE, { tab });
|
||||
emit(WILL_CHANGE, { tab });
|
||||
slottedTabBar.props._tabsDidChange = (tab: string) =>
|
||||
$emit(DID_CHANGE, { tab });
|
||||
}
|
||||
|
||||
if (hasTopSlotTabBar) {
|
||||
childrenToRender = [...filteredContent, ...childrenToRender];
|
||||
} else {
|
||||
childrenToRender = [...childrenToRender, ...filteredContent];
|
||||
emit(DID_CHANGE, { tab });
|
||||
slottedTabBar.props._hasRouterOutlet = !!routerOutlet;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTab) {
|
||||
return h(
|
||||
"ion-tabs",
|
||||
{
|
||||
...props,
|
||||
},
|
||||
slottedContent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(ROU-11056)
|
||||
*
|
||||
* Vue handles the error case for when there is no
|
||||
* associated page matching the tab `href`.
|
||||
*
|
||||
* More investigation is needed to determine if we
|
||||
* override the error handling and provide our own
|
||||
* error message.
|
||||
*/
|
||||
|
||||
return h(
|
||||
"ion-tabs",
|
||||
{
|
||||
@@ -128,7 +157,7 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
"z-index": "0",
|
||||
},
|
||||
},
|
||||
childrenToRender
|
||||
slottedContent
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon
|
||||
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
|
||||
import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js';
|
||||
import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js';
|
||||
import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js';
|
||||
import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js';
|
||||
import { defineCustomElement as defineIonTextarea } from '@ionic/core/components/ion-textarea.js';
|
||||
import { defineCustomElement as defineIonThumbnail } from '@ionic/core/components/ion-thumbnail.js';
|
||||
@@ -824,6 +825,14 @@ export const IonSplitPane = /*@__PURE__*/ defineContainer<JSX.IonSplitPane>('ion
|
||||
]);
|
||||
|
||||
|
||||
export const IonTab = /*@__PURE__*/ defineContainer<JSX.IonTab>('ion-tab', defineIonTab, [
|
||||
'active',
|
||||
'delegate',
|
||||
'tab',
|
||||
'component'
|
||||
]);
|
||||
|
||||
|
||||
export const IonText = /*@__PURE__*/ defineContainer<JSX.IonText>('ion-text', defineIonText, [
|
||||
'color'
|
||||
]);
|
||||
|
||||
@@ -161,6 +161,10 @@ const routes: Array<RouteRecordRaw> = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tabs-basic',
|
||||
component: () => import('@/views/TabsBasic.vue')
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
<ion-item router-link="/tabs-secondary" id="tab-secondary">
|
||||
<ion-label>Tabs Secondary</ion-label>
|
||||
</ion-item>
|
||||
<ion-item router-link="/tabs-basic" id="tab-basic">
|
||||
<ion-label>Tabs with Basic Navigation</ion-label>
|
||||
</ion-item>
|
||||
<ion-item router-link="/lifecycle" id="lifecycle">
|
||||
<ion-label>Lifecycle</ion-label>
|
||||
</ion-item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ion-page data-pageid="tabs">
|
||||
<ion-content>
|
||||
<ion-tabs id="tabs">
|
||||
<ion-tabs id="tabs" @ionTabsWillChange="onTabWillChange" @ionTabsDidChange="onTabDidChange">
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button
|
||||
@@ -47,7 +47,15 @@ export default defineComponent({
|
||||
]
|
||||
}
|
||||
|
||||
return { tabs, addTab }
|
||||
const onTabWillChange = (e: { tab: string }) => {
|
||||
console.log('ionTabsWillChange', e.tab);
|
||||
}
|
||||
|
||||
const onTabDidChange = (e: { tab: string }) => {
|
||||
console.log('ionTabsDidChange', e.tab);
|
||||
}
|
||||
|
||||
return { tabs, addTab, onTabWillChange, onTabDidChange }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
55
packages/vue/test/base/src/views/TabsBasic.vue
Normal file
55
packages/vue/test/base/src/views/TabsBasic.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<ion-page data-pageid="tabs">
|
||||
<ion-content>
|
||||
<ion-tabs id="tabs" @ionTabsWillChange="onTabWillChange" @ionTabsDidChange="onTabDidChange">
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button
|
||||
v-for="tab in tabs"
|
||||
:tab="'tab' + tab.id"
|
||||
:key="tab.id"
|
||||
>
|
||||
<ion-icon :icon="tab.icon" />
|
||||
<ion-label>Tab {{ tab.id }}</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
|
||||
<ion-tab tab="tab1" data-pageid="tab1">
|
||||
<ion-label>Tab 1 Content</ion-label>
|
||||
</ion-tab>
|
||||
<ion-tab tab="tab2" data-pageid="tab2">
|
||||
<ion-label>Tab 2 Content</ion-label>
|
||||
</ion-tab>
|
||||
<ion-tab tab="tab3" data-pageid="tab3">
|
||||
<ion-label>Tab 3 Content</ion-label>
|
||||
</ion-tab>
|
||||
</ion-tabs>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IonTabBar, IonTabButton, IonTabs, IonContent, IonLabel, IonIcon, IonPage, IonTab } from '@ionic/vue';
|
||||
import { ellipse, square, triangle } from 'ionicons/icons';
|
||||
import { ref, defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonContent, IonLabel, IonTabs, IonTabBar, IonTabButton, IonIcon, IonPage, IonTab },
|
||||
setup() {
|
||||
const tabs = ref([
|
||||
{ id: 1, icon: triangle },
|
||||
{ id: 2, icon: ellipse },
|
||||
{ id: 3, icon: square }
|
||||
])
|
||||
|
||||
const onTabWillChange = (e: { tab: string }) => {
|
||||
console.log('ionTabsWillChange', e.tab);
|
||||
}
|
||||
|
||||
const onTabDidChange = (e: { tab: string }) => {
|
||||
console.log('ionTabsDidChange', e.tab);
|
||||
}
|
||||
|
||||
return { tabs, onTabWillChange, onTabDidChange }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,113 +4,6 @@ import { createRouter, createWebHistory } from '@ionic/vue-router';
|
||||
import { IonicVue, IonRouterOutlet, IonPage, IonTabs, IonTabBar } from '@ionic/vue';
|
||||
|
||||
describe('ion-tab-bar', () => {
|
||||
it('should render in the top slot', async () => {
|
||||
const Tabs = {
|
||||
components: { IonPage, IonTabs, IonTabBar, IonRouterOutlet },
|
||||
template: `
|
||||
<ion-page>
|
||||
<ion-tabs>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
<ion-tab-bar slot="top"></ion-tab-bar>
|
||||
</ion-tabs>
|
||||
</ion-page>
|
||||
`,
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes: [
|
||||
{ path: '/', component: Tabs }
|
||||
]
|
||||
});
|
||||
|
||||
router.push('/');
|
||||
await router.isReady();
|
||||
const wrapper = mount(Tabs, {
|
||||
global: {
|
||||
plugins: [router, IonicVue]
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = wrapper.findComponent(IonTabs);
|
||||
const children = tabs.vm.$el.children;
|
||||
expect(children[0].tagName).toEqual('ION-TAB-BAR');
|
||||
expect(children[1].tagName).toEqual('DIV');
|
||||
expect(children[1].className).toEqual('tabs-inner');
|
||||
|
||||
});
|
||||
|
||||
it('should render in the bottom slot', async () => {
|
||||
const Tabs = {
|
||||
components: { IonPage, IonTabs, IonTabBar, IonRouterOutlet },
|
||||
template: `
|
||||
<ion-page>
|
||||
<ion-tabs>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
<ion-tab-bar slot="bottom"></ion-tab-bar>
|
||||
</ion-tabs>
|
||||
</ion-page>
|
||||
`,
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes: [
|
||||
{ path: '/', component: Tabs }
|
||||
]
|
||||
});
|
||||
|
||||
router.push('/');
|
||||
await router.isReady();
|
||||
const wrapper = mount(Tabs, {
|
||||
global: {
|
||||
plugins: [router, IonicVue]
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = wrapper.findComponent(IonTabs);
|
||||
const children = tabs.vm.$el.children;
|
||||
expect(children[0].tagName).toEqual('DIV');
|
||||
expect(children[0].className).toEqual('tabs-inner');
|
||||
expect(children[1].tagName).toEqual('ION-TAB-BAR');
|
||||
});
|
||||
|
||||
it('should render in the default slot', async () => {
|
||||
const Tabs = {
|
||||
components: { IonPage, IonTabs, IonTabBar, IonRouterOutlet },
|
||||
template: `
|
||||
<ion-page>
|
||||
<ion-tabs>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
<ion-tab-bar></ion-tab-bar>
|
||||
</ion-tabs>
|
||||
</ion-page>
|
||||
`,
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes: [
|
||||
{ path: '/', component: Tabs }
|
||||
]
|
||||
});
|
||||
|
||||
router.push('/');
|
||||
await router.isReady();
|
||||
const wrapper = mount(Tabs, {
|
||||
global: {
|
||||
plugins: [router, IonicVue]
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = wrapper.findComponent(IonTabs);
|
||||
const children = tabs.vm.$el.children;
|
||||
const tabsInner = children[0];
|
||||
expect(tabsInner.tagName).toEqual('DIV');
|
||||
expect(tabsInner.className).toEqual('tabs-inner');
|
||||
expect(tabsInner.children[0].tagName).toEqual('ION-ROUTER-OUTLET');
|
||||
});
|
||||
|
||||
// Verifies the fix for https://github.com/ionic-team/ionic-framework/issues/22642
|
||||
it('should not fail on non tab button elements', async () => {
|
||||
const Tabs = {
|
||||
|
||||
Reference in New Issue
Block a user