react-modal-sheet 5.5.0 → 5.7.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 +305 -89
- package/dist/index.d.mts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +604 -558
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +581 -535
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/context.tsx +16 -6
- package/src/hooks/use-modal-effect.ts +147 -85
- 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 +38 -29
- package/src/snap.ts +1 -5
- package/src/types.tsx +13 -1
- package/src/utils.ts +4 -0
package/package.json
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,10 @@
|
|
|
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 {
|
|
5
|
+
import { clamp } from '../utils';
|
|
6
6
|
import { useSafeAreaInsets } from './use-safe-area-insets';
|
|
7
|
+
import { useStableCallback } from './use-stable-callback';
|
|
7
8
|
|
|
8
9
|
export function useModalEffect({
|
|
9
10
|
y,
|
|
@@ -29,114 +30,108 @@ export function useModalEffect({
|
|
|
29
30
|
rootId = undefined;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
useIsomorphicLayoutEffect(() => {
|
|
34
|
-
return () => {
|
|
35
|
-
if (rootId) cleanupModalEffect(rootId);
|
|
36
|
-
};
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
|
-
useIsomorphicLayoutEffect(() => {
|
|
33
|
+
const handleYChange = useStableCallback((yValue: number) => {
|
|
40
34
|
if (!rootId) return;
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
if (!root) return;
|
|
36
|
+
let progress = Math.max(0, 1 - yValue / sheetHeight);
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Start the effect only if we have dragged over the second snap point
|
|
40
|
+
* to make the effect more natural as the sheet will reach it's final
|
|
41
|
+
* position when the user drags it over the second snap point.
|
|
42
|
+
*/
|
|
43
|
+
const snapThresholdPoint =
|
|
44
|
+
snapPoints.length > 1 ? snapPoints[snapPoints.length - 2] : undefined;
|
|
49
45
|
|
|
50
46
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
47
|
+
* If we have snap points, we need to calculate the progress percentage
|
|
48
|
+
* based on the snap point threshold. Note that the maximum value is also
|
|
49
|
+
* different in this case as the range between the start of the effect
|
|
50
|
+
* and its end is different.
|
|
53
51
|
*/
|
|
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
|
-
}
|
|
52
|
+
if (snapThresholdPoint !== undefined) {
|
|
53
|
+
const snapThresholdValue = snapThresholdPoint.snapValueY;
|
|
54
|
+
|
|
55
|
+
if (yValue <= snapThresholdValue) {
|
|
56
|
+
progress = (snapThresholdValue - yValue) / snapThresholdValue;
|
|
57
|
+
} else {
|
|
58
|
+
progress = 0;
|
|
81
59
|
}
|
|
60
|
+
}
|
|
82
61
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
62
|
+
/**
|
|
63
|
+
* If we have a start threshold, we need to calculate the progress
|
|
64
|
+
* percentage based on the start threshold (0 to 1). For example,
|
|
65
|
+
* if the start threshold is 0.5, the progress will be 0 until the sheet
|
|
66
|
+
* is dragged over 50% of the complete drag distance.
|
|
67
|
+
*/
|
|
68
|
+
if (startThreshold !== undefined) {
|
|
69
|
+
const startThresholdValue =
|
|
70
|
+
sheetHeight -
|
|
71
|
+
Math.min(Math.floor(startThreshold * sheetHeight), sheetHeight);
|
|
72
|
+
|
|
73
|
+
if (yValue <= startThresholdValue) {
|
|
74
|
+
progress = (startThresholdValue - yValue) / startThresholdValue;
|
|
75
|
+
} else {
|
|
76
|
+
progress = 0;
|
|
99
77
|
}
|
|
78
|
+
}
|
|
100
79
|
|
|
101
|
-
|
|
102
|
-
|
|
80
|
+
// Make sure progress is between 0 and 1
|
|
81
|
+
progress = Math.max(0, Math.min(1, progress));
|
|
103
82
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const s = transform(progress, [0, 1], [1, (pageWidth - 16) / pageWidth]);
|
|
107
|
-
const borderRadius = transform(progress, [0, 1], [0, 10]);
|
|
83
|
+
updateModalEffect(rootId, { progress, insetTop });
|
|
84
|
+
});
|
|
108
85
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
root.style.borderTopLeftRadius = `${borderRadius}px`;
|
|
112
|
-
});
|
|
86
|
+
const handleYComplete = useStableCallback(() => {
|
|
87
|
+
if (!rootId) return;
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// biome-ignore lint/style/noNonNullAssertion: root is always defined here
|
|
118
|
-
cleanupModalEffect(rootId!);
|
|
119
|
-
}
|
|
89
|
+
// +5 just to take into account some inprecision to ensure the cleanup is done
|
|
90
|
+
if (y.get() + 5 >= sheetHeight) {
|
|
91
|
+
cleanupModalEffect(rootId);
|
|
120
92
|
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!rootId) return;
|
|
97
|
+
|
|
98
|
+
const removeStartListener = y.on('animationStart', () => {
|
|
99
|
+
if (rootId) setupModalEffect(rootId);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const removeChangeListener = y.on('change', (yValue) => {
|
|
103
|
+
handleYChange(yValue);
|
|
104
|
+
});
|
|
121
105
|
|
|
122
|
-
const removeCompleteListener = y.on('animationComplete',
|
|
123
|
-
const removeCancelListener = y.on('animationCancel',
|
|
106
|
+
const removeCompleteListener = y.on('animationComplete', handleYComplete);
|
|
107
|
+
const removeCancelListener = y.on('animationCancel', handleYComplete);
|
|
124
108
|
|
|
125
109
|
return () => {
|
|
110
|
+
if (rootId) cleanupModalEffect(rootId);
|
|
126
111
|
removeStartListener();
|
|
127
112
|
removeChangeListener();
|
|
128
113
|
removeCompleteListener();
|
|
129
114
|
removeCancelListener();
|
|
130
115
|
};
|
|
131
|
-
}, [
|
|
116
|
+
}, [rootId]);
|
|
132
117
|
}
|
|
133
118
|
|
|
134
119
|
function setupModalEffect(rootId: string) {
|
|
135
|
-
const
|
|
120
|
+
const target = getModalEffectTarget(rootId);
|
|
121
|
+
if (!target) return;
|
|
122
|
+
|
|
123
|
+
if (target.type === 'sheet') {
|
|
124
|
+
const sheet = target.element;
|
|
125
|
+
|
|
126
|
+
// No transition as the sheet follows the `y` of the stacked sheet
|
|
127
|
+
sheet.style.transformOrigin = 'center top';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const root = target.element;
|
|
136
132
|
const body = document.querySelector('body') as HTMLBodyElement;
|
|
137
|
-
if (!root) return;
|
|
138
133
|
|
|
139
|
-
body.style.backgroundColor = '#
|
|
134
|
+
body.style.backgroundColor = '#000000';
|
|
140
135
|
root.style.overflow = 'hidden';
|
|
141
136
|
root.style.transitionTimingFunction = 'cubic-bezier(0.32, 0.72, 0, 1)';
|
|
142
137
|
root.style.transitionProperty = 'transform, border-radius';
|
|
@@ -144,10 +139,58 @@ function setupModalEffect(rootId: string) {
|
|
|
144
139
|
root.style.transformOrigin = 'center top';
|
|
145
140
|
}
|
|
146
141
|
|
|
142
|
+
function updateModalEffect(
|
|
143
|
+
rootId: string,
|
|
144
|
+
opts: { progress: number; insetTop: number }
|
|
145
|
+
) {
|
|
146
|
+
const target = getModalEffectTarget(rootId);
|
|
147
|
+
if (!target) return;
|
|
148
|
+
|
|
149
|
+
const { progress, insetTop } = opts;
|
|
150
|
+
|
|
151
|
+
if (target.type === 'sheet') {
|
|
152
|
+
const sheet = target.element;
|
|
153
|
+
|
|
154
|
+
const sheetWidth = sheet.offsetWidth;
|
|
155
|
+
const sheetTop = sheet.offsetTop;
|
|
156
|
+
const scaleEnd = (sheetWidth - 16) / sheetWidth;
|
|
157
|
+
const scale = transform(progress, [0, 1], [1, scaleEnd]);
|
|
158
|
+
// Match the scaled app root's top edge, but cap the final lift for short sheets.
|
|
159
|
+
const translateYEnd = clamp(scaleEnd * (24 + insetTop) - sheetTop, -10, 0);
|
|
160
|
+
const translateY = transform(progress, [0, 1], [0, translateYEnd]);
|
|
161
|
+
|
|
162
|
+
sheet.style.scale = `${scale}`;
|
|
163
|
+
sheet.style.translate = `0 ${translateY}px`;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const root = target.element;
|
|
168
|
+
const pageWidth = window.innerWidth;
|
|
169
|
+
const scaleEnd = (pageWidth - 16) / pageWidth;
|
|
170
|
+
const scale = transform(progress, [0, 1], [1, scaleEnd]);
|
|
171
|
+
const translateY = transform(progress, [0, 1], [0, 24 + insetTop]);
|
|
172
|
+
const borderRadius = transform(progress, [0, 1], [0, 10]);
|
|
173
|
+
|
|
174
|
+
root.style.transform = `scale(${scale}) translate3d(0, ${translateY}px, 0)`;
|
|
175
|
+
root.style.borderTopRightRadius = `${borderRadius}px`;
|
|
176
|
+
root.style.borderTopLeftRadius = `${borderRadius}px`;
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
function cleanupModalEffect(rootId: string) {
|
|
148
|
-
const
|
|
180
|
+
const target = getModalEffectTarget(rootId);
|
|
181
|
+
if (!target) return;
|
|
182
|
+
|
|
183
|
+
if (target.type === 'sheet') {
|
|
184
|
+
const sheet = target.element;
|
|
185
|
+
|
|
186
|
+
sheet.style.removeProperty('transform-origin');
|
|
187
|
+
sheet.style.removeProperty('scale');
|
|
188
|
+
sheet.style.removeProperty('translate');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const root = target.element;
|
|
149
193
|
const body = document.querySelector('body') as HTMLBodyElement;
|
|
150
|
-
if (!root) return;
|
|
151
194
|
|
|
152
195
|
body.style.removeProperty('background-color');
|
|
153
196
|
root.style.removeProperty('overflow');
|
|
@@ -159,3 +202,22 @@ function cleanupModalEffect(rootId: string) {
|
|
|
159
202
|
root.style.removeProperty('border-top-right-radius');
|
|
160
203
|
root.style.removeProperty('border-top-left-radius');
|
|
161
204
|
}
|
|
205
|
+
|
|
206
|
+
function getModalEffectTarget(rootId: string) {
|
|
207
|
+
const root = document.getElementById(rootId) as HTMLDivElement;
|
|
208
|
+
if (!root) return null;
|
|
209
|
+
|
|
210
|
+
// When stacking sheets the effect is applied to the container of the sheet
|
|
211
|
+
const sheetRoot = root.closest('.react-modal-sheet-root');
|
|
212
|
+
|
|
213
|
+
if (sheetRoot) {
|
|
214
|
+
const container = sheetRoot.querySelector(
|
|
215
|
+
'.react-modal-sheet-container'
|
|
216
|
+
) as HTMLDivElement;
|
|
217
|
+
if (!container) return null;
|
|
218
|
+
|
|
219
|
+
return { type: 'sheet', element: container };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { type: 'root', element: root };
|
|
223
|
+
}
|
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
|
package/src/sheet.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import React, {
|
|
11
11
|
forwardRef,
|
|
12
12
|
useImperativeHandle,
|
|
13
|
+
useMemo,
|
|
13
14
|
useRef,
|
|
14
15
|
useState,
|
|
15
16
|
} from 'react';
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
IS_SSR,
|
|
24
25
|
REDUCED_MOTION_TWEEN_CONFIG,
|
|
25
26
|
} from './constants';
|
|
26
|
-
import {
|
|
27
|
+
import { ExposedSheetContext, InternalSheetContext } from './context';
|
|
27
28
|
import { useDimensions } from './hooks/use-dimensions';
|
|
28
29
|
import { useKeyboardAvoidance } from './hooks/use-keyboard-avoidance';
|
|
29
30
|
import { useModalEffect } from './hooks/use-modal-effect';
|
|
@@ -32,8 +33,12 @@ import { useSheetState } from './hooks/use-sheet-state';
|
|
|
32
33
|
import { useStableCallback } from './hooks/use-stable-callback';
|
|
33
34
|
import { classifyDragEnd, computeSnapPoints } from './snap';
|
|
34
35
|
import { styles } from './styles';
|
|
35
|
-
import
|
|
36
|
-
|
|
36
|
+
import type {
|
|
37
|
+
ExposedContextType,
|
|
38
|
+
InternalContextType,
|
|
39
|
+
SheetProps,
|
|
40
|
+
} from './types';
|
|
41
|
+
import { applyStyles, clamp, waitForElement, willOpenKeyboard } from './utils';
|
|
37
42
|
|
|
38
43
|
export const Sheet = forwardRef<any, SheetProps>(
|
|
39
44
|
(
|
|
@@ -70,21 +75,16 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
70
75
|
},
|
|
71
76
|
ref
|
|
72
77
|
) => {
|
|
73
|
-
const [sheetBoundsRef, sheetBounds] = useMeasure();
|
|
78
|
+
const [sheetBoundsRef, sheetBounds] = useMeasure({ offsetSize: true });
|
|
74
79
|
const sheetRef = useRef<HTMLDivElement>(null);
|
|
75
80
|
const sheetHeight = Math.round(sheetBounds.height);
|
|
76
81
|
const [currentSnap, setCurrentSnap] = useState(initialSnap);
|
|
77
|
-
const snapPoints =
|
|
78
|
-
snapPointsProp && sheetHeight > 0
|
|
79
|
-
? computeSnapPoints({ sheetHeight, snapPointsProp })
|
|
80
|
-
: [];
|
|
81
|
-
|
|
82
82
|
const { windowHeight } = useDimensions();
|
|
83
83
|
const closedY = sheetHeight > 0 ? sheetHeight : windowHeight;
|
|
84
84
|
const y = useMotionValue(closedY);
|
|
85
85
|
const yInverted = useTransform(y, (val) => Math.max(sheetHeight - val, 0));
|
|
86
|
+
const yProgress = useTransform(y, (val) => clamp(1 - val / closedY, 0, 1));
|
|
86
87
|
const indicatorRotation = useMotionValue(0);
|
|
87
|
-
|
|
88
88
|
const shouldReduceMotion = useReducedMotion();
|
|
89
89
|
const reduceMotion = Boolean(prefersReducedMotion || shouldReduceMotion);
|
|
90
90
|
const animationOptions: Transition = {
|
|
@@ -100,6 +100,13 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
100
100
|
val + 2 >= closedY ? 'hidden' : 'visible'
|
|
101
101
|
);
|
|
102
102
|
|
|
103
|
+
// Memo snap points as we expose them in context so we want them to be stable
|
|
104
|
+
const snapPoints = useMemo(() => {
|
|
105
|
+
return snapPointsProp && sheetHeight > 0
|
|
106
|
+
? computeSnapPoints({ sheetHeight, snapPointsProp })
|
|
107
|
+
: [];
|
|
108
|
+
}, [sheetHeight, snapPointsProp?.join('')]);
|
|
109
|
+
|
|
103
110
|
const updateSnap = useStableCallback((snapIndex: number) => {
|
|
104
111
|
setCurrentSnap(snapIndex);
|
|
105
112
|
onSnap?.(snapIndex);
|
|
@@ -268,13 +275,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
268
275
|
indicatorRotation.set(0);
|
|
269
276
|
});
|
|
270
277
|
|
|
271
|
-
useImperativeHandle(ref, () => ({
|
|
272
|
-
y,
|
|
273
|
-
yInverted,
|
|
274
|
-
height: sheetHeight,
|
|
275
|
-
snapTo,
|
|
276
|
-
}));
|
|
277
|
-
|
|
278
278
|
useModalEffect({
|
|
279
279
|
y,
|
|
280
280
|
detent,
|
|
@@ -310,23 +310,17 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
310
310
|
const yTo = initialSnapPoint?.snapValueY ?? 0;
|
|
311
311
|
|
|
312
312
|
await animate(y, yTo, animationOptions);
|
|
313
|
-
|
|
314
|
-
if (initialSnap !== undefined) {
|
|
315
|
-
updateSnap(initialSnap);
|
|
316
|
-
}
|
|
317
|
-
|
|
313
|
+
if (initialSnap !== undefined) updateSnap(initialSnap);
|
|
318
314
|
onOpenEnd?.();
|
|
319
315
|
},
|
|
320
316
|
onClosing: async () => {
|
|
321
317
|
onCloseStart?.();
|
|
322
|
-
|
|
323
318
|
await animate(y, closedY, animationOptions);
|
|
324
|
-
|
|
325
319
|
onCloseEnd?.();
|
|
326
320
|
},
|
|
327
321
|
});
|
|
328
322
|
|
|
329
|
-
const dragProps:
|
|
323
|
+
const dragProps: InternalContextType['dragProps'] = {
|
|
330
324
|
drag: 'y',
|
|
331
325
|
dragElastic: 0,
|
|
332
326
|
dragMomentum: false,
|
|
@@ -336,7 +330,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
336
330
|
onDragEnd,
|
|
337
331
|
};
|
|
338
332
|
|
|
339
|
-
const
|
|
333
|
+
const internalContext: InternalContextType = {
|
|
340
334
|
currentSnap,
|
|
341
335
|
detent,
|
|
342
336
|
disableDrag,
|
|
@@ -347,11 +341,24 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
347
341
|
sheetBoundsRef,
|
|
348
342
|
sheetRef,
|
|
349
343
|
unstyled,
|
|
344
|
+
yProgress,
|
|
350
345
|
y,
|
|
351
346
|
};
|
|
352
347
|
|
|
348
|
+
const exposedContext: ExposedContextType = {
|
|
349
|
+
height: sheetHeight,
|
|
350
|
+
currentSnap,
|
|
351
|
+
snapPoints,
|
|
352
|
+
snapTo,
|
|
353
|
+
y,
|
|
354
|
+
yInverted,
|
|
355
|
+
yProgress,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
useImperativeHandle(ref, () => exposedContext);
|
|
359
|
+
|
|
353
360
|
const sheet = (
|
|
354
|
-
<
|
|
361
|
+
<InternalSheetContext.Provider value={internalContext}>
|
|
355
362
|
<motion.div
|
|
356
363
|
{...rest}
|
|
357
364
|
ref={ref}
|
|
@@ -364,9 +371,11 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
364
371
|
...style,
|
|
365
372
|
}}
|
|
366
373
|
>
|
|
367
|
-
{
|
|
374
|
+
<ExposedSheetContext.Provider value={exposedContext}>
|
|
375
|
+
{state !== 'closed' ? children : null}
|
|
376
|
+
</ExposedSheetContext.Provider>
|
|
368
377
|
</motion.div>
|
|
369
|
-
</
|
|
378
|
+
</InternalSheetContext.Provider>
|
|
370
379
|
);
|
|
371
380
|
|
|
372
381
|
if (IS_SSR) return sheet;
|
package/src/snap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PanInfo } from 'motion';
|
|
2
2
|
import type { SheetSnapPoint } from './types';
|
|
3
|
-
import { isAscendingOrder } from './utils';
|
|
3
|
+
import { clamp, isAscendingOrder } from './utils';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Convert negative / percentage snap points to absolute values
|
|
@@ -108,10 +108,6 @@ export function computeSnapPoints({
|
|
|
108
108
|
}));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function clamp(value: number, min: number, max: number) {
|
|
112
|
-
return Math.min(Math.max(value, min), max);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
111
|
function sign(value: number) {
|
|
116
112
|
if (value > 0) return 1;
|
|
117
113
|
if (value < 0) return -1;
|