Skip to content

Commit ea68986

Browse files
committed
feat(modal): added snapBreakpoints to sheet modals
1 parent 1b11b82 commit ea68986

File tree

4 files changed

+71
-5
lines changed

4 files changed

+71
-5
lines changed

core/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,6 +1801,10 @@ export namespace Components {
18011801
* 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.
18021802
*/
18031803
"showBackdrop": boolean;
1804+
/**
1805+
* 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.
1806+
*/
1807+
"snapBreakpoints"?: number[];
18041808
/**
18051809
* An ID corresponding to the trigger element that causes the modal to open when clicked.
18061810
*/
@@ -6622,6 +6626,10 @@ declare namespace LocalJSX {
66226626
* 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.
66236627
*/
66246628
"showBackdrop"?: boolean;
6629+
/**
6630+
* 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.
6631+
*/
6632+
"snapBreakpoints"?: number[];
66256633
/**
66266634
* An ID corresponding to the trigger element that causes the modal to open when clicked.
66276635
*/

core/src/components/modal/gestures/sheet.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createAnimation } from '@utils/animation/animation';
12
import { isIonContent, findClosestIonContent } from '@utils/content';
23
import { createGesture } from '@utils/gesture';
34
import { clamp, raf, getElementRoot } from '@utils/helpers';
@@ -49,6 +50,7 @@ export const createSheetGesture = (
4950
backdropBreakpoint: number,
5051
animation: Animation,
5152
breakpoints: number[] = [],
53+
snapBreakpoints: number[] = [],
5254
getCurrentBreakpoint: () => number,
5355
onDismiss: () => void,
5456
onBreakpointChange: (breakpoint: number) => void
@@ -71,6 +73,10 @@ export const createSheetGesture = (
7173
{ offset: 1, transform: 'translateY(100%)' },
7274
],
7375
BACKDROP_KEYFRAMES: backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop,
76+
CONTENT_KEYFRAMES: [
77+
{ offset: 0, maxHeight: '100%' },
78+
{ offset: 1, maxHeight: '0%'},
79+
],
7480
};
7581

7682
const contentEl = baseEl.querySelector('ion-content');
@@ -79,10 +85,19 @@ export const createSheetGesture = (
7985
let offset = 0;
8086
let canDismissBlocksGesture = false;
8187
const canDismissMaxStep = 0.95;
82-
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
83-
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
8488
const maxBreakpoint = breakpoints[breakpoints.length - 1];
8589
const minBreakpoint = breakpoints[0];
90+
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
91+
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
92+
let contentAnimation: Animation | undefined;
93+
if (snapBreakpoints.length > 0) {
94+
contentAnimation =
95+
animation.addAnimation(
96+
createAnimation('contentAnimation')
97+
.addElement(contentEl!.parentElement!)
98+
.keyframes(SheetDefaults.CONTENT_KEYFRAMES))
99+
.childAnimations.find((ani) => ani.id === 'contentAnimation');
100+
}
86101

87102
const enableBackdrop = () => {
88103
baseEl.style.setProperty('pointer-events', 'auto');
@@ -138,7 +153,7 @@ export const createSheetGesture = (
138153
}
139154
}
140155

141-
if (contentEl && currentBreakpoint !== maxBreakpoint) {
156+
if (contentEl && currentBreakpoint !== maxBreakpoint && !snapBreakpoints.includes(currentBreakpoint)) {
142157
contentEl.scrollY = false;
143158
}
144159

@@ -152,7 +167,14 @@ export const createSheetGesture = (
152167
* and then swipe again. The target content will not be the same between swipes.
153168
*/
154169
const contentEl = findClosestIonContent(detail.event.target! as HTMLElement);
155-
currentBreakpoint = getCurrentBreakpoint();
170+
currentBreakpoint = getCurrentBreakpoint();;
171+
172+
/**
173+
* If we are in a snap breakpoint, we should not allow the swipe to start.
174+
*/
175+
if (snapBreakpoints.includes(currentBreakpoint) && contentEl) {
176+
return false;
177+
}
156178

157179
if (currentBreakpoint === 1 && contentEl) {
158180
/**
@@ -323,6 +345,13 @@ export const createSheetGesture = (
323345
},
324346
]);
325347

348+
if (contentAnimation) {
349+
contentAnimation.keyframes([
350+
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
351+
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
352+
]);
353+
}
354+
326355
animation.progressStep(0);
327356
}
328357

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

@@ -365,6 +394,7 @@ export const createSheetGesture = (
365394
raf(() => {
366395
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
367396
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
397+
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
368398
animation.progressStart(true, 1 - snapToBreakpoint);
369399
currentBreakpoint = snapToBreakpoint;
370400
onBreakpointChange(currentBreakpoint);

core/src/components/modal/modal.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
7575
private wrapperEl?: HTMLElement;
7676
private backdropEl?: HTMLIonBackdropElement;
7777
private sortedBreakpoints?: number[];
78+
private sortedSnapBreakpoints?: number[];
7879
private keyboardOpenCallback?: () => void;
7980
private moveSheetToBreakpoint?: (options: MoveSheetToBreakpointOptions) => Promise<void>;
8081
private inheritedAttributes: Attributes = {};
@@ -130,6 +131,19 @@ export class Modal implements ComponentInterface, OverlayInterface {
130131
*/
131132
@Prop() breakpoints?: number[];
132133

134+
/**
135+
* The snapBreakpoints to use when creating a sheet modal. Each value in the
136+
* array must be a decimal between 0 and 1 where 0 indicates the modal is fully
137+
* closed and 1 indicates the modal is fully open. Values are relative
138+
* to the height of the modal, not the height of the screen. One of the values in this
139+
* array must be the value of the `initialBreakpoint` property and they must be a
140+
* value in `breakpoints` property.
141+
*
142+
* The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints`
143+
* allows the content to scroll, and the modal will only be draggable by the handle.
144+
*/
145+
@Prop() snapBreakpoints?: number[];
146+
133147
/**
134148
* A decimal value between 0 and 1 that indicates the
135149
* initial point the modal will open at when creating a
@@ -354,6 +368,12 @@ export class Modal implements ComponentInterface, OverlayInterface {
354368
}
355369
}
356370

371+
snapBreakpointsChanged(snapBreakpoints: number[] | undefined) {
372+
if (snapBreakpoints !== undefined) {
373+
this.sortedSnapBreakpoints = snapBreakpoints.sort((a, b) => a - b);
374+
}
375+
}
376+
357377
connectedCallback() {
358378
const { el } = this;
359379
prepareOverlay(el);
@@ -429,6 +449,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
429449
raf(() => this.present());
430450
}
431451
this.breakpointsChanged(this.breakpoints);
452+
this.snapBreakpointsChanged(this.snapBreakpoints);
432453

433454
/**
434455
* When binding values in frameworks such as Angular
@@ -680,6 +701,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
680701
backdropBreakpoint,
681702
ani,
682703
this.sortedBreakpoints,
704+
this.sortedSnapBreakpoints,
683705
() => this.currentBreakpoint ?? 0,
684706
() => this.sheetOnDismiss(),
685707
(breakpoint: number) => {

core/src/components/modal/test/sheet/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@
100100
>
101101
Present Sheet Modal (Max breakpoint is not 1)
102102
</button>
103+
<button
104+
id="custom-breakpoint-modal"
105+
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0,0.25, 0.5, 0.75], snapBreakpoints: [0.5, 0.75] })"
106+
>
107+
Present Sheet Modal (SnapBreakpoints)
108+
</button>
103109
<button
104110
id="custom-backdrop-modal"
105111
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })"

0 commit comments

Comments
 (0)