Skip to content

Commit

Permalink
feat(modal): ability to programmatically set current sheet breakpoint (
Browse files Browse the repository at this point in the history
…#24648)

Resolves #23917

Co-authored-by: Sean Perkins <sean@ionic.io>
  • Loading branch information
EinfachHans and sean-perkins committed Mar 21, 2022
1 parent 2a438da commit 3145c76
Show file tree
Hide file tree
Showing 15 changed files with 474 additions and 136 deletions.
9 changes: 7 additions & 2 deletions angular/src/directives/overlays/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -30,6 +30,10 @@ export declare interface IonModal extends Components.IonModal {
* Emitted after the modal has dismissed.
*/
ionModalDidDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal breakpoint has changed.
*/
ionBreakpointDidChange: EventEmitter<CustomEvent<ModalBreakpointChangeEventDetail>>;
/**
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
*/
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -119,6 +123,7 @@ export class IonModal {
'ionModalWillPresent',
'ionModalWillDismiss',
'ionModalDidDismiss',
'ionBreakpointDidChange',
'didPresent',
'willPresent',
'willDismiss',
Expand Down
17 changes: 16 additions & 1 deletion angular/test/test-app/e2e/src/modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

});

});
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<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>
<ion-content>
<ion-list>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}
}
3 changes: 3 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>
ion-modal,method,getCurrentBreakpoint,getCurrentBreakpoint() => Promise<number | undefined>
ion-modal,method,onDidDismiss,onDidDismiss<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,setCurrentBreakpoint,setCurrentBreakpoint(breakpoint: number) => Promise<void>
ion-modal,event,didDismiss,OverlayEventDetail<any>,true
ion-modal,event,didPresent,void,true
ion-modal,event,ionBreakpointDidChange,ModalBreakpointChangeEventDetail,true
ion-modal,event,ionModalDidDismiss,OverlayEventDetail<any>,true
ion-modal,event,ionModalDidPresent,void,true
ion-modal,event,ionModalWillDismiss,OverlayEventDetail<any>,true
Expand Down
14 changes: 13 additions & 1 deletion core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<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.
*/
Expand Down Expand Up @@ -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<void>;
/**
* If `true`, a backdrop will be displayed behind the modal.
*/
Expand Down Expand Up @@ -5294,6 +5302,10 @@ declare namespace LocalJSX {
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
*/
"onDidPresent"?: (event: CustomEvent<void>) => void;
/**
* Emitted after the modal breakpoint has changed.
*/
"onIonBreakpointDidChange"?: (event: CustomEvent<ModalBreakpointChangeEventDetail>) => void;
/**
* Emitted after the modal has dismissed.
*/
Expand Down
53 changes: 47 additions & 6 deletions core/src/components/modal/gestures/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +39,7 @@ export const createSheetGesture = (
backdropBreakpoint: number,
animation: Animation,
breakpoints: number[] = [],
getCurrentBreakpoint: () => number,
onDismiss: () => void,
onBreakpointChange: (breakpoint: number) => void
) => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)})` }
]);

Expand Down Expand Up @@ -313,5 +350,9 @@ export const createSheetGesture = (
onMove,
onEnd
});
return gesture;

return {
gesture,
moveSheetToBreakpoint
};
};
10 changes: 9 additions & 1 deletion core/src/components/modal/modal-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ export interface ModalAnimationOptions {
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;
}

0 comments on commit 3145c76

Please sign in to comment.