mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
feat(modal): ability to programmatically set current sheet breakpoint (#24648)
Resolves #23917 Co-authored-by: Sean Perkins <sean@ionic.io>
This commit is contained in:
@ -11,7 +11,7 @@ import {
|
|||||||
TemplateRef,
|
TemplateRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
|
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 {
|
export declare interface IonModal extends Components.IonModal {
|
||||||
/**
|
/**
|
||||||
@ -30,6 +30,10 @@ export declare interface IonModal extends Components.IonModal {
|
|||||||
* Emitted after the modal has dismissed.
|
* Emitted after the modal has dismissed.
|
||||||
*/
|
*/
|
||||||
ionModalDidDismiss: EventEmitter<CustomEvent>;
|
ionModalDidDismiss: EventEmitter<CustomEvent>;
|
||||||
|
/**
|
||||||
|
* Emitted after the modal breakpoint has changed.
|
||||||
|
*/
|
||||||
|
ionBreakpointDidChange: EventEmitter<CustomEvent<ModalBreakpointChangeEventDetail>>;
|
||||||
/**
|
/**
|
||||||
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
|
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
|
||||||
*/
|
*/
|
||||||
@ -68,7 +72,7 @@ export declare interface IonModal extends Components.IonModal {
|
|||||||
'translucent',
|
'translucent',
|
||||||
'trigger',
|
'trigger',
|
||||||
],
|
],
|
||||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'],
|
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss', 'setCurrentBreakpoint', 'getCurrentBreakpoint'],
|
||||||
})
|
})
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ion-modal',
|
selector: 'ion-modal',
|
||||||
@ -119,6 +123,7 @@ export class IonModal {
|
|||||||
'ionModalWillPresent',
|
'ionModalWillPresent',
|
||||||
'ionModalWillDismiss',
|
'ionModalWillDismiss',
|
||||||
'ionModalDidDismiss',
|
'ionModalDidDismiss',
|
||||||
|
'ionBreakpointDidChange',
|
||||||
'didPresent',
|
'didPresent',
|
||||||
'willPresent',
|
'willPresent',
|
||||||
'willDismiss',
|
'willDismiss',
|
||||||
|
@ -74,5 +74,20 @@ describe('Modals: Inline', () => {
|
|||||||
cy.get('ion-modal').trigger('click', 20, 20);
|
cy.get('ion-modal').trigger('click', 20, 20);
|
||||||
|
|
||||||
cy.get('ion-modal').children('.ion-page').should('not.exist');
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
<ion-button id="open-modal">Open Modal</ion-button>
|
<ion-button id="open-modal">Open Modal</ion-button>
|
||||||
|
|
||||||
<ion-modal [animated]="false" trigger="open-modal" [breakpoints]="[0.1, 0.5, 1]" [initialBreakpoint]="0.5">
|
<ul>
|
||||||
|
<li>
|
||||||
|
breakpointDidChange event count: <span id="breakpointDidChange">{{ breakpointDidChangeCounter }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ion-modal [animated]="false" trigger="open-modal" [breakpoints]="[0.1, 0.5, 1]" [initialBreakpoint]="0.5"
|
||||||
|
(ionBreakpointDidChange)="onBreakpointDidChange()">
|
||||||
<ng-template>
|
<ng-template>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
|
@ -13,9 +13,15 @@ export class ModalInlineComponent implements AfterViewInit {
|
|||||||
|
|
||||||
items: string[] = [];
|
items: string[] = [];
|
||||||
|
|
||||||
|
breakpointDidChangeCounter = 0;
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.items = ['A', 'B', 'C', 'D'];
|
this.items = ['A', 'B', 'C', 'D'];
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBreakpointDidChange() {
|
||||||
|
this.breakpointDidChangeCounter++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -783,11 +783,14 @@ ion-modal,prop,showBackdrop,boolean,true,false,false
|
|||||||
ion-modal,prop,swipeToClose,boolean,false,false,false
|
ion-modal,prop,swipeToClose,boolean,false,false,false
|
||||||
ion-modal,prop,trigger,string | undefined,undefined,false,false
|
ion-modal,prop,trigger,string | undefined,undefined,false,false
|
||||||
ion-modal,method,dismiss,dismiss(data?: any, role?: string | undefined) => Promise<boolean>
|
ion-modal,method,dismiss,dismiss(data?: any, role?: string | undefined) => Promise<boolean>
|
||||||
|
ion-modal,method,getCurrentBreakpoint,getCurrentBreakpoint() => Promise<number | undefined>
|
||||||
ion-modal,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
ion-modal,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||||
ion-modal,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
ion-modal,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||||
ion-modal,method,present,present() => Promise<void>
|
ion-modal,method,present,present() => Promise<void>
|
||||||
|
ion-modal,method,setCurrentBreakpoint,setCurrentBreakpoint(breakpoint: number) => Promise<void>
|
||||||
ion-modal,event,didDismiss,OverlayEventDetail<any>,true
|
ion-modal,event,didDismiss,OverlayEventDetail<any>,true
|
||||||
ion-modal,event,didPresent,void,true
|
ion-modal,event,didPresent,void,true
|
||||||
|
ion-modal,event,ionBreakpointDidChange,ModalBreakpointChangeEventDetail,true
|
||||||
ion-modal,event,ionModalDidDismiss,OverlayEventDetail<any>,true
|
ion-modal,event,ionModalDidDismiss,OverlayEventDetail<any>,true
|
||||||
ion-modal,event,ionModalDidPresent,void,true
|
ion-modal,event,ionModalDidPresent,void,true
|
||||||
ion-modal,event,ionModalWillDismiss,OverlayEventDetail<any>,true
|
ion-modal,event,ionModalWillDismiss,OverlayEventDetail<any>,true
|
||||||
|
14
core/src/components.d.ts
vendored
14
core/src/components.d.ts
vendored
@ -5,7 +5,7 @@
|
|||||||
* It contains typing information for all components that exist in this project.
|
* It contains typing information for all components that exist in this project.
|
||||||
*/
|
*/
|
||||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
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 { IonicSafeString } from "./utils/sanitization";
|
||||||
import { AlertAttributes } from "./components/alert/alert-interface";
|
import { AlertAttributes } from "./components/alert/alert-interface";
|
||||||
import { CounterFormatter } from "./components/item/item-interface";
|
import { CounterFormatter } from "./components/item/item-interface";
|
||||||
@ -1541,6 +1541,10 @@ export namespace Components {
|
|||||||
* Animation to use when the modal is presented.
|
* Animation to use when the modal is presented.
|
||||||
*/
|
*/
|
||||||
"enterAnimation"?: AnimationBuilder;
|
"enterAnimation"?: AnimationBuilder;
|
||||||
|
/**
|
||||||
|
* Returns the current breakpoint of a sheet style modal
|
||||||
|
*/
|
||||||
|
"getCurrentBreakpoint": () => Promise<number | undefined>;
|
||||||
/**
|
/**
|
||||||
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
|
* 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.
|
* 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;
|
"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<void>;
|
||||||
/**
|
/**
|
||||||
* If `true`, a backdrop will be displayed behind the modal.
|
* 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.
|
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
|
||||||
*/
|
*/
|
||||||
"onDidPresent"?: (event: CustomEvent<void>) => void;
|
"onDidPresent"?: (event: CustomEvent<void>) => void;
|
||||||
|
/**
|
||||||
|
* Emitted after the modal breakpoint has changed.
|
||||||
|
*/
|
||||||
|
"onIonBreakpointDidChange"?: (event: CustomEvent<ModalBreakpointChangeEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted after the modal has dismissed.
|
* Emitted after the modal has dismissed.
|
||||||
*/
|
*/
|
||||||
|
@ -5,6 +5,32 @@ import { getBackdropValueForSheet } from '../utils';
|
|||||||
|
|
||||||
import { calculateSpringStep, handleCanDismiss } 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 = (
|
export const createSheetGesture = (
|
||||||
baseEl: HTMLIonModalElement,
|
baseEl: HTMLIonModalElement,
|
||||||
backdropEl: HTMLIonBackdropElement,
|
backdropEl: HTMLIonBackdropElement,
|
||||||
@ -13,6 +39,7 @@ export const createSheetGesture = (
|
|||||||
backdropBreakpoint: number,
|
backdropBreakpoint: number,
|
||||||
animation: Animation,
|
animation: Animation,
|
||||||
breakpoints: number[] = [],
|
breakpoints: number[] = [],
|
||||||
|
getCurrentBreakpoint: () => number,
|
||||||
onDismiss: () => void,
|
onDismiss: () => void,
|
||||||
onBreakpointChange: (breakpoint: number) => void
|
onBreakpointChange: (breakpoint: number) => void
|
||||||
) => {
|
) => {
|
||||||
@ -113,6 +140,7 @@ export const createSheetGesture = (
|
|||||||
* allow for scrolling on the content.
|
* allow for scrolling on the content.
|
||||||
*/
|
*/
|
||||||
const content = (detail.event.target! as HTMLElement).closest('ion-content');
|
const content = (detail.event.target! as HTMLElement).closest('ion-content');
|
||||||
|
currentBreakpoint = getCurrentBreakpoint();
|
||||||
|
|
||||||
if (currentBreakpoint === 1 && content) {
|
if (currentBreakpoint === 1 && content) {
|
||||||
return false;
|
return false;
|
||||||
@ -206,30 +234,39 @@ export const createSheetGesture = (
|
|||||||
return Math.abs(b - diff) < Math.abs(a - diff) ? b : a;
|
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
|
* canDismiss should only prevent snapping
|
||||||
* when users are trying to dismiss. If canDismiss
|
* when users are trying to dismiss. If canDismiss
|
||||||
* is present but the user is trying to swipe upwards,
|
* is present but the user is trying to swipe upwards,
|
||||||
* we should allow that to happen,
|
* we should allow that to happen,
|
||||||
*/
|
*/
|
||||||
const shouldPreventDismiss = canDismissBlocksGesture && closest === 0;
|
const shouldPreventDismiss = canDismiss && breakpoint === 0;
|
||||||
const snapToBreakpoint = shouldPreventDismiss ? currentBreakpoint : closest;
|
const snapToBreakpoint = shouldPreventDismiss ? currentBreakpoint : breakpoint;
|
||||||
|
|
||||||
const shouldRemainOpen = snapToBreakpoint !== 0;
|
const shouldRemainOpen = snapToBreakpoint !== 0;
|
||||||
currentBreakpoint = 0;
|
|
||||||
|
|
||||||
|
currentBreakpoint = 0;
|
||||||
/**
|
/**
|
||||||
* Update the animation so that it plays from
|
* Update the animation so that it plays from
|
||||||
* the last offset to the closest snap point.
|
* the last offset to the closest snap point.
|
||||||
*/
|
*/
|
||||||
if (wrapperAnimation && backdropAnimation) {
|
if (wrapperAnimation && backdropAnimation) {
|
||||||
wrapperAnimation.keyframes([
|
wrapperAnimation.keyframes([
|
||||||
{ offset: 0, transform: `translateY(${offset * 100}%)` },
|
{ offset: 0, transform: `translateY(${breakpointOffset * 100}%)` },
|
||||||
{ offset: 1, transform: `translateY(${(1 - snapToBreakpoint) * 100}%)` }
|
{ offset: 1, transform: `translateY(${(1 - snapToBreakpoint) * 100}%)` }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
backdropAnimation.keyframes([
|
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)})` }
|
{ offset: 1, opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(snapToBreakpoint, backdropBreakpoint)})` }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -313,5 +350,9 @@ export const createSheetGesture = (
|
|||||||
onMove,
|
onMove,
|
||||||
onEnd
|
onEnd
|
||||||
});
|
});
|
||||||
return gesture;
|
|
||||||
|
return {
|
||||||
|
gesture,
|
||||||
|
moveSheetToBreakpoint
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -33,4 +33,12 @@ export interface ModalAnimationOptions {
|
|||||||
backdropBreakpoint?: number;
|
backdropBreakpoint?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalAttributes extends JSXBase.HTMLAttributes<HTMLElement> {}
|
export interface ModalAttributes extends JSXBase.HTMLAttributes<HTMLElement> { }
|
||||||
|
|
||||||
|
export interface ModalBreakpointChangeEventDetail {
|
||||||
|
breakpoint: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalCustomEvent extends CustomEvent {
|
||||||
|
target: HTMLIonModalElement;
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { printIonWarning } from '@utils/logging';
|
|||||||
|
|
||||||
import { config } from '../../global/config';
|
import { config } from '../../global/config';
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
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 { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||||
import { raf } from '../../utils/helpers';
|
import { raf } from '../../utils/helpers';
|
||||||
import { KEYBOARD_DID_OPEN } from '../../utils/keyboard/keyboard';
|
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 { iosLeaveAnimation } from './animations/ios.leave';
|
||||||
import { mdEnterAnimation } from './animations/md.enter';
|
import { mdEnterAnimation } from './animations/md.enter';
|
||||||
import { mdLeaveAnimation } from './animations/md.leave';
|
import { mdLeaveAnimation } from './animations/md.leave';
|
||||||
import { createSheetGesture } from './gestures/sheet';
|
import { MoveSheetToBreakpointOptions, createSheetGesture } from './gestures/sheet';
|
||||||
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
|
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,7 +46,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
private currentBreakpoint?: number;
|
private currentBreakpoint?: number;
|
||||||
private wrapperEl?: HTMLElement;
|
private wrapperEl?: HTMLElement;
|
||||||
private backdropEl?: HTMLIonBackdropElement;
|
private backdropEl?: HTMLIonBackdropElement;
|
||||||
|
private sortedBreakpoints?: number[];
|
||||||
private keyboardOpenCallback?: () => void;
|
private keyboardOpenCallback?: () => void;
|
||||||
|
private moveSheetToBreakpoint?: (options: MoveSheetToBreakpointOptions) => void;
|
||||||
|
|
||||||
private inline = false;
|
private inline = false;
|
||||||
private workingDelegate?: FrameworkDelegate;
|
private workingDelegate?: FrameworkDelegate;
|
||||||
@ -56,6 +58,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
// Whether or not modal is being dismissed via gesture
|
// Whether or not modal is being dismissed via gesture
|
||||||
private gestureAnimationDismissing = false;
|
private gestureAnimationDismissing = false;
|
||||||
|
|
||||||
lastFocus?: HTMLElement;
|
lastFocus?: HTMLElement;
|
||||||
animation?: Animation;
|
animation?: Animation;
|
||||||
|
|
||||||
@ -237,6 +240,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
*/
|
*/
|
||||||
@Event({ eventName: 'ionModalDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
@Event({ eventName: 'ionModalDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted after the modal breakpoint has changed.
|
||||||
|
*/
|
||||||
|
@Event() ionBreakpointDidChange!: EventEmitter<ModalBreakpointChangeEventDetail>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted after the modal has presented.
|
* Emitted after the modal has presented.
|
||||||
* Shorthand for ionModalWillDismiss.
|
* 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() {
|
connectedCallback() {
|
||||||
prepareOverlay(this.el);
|
prepareOverlay(this.el);
|
||||||
}
|
}
|
||||||
@ -282,7 +296,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
* not assign the default incrementing ID.
|
* not assign the default incrementing ID.
|
||||||
*/
|
*/
|
||||||
this.modalId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-modal-${this.modalIndex}`;
|
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)) {
|
if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) {
|
||||||
printIonWarning('Your breakpoints array must include the initialBreakpoint value.')
|
printIonWarning('Your breakpoints array must include the initialBreakpoint value.')
|
||||||
@ -301,7 +319,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
if (this.isOpen === true) {
|
if (this.isOpen === true) {
|
||||||
raf(() => this.present());
|
raf(() => this.present());
|
||||||
}
|
}
|
||||||
|
this.breakpointsChanged(this.breakpoints);
|
||||||
this.configureTriggerInteraction();
|
this.configureTriggerInteraction();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,40 +526,50 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
ani.progressStart(true, 1);
|
ani.progressStart(true, 1);
|
||||||
|
|
||||||
const sortedBreakpoints = (this.breakpoints?.sort((a, b) => a - b)) || [];
|
const { gesture, moveSheetToBreakpoint } = createSheetGesture(
|
||||||
|
|
||||||
this.gesture = createSheetGesture(
|
|
||||||
this.el,
|
this.el,
|
||||||
this.backdropEl!,
|
this.backdropEl!,
|
||||||
wrapperEl,
|
wrapperEl,
|
||||||
initialBreakpoint,
|
initialBreakpoint,
|
||||||
backdropBreakpoint,
|
backdropBreakpoint,
|
||||||
ani,
|
ani,
|
||||||
sortedBreakpoints,
|
this.sortedBreakpoints,
|
||||||
() => {
|
() => this.currentBreakpoint ?? 0,
|
||||||
/**
|
() => this.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 () => {
|
|
||||||
await this.dismiss(undefined, 'gesture');
|
|
||||||
this.gestureAnimationDismissing = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(breakpoint: number) => {
|
(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);
|
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.
|
* Dismiss the modal overlay after it has been presented.
|
||||||
*
|
*
|
||||||
@ -623,6 +651,44 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
return eventMethod(this.el, 'ionModalWillDismiss');
|
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<void> {
|
||||||
|
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<number | undefined> {
|
||||||
|
return this.currentBreakpoint;
|
||||||
|
}
|
||||||
|
|
||||||
private onBackdropTap = () => {
|
private onBackdropTap = () => {
|
||||||
this.dismiss(undefined, BACKDROP);
|
this.dismiss(undefined, BACKDROP);
|
||||||
}
|
}
|
||||||
|
@ -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 `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.
|
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
|
## 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()`.
|
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
|
```typescript
|
||||||
@ -107,6 +109,15 @@ interface ModalOptions {
|
|||||||
leaveAnimation?: AnimationBuilder;
|
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
|
## Dismissing
|
||||||
|
|
||||||
@ -1580,16 +1591,17 @@ export default {
|
|||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
| Event | Description | Type |
|
| Event | Description | Type |
|
||||||
| --------------------- | -------------------------------------------------------------------------- | -------------------------------------- |
|
| ------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||||
| `didDismiss` | Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
| `didDismiss` | Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||||
| `didPresent` | Emitted after the modal has presented. Shorthand for ionModalWillDismiss. | `CustomEvent<void>` |
|
| `didPresent` | Emitted after the modal has presented. Shorthand for ionModalWillDismiss. | `CustomEvent<void>` |
|
||||||
| `ionModalDidDismiss` | Emitted after the modal has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
| `ionBreakpointDidChange` | Emitted after the modal breakpoint has changed. | `CustomEvent<ModalBreakpointChangeEventDetail>` |
|
||||||
| `ionModalDidPresent` | Emitted after the modal has presented. | `CustomEvent<void>` |
|
| `ionModalDidDismiss` | Emitted after the modal has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||||
| `ionModalWillDismiss` | Emitted before the modal has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
| `ionModalDidPresent` | Emitted after the modal has presented. | `CustomEvent<void>` |
|
||||||
| `ionModalWillPresent` | Emitted before the modal has presented. | `CustomEvent<void>` |
|
| `ionModalWillDismiss` | Emitted before the modal has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||||
| `willDismiss` | Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
| `ionModalWillPresent` | Emitted before the modal has presented. | `CustomEvent<void>` |
|
||||||
| `willPresent` | Emitted before the modal has presented. Shorthand for ionModalWillPresent. | `CustomEvent<void>` |
|
| `willDismiss` | Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||||
|
| `willPresent` | Emitted before the modal has presented. Shorthand for ionModalWillPresent. | `CustomEvent<void>` |
|
||||||
|
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
@ -1604,6 +1616,16 @@ Type: `Promise<boolean>`
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### `getCurrentBreakpoint() => Promise<number | undefined>`
|
||||||
|
|
||||||
|
Returns the current breakpoint of a sheet style modal
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
Type: `Promise<number | undefined>`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### `onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>`
|
### `onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>`
|
||||||
|
|
||||||
Returns a promise that resolves when the modal did dismiss.
|
Returns a promise that resolves when the modal did dismiss.
|
||||||
@ -1634,6 +1656,17 @@ Type: `Promise<void>`
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### `setCurrentBreakpoint(breakpoint: number) => Promise<void>`
|
||||||
|
|
||||||
|
Move a sheet style modal to a specific breakpoint. The breakpoint value must
|
||||||
|
be a value defined in your `breakpoints` array.
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
Type: `Promise<void>`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Slots
|
## Slots
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { testModal } from '../test.utils';
|
import { openModal, testModal } from '../test.utils';
|
||||||
import { newE2EPage } from '@stencil/core/testing';
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
const DIRECTORY = 'basic';
|
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 page.mouse.click(20, 20);
|
||||||
await ionModalDidDismiss.next();
|
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();
|
||||||
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { newE2EPage } from '@stencil/core/testing';
|
import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing';
|
||||||
import { testModal } from '../test.utils';
|
import { openModal, testModal } from '../test.utils';
|
||||||
import { getActiveElement, getActiveElementParent } from '@utils/test';
|
import { getActiveElement, getActiveElementParent, dragElementBy } from '@utils/test';
|
||||||
|
|
||||||
const DIRECTORY = 'sheet';
|
const DIRECTORY = 'sheet';
|
||||||
|
|
||||||
@ -117,3 +117,95 @@ test('input should not be focusable when backdrop is active', async () => {
|
|||||||
const parentEl = await getActiveElement(page);
|
const parentEl = await getActiveElement(page);
|
||||||
expect(parentEl.tagName).toEqual('ION-BUTTON');
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Modal - Sheet</title>
|
<title>Modal - Sheet</title>
|
||||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport"
|
||||||
|
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
@ -15,62 +16,61 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="Ionic App" />
|
<meta name="apple-mobile-web-app-title" content="Ionic App" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<style>
|
<style>
|
||||||
.custom-height {
|
.custom-height {
|
||||||
--height: 50%;
|
--height: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-handle::part(handle) {
|
.custom-handle::part(handle) {
|
||||||
top: -16px;
|
top: -16px;
|
||||||
background: rgba(255, 255, 255, 0.53);
|
background: rgba(255, 255, 255, 0.53);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-handle::part(content) {
|
.custom-handle::part(content) {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
background-color: #ea445a;
|
background-color: #ea445a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.green {
|
.green {
|
||||||
background-color: #76d672;
|
background-color: #76d672;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
background-color: #3478f6;
|
background-color: #3478f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yellow {
|
.yellow {
|
||||||
background-color: #ffff80;
|
background-color: #ffff80;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pink {
|
.pink {
|
||||||
background-color: #ff6b86;
|
background-color: #ff6b86;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
background-color: #7e34f6;
|
background-color: #7e34f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.black {
|
.black {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.orange {
|
.orange {
|
||||||
background-color: #f69234;
|
background-color: #f69234;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -91,15 +91,30 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-button id="sheet-modal" onclick="presentModal()">Present Sheet Modal</ion-button>
|
<ion-button id="sheet-modal" onclick="presentModal()">Present Sheet Modal</ion-button>
|
||||||
<ion-button id="custom-breakpoint-modal" onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.5, 1] })">Present Sheet Modal (Custom Breakpoints)</ion-button>
|
<ion-button id="custom-breakpoint-modal"
|
||||||
<ion-button id="custom-breakpoint-modal" onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.5, 0.75] })">Present Sheet Modal (Max breakpoint is not 1)</ion-button>
|
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.5, 1] })">Present Sheet Modal (Custom
|
||||||
<ion-button id="custom-backdrop-modal" onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })">Present Sheet Modal (Custom Backdrop Breakpoint)</ion-button>
|
Breakpoints)</ion-button>
|
||||||
<ion-button id="custom-backdrop-off-center-modal" onClick="presentModal({ backdropBreakpoint: 0.3, initialBreakpoint: 0.3, breakpoints: [0.3, 0.5, 0.7, 1] })">Present Sheet Modal (Backdrop breakpoint is off-center)</ion-button>
|
<ion-button id="custom-breakpoint-modal"
|
||||||
<ion-button id="custom-height-modal" onclick="presentModal({ cssClass: 'custom-height' })">Present Sheet Modal (Custom Height)</ion-button>
|
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.5, 0.75] })">Present Sheet Modal (Max
|
||||||
<ion-button id="custom-handle-modal" onclick="presentModal({ cssClass: 'custom-handle' })">Present Sheet Modal (Custom Handle)</ion-button>
|
breakpoint is not 1)</ion-button>
|
||||||
<ion-button id="no-handle-modal" onclick="presentModal({ handle: false })">Present Sheet Modal (No Handle)</ion-button>
|
<ion-button id="custom-backdrop-modal"
|
||||||
<ion-button id="backdrop-active" onclick="presentModal({ backdropBreakpoint: 0.3, initialBreakpoint: 0.5, breakpoints: [0.3, 0.5, 0.7, 1] })">Backdrop is active</ion-button>
|
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })">Present Sheet Modal (Custom
|
||||||
<ion-button id="backdrop-inactive" onclick="presentModal({ cssClass: 'backdrop-inactive', backdropBreakpoint: 0.5, initialBreakpoint: 0.3, breakpoints: [0.3, 0.5, 0.7, 1] })">Backdrop is inactive</ion-button>
|
Backdrop Breakpoint)</ion-button>
|
||||||
|
<ion-button id="custom-backdrop-off-center-modal"
|
||||||
|
onClick="presentModal({ backdropBreakpoint: 0.3, initialBreakpoint: 0.3, breakpoints: [0.3, 0.5, 0.7, 1] })">
|
||||||
|
Present Sheet Modal (Backdrop breakpoint is off-center)</ion-button>
|
||||||
|
<ion-button id="custom-height-modal" onclick="presentModal({ cssClass: 'custom-height' })">Present Sheet Modal
|
||||||
|
(Custom Height)</ion-button>
|
||||||
|
<ion-button id="custom-handle-modal" onclick="presentModal({ cssClass: 'custom-handle' })">Present Sheet Modal
|
||||||
|
(Custom Handle)</ion-button>
|
||||||
|
<ion-button id="no-handle-modal" onclick="presentModal({ handle: false })">Present Sheet Modal (No Handle)
|
||||||
|
</ion-button>
|
||||||
|
<ion-button id="backdrop-active"
|
||||||
|
onclick="presentModal({ backdropBreakpoint: 0.3, initialBreakpoint: 0.5, breakpoints: [0.3, 0.5, 0.7, 1] })">
|
||||||
|
Backdrop is active</ion-button>
|
||||||
|
<ion-button id="backdrop-inactive"
|
||||||
|
onclick="presentModal({ cssClass: 'backdrop-inactive', backdropBreakpoint: 0.5, initialBreakpoint: 0.3, breakpoints: [0.3, 0.5, 0.7, 1] })">
|
||||||
|
Backdrop is inactive</ion-button>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="grid-item red"></div>
|
<div class="grid-item red"></div>
|
||||||
@ -117,13 +132,10 @@
|
|||||||
|
|
||||||
</ion-app>
|
</ion-app>
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("ionModalDidDismiss", function (e) { console.log('DidDismiss', e) })
|
|
||||||
window.addEventListener("ionModalWillDismiss", function (e) { console.log('WillDismiss', e) })
|
|
||||||
|
|
||||||
function createModal(options) {
|
function createModal(options) {
|
||||||
let items = '';
|
let items = '';
|
||||||
|
|
||||||
for (var i = 0; i < 25; i++ ) {
|
for (var i = 0; i < 25; i++) {
|
||||||
items += `<ion-item>Item ${i}</ion-item>`;
|
items += `<ion-item>Item ${i}</ion-item>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,28 @@
|
|||||||
import { newE2EPage } from '@stencil/core/testing';
|
import { E2EPage, newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
import { generateE2EUrl } from '@utils/test';
|
import { generateE2EUrl } from '@utils/test';
|
||||||
|
|
||||||
|
export const openModal = async (
|
||||||
|
page: E2EPage,
|
||||||
|
selector: string
|
||||||
|
) => {
|
||||||
|
const ionModalWillPresent = await page.spyOnEvent('ionModalWillPresent');
|
||||||
|
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||||
|
|
||||||
|
await page.click(selector);
|
||||||
|
|
||||||
|
await ionModalWillPresent.next();
|
||||||
|
await ionModalDidPresent.next();
|
||||||
|
|
||||||
|
await page.waitForSelector(selector);
|
||||||
|
|
||||||
|
const modal = await page.find('ion-modal');
|
||||||
|
await modal.waitForVisible();
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
export const testModal = async (
|
export const testModal = async (
|
||||||
type: string,
|
type: string,
|
||||||
selector: string,
|
selector: string,
|
||||||
@ -15,21 +36,10 @@ export const testModal = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const screenshotCompares = [];
|
const screenshotCompares = [];
|
||||||
const ionModalWillPresent = await page.spyOnEvent('ionModalWillPresent');
|
|
||||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
|
||||||
const ionModalWillDismiss = await page.spyOnEvent('ionModalWillDismiss');
|
const ionModalWillDismiss = await page.spyOnEvent('ionModalWillDismiss');
|
||||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||||
|
|
||||||
await page.click(selector);
|
let modal = await openModal(page, selector);
|
||||||
|
|
||||||
await ionModalWillPresent.next();
|
|
||||||
await ionModalDidPresent.next();
|
|
||||||
|
|
||||||
await page.waitForSelector(selector);
|
|
||||||
|
|
||||||
let modal = await page.find('ion-modal');
|
|
||||||
await modal.waitForVisible();
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
|
|
||||||
screenshotCompares.push(await page.compareScreenshot());
|
screenshotCompares.push(await page.compareScreenshot());
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ export const listenForEvent = async (page: any, eventType: string, element: any,
|
|||||||
* element.
|
* element.
|
||||||
*/
|
*/
|
||||||
export const dragElementBy = async (
|
export const dragElementBy = async (
|
||||||
element: any,
|
element: ElementHandle<Element>,
|
||||||
page: any,
|
page: any,
|
||||||
x = 0,
|
x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
|
Reference in New Issue
Block a user