react-modal-sheet 5.4.1 → 5.6.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 +263 -88
- package/dist/index.d.mts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +668 -610
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +664 -606
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/context.tsx +16 -6
- package/src/hooks/use-modal-effect.ts +87 -82
- package/src/index.tsx +11 -16
- package/src/{SheetBackdrop.tsx → sheet-backdrop.tsx} +3 -12
- package/src/{SheetContainer.tsx → sheet-container.tsx} +2 -2
- package/src/{SheetContent.tsx → sheet-content.tsx} +9 -3
- package/src/{SheetDragIndicator.tsx → sheet-drag-indicator.tsx} +2 -2
- package/src/{SheetHeader.tsx → sheet-header.tsx} +3 -3
- package/src/sheet.tsx +58 -85
- package/src/snap.ts +202 -103
- package/src/types.tsx +13 -1
- package/src/utils.ts +4 -0
package/package.json
CHANGED
package/src/constants.ts
CHANGED
package/src/context.tsx
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import type { ExposedContextType, InternalContextType } from './types';
|
|
3
3
|
|
|
4
|
-
export const
|
|
5
|
-
undefined
|
|
6
|
-
);
|
|
4
|
+
export const InternalSheetContext = createContext<
|
|
5
|
+
InternalContextType | undefined
|
|
6
|
+
>(undefined);
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
8
|
+
export const ExposedSheetContext = createContext<
|
|
9
|
+
ExposedContextType | undefined
|
|
10
|
+
>(undefined);
|
|
11
|
+
|
|
12
|
+
export function useInternalContext() {
|
|
13
|
+
const context = useContext(InternalSheetContext);
|
|
14
|
+
if (!context) throw new Error('Sheet context error');
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useExposedContext() {
|
|
19
|
+
const context = useContext(ExposedSheetContext);
|
|
10
20
|
if (!context) throw new Error('Sheet context error');
|
|
11
21
|
return context;
|
|
12
22
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type MotionValue, transform } from 'motion';
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
3
|
|
|
4
4
|
import type { SheetDetent, SheetSnapPoint } from '../types';
|
|
5
|
-
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
6
5
|
import { useSafeAreaInsets } from './use-safe-area-insets';
|
|
6
|
+
import { useStableCallback } from './use-stable-callback';
|
|
7
7
|
|
|
8
8
|
export function useModalEffect({
|
|
9
9
|
y,
|
|
@@ -29,114 +29,105 @@ export function useModalEffect({
|
|
|
29
29
|
rootId = undefined;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
useIsomorphicLayoutEffect(() => {
|
|
34
|
-
return () => {
|
|
35
|
-
if (rootId) cleanupModalEffect(rootId);
|
|
36
|
-
};
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
|
-
useIsomorphicLayoutEffect(() => {
|
|
32
|
+
const handleYChange = useStableCallback((yValue: number) => {
|
|
40
33
|
if (!rootId) return;
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
if (!root) return;
|
|
35
|
+
let progress = Math.max(0, 1 - yValue / sheetHeight);
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Start the effect only if we have dragged over the second snap point
|
|
39
|
+
* to make the effect more natural as the sheet will reach it's final
|
|
40
|
+
* position when the user drags it over the second snap point.
|
|
41
|
+
*/
|
|
42
|
+
const snapThresholdPoint =
|
|
43
|
+
snapPoints.length > 1 ? snapPoints[snapPoints.length - 2] : undefined;
|
|
49
44
|
|
|
50
45
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
46
|
+
* If we have snap points, we need to calculate the progress percentage
|
|
47
|
+
* based on the snap point threshold. Note that the maximum value is also
|
|
48
|
+
* different in this case as the range between the start of the effect
|
|
49
|
+
* and its end is different.
|
|
53
50
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* to make the effect more natural as the sheet will reach it's final
|
|
62
|
-
* position when the user drags it over the second snap point.
|
|
63
|
-
*/
|
|
64
|
-
const snapThresholdPoint =
|
|
65
|
-
snapPoints.length > 1 ? snapPoints[snapPoints.length - 2] : undefined;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* If we have snap points, we need to calculate the progress percentage
|
|
69
|
-
* based on the snap point threshold. Note that the maximum value is also
|
|
70
|
-
* different in this case as the range between the start of the effect
|
|
71
|
-
* and its end is different.
|
|
72
|
-
*/
|
|
73
|
-
if (snapThresholdPoint !== undefined) {
|
|
74
|
-
const snapThresholdValue = snapThresholdPoint.snapValueY;
|
|
75
|
-
|
|
76
|
-
if (yValue <= snapThresholdValue) {
|
|
77
|
-
progress = (snapThresholdValue - yValue) / snapThresholdValue;
|
|
78
|
-
} else {
|
|
79
|
-
progress = 0;
|
|
80
|
-
}
|
|
51
|
+
if (snapThresholdPoint !== undefined) {
|
|
52
|
+
const snapThresholdValue = snapThresholdPoint.snapValueY;
|
|
53
|
+
|
|
54
|
+
if (yValue <= snapThresholdValue) {
|
|
55
|
+
progress = (snapThresholdValue - yValue) / snapThresholdValue;
|
|
56
|
+
} else {
|
|
57
|
+
progress = 0;
|
|
81
58
|
}
|
|
59
|
+
}
|
|
82
60
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
61
|
+
/**
|
|
62
|
+
* If we have a start threshold, we need to calculate the progress
|
|
63
|
+
* percentage based on the start threshold (0 to 1). For example,
|
|
64
|
+
* if the start threshold is 0.5, the progress will be 0 until the sheet
|
|
65
|
+
* is dragged over 50% of the complete drag distance.
|
|
66
|
+
*/
|
|
67
|
+
if (startThreshold !== undefined) {
|
|
68
|
+
const startThresholdValue =
|
|
69
|
+
sheetHeight -
|
|
70
|
+
Math.min(Math.floor(startThreshold * sheetHeight), sheetHeight);
|
|
71
|
+
|
|
72
|
+
if (yValue <= startThresholdValue) {
|
|
73
|
+
progress = (startThresholdValue - yValue) / startThresholdValue;
|
|
74
|
+
} else {
|
|
75
|
+
progress = 0;
|
|
99
76
|
}
|
|
77
|
+
}
|
|
100
78
|
|
|
101
|
-
|
|
102
|
-
|
|
79
|
+
// Make sure progress is between 0 and 1
|
|
80
|
+
progress = Math.max(0, Math.min(1, progress));
|
|
103
81
|
|
|
104
|
-
|
|
105
|
-
const ty = transform(progress, [0, 1], [0, 24 + insetTop]);
|
|
106
|
-
const s = transform(progress, [0, 1], [1, (pageWidth - 16) / pageWidth]);
|
|
107
|
-
const borderRadius = transform(progress, [0, 1], [0, 10]);
|
|
82
|
+
const pageWidth = window.innerWidth;
|
|
108
83
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
updateModalEffect(rootId, {
|
|
85
|
+
scale: transform(progress, [0, 1], [1, (pageWidth - 16) / pageWidth]),
|
|
86
|
+
translateY: transform(progress, [0, 1], [0, 24 + insetTop]),
|
|
87
|
+
borderRadius: transform(progress, [0, 1], [0, 10]),
|
|
112
88
|
});
|
|
89
|
+
});
|
|
113
90
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
91
|
+
const handleYComplete = useStableCallback(() => {
|
|
92
|
+
if (!rootId) return;
|
|
93
|
+
|
|
94
|
+
// +5 just to take into account some inprecision to ensure the cleanup is done
|
|
95
|
+
if (y.get() + 5 >= sheetHeight) {
|
|
96
|
+
cleanupModalEffect(rootId);
|
|
120
97
|
}
|
|
98
|
+
});
|
|
121
99
|
|
|
122
|
-
|
|
123
|
-
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!rootId) return;
|
|
102
|
+
|
|
103
|
+
const removeStartListener = y.on('animationStart', () => {
|
|
104
|
+
if (rootId) setupModalEffect(rootId);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const removeChangeListener = y.on('change', (yValue) => {
|
|
108
|
+
handleYChange(yValue);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const removeCompleteListener = y.on('animationComplete', handleYComplete);
|
|
112
|
+
const removeCancelListener = y.on('animationCancel', handleYComplete);
|
|
124
113
|
|
|
125
114
|
return () => {
|
|
115
|
+
if (rootId) cleanupModalEffect(rootId);
|
|
126
116
|
removeStartListener();
|
|
127
117
|
removeChangeListener();
|
|
128
118
|
removeCompleteListener();
|
|
129
119
|
removeCancelListener();
|
|
130
120
|
};
|
|
131
|
-
}, [
|
|
121
|
+
}, [rootId]);
|
|
132
122
|
}
|
|
133
123
|
|
|
134
124
|
function setupModalEffect(rootId: string) {
|
|
135
125
|
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
136
|
-
const body = document.querySelector('body') as HTMLBodyElement;
|
|
137
126
|
if (!root) return;
|
|
138
127
|
|
|
139
|
-
body
|
|
128
|
+
const body = document.querySelector('body') as HTMLBodyElement;
|
|
129
|
+
|
|
130
|
+
body.style.backgroundColor = '#000000';
|
|
140
131
|
root.style.overflow = 'hidden';
|
|
141
132
|
root.style.transitionTimingFunction = 'cubic-bezier(0.32, 0.72, 0, 1)';
|
|
142
133
|
root.style.transitionProperty = 'transform, border-radius';
|
|
@@ -144,11 +135,25 @@ function setupModalEffect(rootId: string) {
|
|
|
144
135
|
root.style.transformOrigin = 'center top';
|
|
145
136
|
}
|
|
146
137
|
|
|
138
|
+
function updateModalEffect(
|
|
139
|
+
rootId: string,
|
|
140
|
+
opts: { scale: number; translateY: number; borderRadius: number }
|
|
141
|
+
) {
|
|
142
|
+
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
143
|
+
if (!root) return;
|
|
144
|
+
|
|
145
|
+
const { scale, translateY, borderRadius } = opts;
|
|
146
|
+
root.style.transform = `scale(${scale}) translate3d(0, ${translateY}px, 0)`;
|
|
147
|
+
root.style.borderTopRightRadius = `${borderRadius}px`;
|
|
148
|
+
root.style.borderTopLeftRadius = `${borderRadius}px`;
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
function cleanupModalEffect(rootId: string) {
|
|
148
152
|
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
149
|
-
const body = document.querySelector('body') as HTMLBodyElement;
|
|
150
153
|
if (!root) return;
|
|
151
154
|
|
|
155
|
+
const body = document.querySelector('body') as HTMLBodyElement;
|
|
156
|
+
|
|
152
157
|
body.style.removeProperty('background-color');
|
|
153
158
|
root.style.removeProperty('overflow');
|
|
154
159
|
root.style.removeProperty('transition-timing-function');
|
package/src/index.tsx
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { SheetBackdrop } from './SheetBackdrop';
|
|
4
|
-
import { SheetContainer } from './SheetContainer';
|
|
5
|
-
import { SheetContent } from './SheetContent';
|
|
6
|
-
import { SheetDragIndicator } from './SheetDragIndicator';
|
|
7
|
-
import { SheetHeader } from './SheetHeader';
|
|
1
|
+
import { useExposedContext } from './context';
|
|
8
2
|
import { Sheet as SheetBase } from './sheet';
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
snapTo: (index: number) => Promise<void>;
|
|
16
|
-
}
|
|
3
|
+
import { SheetBackdrop } from './sheet-backdrop';
|
|
4
|
+
import { SheetContainer } from './sheet-container';
|
|
5
|
+
import { SheetContent } from './sheet-content';
|
|
6
|
+
import { SheetDragIndicator } from './sheet-drag-indicator';
|
|
7
|
+
import { SheetHeader } from './sheet-header';
|
|
8
|
+
import type { ExposedContextType, SheetCompound } from './types';
|
|
17
9
|
|
|
18
10
|
export const Sheet: SheetCompound = Object.assign(SheetBase, {
|
|
19
11
|
Container: SheetContainer,
|
|
@@ -21,12 +13,15 @@ export const Sheet: SheetCompound = Object.assign(SheetBase, {
|
|
|
21
13
|
DragIndicator: SheetDragIndicator,
|
|
22
14
|
Content: SheetContent,
|
|
23
15
|
Backdrop: SheetBackdrop,
|
|
16
|
+
useContext: useExposedContext,
|
|
24
17
|
});
|
|
25
18
|
|
|
26
19
|
export { useScrollPosition } from './hooks/use-scroll-position';
|
|
27
20
|
export { useVirtualKeyboard } from './hooks/use-virtual-keyboard';
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
export interface SheetContext extends ExposedContextType {}
|
|
23
|
+
export interface SheetRef extends ExposedContextType {}
|
|
24
|
+
|
|
30
25
|
export type {
|
|
31
26
|
SheetBackdropProps,
|
|
32
27
|
SheetContainerProps,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type MotionStyle, motion } from 'motion/react';
|
|
2
2
|
import React, { forwardRef } from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useInternalContext } from './context';
|
|
5
5
|
import { styles } from './styles';
|
|
6
6
|
import { type SheetBackdropProps } from './types';
|
|
7
7
|
import { applyStyles } from './utils';
|
|
@@ -10,30 +10,21 @@ const isClickable = (props: any) => !!props.onClick || !!props.onTap;
|
|
|
10
10
|
|
|
11
11
|
export const SheetBackdrop = forwardRef<any, SheetBackdropProps>(
|
|
12
12
|
({ style, className = '', unstyled, ...rest }, ref) => {
|
|
13
|
-
const sheetContext =
|
|
13
|
+
const sheetContext = useInternalContext();
|
|
14
14
|
const clickable = isClickable(rest);
|
|
15
15
|
const Comp = clickable ? motion.button : motion.div;
|
|
16
16
|
const pointerEvents = clickable ? 'auto' : 'none';
|
|
17
17
|
const isUnstyled = unstyled ?? sheetContext.unstyled;
|
|
18
18
|
|
|
19
19
|
const backdropStyle: MotionStyle = {
|
|
20
|
+
opacity: sheetContext.yProgress,
|
|
20
21
|
...applyStyles(styles.backdrop, isUnstyled),
|
|
21
22
|
...style,
|
|
22
23
|
pointerEvents,
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
const animationProps = sheetContext.prefersReducedMotion
|
|
26
|
-
? {}
|
|
27
|
-
: {
|
|
28
|
-
initial: { opacity: 0 },
|
|
29
|
-
animate: { opacity: 1 },
|
|
30
|
-
exit: { opacity: 0 },
|
|
31
|
-
transition: { duration: 1 },
|
|
32
|
-
};
|
|
33
|
-
|
|
34
26
|
return (
|
|
35
27
|
<Comp
|
|
36
|
-
{...animationProps}
|
|
37
28
|
{...(rest as any)}
|
|
38
29
|
ref={ref}
|
|
39
30
|
className={`react-modal-sheet-backdrop ${className}`}
|
|
@@ -2,14 +2,14 @@ import { type MotionStyle, motion } from 'motion/react';
|
|
|
2
2
|
import React, { forwardRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { DEFAULT_HEIGHT } from './constants';
|
|
5
|
-
import {
|
|
5
|
+
import { useInternalContext } from './context';
|
|
6
6
|
import { styles } from './styles';
|
|
7
7
|
import { type SheetContainerProps } from './types';
|
|
8
8
|
import { applyStyles, mergeRefs } from './utils';
|
|
9
9
|
|
|
10
10
|
export const SheetContainer = forwardRef<any, SheetContainerProps>(
|
|
11
11
|
({ children, style, className = '', unstyled, ...rest }, ref) => {
|
|
12
|
-
const sheetContext =
|
|
12
|
+
const sheetContext = useInternalContext();
|
|
13
13
|
|
|
14
14
|
const isUnstyled = unstyled ?? sheetContext.unstyled;
|
|
15
15
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type MotionStyle, motion } from 'motion/react';
|
|
2
2
|
import React, { forwardRef } from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useInternalContext } from './context';
|
|
5
5
|
import { useDragConstraints } from './hooks/use-drag-constraints';
|
|
6
6
|
import { useScrollPosition } from './hooks/use-scroll-position';
|
|
7
7
|
import { styles } from './styles';
|
|
@@ -24,7 +24,7 @@ export const SheetContent = forwardRef<any, SheetContentProps>(
|
|
|
24
24
|
},
|
|
25
25
|
ref
|
|
26
26
|
) => {
|
|
27
|
-
const sheetContext =
|
|
27
|
+
const sheetContext = useInternalContext();
|
|
28
28
|
const dragConstraints = useDragConstraints();
|
|
29
29
|
|
|
30
30
|
const scroll = useScrollPosition({
|
|
@@ -84,7 +84,13 @@ export const SheetContent = forwardRef<any, SheetContentProps>(
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
if (disableScroll) {
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* NOTE: use `clip` to avoid glitchy behavior where using `hidden`
|
|
89
|
+
* would actually allow scrolling but just not show the scroll content.
|
|
90
|
+
* Using `hidden` seems to work fine on iOS but on Android it causes
|
|
91
|
+
* the up drag gesture to cancel occasionally causing bad UX.
|
|
92
|
+
*/
|
|
93
|
+
scrollStyle.overflowY = 'clip';
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
if (dragProps !== undefined) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { motion, useTransform } from 'motion/react';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useInternalContext } from './context';
|
|
5
5
|
import { styles } from './styles';
|
|
6
6
|
import type { SheetDragIndicatorProps } from './types';
|
|
7
7
|
import { applyStyles } from './utils';
|
|
@@ -12,7 +12,7 @@ export function SheetDragIndicator({
|
|
|
12
12
|
unstyled,
|
|
13
13
|
...rest
|
|
14
14
|
}: SheetDragIndicatorProps) {
|
|
15
|
-
const sheetContext =
|
|
15
|
+
const sheetContext = useInternalContext();
|
|
16
16
|
|
|
17
17
|
const indicator1Transform = useTransform(
|
|
18
18
|
sheetContext.indicatorRotation,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { motion } from 'motion/react';
|
|
2
2
|
import React, { forwardRef } from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useInternalContext } from './context';
|
|
5
5
|
import { useDragConstraints } from './hooks/use-drag-constraints';
|
|
6
|
-
import { SheetDragIndicator } from './
|
|
6
|
+
import { SheetDragIndicator } from './sheet-drag-indicator';
|
|
7
7
|
import { styles } from './styles';
|
|
8
8
|
import { type SheetHeaderProps } from './types';
|
|
9
9
|
import { applyStyles, mergeRefs } from './utils';
|
|
@@ -13,7 +13,7 @@ export const SheetHeader = forwardRef<any, SheetHeaderProps>(
|
|
|
13
13
|
{ children, style, disableDrag, unstyled, className = '', ...rest },
|
|
14
14
|
ref
|
|
15
15
|
) => {
|
|
16
|
-
const sheetContext =
|
|
16
|
+
const sheetContext = useInternalContext();
|
|
17
17
|
const dragConstraints = useDragConstraints();
|
|
18
18
|
const dragProps =
|
|
19
19
|
disableDrag || sheetContext.disableDrag
|