feat(react, vue, angular): use tabs without router (#29794)

Issue number: resolves #25184 

---------

Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
This commit is contained in:
Maria Hutt
2024-08-26 08:13:20 -07:00
committed by GitHub
parent 4580edc21f
commit 867066b4eb
38 changed files with 1946 additions and 1281 deletions

View File

@ -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();
}