Skip to content

Commit cbfe7cc

Browse files
authored
fix(angular): forward generic type parameter on ModalOptions and PopoverOptions (#31022)
Issue number: resolves #31012 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? `ModalOptions` and `PopoverOptions` in `@ionic/angular` are non-generic type aliases. Using `ModalOptions<typeof MyComponent>` causes a typescript error, even though the core `@ionic/core` types accept a generic parameter. ## What is the new behavior? `ModalOptions` and `PopoverOptions` now forward the generic type parameter from their core counterparts, allowing usage like` ModalOptions<typeof MyComponent>`. The default parameter preserves backward compatibility and existing code using ModalOptions without a generic should continue to work. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information This was the initial and intended behavior, but got broken in #30899 unintentionally. This PR fixes the issue and creates a test to ensure it doesn't happen again. Current dev build: ``` 8.8.2-dev.11773931429.15b2a51c ```
1 parent 5fdaba2 commit cbfe7cc

File tree

6 files changed

+104
-3
lines changed

6 files changed

+104
-3
lines changed
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import type { Injector } from '@angular/core';
2-
import type { ModalOptions as CoreModalOptions, PopoverOptions as CorePopoverOptions } from '@ionic/core/components';
2+
import type {
3+
ComponentRef,
4+
ModalOptions as CoreModalOptions,
5+
PopoverOptions as CorePopoverOptions,
6+
} from '@ionic/core/components';
37

48
/**
59
* Modal options with Angular-specific injector support.
610
* Extends @ionic/core ModalOptions with an optional injector property.
711
*/
8-
export type ModalOptions = CoreModalOptions & {
12+
export type ModalOptions<T extends ComponentRef = ComponentRef> = CoreModalOptions<T> & {
913
injector?: Injector;
1014
};
1115

1216
/**
1317
* Popover options with Angular-specific injector support.
1418
* Extends @ionic/core PopoverOptions with an optional injector property.
1519
*/
16-
export type PopoverOptions = CorePopoverOptions & {
20+
export type PopoverOptions<T extends ComponentRef = ComponentRef> = CorePopoverOptions<T> & {
1721
injector?: Injector;
1822
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Modal Options: Generic Type Parameter', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/standalone/modal-options-generic');
6+
});
7+
8+
test('should open modal created with ModalOptions<typeof Component>', async ({ page }, testInfo) => {
9+
testInfo.annotations.push({
10+
type: 'issue',
11+
description: 'https://github.com/ionic-team/ionic-framework/issues/31012',
12+
});
13+
14+
await page.locator('ion-button#open-modal').click();
15+
16+
await expect(page.locator('ion-modal')).toBeVisible();
17+
18+
const greeting = page.locator('#greeting');
19+
await expect(greeting).toHaveText('hello world');
20+
21+
await page.locator('#close-modal').click();
22+
await expect(page.locator('ion-modal')).not.toBeVisible();
23+
});
24+
});

packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const routes: Routes = [
2424
},
2525
{ path: 'programmatic-modal', loadComponent: () => import('../programmatic-modal/programmatic-modal.component').then(c => c.ProgrammaticModalComponent) },
2626
{ path: 'modal-custom-injector', loadComponent: () => import('../modal-custom-injector/modal-custom-injector.component').then(c => c.ModalCustomInjectorComponent) },
27+
{ path: 'modal-options-generic', loadComponent: () => import('../modal-options-generic/modal-options-generic.component').then(c => c.ModalOptionsGenericComponent) },
2728
{ path: 'popover-custom-injector', loadComponent: () => import('../popover-custom-injector/popover-custom-injector.component').then(c => c.PopoverCustomInjectorComponent) },
2829
{ path: 'router-outlet', loadComponent: () => import('../router-outlet/router-outlet.component').then(c => c.RouterOutletComponent) },
2930
{ path: 'back-button', loadComponent: () => import('../back-button/back-button.component').then(c => c.BackButtonComponent) },

packages/angular/test/base/src/app/standalone/home-page/home-page.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@
125125
Modal Custom Injector Test
126126
</ion-label>
127127
</ion-item>
128+
<ion-item routerLink="/standalone/modal-options-generic">
129+
<ion-label>
130+
Modal Options Generic Test
131+
</ion-label>
132+
</ion-item>
128133
<ion-item routerLink="/standalone/overlay-controllers">
129134
<ion-label>
130135
Overlay Controllers Test
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component, inject } from '@angular/core';
2+
import { IonContent, IonHeader, IonTitle, IonToolbar, IonButton, ModalController } from '@ionic/angular/standalone';
3+
import type { ModalOptions } from '@ionic/angular/standalone';
4+
import { GenericModalComponent } from './modal/modal.component';
5+
6+
@Component({
7+
selector: 'app-modal-options-generic',
8+
template: `
9+
<ion-header>
10+
<ion-toolbar>
11+
<ion-title>Modal Options Generic Test</ion-title>
12+
</ion-toolbar>
13+
</ion-header>
14+
<ion-content class="ion-padding">
15+
<ion-button id="open-modal" (click)="openModal()">
16+
Open Modal
17+
</ion-button>
18+
</ion-content>
19+
`,
20+
standalone: true,
21+
imports: [IonContent, IonHeader, IonTitle, IonToolbar, IonButton],
22+
})
23+
export class ModalOptionsGenericComponent {
24+
private modalController = inject(ModalController);
25+
26+
async openModal() {
27+
// Regression: ModalOptions<T> must accept a generic type parameter (#31012)
28+
const opts: ModalOptions<typeof GenericModalComponent> = {
29+
component: GenericModalComponent,
30+
componentProps: {
31+
greeting: 'hello world',
32+
},
33+
};
34+
35+
const modal = await this.modalController.create(opts);
36+
await modal.present();
37+
}
38+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Component, Input } from '@angular/core';
2+
import { IonContent, IonHeader, IonTitle, IonToolbar, IonButton, IonButtons } from '@ionic/angular/standalone';
3+
4+
@Component({
5+
selector: 'app-generic-modal',
6+
template: `
7+
<ion-header>
8+
<ion-toolbar>
9+
<ion-title>Generic Modal</ion-title>
10+
<ion-buttons slot="end">
11+
<ion-button id="close-modal" (click)="dismiss()">Close</ion-button>
12+
</ion-buttons>
13+
</ion-toolbar>
14+
</ion-header>
15+
<ion-content>
16+
<p id="greeting">{{ greeting }}</p>
17+
</ion-content>
18+
`,
19+
standalone: true,
20+
imports: [IonContent, IonHeader, IonTitle, IonToolbar, IonButton, IonButtons],
21+
})
22+
export class GenericModalComponent {
23+
@Input() greeting = '';
24+
modal: HTMLIonModalElement | undefined;
25+
26+
dismiss() {
27+
this.modal?.dismiss();
28+
}
29+
}

0 commit comments

Comments
 (0)