react-modal-sheet 5.5.0 → 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 +560 -554
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +537 -531
- 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 +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 +37 -28
- 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,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
|
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
|
(
|
|
@@ -74,17 +79,12 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
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;
|
package/src/types.tsx
CHANGED
|
@@ -103,7 +103,7 @@ export type SheetSnapPoint = {
|
|
|
103
103
|
snapValueY: number; // Y value is inverted as `y = 0` means sheet is at the top
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
export interface
|
|
106
|
+
export interface InternalContextType {
|
|
107
107
|
currentSnap?: number;
|
|
108
108
|
detent: SheetDetent;
|
|
109
109
|
disableDrag: boolean;
|
|
@@ -114,9 +114,20 @@ export interface SheetContextType {
|
|
|
114
114
|
sheetBoundsRef: (node: HTMLDivElement | null) => void;
|
|
115
115
|
sheetRef: RefObject<any>;
|
|
116
116
|
unstyled: boolean;
|
|
117
|
+
yProgress: MotionValue<number>;
|
|
117
118
|
y: MotionValue<any>;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
export interface ExposedContextType {
|
|
122
|
+
currentSnap?: number;
|
|
123
|
+
height: number;
|
|
124
|
+
snapPoints: SheetSnapPoint[];
|
|
125
|
+
snapTo: (index: number) => void;
|
|
126
|
+
y: MotionValue<number>;
|
|
127
|
+
yInverted: MotionValue<number>;
|
|
128
|
+
yProgress: MotionValue<number>;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
export interface SheetScrollerContextType {
|
|
121
132
|
disableDrag: boolean;
|
|
122
133
|
setDragDisabled: () => void;
|
|
@@ -149,6 +160,7 @@ interface SheetCompoundComponent {
|
|
|
149
160
|
DragIndicator: FunctionComponent<SheetDragIndicatorProps>;
|
|
150
161
|
Content: ContentComponent;
|
|
151
162
|
Backdrop: BackdropComponent;
|
|
163
|
+
useContext: () => ExposedContextType;
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
export type SheetCompound = SheetComponent & SheetCompoundComponent;
|
package/src/utils.ts
CHANGED
|
@@ -114,3 +114,7 @@ export function willOpenKeyboard(target: Element) {
|
|
|
114
114
|
export function isHTTPS() {
|
|
115
115
|
return typeof window !== 'undefined' && window.isSecureContext;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
export function clamp(value: number, min: number, max: number) {
|
|
119
|
+
return Math.min(Math.max(value, min), max);
|
|
120
|
+
}
|