react-modal-sheet 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -4
- package/dist/index.d.mts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +83 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SheetScroller.tsx +49 -18
- package/src/sheet.tsx +33 -27
- package/src/styles.ts +2 -0
- package/src/types.tsx +6 -4
- package/src/utils.ts +34 -0
package/README.md
CHANGED
|
@@ -159,7 +159,9 @@ The `y` value is an internal `MotionValue` that represents the distance to the t
|
|
|
159
159
|
|
|
160
160
|
Similarly to the `snapTo` method the `y` value can be accessed via a ref.
|
|
161
161
|
|
|
162
|
-
The `y` value can be useful for certain
|
|
162
|
+
The `y` value can be useful for certain situtations, eg. when you want to combine snap points with scrollable sheet content and ensure that the content stays properly scrollable at any snap point.
|
|
163
|
+
|
|
164
|
+
Below you can see a simplified example of this situation:
|
|
163
165
|
|
|
164
166
|
```tsx
|
|
165
167
|
import { Sheet, SheetRef } from 'react-modal-sheet';
|
|
@@ -202,6 +204,11 @@ function Example() {
|
|
|
202
204
|
}
|
|
203
205
|
```
|
|
204
206
|
|
|
207
|
+
> [!NOTE]
|
|
208
|
+
> As this use case is quite a common one, the `Sheet.Scroller` component has a built-in prop called `autoPadding` which automatically keeps the bottom padding in sync with the animated `y` value.
|
|
209
|
+
>
|
|
210
|
+
> See [Automatic padding with snap points](#automatic-padding-with-snap-points) for more details.
|
|
211
|
+
|
|
205
212
|
### Detents
|
|
206
213
|
|
|
207
214
|
By default the sheet will take the full height of the page minus top padding and safe area inset. If you want the sheet height to be based on it's content you can pass `detent="content-height"` prop to the `Sheet` component:
|
|
@@ -280,9 +287,11 @@ Sheet scroller can be used to make the whole sheet content or parts of it scroll
|
|
|
280
287
|
|
|
281
288
|
#### Scroller props
|
|
282
289
|
|
|
283
|
-
| Name
|
|
284
|
-
|
|
|
285
|
-
| `draggableAt`
|
|
290
|
+
| Name | Required | Default | Description |
|
|
291
|
+
| --------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
292
|
+
| `draggableAt` | no | `"top"` | Should the drag be enabled when the element is scrolled either to the top, bottom, or both. Available values: `top`, `bottom`, `both`. |
|
|
293
|
+
| `disableScroll` | no | false | Disable scrolling. This can be combined with snap points to make the sheet scroller only scrollable at a given snap point (usually the top position). |
|
|
294
|
+
| `autoPadding` | no | false | Automatically apply padding bottom to the scroller based on the dragged distance to ensure that content can be scrolled to the bottom at any snap point. |
|
|
286
295
|
|
|
287
296
|
### `Sheet.Backdrop`
|
|
288
297
|
|
|
@@ -340,6 +349,72 @@ function ScrollableExample() {
|
|
|
340
349
|
}
|
|
341
350
|
```
|
|
342
351
|
|
|
352
|
+
#### Disabling scroll manually
|
|
353
|
+
|
|
354
|
+
You can also disable scrolling manually by passing `disableScroll` prop to the `Sheet.Scroller` component. This can be useful if you want to have a scrollable sheet but only at a certain snap point.
|
|
355
|
+
|
|
356
|
+
For example, if you have a snap point at the top of the sheet and you want to make the content scrollable only when the sheet is at that snap point, you can do the following:
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
const snapPoints = [1, 0.5, 0];
|
|
360
|
+
const initialSnap = 1;
|
|
361
|
+
|
|
362
|
+
function DisableScrollExample() {
|
|
363
|
+
const [currentSnap, setCurrentSnap] = useState(initialSnap);
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<Sheet
|
|
367
|
+
initialSnap={initialSnap}
|
|
368
|
+
snapPoints={snapPoints}
|
|
369
|
+
onSnap={setCurrentSnap}
|
|
370
|
+
>
|
|
371
|
+
<Sheet.Container>
|
|
372
|
+
<Sheet.Header />
|
|
373
|
+
<Sheet.Content>
|
|
374
|
+
<Sheet.Scroller draggableAt="top" disableScroll={currentSnap !== 0}>
|
|
375
|
+
{/*...*/}
|
|
376
|
+
</Sheet.Scroller>
|
|
377
|
+
</Sheet.Content>
|
|
378
|
+
</Sheet.Container>
|
|
379
|
+
<Sheet.Backdrop />
|
|
380
|
+
</Sheet>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### Automatic padding with snap points
|
|
386
|
+
|
|
387
|
+
If you are using snap points and want to make sure that the content is scrollable at all snap points you can use the `autoPadding` prop on the `Sheet.Scroller` component.
|
|
388
|
+
|
|
389
|
+
This will automatically apply padding to the bottom of the scroller based on the dragged distance to ensure that content can be scrolled to the bottom at any snap point.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import { Sheet } from 'react-modal-sheet';
|
|
393
|
+
import { useState } from 'react';
|
|
394
|
+
|
|
395
|
+
const snapPoints = [-50, 0.5, 0];
|
|
396
|
+
const initialSnap = 1;
|
|
397
|
+
|
|
398
|
+
function AutoPaddingExample() {
|
|
399
|
+
return (
|
|
400
|
+
<Sheet initialSnap={initialSnap} snapPoints={snapPoints}>
|
|
401
|
+
<Sheet.Container>
|
|
402
|
+
<Sheet.Header />
|
|
403
|
+
<Sheet.Content>
|
|
404
|
+
<Sheet.Scroller autoPadding draggableAt="both">
|
|
405
|
+
{/*...*/}
|
|
406
|
+
</Sheet.Scroller>
|
|
407
|
+
</Sheet.Content>
|
|
408
|
+
</Sheet.Container>
|
|
409
|
+
<Sheet.Backdrop />
|
|
410
|
+
</Sheet>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
> [!NOTE]
|
|
416
|
+
> For backwards compatibility the `autoPadding` prop is disabled by default but it will be enabled by default in the next major version.
|
|
417
|
+
|
|
343
418
|
### 🪟 iOS Modal View effect
|
|
344
419
|
|
|
345
420
|
In addition to the `Sheet.Backdrop` it's possible to apply a scaling effect to the main app element to highlight the modality of the bottom sheet. This effect mimics the [iOS Modal View](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/modality/) presentation style to bring more focus to the sheet and add some delight to the user experience.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EasingDefinition, motion, MotionProps, MotionValue } from 'motion/react';
|
|
2
|
-
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes
|
|
2
|
+
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
interface SheetEvents {
|
|
5
5
|
onOpenStart?: () => void;
|
|
@@ -12,6 +12,7 @@ type SheetDetent = 'full-height' | 'content-height';
|
|
|
12
12
|
type CommonProps = MotionProps & {
|
|
13
13
|
className?: string;
|
|
14
14
|
};
|
|
15
|
+
type MotionDivProps = ComponentPropsWithoutRef<typeof motion.div>;
|
|
15
16
|
interface SheetTweenConfig {
|
|
16
17
|
ease: EasingDefinition;
|
|
17
18
|
duration: number;
|
|
@@ -34,7 +35,7 @@ type SheetProps = {
|
|
|
34
35
|
dragVelocityThreshold?: number;
|
|
35
36
|
dragCloseThreshold?: number;
|
|
36
37
|
modalEffectThreshold?: number;
|
|
37
|
-
} & SheetEvents &
|
|
38
|
+
} & SheetEvents & MotionDivProps;
|
|
38
39
|
type SheetContainerProps = Omit<CommonProps, 'initial' | 'animate' | 'exit' | 'onAnimationComplete'> & {
|
|
39
40
|
children: ReactNode;
|
|
40
41
|
};
|
|
@@ -43,8 +44,10 @@ type SheetDraggableProps = Omit<CommonProps, 'drag' | 'dragElastic' | 'dragConst
|
|
|
43
44
|
disableDrag?: boolean;
|
|
44
45
|
};
|
|
45
46
|
type SheetBackdropProps = Omit<CommonProps, 'initial' | 'animate' | 'exit'>;
|
|
46
|
-
type SheetScrollerProps =
|
|
47
|
+
type SheetScrollerProps = MotionDivProps & {
|
|
47
48
|
draggableAt?: 'top' | 'bottom' | 'both';
|
|
49
|
+
disableScroll?: boolean;
|
|
50
|
+
autoPadding?: boolean;
|
|
48
51
|
};
|
|
49
52
|
type ContainerComponent = ForwardRefExoticComponent<SheetContainerProps & RefAttributes<any>>;
|
|
50
53
|
type DraggableComponent = ForwardRefExoticComponent<SheetDraggableProps & RefAttributes<any>>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EasingDefinition, motion, MotionProps, MotionValue } from 'motion/react';
|
|
2
|
-
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes
|
|
2
|
+
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
interface SheetEvents {
|
|
5
5
|
onOpenStart?: () => void;
|
|
@@ -12,6 +12,7 @@ type SheetDetent = 'full-height' | 'content-height';
|
|
|
12
12
|
type CommonProps = MotionProps & {
|
|
13
13
|
className?: string;
|
|
14
14
|
};
|
|
15
|
+
type MotionDivProps = ComponentPropsWithoutRef<typeof motion.div>;
|
|
15
16
|
interface SheetTweenConfig {
|
|
16
17
|
ease: EasingDefinition;
|
|
17
18
|
duration: number;
|
|
@@ -34,7 +35,7 @@ type SheetProps = {
|
|
|
34
35
|
dragVelocityThreshold?: number;
|
|
35
36
|
dragCloseThreshold?: number;
|
|
36
37
|
modalEffectThreshold?: number;
|
|
37
|
-
} & SheetEvents &
|
|
38
|
+
} & SheetEvents & MotionDivProps;
|
|
38
39
|
type SheetContainerProps = Omit<CommonProps, 'initial' | 'animate' | 'exit' | 'onAnimationComplete'> & {
|
|
39
40
|
children: ReactNode;
|
|
40
41
|
};
|
|
@@ -43,8 +44,10 @@ type SheetDraggableProps = Omit<CommonProps, 'drag' | 'dragElastic' | 'dragConst
|
|
|
43
44
|
disableDrag?: boolean;
|
|
44
45
|
};
|
|
45
46
|
type SheetBackdropProps = Omit<CommonProps, 'initial' | 'animate' | 'exit'>;
|
|
46
|
-
type SheetScrollerProps =
|
|
47
|
+
type SheetScrollerProps = MotionDivProps & {
|
|
47
48
|
draggableAt?: 'top' | 'bottom' | 'both';
|
|
49
|
+
disableScroll?: boolean;
|
|
50
|
+
autoPadding?: boolean;
|
|
48
51
|
};
|
|
49
52
|
type ContainerComponent = ForwardRefExoticComponent<SheetContainerProps & RefAttributes<any>>;
|
|
50
53
|
type DraggableComponent = ForwardRefExoticComponent<SheetDraggableProps & RefAttributes<any>>;
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,26 @@ function getSnapPoints({
|
|
|
52
52
|
);
|
|
53
53
|
return snapPointValues;
|
|
54
54
|
}
|
|
55
|
+
function getClosestSnapPoint({
|
|
56
|
+
snapPoints,
|
|
57
|
+
currentY,
|
|
58
|
+
sheetHeight,
|
|
59
|
+
detent
|
|
60
|
+
}) {
|
|
61
|
+
const snapInverse = snapPoints.map(
|
|
62
|
+
(p) => sheetHeight - Math.min(p, sheetHeight)
|
|
63
|
+
);
|
|
64
|
+
if (detent === "content-height" && !snapInverse.includes(0)) {
|
|
65
|
+
snapInverse.unshift(0);
|
|
66
|
+
}
|
|
67
|
+
const snapTo = getClosest(snapInverse, currentY);
|
|
68
|
+
const snapIndex = snapInverse.indexOf(snapTo);
|
|
69
|
+
const snapY = validateSnapTo({
|
|
70
|
+
snapTo: getClosest(snapInverse, currentY),
|
|
71
|
+
sheetHeight
|
|
72
|
+
});
|
|
73
|
+
return { snapY, snapIndex };
|
|
74
|
+
}
|
|
55
75
|
function getClosest(nums, goal) {
|
|
56
76
|
let closest = nums[0];
|
|
57
77
|
let minDifference = Math.abs(nums[0] - goal);
|
|
@@ -194,6 +214,7 @@ var styles = {
|
|
|
194
214
|
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
|
195
215
|
touchAction: "none",
|
|
196
216
|
border: "none",
|
|
217
|
+
userSelect: "none",
|
|
197
218
|
WebkitTapHighlightColor: "transparent"
|
|
198
219
|
},
|
|
199
220
|
container: {
|
|
@@ -236,7 +257,8 @@ var styles = {
|
|
|
236
257
|
},
|
|
237
258
|
scroller: {
|
|
238
259
|
height: "100%",
|
|
239
|
-
overflowY: "auto"
|
|
260
|
+
overflowY: "auto",
|
|
261
|
+
overscrollBehaviorY: "none"
|
|
240
262
|
}
|
|
241
263
|
};
|
|
242
264
|
var useIsomorphicLayoutEffect = IS_SSR ? React2.useEffect : React2.useLayoutEffect;
|
|
@@ -667,11 +689,21 @@ var Sheet = React2.forwardRef(
|
|
|
667
689
|
onCloseEnd
|
|
668
690
|
};
|
|
669
691
|
});
|
|
670
|
-
const onDrag = useStableCallback((_,
|
|
692
|
+
const onDrag = useStableCallback((_, info) => {
|
|
693
|
+
const currentY = y.get();
|
|
694
|
+
if (snapPointsProp) {
|
|
695
|
+
const sheetHeight = getSheetHeight(sheetRef);
|
|
696
|
+
const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight });
|
|
697
|
+
const topSnapPoint = sheetHeight - snapPoints[0];
|
|
698
|
+
if (info.delta.y < 0 && currentY <= topSnapPoint) {
|
|
699
|
+
y.set(topSnapPoint);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
671
703
|
const velocity = y.getVelocity();
|
|
672
704
|
if (velocity > 0) indicatorRotation.set(10);
|
|
673
705
|
if (velocity < 0) indicatorRotation.set(-10);
|
|
674
|
-
y.set(Math.max(
|
|
706
|
+
y.set(Math.max(currentY + info.delta.y, 0));
|
|
675
707
|
});
|
|
676
708
|
const onDragStart = useStableCallback(() => {
|
|
677
709
|
const focusedElement = document.activeElement;
|
|
@@ -690,18 +722,13 @@ var Sheet = React2.forwardRef(
|
|
|
690
722
|
let yTo = 0;
|
|
691
723
|
const snapPoints = snapPointsProp ? getSnapPoints({ snapPointsProp, sheetHeight }) : void 0;
|
|
692
724
|
if (snapPoints) {
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
const snapTo = getClosest(snapInverse, currentY);
|
|
700
|
-
const snapIndex = snapInverse.indexOf(snapTo);
|
|
701
|
-
yTo = validateSnapTo({
|
|
702
|
-
snapTo: getClosest(snapInverse, currentY),
|
|
703
|
-
sheetHeight
|
|
725
|
+
const { snapY, snapIndex } = getClosestSnapPoint({
|
|
726
|
+
snapPoints,
|
|
727
|
+
currentY,
|
|
728
|
+
sheetHeight,
|
|
729
|
+
detent
|
|
704
730
|
});
|
|
731
|
+
yTo = snapY;
|
|
705
732
|
onSnap == null ? void 0 : onSnap(snapIndex);
|
|
706
733
|
} else if (currentY / sheetHeight > dragCloseThreshold) {
|
|
707
734
|
yTo = sheetHeight;
|
|
@@ -725,9 +752,9 @@ var Sheet = React2.forwardRef(
|
|
|
725
752
|
const snapIndex = initialSnap;
|
|
726
753
|
const sheetHeight = getSheetHeight(sheetRef);
|
|
727
754
|
const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight });
|
|
728
|
-
const
|
|
729
|
-
if (
|
|
730
|
-
yTo =
|
|
755
|
+
const snapY = getSnapY({ sheetHeight, snapPoints, snapIndex });
|
|
756
|
+
if (snapY !== null) {
|
|
757
|
+
yTo = snapY;
|
|
731
758
|
}
|
|
732
759
|
}
|
|
733
760
|
react.animate(y, yTo, animationOptions);
|
|
@@ -741,11 +768,11 @@ var Sheet = React2.forwardRef(
|
|
|
741
768
|
}
|
|
742
769
|
const sheetHeight = getSheetHeight(sheetRef);
|
|
743
770
|
const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight });
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
react.animate(y,
|
|
771
|
+
const snapY = getSnapY({ sheetHeight, snapPoints, snapIndex });
|
|
772
|
+
if (snapY !== null) {
|
|
773
|
+
react.animate(y, snapY, animationOptions);
|
|
747
774
|
onSnap == null ? void 0 : onSnap(snapIndex);
|
|
748
|
-
if (
|
|
775
|
+
if (snapY + 1 >= sheetHeight) {
|
|
749
776
|
onClose();
|
|
750
777
|
}
|
|
751
778
|
}
|
|
@@ -962,9 +989,22 @@ var SheetBackdrop = React2.forwardRef(
|
|
|
962
989
|
);
|
|
963
990
|
SheetBackdrop.displayName = "SheetBackdrop";
|
|
964
991
|
var SheetScroller = React2.forwardRef(
|
|
965
|
-
({
|
|
966
|
-
|
|
992
|
+
({
|
|
993
|
+
autoPadding = true,
|
|
994
|
+
disableScroll = false,
|
|
995
|
+
draggableAt = "top",
|
|
996
|
+
children,
|
|
997
|
+
style: styleProp,
|
|
998
|
+
className = "",
|
|
999
|
+
...rest
|
|
1000
|
+
}, ref) => {
|
|
1001
|
+
const { y } = useSheetContext();
|
|
1002
|
+
const { setDragEnabled, setDragDisabled } = useSheetScrollerContext();
|
|
967
1003
|
function determineDragState(element) {
|
|
1004
|
+
if (disableScroll) {
|
|
1005
|
+
setDragEnabled();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
968
1008
|
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
969
1009
|
const isScrollable2 = scrollHeight > clientHeight;
|
|
970
1010
|
if (!isScrollable2) return;
|
|
@@ -972,31 +1012,37 @@ var SheetScroller = React2.forwardRef(
|
|
|
972
1012
|
const isAtBottom = scrollHeight - scrollTop === clientHeight;
|
|
973
1013
|
const shouldEnable = draggableAt === "top" && isAtTop || draggableAt === "bottom" && isAtBottom || draggableAt === "both" && (isAtTop || isAtBottom);
|
|
974
1014
|
if (shouldEnable) {
|
|
975
|
-
|
|
1015
|
+
setDragEnabled();
|
|
976
1016
|
} else {
|
|
977
|
-
|
|
1017
|
+
setDragDisabled();
|
|
978
1018
|
}
|
|
979
1019
|
}
|
|
980
|
-
function onScroll(
|
|
981
|
-
determineDragState(
|
|
982
|
-
if (rest.onScroll)
|
|
983
|
-
rest.onScroll(e);
|
|
984
|
-
}
|
|
1020
|
+
function onScroll(event) {
|
|
1021
|
+
determineDragState(event.currentTarget);
|
|
1022
|
+
if (rest.onScroll) rest.onScroll(event);
|
|
985
1023
|
}
|
|
986
|
-
function onTouchStart(
|
|
987
|
-
determineDragState(
|
|
988
|
-
if (rest.onTouchStart)
|
|
989
|
-
rest.onTouchStart(e);
|
|
990
|
-
}
|
|
1024
|
+
function onTouchStart(event) {
|
|
1025
|
+
determineDragState(event.currentTarget);
|
|
1026
|
+
if (rest.onTouchStart) rest.onTouchStart(event);
|
|
991
1027
|
}
|
|
992
1028
|
const scrollProps = isTouchDevice() ? { onScroll, onTouchStart } : void 0;
|
|
1029
|
+
const style = {
|
|
1030
|
+
...styles.scroller,
|
|
1031
|
+
...styleProp
|
|
1032
|
+
};
|
|
1033
|
+
if (autoPadding) {
|
|
1034
|
+
style.paddingBottom = y;
|
|
1035
|
+
}
|
|
1036
|
+
if (disableScroll) {
|
|
1037
|
+
style.overflowY = "hidden";
|
|
1038
|
+
}
|
|
993
1039
|
return /* @__PURE__ */ React2__default.default.createElement(
|
|
994
|
-
|
|
1040
|
+
react.motion.div,
|
|
995
1041
|
{
|
|
996
1042
|
...rest,
|
|
997
1043
|
ref,
|
|
998
1044
|
className: `react-modal-sheet-scroller ${className}`,
|
|
999
|
-
style
|
|
1045
|
+
style,
|
|
1000
1046
|
...scrollProps
|
|
1001
1047
|
},
|
|
1002
1048
|
children
|