From 060dc9bb42fa9865576b35d03551752d33552ee8 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 5 May 2026 15:45:51 -0700 Subject: [PATCH 1/2] feat(tab): modular ionic migration and lazy-load specsx --- core/src/components.d.ts | 8 ---- core/src/components/tab/tab.tsx | 1 - core/src/components/tab/test/tab.spec.ts | 45 +++++++++++++++++++ packages/angular/src/directives/proxies.ts | 4 +- .../standalone/src/directives/proxies.ts | 4 +- 5 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 core/src/components/tab/test/tab.spec.ts diff --git a/core/src/components.d.ts b/core/src/components.d.ts index fb7e5f9cff6..ba374706e35 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3928,10 +3928,6 @@ export namespace Components { * A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. */ "tab": string; - /** - * The theme determines the visual appearance of the component. - */ - "theme"?: "ios" | "md" | "ionic"; } interface IonTabBar { /** @@ -9941,10 +9937,6 @@ declare namespace LocalJSX { * A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. */ "tab": string; - /** - * The theme determines the visual appearance of the component. - */ - "theme"?: "ios" | "md" | "ionic"; } interface IonTabBar { /** diff --git a/core/src/components/tab/tab.tsx b/core/src/components/tab/tab.tsx index ef9f5ac87cf..aabb9768be2 100644 --- a/core/src/components/tab/tab.tsx +++ b/core/src/components/tab/tab.tsx @@ -7,7 +7,6 @@ import type { ComponentRef, FrameworkDelegate } from '../../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. */ @Component({ tag: 'ion-tab', diff --git a/core/src/components/tab/test/tab.spec.ts b/core/src/components/tab/test/tab.spec.ts new file mode 100644 index 00000000000..8f80b1a42b0 --- /dev/null +++ b/core/src/components/tab/test/tab.spec.ts @@ -0,0 +1,45 @@ +import { newSpecPage } from '@stencil/core/testing'; + +import type { FrameworkDelegate } from '../../../interface'; +import { Tab } from '../tab'; + +const mockDelegate = (attachViewToDom: FrameworkDelegate['attachViewToDom']): FrameworkDelegate => ({ + attachViewToDom, + removeViewFromDom: jest.fn().mockResolvedValue(undefined), +}); + +describe('ion-tab: lazy loading', () => { + it('should attach the component only once across multiple setActive calls', async () => { + const page = await newSpecPage({ + components: [Tab], + html: '', + }); + + const tabEl = page.body.querySelector('ion-tab') as any; + const attachViewToDom = jest.fn().mockResolvedValue(document.createElement('div')); + tabEl.delegate = mockDelegate(attachViewToDom); + + await tabEl.setActive(); + await tabEl.setActive(); + + expect(attachViewToDom).toHaveBeenCalledTimes(1); + }); + + it('should not retry attach after a failed first attempt', async () => { + const page = await newSpecPage({ + components: [Tab], + html: '', + }); + + const tabEl = page.body.querySelector('ion-tab') as any; + const attachViewToDom = jest.fn().mockRejectedValue(new Error('attach failed')); + tabEl.delegate = mockDelegate(attachViewToDom); + + // First call rejects because the delegate throws. The loaded flag is already + // set to true before the attempt, so the failure is permanent. + await expect(tabEl.setActive()).rejects.toThrow('attach failed'); + await tabEl.setActive(); + + expect(attachViewToDom).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 209bb68e4a9..ce66209d92d 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2336,7 +2336,7 @@ export declare interface IonSplitPane extends Components.IonSplitPane { @ProxyCmp({ - inputs: ['component', 'mode', 'tab', 'theme'], + inputs: ['component', 'mode', 'tab'], methods: ['setActive'] }) @Component({ @@ -2344,7 +2344,7 @@ export declare interface IonSplitPane extends Components.IonSplitPane { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['component', 'mode', 'tab', 'theme'], + inputs: ['component', 'mode', 'tab'], }) export class IonTab { protected el: HTMLIonTabElement; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index ecff6aba767..9051dd0a36c 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -2077,7 +2077,7 @@ export declare interface IonSplitPane extends Components.IonSplitPane { @ProxyCmp({ defineCustomElementFn: defineIonTab, - inputs: ['component', 'mode', 'tab', 'theme'], + inputs: ['component', 'mode', 'tab'], methods: ['setActive'] }) @Component({ @@ -2085,7 +2085,7 @@ export declare interface IonSplitPane extends Components.IonSplitPane { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['component', 'mode', 'tab', 'theme'], + inputs: ['component', 'mode', 'tab'], standalone: true }) export class IonTab { From cb81ec0ad25c6648282edba16968ac5b35b51e44 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 5 May 2026 15:56:51 -0700 Subject: [PATCH 2/2] chore(): run build --- core/api.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/api.txt b/core/api.txt index f2af11afc9b..0ae3d2a0730 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2547,7 +2547,6 @@ ion-tab,shadow ion-tab,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false ion-tab,prop,mode,"ios" | "md",undefined,false,false ion-tab,prop,tab,string,undefined,true,false -ion-tab,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-tab,method,setActive,setActive() => Promise ion-tab-bar,shadow