Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2617,7 +2617,6 @@ ion-tab-button,part,native

ion-tabs,shadow
ion-tabs,prop,mode,"ios" | "md",undefined,false,false
ion-tabs,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-tabs,method,getSelected,getSelected() => Promise<string | undefined>
ion-tabs,method,getTab,getTab(tab: string | HTMLIonTabElement) => Promise<HTMLIonTabElement | undefined>
ion-tabs,method,select,select(tab: string | HTMLIonTabElement) => Promise<boolean>
Expand Down
8 changes: 0 additions & 8 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4034,10 +4034,6 @@ export namespace Components {
*/
"select": (tab: string | HTMLIonTabElement) => Promise<boolean>;
"setRouteId": (id: string) => Promise<RouteWrite>;
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* @default false
*/
Expand Down Expand Up @@ -10049,10 +10045,6 @@ declare namespace LocalJSX {
* Emitted when the navigation is about to transition to a new component.
*/
"onIonTabsWillChange"?: (event: IonTabsCustomEvent<{ tab: string }>) => void;
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* @default false
*/
Expand Down
6 changes: 3 additions & 3 deletions core/src/components/tabs/tabs.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import "../../themes/native/native.globals";
@use "../../themes/mixins" as mixins;

:host {
@include position(0, 0, 0, 0);
@include mixins.position(0, 0, 0, 0);

display: flex;
position: absolute;
Expand All @@ -12,7 +12,7 @@
height: 100%;

contain: layout size style;
z-index: $z-index-page-container;
z-index: 0;
}

.tabs-inner {
Expand Down
23 changes: 10 additions & 13 deletions core/src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { TabButtonClickEventDetail } from '../tab-bar/tab-bar-interface';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
* @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component.
*
* @slot - Content is placed between the named slots if provided without a slot.
* @slot top - Content is placed at the top of the screen.
Expand Down Expand Up @@ -82,7 +81,7 @@ export class Tabs implements NavOutlet {
return;
}

const tab = this.selectedTab ? this.selectedTab.tab : undefined;
const tab = this.selectedTab?.tab;

// If tabs has no selected tab but tab-bar already has a selected-tab set,
// don't overwrite it. This handles cases where tab-bar is used without ion-tab elements.
Expand Down Expand Up @@ -130,7 +129,7 @@ export class Tabs implements NavOutlet {
*/
@Method()
getSelected(): Promise<string | undefined> {
return Promise.resolve(this.selectedTab ? this.selectedTab.tab : undefined);
return Promise.resolve(this.selectedTab?.tab);
}

/** @internal */
Expand Down Expand Up @@ -158,7 +157,7 @@ export class Tabs implements NavOutlet {

private setActive(selectedTab: HTMLIonTabElement): Promise<void> {
if (this.transitioning) {
return Promise.reject('transitioning already happening');
return Promise.reject(new Error('transitioning already happening'));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change attaches a stack trace to the rejection, making it easier to identify where the error originated when debugging.

}

this.transitioning = true;
Expand Down Expand Up @@ -190,10 +189,7 @@ export class Tabs implements NavOutlet {

private notifyRouter() {
if (this.useRouter) {
const router = document.querySelector('ion-router');
if (router) {
return router.navChanged('forward');
}
return this.router?.navChanged('forward');
}
return Promise.resolve(false);
}
Expand All @@ -207,13 +203,14 @@ export class Tabs implements NavOutlet {
return Array.from(this.el.querySelectorAll('ion-tab'));
}

private get router() {
return document.querySelector('ion-router');
}

private onTabClicked = (ev: CustomEvent<TabButtonClickEventDetail>) => {
const { href, tab } = ev.detail;
if (this.useRouter && href !== undefined) {
const router = document.querySelector('ion-router');
if (router) {
router.push(href);
}
this.router?.push(href);
} else {
this.select(tab);
}
Expand All @@ -236,7 +233,7 @@ const getTab = (tabs: HTMLIonTabElement[], tab: string | HTMLIonTabElement): HTM
const tabEl = typeof tab === 'string' ? tabs.find((t) => t.tab === tab) : tab;

if (!tabEl) {
printIonError(`[ion-tabs] - Tab with id: "${tabEl}" does not exist`);
printIonError(`[ion-tabs] - Tab with id: "${tab}" does not exist`);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kept showing as [ion-tabs] - Tab with id: "undefined" does not exist so I fixed it by passing the correct variable.

}
return tabEl;
};
4 changes: 1 addition & 3 deletions core/src/components/tabs/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@
</ion-header>
<ion-content class="ion-padding">
<h1>Tab One</h1>
<button class="expand" onclick="updateBadgeCount()">Update Badge Count</button>
<button class="expand" onclick="updateBadgeColor()">Update Badge Color</button>
Comment on lines -45 to -46
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These buttons weren't doing anything. I do plan on adding updateBadgeCount to the tab-bar/test/badge page when we migrate.


<ion-fab slot="fixed" horizontal="end" vertical="bottom">
<ion-fab-button class="custom-white">
Expand Down Expand Up @@ -129,7 +127,7 @@ <h1>Hidden Tab</h1>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
Page Four
<h1>Page Four</h1>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the setup of the other 3 pages.

</ion-content>
`;
}
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/tabs/test/basic/tabs.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('tabs: basic'), () => {
test('should show correct tab when clicking the tab button', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/tabs/test/placements/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Tab - Basic</title>
<title>Tab - Placements</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/tabs/test/placements/tabs.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('tabs: placement'), () => {
test.beforeEach(async ({ page }) => {
Expand Down
174 changes: 174 additions & 0 deletions core/src/components/tabs/test/tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { newSpecPage } from '@stencil/core/testing';

import { Tab } from '../../tab/tab';
import { Tabs } from '../tabs';

const HTML = `
<ion-tabs>
<ion-tab tab="tab-one"></ion-tab>
<ion-tab tab="tab-two"></ion-tab>
</ion-tabs>
`;

describe('ion-tabs', () => {
describe('getSelected()', () => {
it('should return the name of the initially selected tab', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
expect(await tabsEl.getSelected()).toBe('tab-one');
});
});

describe('getTab()', () => {
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
// getTab() calls printIonError when the tab id is not found, suppress to keep test output clean
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
consoleErrorSpy.mockRestore();
});

it('should return the element for an existing tab id', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
const tabEl = page.body.querySelector('ion-tab[tab="tab-two"]')!;
expect(await tabsEl.getTab('tab-two')).toBe(tabEl);
});

it('should return undefined for a non-existent tab id', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
expect(await tabsEl.getTab('does-not-exist')).toBeUndefined();
});
});

describe('select()', () => {
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
// select() calls printIonError when the tab id is not found, suppress to keep test output clean
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
consoleErrorSpy.mockRestore();
});

it('should switch to the specified tab and return true', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
const result = await tabsEl.select('tab-two');

expect(result).toBe(true);
expect(await tabsEl.getSelected()).toBe('tab-two');
});

it('should return false when selecting the already active tab', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
const result = await tabsEl.select('tab-one');

expect(result).toBe(false);
});

it('should return false when selecting a non-existent tab id', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
const result = await tabsEl.select('does-not-exist');

expect(result).toBe(false);
});
});

describe('events', () => {
it('should emit ionTabsWillChange and ionTabsDidChange when switching tabs', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;

const willChangeSpy = jest.fn();
const didChangeSpy = jest.fn();
tabsEl.addEventListener('ionTabsWillChange', willChangeSpy);
tabsEl.addEventListener('ionTabsDidChange', didChangeSpy);

await tabsEl.select('tab-two');
await page.waitForChanges();

expect(willChangeSpy).toHaveBeenCalledWith(expect.objectContaining({ detail: { tab: 'tab-two' } }));
expect(didChangeSpy).toHaveBeenCalledWith(expect.objectContaining({ detail: { tab: 'tab-two' } }));
});

it('should not emit ionTabsDidChange when selecting the already active tab', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;

const didChangeSpy = jest.fn();
tabsEl.addEventListener('ionTabsDidChange', didChangeSpy);

await tabsEl.select('tab-one');

expect(didChangeSpy).not.toHaveBeenCalled();
});
});

describe('error handling', () => {
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
// select() calls printIonError when the tab id is not found, suppress to keep test output clean
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
consoleErrorSpy.mockRestore();
});

it('should log the correct tab id when selecting a non-existent tab', async () => {
const page = await newSpecPage({
components: [Tabs, Tab],
html: HTML,
});

const tabsEl = page.body.querySelector('ion-tabs')!;
await tabsEl.select('does-not-exist');

expect(consoleErrorSpy).toHaveBeenCalledWith(
'[Ionic Error]: [ion-tabs] - Tab with id: "does-not-exist" does not exist'
);
});
});
});
2 changes: 1 addition & 1 deletion core/src/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export {
export { SearchbarCustomEvent } from './components/searchbar/searchbar-interface';
export { SegmentCustomEvent } from './components/segment/segment-interface';
export { SelectCustomEvent, SelectCompareFn } from './components/select/select-interface';
export { TabsCustomEvent } from './components/tabs/tabs-interface';
export { TabsCustomEvent } from './components/tabs/tabs.interfaces';
export { TextareaCustomEvent } from './components/textarea/textarea-interface';
export { ToastOptions } from './components/toast/toast-interface';
export { ToggleCustomEvent } from './components/toggle/toggle-interface';
Expand Down
Loading