Skip to content

Commit

Permalink
feat(modal): added snapBreakpoints to sheet modals
Browse files Browse the repository at this point in the history
  • Loading branch information
kumibrr committed Dec 22, 2024
1 parent 1b11b82 commit ea68986
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 5 deletions.
8 changes: 8 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,10 @@ export namespace Components {
* If `true`, a backdrop will be displayed behind the modal. This property controls whether or not the backdrop darkens the screen when the modal is presented. It does not control whether or not the backdrop is active or present in the DOM.
*/
"showBackdrop": boolean;
/**
* The snapBreakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property and they must be a value in `breakpoints` property. The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints` allows the content to scroll, and the modal will only be draggable by the handle.
*/
"snapBreakpoints"?: number[];
/**
* An ID corresponding to the trigger element that causes the modal to open when clicked.
*/
Expand Down Expand Up @@ -6622,6 +6626,10 @@ declare namespace LocalJSX {
* If `true`, a backdrop will be displayed behind the modal. This property controls whether or not the backdrop darkens the screen when the modal is presented. It does not control whether or not the backdrop is active or present in the DOM.
*/
"showBackdrop"?: boolean;
/**
* The snapBreakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property and they must be a value in `breakpoints` property. The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints` allows the content to scroll, and the modal will only be draggable by the handle.
*/
"snapBreakpoints"?: number[];
/**
* An ID corresponding to the trigger element that causes the modal to open when clicked.
*/
Expand Down
40 changes: 35 additions & 5 deletions core/src/components/modal/gestures/sheet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createAnimation } from '@utils/animation/animation';
import { isIonContent, findClosestIonContent } from '@utils/content';
import { createGesture } from '@utils/gesture';
import { clamp, raf, getElementRoot } from '@utils/helpers';
Expand Down Expand Up @@ -49,6 +50,7 @@ export const createSheetGesture = (
backdropBreakpoint: number,
animation: Animation,
breakpoints: number[] = [],
snapBreakpoints: number[] = [],
getCurrentBreakpoint: () => number,
onDismiss: () => void,
onBreakpointChange: (breakpoint: number) => void
Expand All @@ -71,6 +73,10 @@ export const createSheetGesture = (
{ offset: 1, transform: 'translateY(100%)' },
],
BACKDROP_KEYFRAMES: backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop,
CONTENT_KEYFRAMES: [
{ offset: 0, maxHeight: '100%' },
{ offset: 1, maxHeight: '0%'},
],
};

const contentEl = baseEl.querySelector('ion-content');
Expand All @@ -79,10 +85,19 @@ export const createSheetGesture = (
let offset = 0;
let canDismissBlocksGesture = false;
const canDismissMaxStep = 0.95;
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
const maxBreakpoint = breakpoints[breakpoints.length - 1];
const minBreakpoint = breakpoints[0];
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
let contentAnimation: Animation | undefined;
if (snapBreakpoints.length > 0) {
contentAnimation =
animation.addAnimation(
createAnimation('contentAnimation')
.addElement(contentEl!.parentElement!)
.keyframes(SheetDefaults.CONTENT_KEYFRAMES))
.childAnimations.find((ani) => ani.id === 'contentAnimation');
}

const enableBackdrop = () => {
baseEl.style.setProperty('pointer-events', 'auto');
Expand Down Expand Up @@ -138,7 +153,7 @@ export const createSheetGesture = (
}
}

if (contentEl && currentBreakpoint !== maxBreakpoint) {
if (contentEl && currentBreakpoint !== maxBreakpoint && !snapBreakpoints.includes(currentBreakpoint)) {
contentEl.scrollY = false;
}

Expand All @@ -152,7 +167,14 @@ export const createSheetGesture = (
* and then swipe again. The target content will not be the same between swipes.
*/
const contentEl = findClosestIonContent(detail.event.target! as HTMLElement);
currentBreakpoint = getCurrentBreakpoint();
currentBreakpoint = getCurrentBreakpoint();;

/**
* If we are in a snap breakpoint, we should not allow the swipe to start.
*/
if (snapBreakpoints.includes(currentBreakpoint) && contentEl) {
return false;
}

if (currentBreakpoint === 1 && contentEl) {
/**
Expand Down Expand Up @@ -323,6 +345,13 @@ export const createSheetGesture = (
},
]);

if (contentAnimation) {
contentAnimation.keyframes([
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
]);
}

animation.progressStep(0);
}

Expand All @@ -345,7 +374,7 @@ export const createSheetGesture = (
* re-enabled. Native iOS allows for scrolling on the sheet modal as soon
* as the gesture is released, so we align with that.
*/
if (contentEl && snapToBreakpoint === breakpoints[breakpoints.length - 1]) {
if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || snapBreakpoints.includes(snapToBreakpoint))) {
contentEl.scrollY = true;
}

Expand All @@ -365,6 +394,7 @@ export const createSheetGesture = (
raf(() => {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
animation.progressStart(true, 1 - snapToBreakpoint);
currentBreakpoint = snapToBreakpoint;
onBreakpointChange(currentBreakpoint);
Expand Down
22 changes: 22 additions & 0 deletions core/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
private wrapperEl?: HTMLElement;
private backdropEl?: HTMLIonBackdropElement;
private sortedBreakpoints?: number[];
private sortedSnapBreakpoints?: number[];
private keyboardOpenCallback?: () => void;
private moveSheetToBreakpoint?: (options: MoveSheetToBreakpointOptions) => Promise<void>;
private inheritedAttributes: Attributes = {};
Expand Down Expand Up @@ -130,6 +131,19 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Prop() breakpoints?: number[];

/**
* The snapBreakpoints to use when creating a sheet modal. Each value in the
* array must be a decimal between 0 and 1 where 0 indicates the modal is fully
* closed and 1 indicates the modal is fully open. Values are relative
* to the height of the modal, not the height of the screen. One of the values in this
* array must be the value of the `initialBreakpoint` property and they must be a
* value in `breakpoints` property.
*
* The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints`
* allows the content to scroll, and the modal will only be draggable by the handle.
*/
@Prop() snapBreakpoints?: number[];

/**
* A decimal value between 0 and 1 that indicates the
* initial point the modal will open at when creating a
Expand Down Expand Up @@ -354,6 +368,12 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
}

snapBreakpointsChanged(snapBreakpoints: number[] | undefined) {
if (snapBreakpoints !== undefined) {
this.sortedSnapBreakpoints = snapBreakpoints.sort((a, b) => a - b);
}
}

connectedCallback() {
const { el } = this;
prepareOverlay(el);
Expand Down Expand Up @@ -429,6 +449,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
raf(() => this.present());
}
this.breakpointsChanged(this.breakpoints);
this.snapBreakpointsChanged(this.snapBreakpoints);

/**
* When binding values in frameworks such as Angular
Expand Down Expand Up @@ -680,6 +701,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
backdropBreakpoint,
ani,
this.sortedBreakpoints,
this.sortedSnapBreakpoints,
() => this.currentBreakpoint ?? 0,
() => this.sheetOnDismiss(),
(breakpoint: number) => {
Expand Down
6 changes: 6 additions & 0 deletions core/src/components/modal/test/sheet/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
>
Present Sheet Modal (Max breakpoint is not 1)
</button>
<button
id="custom-breakpoint-modal"
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0,0.25, 0.5, 0.75], snapBreakpoints: [0.5, 0.75] })"
>
Present Sheet Modal (SnapBreakpoints)
</button>
<button
id="custom-backdrop-modal"
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })"
Expand Down

0 comments on commit ea68986

Please sign in to comment.