- `,
+
+ `,
styles: [
`
:host {
@@ -41,15 +55,28 @@ import { StackEvent } from './stack-utils';
],
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
-export class IonTabs {
+export class IonTabs implements AfterContentInit, AfterContentChecked {
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;
+ @ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef;
+
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
+ @ContentChildren(IonTabBar) tabBars: QueryList;
@Output() ionTabsWillChange = new EventEmitter<{ tab: string }>();
@Output() ionTabsDidChange = new EventEmitter<{ tab: string }>();
+ private tabBarSlot = 'bottom';
+
constructor(private navCtrl: NavController) {}
+ ngAfterContentInit(): void {
+ this.detectSlotChanges();
+ }
+
+ ngAfterContentChecked(): void {
+ this.detectSlotChanges();
+ }
+
/**
* @internal
*/
@@ -137,4 +164,48 @@ export class IonTabs {
getSelected(): string | undefined {
return this.outlet.getActiveStackId();
}
+
+ /**
+ * Detects changes to the slot attribute of the tab bar.
+ *
+ * If the slot attribute has changed, then the tab bar
+ * should be relocated to the new slot position.
+ */
+ private detectSlotChanges(): void {
+ this.tabBars.forEach((tabBar: any) => {
+ // el is a protected attribute from the generated component wrapper
+ const currentSlot = tabBar.el.getAttribute('slot');
+
+ if (currentSlot !== this.tabBarSlot) {
+ this.tabBarSlot = currentSlot;
+ this.relocateTabBar();
+ }
+ });
+ }
+
+ /**
+ * Relocates the tab bar to the new slot position.
+ */
+ private relocateTabBar(): void {
+ /**
+ * `el` is a protected attribute from the generated component wrapper.
+ * To avoid having to manually create the wrapper for tab bar, we
+ * cast the tab bar to any and access the protected attribute.
+ */
+ const tabBar = (this.tabBar as any).el as HTMLElement;
+
+ if (this.tabBarSlot === 'top') {
+ /**
+ * A tab bar with a slot of "top" should be inserted
+ * at the top of the container.
+ */
+ this.tabsInner.nativeElement.before(tabBar);
+ } else {
+ /**
+ * A tab bar with a slot of "bottom" or without a slot
+ * should be inserted at the end of the container.
+ */
+ this.tabsInner.nativeElement.after(tabBar);
+ }
+ }
}
diff --git a/angular/test/base/e2e/src/tabs.spec.ts b/angular/test/base/e2e/src/tabs.spec.ts
index 618c064751..48d1dd5c6d 100644
--- a/angular/test/base/e2e/src/tabs.spec.ts
+++ b/angular/test/base/e2e/src/tabs.spec.ts
@@ -435,6 +435,30 @@ describe('Tabs', () => {
})
})
+it('Tabs should support conditional slots', () => {
+ cy.visit('/tabs-slots');
+
+ cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 1);
+
+ // Click the button to change the slot to the top
+ cy.get('#set-slot-top').click();
+
+ // The tab bar should be removed from the bottom
+ cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 0);
+
+ // The tab bar should be added to the top
+ cy.get('ion-tabs ion-tab-bar + .tabs-inner').should('have.length', 1);
+
+ // Click the button to change the slot to the bottom
+ cy.get('#set-slot-bottom').click();
+
+ // The tab bar should be removed from the top
+ cy.get('ion-tabs ion-tab-bar + .tabs-inner').should('have.length', 0);
+
+ // The tab bar should be added to the bottom
+ cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 1);
+});
+
function testTabTitle(title) {
const tab = getSelectedTab();
diff --git a/angular/test/base/src/app/app-routing.module.ts b/angular/test/base/src/app/app-routing.module.ts
index c30f654b0e..183d235e94 100644
--- a/angular/test/base/src/app/app-routing.module.ts
+++ b/angular/test/base/src/app/app-routing.module.ts
@@ -57,6 +57,10 @@ const routes: Routes = [
path: 'tabs-global',
loadChildren: () => import('./tabs-global/tabs-global.module').then(m => m.TabsGlobalModule)
},
+ {
+ path: 'tabs-slots',
+ loadComponent: () => import('./tabs-slots.component').then(c => c.TabsSlotsComponent)
+ },
{
path: 'nested-outlet',
component: NestedOutletComponent,
diff --git a/angular/test/base/src/app/tabs-slots.component.ts b/angular/test/base/src/app/tabs-slots.component.ts
new file mode 100644
index 0000000000..68715ff9c2
--- /dev/null
+++ b/angular/test/base/src/app/tabs-slots.component.ts
@@ -0,0 +1,32 @@
+import { Component } from "@angular/core";
+import { IonicModule } from "@ionic/angular";
+
+/**
+ * Test purpose: Validates that the tab bar is relocated to the
+ * correct location when the slot attribute changes or is bound
+ * to a variable.
+ */
+@Component({
+ selector: 'app-tabs-slots',
+ template: `
+
+
+
+
+
+
+
+
+ `,
+ imports: [IonicModule],
+ standalone: true
+})
+export class TabsSlotsComponent {
+
+ slot?: string;
+
+ setSlot(newSlot: string) {
+ this.slot = newSlot;
+ }
+
+}