Skip to content

Commit c61265a

Browse files
authored
fix(modal): prevent sheet gesture crash with late-bound breakpoints (#31202)
Issue number: internal --------- ## What is the current behavior? Currently, when an `ion-modal`'s `breakpoints` are set after the component has loaded, `sortedBreakpoints` stays empty. `breakpointsChanged` had no `@Watch`, so it only ran once from the manual call in `componentDidLoad`. `present()` still recalculates `isSheetModal` to `true` from the late-set `breakpoints` (added in #30839), so the sheet gesture initializes with an empty breakpoints array and throws `Reduce of empty array with no initial value` on the `breakpoints.reduce(...)` in `sheet.ts` when the sheet is swiped, which breaks swiping entirely. This shows up with framework bindings that land after the web component loads, e.g., an inline `ion-modal` inside an Angular component rendered through a plain `router-outlet` with zoneless change detection. It doesn't reproduce with zone change detection, with `ModalController.create`, or with the modal inside `ion-router-outlet`, since those set `breakpoints` before the modal presents. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> Now `breakpointsChanged` has `@Watch('breakpoints')`, so `breakpoints` set after the component loads update `sortedBreakpoints`. This mirrors how `trigger`/`triggerChanged` already handles late binding (a watch plus a manual call in `componentDidLoad`). The sheet gesture then gets the real breakpoints and swiping works. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information Sheet modal preview: - https://ionic-framework-git-fix-modal-late-breakpoints-ionic1.vercel.app/src/components/modal/test/sheet Note that the preview won't show *this* working, but will show the modal at least isn't broken from this change. Current dev build: ``` 8.8.10-dev.11781019195.14a7eeaf ```
1 parent b8e8a41 commit c61265a

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

core/src/components/modal/modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
438438
*/
439439
@Event() ionDragEnd!: EventEmitter<ModalDragEventDetail>;
440440

441+
@Watch('breakpoints')
441442
breakpointsChanged(breakpoints: number[] | undefined) {
442443
if (breakpoints !== undefined) {
443444
this.sortedBreakpoints = breakpoints.sort((a, b) => a - b);

core/src/components/modal/test/sheet/modal.e2e.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,51 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
399399
expect(Object.keys(dragEndEvent.detail).length).toBe(5);
400400
});
401401
});
402+
403+
test.describe(title('sheet modal: late breakpoints binding'), () => {
404+
test('should not crash when swiped after breakpoints are set after the modal loads', async ({ page }) => {
405+
const pageErrors: string[] = [];
406+
page.on('pageerror', (err) => pageErrors.push(err.message));
407+
408+
await page.setContent(
409+
`
410+
<ion-modal initial-breakpoint="1">
411+
<ion-content>Modal Content</ion-content>
412+
</ion-modal>
413+
`,
414+
config
415+
);
416+
417+
const modal = page.locator('ion-modal');
418+
419+
/**
420+
* Simulates a JS framework (e.g. Angular with zoneless change detection)
421+
* applying the `breakpoints` binding after the web component has finished
422+
* loading. `setContent` resolves after `componentDidLoad`, so this lands
423+
* too late for the manual `breakpointsChanged()` call in `componentDidLoad`
424+
* to pick it up
425+
*/
426+
await modal.evaluate((el: HTMLIonModalElement) => {
427+
el.breakpoints = [0, 1];
428+
});
429+
430+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
431+
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
432+
433+
await modal.evaluate((el: HTMLIonModalElement) => el.present());
434+
await ionModalDidPresent.next();
435+
436+
// Swiping the sheet down should snap it to breakpoint 0 and dismiss it
437+
// without throwing an error
438+
const handle = page.locator('ion-modal .modal-handle');
439+
await expect(handle).toBeVisible();
440+
await dragElementBy(handle, page, 0, 600);
441+
442+
// Flush any pending errors from the gesture's end handler
443+
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
444+
expect(pageErrors).toEqual([]);
445+
446+
await ionModalDidDismiss.next();
447+
});
448+
});
402449
});

0 commit comments

Comments
 (0)