diff --git a/angular/src/directives/overlays/modal.ts b/angular/src/directives/overlays/modal.ts index 7cb134e9cd..664a99a785 100644 --- a/angular/src/directives/overlays/modal.ts +++ b/angular/src/directives/overlays/modal.ts @@ -11,7 +11,7 @@ import { TemplateRef, } from '@angular/core'; import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils'; -import { Components } from '@ionic/core'; +import { Components, ModalBreakpointChangeEventDetail } from '@ionic/core'; export declare interface IonModal extends Components.IonModal { /** @@ -30,6 +30,10 @@ export declare interface IonModal extends Components.IonModal { * Emitted after the modal has dismissed. */ ionModalDidDismiss: EventEmitter; + /** + * Emitted after the modal breakpoint has changed. + */ + ionBreakpointDidChange: EventEmitter>; /** * Emitted after the modal has presented. Shorthand for ionModalWillDismiss. */ @@ -68,7 +72,7 @@ export declare interface IonModal extends Components.IonModal { 'translucent', 'trigger', ], - methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'], + methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss', 'setCurrentBreakpoint', 'getCurrentBreakpoint'], }) @Component({ selector: 'ion-modal', @@ -119,6 +123,7 @@ export class IonModal { 'ionModalWillPresent', 'ionModalWillDismiss', 'ionModalDidDismiss', + 'ionBreakpointDidChange', 'didPresent', 'willPresent', 'willDismiss', diff --git a/angular/test/test-app/e2e/src/modal.spec.ts b/angular/test/test-app/e2e/src/modal.spec.ts index 83c38c731a..258b429ee9 100644 --- a/angular/test/test-app/e2e/src/modal.spec.ts +++ b/angular/test/test-app/e2e/src/modal.spec.ts @@ -74,5 +74,20 @@ describe('Modals: Inline', () => { cy.get('ion-modal').trigger('click', 20, 20); cy.get('ion-modal').children('.ion-page').should('not.exist'); - }) + }); + + describe('setting the current breakpoint', () => { + + it('should emit ionBreakpointDidChange', () => { + cy.get('#open-modal').click(); + + cy.get('ion-modal').then(modal => { + (modal.get(0) as any).setCurrentBreakpoint(1); + }); + + cy.get('#breakpointDidChange').should('have.text', '1'); + }); + + }); + }); diff --git a/angular/test/test-app/src/app/modal-inline/modal-inline.component.html b/angular/test/test-app/src/app/modal-inline/modal-inline.component.html index ae962516b3..b11f288cd7 100644 --- a/angular/test/test-app/src/app/modal-inline/modal-inline.component.html +++ b/angular/test/test-app/src/app/modal-inline/modal-inline.component.html @@ -1,6 +1,13 @@ Open Modal - +
    +
  • + breakpointDidChange event count: {{ breakpointDidChangeCounter }} +
  • +
+ + diff --git a/angular/test/test-app/src/app/modal-inline/modal-inline.component.ts b/angular/test/test-app/src/app/modal-inline/modal-inline.component.ts index 1167505685..8ba5878bfc 100644 --- a/angular/test/test-app/src/app/modal-inline/modal-inline.component.ts +++ b/angular/test/test-app/src/app/modal-inline/modal-inline.component.ts @@ -13,9 +13,15 @@ export class ModalInlineComponent implements AfterViewInit { items: string[] = []; + breakpointDidChangeCounter = 0; + ngAfterViewInit(): void { setTimeout(() => { this.items = ['A', 'B', 'C', 'D']; }, 1000); } + + onBreakpointDidChange() { + this.breakpointDidChangeCounter++; + } } diff --git a/core/api.txt b/core/api.txt index f5a0b89220..697f911a14 100644 --- a/core/api.txt +++ b/core/api.txt @@ -783,11 +783,14 @@ ion-modal,prop,showBackdrop,boolean,true,false,false ion-modal,prop,swipeToClose,boolean,false,false,false ion-modal,prop,trigger,string | undefined,undefined,false,false ion-modal,method,dismiss,dismiss(data?: any, role?: string | undefined) => Promise +ion-modal,method,getCurrentBreakpoint,getCurrentBreakpoint() => Promise ion-modal,method,onDidDismiss,onDidDismiss() => Promise> ion-modal,method,onWillDismiss,onWillDismiss() => Promise> ion-modal,method,present,present() => Promise +ion-modal,method,setCurrentBreakpoint,setCurrentBreakpoint(breakpoint: number) => Promise ion-modal,event,didDismiss,OverlayEventDetail,true ion-modal,event,didPresent,void,true +ion-modal,event,ionBreakpointDidChange,ModalBreakpointChangeEventDetail,true ion-modal,event,ionModalDidDismiss,OverlayEventDetail,true ion-modal,event,ionModalDidPresent,void,true ion-modal,event,ionModalWillDismiss,OverlayEventDetail,true diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 5e709e1759..f93983dc60 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; +import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; import { IonicSafeString } from "./utils/sanitization"; import { AlertAttributes } from "./components/alert/alert-interface"; import { CounterFormatter } from "./components/item/item-interface"; @@ -1541,6 +1541,10 @@ export namespace Components { * Animation to use when the modal is presented. */ "enterAnimation"?: AnimationBuilder; + /** + * Returns the current breakpoint of a sheet style modal + */ + "getCurrentBreakpoint": () => Promise; /** * The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties. */ @@ -1587,6 +1591,10 @@ export namespace Components { * The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode. */ "presentingElement"?: HTMLElement; + /** + * Move a sheet style modal to a specific breakpoint. The breakpoint value must be a value defined in your `breakpoints` array. + */ + "setCurrentBreakpoint": (breakpoint: number) => Promise; /** * If `true`, a backdrop will be displayed behind the modal. */ @@ -5294,6 +5302,10 @@ declare namespace LocalJSX { * Emitted after the modal has presented. Shorthand for ionModalWillDismiss. */ "onDidPresent"?: (event: CustomEvent) => void; + /** + * Emitted after the modal breakpoint has changed. + */ + "onIonBreakpointDidChange"?: (event: CustomEvent) => void; /** * Emitted after the modal has dismissed. */ diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 65d164543e..5d768551fa 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -5,6 +5,32 @@ import { getBackdropValueForSheet } from '../utils'; import { calculateSpringStep, handleCanDismiss } from './utils'; +export interface MoveSheetToBreakpointOptions { + /** + * The breakpoint value to move the sheet to. + */ + breakpoint: number; + /** + * The offset value between the current breakpoint and the new breakpoint. + * + * For breakpoint changes as a result of a touch gesture, this value + * will be calculated internally. + * + * For breakpoint changes as a result of dynamically setting the value, + * this value should be the difference between the new and old breakpoint. + * For example: + * - breakpoints: [0, 0.25, 0.5, 0.75, 1] + * - Current breakpoint value is 1. + * - Setting the breakpoint to 0.25. + * - The offset value should be 0.75 (1 - 0.25). + */ + breakpointOffset: number; + /** + * `true` if the sheet can be transitioned and dismissed off the view. + */ + canDismiss?: boolean; +} + export const createSheetGesture = ( baseEl: HTMLIonModalElement, backdropEl: HTMLIonBackdropElement, @@ -13,6 +39,7 @@ export const createSheetGesture = ( backdropBreakpoint: number, animation: Animation, breakpoints: number[] = [], + getCurrentBreakpoint: () => number, onDismiss: () => void, onBreakpointChange: (breakpoint: number) => void ) => { @@ -113,6 +140,7 @@ export const createSheetGesture = ( * allow for scrolling on the content. */ const content = (detail.event.target! as HTMLElement).closest('ion-content'); + currentBreakpoint = getCurrentBreakpoint(); if (currentBreakpoint === 1 && content) { return false; @@ -206,30 +234,39 @@ export const createSheetGesture = ( return Math.abs(b - diff) < Math.abs(a - diff) ? b : a; }); + moveSheetToBreakpoint({ + breakpoint: closest, + breakpointOffset: offset, + canDismiss: canDismissBlocksGesture, + }); + }; + + const moveSheetToBreakpoint = (options: MoveSheetToBreakpointOptions) => { + const { breakpoint, canDismiss, breakpointOffset } = options; /** * canDismiss should only prevent snapping * when users are trying to dismiss. If canDismiss * is present but the user is trying to swipe upwards, * we should allow that to happen, */ - const shouldPreventDismiss = canDismissBlocksGesture && closest === 0; - const snapToBreakpoint = shouldPreventDismiss ? currentBreakpoint : closest; + const shouldPreventDismiss = canDismiss && breakpoint === 0; + const snapToBreakpoint = shouldPreventDismiss ? currentBreakpoint : breakpoint; const shouldRemainOpen = snapToBreakpoint !== 0; - currentBreakpoint = 0; + currentBreakpoint = 0; /** * Update the animation so that it plays from * the last offset to the closest snap point. */ if (wrapperAnimation && backdropAnimation) { wrapperAnimation.keyframes([ - { offset: 0, transform: `translateY(${offset * 100}%)` }, + { offset: 0, transform: `translateY(${breakpointOffset * 100}%)` }, { offset: 1, transform: `translateY(${(1 - snapToBreakpoint) * 100}%)` } ]); backdropAnimation.keyframes([ - { offset: 0, opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(1 - offset, backdropBreakpoint)})` }, + { offset: 0, opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(1 - breakpointOffset, backdropBreakpoint)})` }, { offset: 1, opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(snapToBreakpoint, backdropBreakpoint)})` } ]); @@ -313,5 +350,9 @@ export const createSheetGesture = ( onMove, onEnd }); - return gesture; + + return { + gesture, + moveSheetToBreakpoint + }; }; diff --git a/core/src/components/modal/modal-interface.ts b/core/src/components/modal/modal-interface.ts index ad7c3d48b2..a765b934b5 100644 --- a/core/src/components/modal/modal-interface.ts +++ b/core/src/components/modal/modal-interface.ts @@ -33,4 +33,12 @@ export interface ModalAnimationOptions { backdropBreakpoint?: number; } -export interface ModalAttributes extends JSXBase.HTMLAttributes {} +export interface ModalAttributes extends JSXBase.HTMLAttributes { } + +export interface ModalBreakpointChangeEventDetail { + breakpoint: number; +} + +export interface ModalCustomEvent extends CustomEvent { + target: HTMLIonModalElement; +} diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index b8a196dd4a..180c2ad1b0 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -3,7 +3,7 @@ import { printIonWarning } from '@utils/logging'; import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; -import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, ModalAttributes, OverlayEventDetail, OverlayInterface } from '../../interface'; +import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, ModalAttributes, ModalBreakpointChangeEventDetail, OverlayEventDetail, OverlayInterface } from '../../interface'; import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate'; import { raf } from '../../utils/helpers'; import { KEYBOARD_DID_OPEN } from '../../utils/keyboard/keyboard'; @@ -15,7 +15,7 @@ import { iosEnterAnimation } from './animations/ios.enter'; import { iosLeaveAnimation } from './animations/ios.leave'; import { mdEnterAnimation } from './animations/md.enter'; import { mdLeaveAnimation } from './animations/md.leave'; -import { createSheetGesture } from './gestures/sheet'; +import { MoveSheetToBreakpointOptions, createSheetGesture } from './gestures/sheet'; import { createSwipeToCloseGesture } from './gestures/swipe-to-close'; /** @@ -46,7 +46,9 @@ export class Modal implements ComponentInterface, OverlayInterface { private currentBreakpoint?: number; private wrapperEl?: HTMLElement; private backdropEl?: HTMLIonBackdropElement; + private sortedBreakpoints?: number[]; private keyboardOpenCallback?: () => void; + private moveSheetToBreakpoint?: (options: MoveSheetToBreakpointOptions) => void; private inline = false; private workingDelegate?: FrameworkDelegate; @@ -56,6 +58,7 @@ export class Modal implements ComponentInterface, OverlayInterface { // Whether or not modal is being dismissed via gesture private gestureAnimationDismissing = false; + lastFocus?: HTMLElement; animation?: Animation; @@ -237,6 +240,11 @@ export class Modal implements ComponentInterface, OverlayInterface { */ @Event({ eventName: 'ionModalDidDismiss' }) didDismiss!: EventEmitter; + /** + * Emitted after the modal breakpoint has changed. + */ + @Event() ionBreakpointDidChange!: EventEmitter; + /** * Emitted after the modal has presented. * Shorthand for ionModalWillDismiss. @@ -270,6 +278,12 @@ export class Modal implements ComponentInterface, OverlayInterface { } } + breakpointsChanged(breakpoints: number[] | undefined) { + if (breakpoints !== undefined) { + this.sortedBreakpoints = breakpoints.sort((a, b) => a - b); + } + } + connectedCallback() { prepareOverlay(this.el); } @@ -282,7 +296,11 @@ export class Modal implements ComponentInterface, OverlayInterface { * not assign the default incrementing ID. */ this.modalId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-modal-${this.modalIndex}`; - this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined; + const isSheetModal = this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined; + + if (isSheetModal) { + this.currentBreakpoint = this.initialBreakpoint; + } if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) { printIonWarning('Your breakpoints array must include the initialBreakpoint value.') @@ -301,7 +319,7 @@ export class Modal implements ComponentInterface, OverlayInterface { if (this.isOpen === true) { raf(() => this.present()); } - + this.breakpointsChanged(this.breakpoints); this.configureTriggerInteraction(); } @@ -508,40 +526,50 @@ export class Modal implements ComponentInterface, OverlayInterface { ani.progressStart(true, 1); - const sortedBreakpoints = (this.breakpoints?.sort((a, b) => a - b)) || []; - - this.gesture = createSheetGesture( + const { gesture, moveSheetToBreakpoint } = createSheetGesture( this.el, this.backdropEl!, wrapperEl, initialBreakpoint, backdropBreakpoint, ani, - sortedBreakpoints, - () => { - /** - * While the gesture animation is finishing - * it is possible for a user to tap the backdrop. - * This would result in the dismiss animation - * being played again. Typically this is avoided - * by setting `presented = false` on the overlay - * component; however, we cannot do that here as - * that would prevent the element from being - * removed from the DOM. - */ - this.gestureAnimationDismissing = true; - this.animation!.onFinish(async () => { - await this.dismiss(undefined, 'gesture'); - this.gestureAnimationDismissing = false; - }); - }, + this.sortedBreakpoints, + () => this.currentBreakpoint ?? 0, + () => this.sheetOnDismiss(), (breakpoint: number) => { - this.currentBreakpoint = breakpoint; + if (this.currentBreakpoint !== breakpoint) { + this.currentBreakpoint = breakpoint; + this.ionBreakpointDidChange.emit({ breakpoint }); + } } ); + + this.gesture = gesture; + this.moveSheetToBreakpoint = moveSheetToBreakpoint; + this.gesture.enable(true); } + private sheetOnDismiss() { + /** + * While the gesture animation is finishing + * it is possible for a user to tap the backdrop. + * This would result in the dismiss animation + * being played again. Typically this is avoided + * by setting `presented = false` on the overlay + * component; however, we cannot do that here as + * that would prevent the element from being + * removed from the DOM. + */ + this.gestureAnimationDismissing = true; + this.animation!.onFinish(async () => { + this.currentBreakpoint = 0; + this.ionBreakpointDidChange.emit({ breakpoint: this.currentBreakpoint }); + await this.dismiss(undefined, 'gesture'); + this.gestureAnimationDismissing = false; + }); + } + /** * Dismiss the modal overlay after it has been presented. * @@ -623,6 +651,44 @@ export class Modal implements ComponentInterface, OverlayInterface { return eventMethod(this.el, 'ionModalWillDismiss'); } + /** + * Move a sheet style modal to a specific breakpoint. The breakpoint value must + * be a value defined in your `breakpoints` array. + */ + @Method() + async setCurrentBreakpoint(breakpoint: number): Promise { + if (!this.isSheetModal) { + printIonWarning('setCurrentBreakpoint is only supported on sheet modals.'); + return; + } + if (!this.breakpoints!.includes(breakpoint)) { + printIonWarning(`Attempted to set invalid breakpoint value ${breakpoint}. Please double check that the breakpoint value is part of your defined breakpoints.`); + return; + } + + const { currentBreakpoint, moveSheetToBreakpoint, canDismiss, breakpoints } = this; + + if (currentBreakpoint === breakpoint) { + return; + } + + if (moveSheetToBreakpoint) { + moveSheetToBreakpoint({ + breakpoint, + breakpointOffset: 1 - currentBreakpoint!, + canDismiss: canDismiss !== undefined && canDismiss !== true && breakpoints![0] === 0, + }); + } + } + + /** + * Returns the current breakpoint of a sheet style modal + */ + @Method() + async getCurrentBreakpoint(): Promise { + return this.currentBreakpoint; + } + private onBackdropTap = () => { this.dismiss(undefined, BACKDROP); } diff --git a/core/src/components/modal/readme.md b/core/src/components/modal/readme.md index 178dc83c17..6fdd26dc08 100644 --- a/core/src/components/modal/readme.md +++ b/core/src/components/modal/readme.md @@ -54,7 +54,7 @@ Developers can create a sheet modal effect similar to the drawer components avai The `breakpoints` property accepts an array which states each breakpoint that the sheet can snap to when swiped. A `breakpoints` property of `[0, 0.5, 1]` would indicate that the sheet can be swiped to show 0% of the modal, 50% of the modal, and 100% of the modal. When the modal is swiped to 0%, the modal will be automatically dismissed. -The `initialBreakpoint` property is required so that the sheet modal knows which breakpoint to start at when presenting. The `initalBreakpoint` value must also exist in the `breakpoints` array. Given a `breakpoints` value of `[0, 0.5, 1]`, an `initialBreakpoint` value of `0.5` would be valid as `0.5` is in the `breakpoints` array. An `initialBreakpoint` value of `0.25` would not be valid as `0.25` does not exist in the `breakpoints` array. +The `initialBreakpoint` property is required so that the sheet modal knows which breakpoint to start at when presenting. The `initialBreakpoint` value must also exist in the `breakpoints` array. Given a `breakpoints` value of `[0, 0.5, 1]`, an `initialBreakpoint` value of `0.5` would be valid as `0.5` is in the `breakpoints` array. An `initialBreakpoint` value of `0.25` would not be valid as `0.25` does not exist in the `breakpoints` array. The `backdropBreakpoint` property can be used to customize the point at which the `ion-backdrop` will begin to fade in. This is useful when creating interfaces that have content underneath the sheet that should remain interactive. A common use case is a sheet modal that overlays a map where the map is interactive until the sheet is fully expanded. @@ -86,6 +86,8 @@ Note that setting a callback function will cause the swipe gesture to be interru ## Interfaces +### ModalOptions + Below you will find all of the options available to you when using the `modalController`. These options should be supplied when calling `modalController.create()`. ```typescript @@ -107,6 +109,15 @@ interface ModalOptions { leaveAnimation?: AnimationBuilder; } ``` +### ModalCustomEvent + +While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing with Ionic events emitted from this component. + +```typescript +interface ModalCustomEvent extends CustomEvent { + target: HTMLIonModalElement; +} +``` ## Dismissing @@ -1580,16 +1591,17 @@ export default { ## Events -| Event | Description | Type | -| --------------------- | -------------------------------------------------------------------------- | -------------------------------------- | -| `didDismiss` | Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss. | `CustomEvent>` | -| `didPresent` | Emitted after the modal has presented. Shorthand for ionModalWillDismiss. | `CustomEvent` | -| `ionModalDidDismiss` | Emitted after the modal has dismissed. | `CustomEvent>` | -| `ionModalDidPresent` | Emitted after the modal has presented. | `CustomEvent` | -| `ionModalWillDismiss` | Emitted before the modal has dismissed. | `CustomEvent>` | -| `ionModalWillPresent` | Emitted before the modal has presented. | `CustomEvent` | -| `willDismiss` | Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss. | `CustomEvent>` | -| `willPresent` | Emitted before the modal has presented. Shorthand for ionModalWillPresent. | `CustomEvent` | +| Event | Description | Type | +| ------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------- | +| `didDismiss` | Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss. | `CustomEvent>` | +| `didPresent` | Emitted after the modal has presented. Shorthand for ionModalWillDismiss. | `CustomEvent` | +| `ionBreakpointDidChange` | Emitted after the modal breakpoint has changed. | `CustomEvent` | +| `ionModalDidDismiss` | Emitted after the modal has dismissed. | `CustomEvent>` | +| `ionModalDidPresent` | Emitted after the modal has presented. | `CustomEvent` | +| `ionModalWillDismiss` | Emitted before the modal has dismissed. | `CustomEvent>` | +| `ionModalWillPresent` | Emitted before the modal has presented. | `CustomEvent` | +| `willDismiss` | Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss. | `CustomEvent>` | +| `willPresent` | Emitted before the modal has presented. Shorthand for ionModalWillPresent. | `CustomEvent` | ## Methods @@ -1604,6 +1616,16 @@ Type: `Promise` +### `getCurrentBreakpoint() => Promise` + +Returns the current breakpoint of a sheet style modal + +#### Returns + +Type: `Promise` + + + ### `onDidDismiss() => Promise>` Returns a promise that resolves when the modal did dismiss. @@ -1634,6 +1656,17 @@ Type: `Promise` +### `setCurrentBreakpoint(breakpoint: number) => Promise` + +Move a sheet style modal to a specific breakpoint. The breakpoint value must +be a value defined in your `breakpoints` array. + +#### Returns + +Type: `Promise` + + + ## Slots diff --git a/core/src/components/modal/test/basic/e2e.ts b/core/src/components/modal/test/basic/e2e.ts index 15b297ed8e..95786cbbb6 100644 --- a/core/src/components/modal/test/basic/e2e.ts +++ b/core/src/components/modal/test/basic/e2e.ts @@ -1,4 +1,4 @@ -import { testModal } from '../test.utils'; +import { openModal, testModal } from '../test.utils'; import { newE2EPage } from '@stencil/core/testing'; const DIRECTORY = 'basic'; @@ -96,3 +96,31 @@ test('it should dismiss the modal when clicking the backdrop', async () => { await page.mouse.click(20, 20); await ionModalDidDismiss.next(); }) + +test('modal: setting the breakpoint should warn the developer', async () => { + const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' }); + + const warnings = []; + + page.on('console', (ev) => { + if (ev.type() === 'warning') { + warnings.push(ev.text()); + } + }); + + const modal = await openModal(page, '#basic-modal'); + + await modal.callMethod('setCurrentBreakpoint', 0.5); + + expect(warnings.length).toBe(1); + expect(warnings[0]).toBe('[Ionic Warning]: setCurrentBreakpoint is only supported on sheet modals.'); +}); + +test('modal: getting the breakpoint should return undefined', async () => { + const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' }); + + const modal = await openModal(page, '#basic-modal'); + + const breakpoint = await modal.callMethod('getCurrentBreakpoint'); + expect(breakpoint).toBeUndefined(); +}) diff --git a/core/src/components/modal/test/sheet/e2e.ts b/core/src/components/modal/test/sheet/e2e.ts index 0a0b7b045f..b9fa7e4706 100644 --- a/core/src/components/modal/test/sheet/e2e.ts +++ b/core/src/components/modal/test/sheet/e2e.ts @@ -1,6 +1,6 @@ -import { newE2EPage } from '@stencil/core/testing'; -import { testModal } from '../test.utils'; -import { getActiveElement, getActiveElementParent } from '@utils/test'; +import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; +import { openModal, testModal } from '../test.utils'; +import { getActiveElement, getActiveElementParent, dragElementBy } from '@utils/test'; const DIRECTORY = 'sheet'; @@ -117,3 +117,95 @@ test('input should not be focusable when backdrop is active', async () => { const parentEl = await getActiveElement(page); expect(parentEl.tagName).toEqual('ION-BUTTON'); }); + +describe('modal: sheet: setting the breakpoint', () => { + + let page: E2EPage; + + beforeEach(async () => { + page = await newE2EPage({ + url: '/src/components/modal/test/sheet?ionic:_testing=true' + }); + }); + + describe('setting an invalid value', () => { + + let warnings: string[]; + let modal: E2EElement; + + beforeEach(async () => { + warnings = []; + page.on('console', (ev) => { + if (ev.type() === 'warning') { + warnings.push(ev.text()); + } + }); + + modal = await openModal(page, '#sheet-modal'); + + await modal.callMethod('setCurrentBreakpoint', 0.01); + }); + + it('should not change the breakpoint', async () => { + const breakpoint = await modal.callMethod('getCurrentBreakpoint'); + expect(breakpoint).toBe(0.25); + }); + + it('should console a warning to developers', async () => { + expect(warnings.length).toBe(1); + expect(warnings[0]).toBe('[Ionic Warning]: Attempted to set invalid breakpoint value 0.01. Please double check that the breakpoint value is part of your defined breakpoints.'); + }); + + }); + + describe('setting the breakpoint to a valid value', () => { + + it('should update the current breakpoint', async () => { + const modal = await openModal(page, '#sheet-modal'); + + await modal.callMethod('setCurrentBreakpoint', 0.5); + await modal.waitForEvent('ionBreakpointDidChange'); + + const breakpoint = await modal.callMethod('getCurrentBreakpoint'); + expect(breakpoint).toBe(0.5); + }); + + it('should emit ionBreakpointDidChange', async () => { + const modal = await openModal(page, '#sheet-modal'); + + const ionBreakpointDidChangeSpy = await modal.spyOnEvent('ionBreakpointDidChange'); + + await modal.callMethod('setCurrentBreakpoint', 0.5); + await modal.waitForEvent('ionBreakpointDidChange'); + + expect(ionBreakpointDidChangeSpy).toHaveReceivedEventTimes(1); + }); + + it('should emit ionBreakpointDidChange when breakpoint is set to 0', async () => { + const modal = await openModal(page, '#sheet-modal'); + + const ionBreakpointDidChangeSpy = await modal.spyOnEvent('ionBreakpointDidChange'); + + await modal.callMethod('setCurrentBreakpoint', 0); + await modal.waitForEvent('ionBreakpointDidChange'); + + expect(ionBreakpointDidChangeSpy).toHaveReceivedEventTimes(1); + }); + + }); + + it('should emit ionBreakpointDidChange when the sheet is swiped to breakpoint 0', async () => { + const modal = await openModal(page, '#sheet-modal'); + + const ionBreakpointDidChangeSpy = await modal.spyOnEvent('ionBreakpointDidChange'); + + const headerEl = await page.$('ion-modal ion-header'); + + await dragElementBy(headerEl, page, 0, 500); + + await modal.waitForEvent('ionBreakpointDidChange'); + + expect(ionBreakpointDidChangeSpy).toHaveReceivedEventTimes(1); + }); + +}); diff --git a/core/src/components/modal/test/sheet/index.html b/core/src/components/modal/test/sheet/index.html index efd10630e9..6ee2cd9854 100644 --- a/core/src/components/modal/test/sheet/index.html +++ b/core/src/components/modal/test/sheet/index.html @@ -4,7 +4,8 @@ Modal - Sheet - + @@ -15,62 +16,61 @@ @@ -91,15 +91,30 @@ Present Sheet Modal - Present Sheet Modal (Custom Breakpoints) - Present Sheet Modal (Max breakpoint is not 1) - Present Sheet Modal (Custom Backdrop Breakpoint) - Present Sheet Modal (Backdrop breakpoint is off-center) - Present Sheet Modal (Custom Height) - Present Sheet Modal (Custom Handle) - Present Sheet Modal (No Handle) - Backdrop is active - Backdrop is inactive + Present Sheet Modal (Custom + Breakpoints) + Present Sheet Modal (Max + breakpoint is not 1) + Present Sheet Modal (Custom + Backdrop Breakpoint) + + Present Sheet Modal (Backdrop breakpoint is off-center) + Present Sheet Modal + (Custom Height) + Present Sheet Modal + (Custom Handle) + Present Sheet Modal (No Handle) + + + Backdrop is active + + Backdrop is inactive
@@ -117,13 +132,10 @@