react-modal-sheet 4.0.2 → 4.2.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 +46 -26
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +401 -309
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +402 -310
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SheetContainer.tsx +5 -18
- package/src/SheetContent.tsx +1 -1
- package/src/SheetHeader.tsx +1 -1
- package/src/context.tsx +4 -4
- package/src/hooks/use-dimensions.ts +30 -0
- package/src/hooks/use-drag-constraints.ts +14 -0
- package/src/hooks/use-event-callbacks.ts +44 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-modal-effect.ts +165 -0
- package/src/{use-prevent-scroll.ts → hooks/use-prevent-scroll.ts} +2 -2
- package/src/hooks/use-safe-area-insets.ts +40 -0
- package/src/hooks/use-stable-callback.ts +18 -0
- package/src/sheet.tsx +96 -72
- package/src/styles.ts +1 -1
- package/src/types.tsx +6 -3
- package/src/utils.ts +88 -10
- package/src/hooks.tsx +0 -197
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { transform, type MotionValue } from 'motion';
|
|
2
|
+
import { type RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
5
|
+
import { useSafeAreaInsets } from './use-safe-area-insets';
|
|
6
|
+
import { getSheetHeight, getSnapPoints } from '../utils';
|
|
7
|
+
|
|
8
|
+
export function useModalEffect({
|
|
9
|
+
y,
|
|
10
|
+
rootId,
|
|
11
|
+
sheetRef,
|
|
12
|
+
snapPointsProp,
|
|
13
|
+
startThreshold,
|
|
14
|
+
}: {
|
|
15
|
+
y: MotionValue<number>;
|
|
16
|
+
rootId?: string;
|
|
17
|
+
sheetRef: RefObject<HTMLDivElement | null>;
|
|
18
|
+
snapPointsProp?: number[];
|
|
19
|
+
startThreshold?: number;
|
|
20
|
+
}) {
|
|
21
|
+
const insetTop = useSafeAreaInsets().top;
|
|
22
|
+
|
|
23
|
+
// Cleanup on unmount
|
|
24
|
+
useIsomorphicLayoutEffect(() => {
|
|
25
|
+
return () => {
|
|
26
|
+
if (rootId) cleanupModalEffect(rootId);
|
|
27
|
+
};
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useIsomorphicLayoutEffect(() => {
|
|
31
|
+
if (!rootId) return;
|
|
32
|
+
|
|
33
|
+
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
34
|
+
if (!root) return;
|
|
35
|
+
|
|
36
|
+
let sheetHeight = 0;
|
|
37
|
+
|
|
38
|
+
const removeStartListener = y.on('animationStart', () => {
|
|
39
|
+
/**
|
|
40
|
+
* Only calculate the sheet height when the animation starts to avoid
|
|
41
|
+
* unnecessary layout calculations when y value changes.
|
|
42
|
+
*/
|
|
43
|
+
sheetHeight = getSheetHeight(sheetRef);
|
|
44
|
+
|
|
45
|
+
// biome-ignore lint/style/noNonNullAssertion: root is always defined here
|
|
46
|
+
setupModalEffect(rootId!);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* NOTE: The `y` value gets smaller when the sheet is opened and larger
|
|
51
|
+
* when the sheet is being closed.
|
|
52
|
+
*/
|
|
53
|
+
const removeChangeListener = y.on('change', (yValue) => {
|
|
54
|
+
if (!root) return;
|
|
55
|
+
|
|
56
|
+
let progress = Math.max(0, 1 - yValue / sheetHeight);
|
|
57
|
+
|
|
58
|
+
const snapPoints = snapPointsProp
|
|
59
|
+
? getSnapPoints({ snapPointsProp, sheetHeight })
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start the effect only if we have dragged over the second snap point
|
|
64
|
+
* to make the effect more natural as the sheet will reach it's final
|
|
65
|
+
* position when the user drags it over the second snap point.
|
|
66
|
+
*/
|
|
67
|
+
const snapThresholdPoint =
|
|
68
|
+
snapPoints && snapPoints.length > 1 ? snapPoints[1] : undefined;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* If we have snap points, we need to calculate the progress percentage
|
|
72
|
+
* based on the snap point threshold. Note that the maximum value is also
|
|
73
|
+
* different in this case as the range between the start of the effect
|
|
74
|
+
* and its end is different.
|
|
75
|
+
*/
|
|
76
|
+
if (snapThresholdPoint !== undefined) {
|
|
77
|
+
const snapThresholdValue =
|
|
78
|
+
sheetHeight - Math.min(snapThresholdPoint, sheetHeight);
|
|
79
|
+
|
|
80
|
+
if (yValue <= snapThresholdValue) {
|
|
81
|
+
progress = (snapThresholdValue - yValue) / snapThresholdValue;
|
|
82
|
+
} else {
|
|
83
|
+
progress = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* If we have a start threshold, we need to calculate the progress
|
|
89
|
+
* percentage based on the start threshold (0 to 1). For example,
|
|
90
|
+
* if the start threshold is 0.5, the progress will be 0 until the sheet
|
|
91
|
+
* is dragged over 50% of the complete drag distance.
|
|
92
|
+
*/
|
|
93
|
+
if (startThreshold !== undefined) {
|
|
94
|
+
const startThresholdValue =
|
|
95
|
+
sheetHeight -
|
|
96
|
+
Math.min(Math.floor(startThreshold * sheetHeight), sheetHeight);
|
|
97
|
+
|
|
98
|
+
if (yValue <= startThresholdValue) {
|
|
99
|
+
progress = (startThresholdValue - yValue) / startThresholdValue;
|
|
100
|
+
} else {
|
|
101
|
+
progress = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Make sure progress is between 0 and 1
|
|
106
|
+
progress = Math.max(0, Math.min(1, progress));
|
|
107
|
+
|
|
108
|
+
const pageWidth = window.innerWidth;
|
|
109
|
+
const ty = transform(progress, [0, 1], [0, 24 + insetTop]);
|
|
110
|
+
const s = transform(progress, [0, 1], [1, (pageWidth - 16) / pageWidth]);
|
|
111
|
+
const borderRadius = transform(progress, [0, 1], [0, 10]);
|
|
112
|
+
|
|
113
|
+
root.style.transform = `scale(${s}) translate3d(0, ${ty}px, 0)`;
|
|
114
|
+
root.style.borderTopRightRadius = `${borderRadius}px`;
|
|
115
|
+
root.style.borderTopLeftRadius = `${borderRadius}px`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function onCompleted() {
|
|
119
|
+
// -5 just to take into account some inprecision to ensure the cleanup is done
|
|
120
|
+
if (y.get() - 5 >= sheetHeight) {
|
|
121
|
+
// biome-ignore lint/style/noNonNullAssertion: root is always defined here
|
|
122
|
+
cleanupModalEffect(rootId!);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const removeCompleteListener = y.on('animationComplete', onCompleted);
|
|
127
|
+
const removeCancelListener = y.on('animationCancel', onCompleted);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
removeStartListener();
|
|
131
|
+
removeChangeListener();
|
|
132
|
+
removeCompleteListener();
|
|
133
|
+
removeCancelListener();
|
|
134
|
+
};
|
|
135
|
+
}, [y, rootId, insetTop, startThreshold, snapPointsProp]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setupModalEffect(rootId: string) {
|
|
139
|
+
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
140
|
+
const body = document.querySelector('body') as HTMLBodyElement;
|
|
141
|
+
if (!root) return;
|
|
142
|
+
|
|
143
|
+
body.style.backgroundColor = '#000';
|
|
144
|
+
root.style.overflow = 'hidden';
|
|
145
|
+
root.style.transitionTimingFunction = 'cubic-bezier(0.32, 0.72, 0, 1)';
|
|
146
|
+
root.style.transitionProperty = 'transform, border-radius';
|
|
147
|
+
root.style.transitionDuration = '0.5s';
|
|
148
|
+
root.style.transformOrigin = 'center top';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cleanupModalEffect(rootId: string) {
|
|
152
|
+
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
153
|
+
const body = document.querySelector('body') as HTMLBodyElement;
|
|
154
|
+
if (!root) return;
|
|
155
|
+
|
|
156
|
+
body.style.removeProperty('background-color');
|
|
157
|
+
root.style.removeProperty('overflow');
|
|
158
|
+
root.style.removeProperty('transition-timing-function');
|
|
159
|
+
root.style.removeProperty('transition-property');
|
|
160
|
+
root.style.removeProperty('transition-duration');
|
|
161
|
+
root.style.removeProperty('transform-origin');
|
|
162
|
+
root.style.removeProperty('transform');
|
|
163
|
+
root.style.removeProperty('border-top-right-radius');
|
|
164
|
+
root.style.removeProperty('border-top-left-radius');
|
|
165
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This code originates from https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/usePreventScroll.ts
|
|
2
2
|
|
|
3
|
-
import { useIsomorphicLayoutEffect } from './
|
|
4
|
-
import { isIOS } from '
|
|
3
|
+
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
4
|
+
import { isIOS } from '../utils';
|
|
5
5
|
|
|
6
6
|
const KEYBOARD_BUFFER = 24;
|
|
7
7
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { IS_SSR } from '../constants';
|
|
4
|
+
|
|
5
|
+
export function useSafeAreaInsets() {
|
|
6
|
+
const [insets] = useState(() => {
|
|
7
|
+
const fallback = { top: 0, left: 0, right: 0, bottom: 0 };
|
|
8
|
+
|
|
9
|
+
if (IS_SSR) return fallback;
|
|
10
|
+
|
|
11
|
+
const root = document.querySelector<HTMLElement>(':root');
|
|
12
|
+
|
|
13
|
+
if (!root) return fallback;
|
|
14
|
+
|
|
15
|
+
root.style.setProperty('--rms-sat', 'env(safe-area-inset-top)');
|
|
16
|
+
root.style.setProperty('--rms-sal', 'env(safe-area-inset-left)');
|
|
17
|
+
root.style.setProperty('--rms-sar', 'env(safe-area-inset-right)');
|
|
18
|
+
root.style.setProperty('--rms-sab', 'env(safe-area-inset-bottom)');
|
|
19
|
+
|
|
20
|
+
const computedStyle = getComputedStyle(root);
|
|
21
|
+
const sat = getComputedValue(computedStyle, '--rms-sat');
|
|
22
|
+
const sal = getComputedValue(computedStyle, '--rms-sal');
|
|
23
|
+
const sar = getComputedValue(computedStyle, '--rms-sar');
|
|
24
|
+
const sab = getComputedValue(computedStyle, '--rms-sab');
|
|
25
|
+
|
|
26
|
+
root.style.removeProperty('--rms-sat');
|
|
27
|
+
root.style.removeProperty('--rms-sal');
|
|
28
|
+
root.style.removeProperty('--rms-sar');
|
|
29
|
+
root.style.removeProperty('--rms-sab');
|
|
30
|
+
|
|
31
|
+
return { top: sat, left: sal, right: sar, bottom: sab };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return insets;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getComputedValue(computed: CSSStyleDeclaration, property: string) {
|
|
38
|
+
const strValue = computed.getPropertyValue(property).replace('px', '').trim();
|
|
39
|
+
return parseInt(strValue, 10) || 0;
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
4
|
+
|
|
5
|
+
export function useStableCallback<T extends (...args: any[]) => any>(
|
|
6
|
+
handler: T
|
|
7
|
+
) {
|
|
8
|
+
const handlerRef = useRef<T>(undefined);
|
|
9
|
+
|
|
10
|
+
useIsomorphicLayoutEffect(() => {
|
|
11
|
+
handlerRef.current = handler;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return useCallback((...args: any[]) => {
|
|
15
|
+
const fn = handlerRef.current;
|
|
16
|
+
return fn?.(...args);
|
|
17
|
+
}, []) as T;
|
|
18
|
+
}
|
package/src/sheet.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createPortal } from 'react-dom';
|
|
2
|
+
|
|
1
3
|
import React, {
|
|
2
4
|
Children,
|
|
3
5
|
cloneElement,
|
|
@@ -8,8 +10,6 @@ import React, {
|
|
|
8
10
|
useRef,
|
|
9
11
|
} from 'react';
|
|
10
12
|
|
|
11
|
-
import { createPortal } from 'react-dom';
|
|
12
|
-
|
|
13
13
|
import {
|
|
14
14
|
animate,
|
|
15
15
|
AnimatePresence,
|
|
@@ -21,13 +21,6 @@ import {
|
|
|
21
21
|
useTransform,
|
|
22
22
|
} from 'motion/react';
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
useModalEffect,
|
|
26
|
-
useDimensions,
|
|
27
|
-
useIsomorphicLayoutEffect,
|
|
28
|
-
useEffectEvent,
|
|
29
|
-
} from './hooks';
|
|
30
|
-
|
|
31
24
|
import {
|
|
32
25
|
REDUCED_MOTION_TWEEN_CONFIG,
|
|
33
26
|
DEFAULT_TWEEN_CONFIG,
|
|
@@ -36,11 +29,22 @@ import {
|
|
|
36
29
|
IS_SSR,
|
|
37
30
|
} from './constants';
|
|
38
31
|
|
|
32
|
+
import {
|
|
33
|
+
getClosest,
|
|
34
|
+
getSheetHeight,
|
|
35
|
+
getSnapY,
|
|
36
|
+
getSnapPoints,
|
|
37
|
+
validateSnapTo,
|
|
38
|
+
} from './utils';
|
|
39
|
+
|
|
39
40
|
import { type SheetContextType, type SheetProps } from './types';
|
|
40
41
|
import { SheetScrollerContextProvider, SheetContext } from './context';
|
|
41
|
-
import { getClosest, inDescendingOrder, validateSnapTo } from './utils';
|
|
42
|
-
import { usePreventScroll } from './use-prevent-scroll';
|
|
43
42
|
import { styles } from './styles';
|
|
43
|
+
import { useModalEffect } from './hooks/use-modal-effect';
|
|
44
|
+
import { useDimensions } from './hooks/use-dimensions';
|
|
45
|
+
import { useIsomorphicLayoutEffect } from './hooks/use-isomorphic-layout-effect';
|
|
46
|
+
import { useStableCallback } from './hooks/use-stable-callback';
|
|
47
|
+
import { usePreventScroll } from './hooks/use-prevent-scroll';
|
|
44
48
|
|
|
45
49
|
export const Sheet = forwardRef<any, SheetProps>(
|
|
46
50
|
(
|
|
@@ -56,6 +60,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
56
60
|
isOpen,
|
|
57
61
|
snapPoints: snapPointsProp,
|
|
58
62
|
rootId,
|
|
63
|
+
modalEffectRootId,
|
|
59
64
|
mountPoint,
|
|
60
65
|
style,
|
|
61
66
|
detent = 'full-height',
|
|
@@ -65,6 +70,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
65
70
|
tweenConfig = DEFAULT_TWEEN_CONFIG,
|
|
66
71
|
dragVelocityThreshold = DEFAULT_DRAG_VELOCITY_THRESHOLD,
|
|
67
72
|
dragCloseThreshold = DEFAULT_DRAG_CLOSE_THRESHOLD,
|
|
73
|
+
modalEffectThreshold,
|
|
68
74
|
...rest
|
|
69
75
|
},
|
|
70
76
|
ref
|
|
@@ -110,25 +116,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
110
116
|
};
|
|
111
117
|
});
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (snapPoints) {
|
|
116
|
-
// Convert negative / percentage snap points to absolute values
|
|
117
|
-
snapPoints = snapPoints.map((point) => {
|
|
118
|
-
// Percentage values e.g. between 0.0 and 1.0
|
|
119
|
-
if (point > 0 && point <= 1) return Math.round(point * windowHeight);
|
|
120
|
-
return point < 0 ? windowHeight + point : point; // negative values
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
console.assert(
|
|
124
|
-
inDescendingOrder(snapPoints) || windowHeight === 0,
|
|
125
|
-
`Snap points need to be in descending order got: [${snapPoints.join(
|
|
126
|
-
', '
|
|
127
|
-
)}]`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const onDrag = useEffectEvent((_, { delta }: PanInfo) => {
|
|
119
|
+
const onDrag = useStableCallback((_, { delta }: PanInfo) => {
|
|
132
120
|
// Update drag indicator rotation based on drag velocity
|
|
133
121
|
const velocity = y.getVelocity();
|
|
134
122
|
|
|
@@ -139,7 +127,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
139
127
|
y.set(Math.max(y.get() + delta.y, 0));
|
|
140
128
|
});
|
|
141
129
|
|
|
142
|
-
const onDragStart =
|
|
130
|
+
const onDragStart = useStableCallback(() => {
|
|
143
131
|
// Find focused input inside the sheet and blur it when dragging starts
|
|
144
132
|
// to prevent a weird ghost caret "bug" on mobile
|
|
145
133
|
const focusedElement = document.activeElement as HTMLElement | null;
|
|
@@ -155,51 +143,54 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
155
143
|
}
|
|
156
144
|
});
|
|
157
145
|
|
|
158
|
-
const onDragEnd =
|
|
146
|
+
const onDragEnd = useStableCallback((_, { velocity }: PanInfo) => {
|
|
159
147
|
if (velocity.y > dragVelocityThreshold) {
|
|
160
148
|
// User flicked the sheet down
|
|
161
149
|
onClose();
|
|
162
150
|
} else {
|
|
163
|
-
|
|
164
|
-
const sheetHeight = sheetRef.current!.getBoundingClientRect().height;
|
|
151
|
+
const sheetHeight = getSheetHeight(sheetRef);
|
|
165
152
|
const currentY = y.get();
|
|
166
153
|
|
|
167
|
-
let
|
|
154
|
+
let yTo = 0;
|
|
155
|
+
|
|
156
|
+
const snapPoints = snapPointsProp
|
|
157
|
+
? getSnapPoints({ snapPointsProp, sheetHeight })
|
|
158
|
+
: undefined;
|
|
168
159
|
|
|
169
160
|
if (snapPoints) {
|
|
170
|
-
|
|
161
|
+
// Inverse values are the values that can be passed to `animate`
|
|
162
|
+
const snapInverse = snapPoints.map(
|
|
171
163
|
(p) => sheetHeight - Math.min(p, sheetHeight)
|
|
172
164
|
);
|
|
173
165
|
|
|
174
166
|
// Allow snapping to the top of the sheet if detent is set to `content-height`
|
|
175
|
-
if (detent === 'content-height' && !
|
|
176
|
-
|
|
167
|
+
if (detent === 'content-height' && !snapInverse.includes(0)) {
|
|
168
|
+
snapInverse.unshift(0);
|
|
177
169
|
}
|
|
178
170
|
|
|
179
171
|
// Get the closest snap point
|
|
180
|
-
snapTo = getClosest(
|
|
172
|
+
const snapTo = getClosest(snapInverse, currentY);
|
|
173
|
+
const snapIndex = snapInverse.indexOf(snapTo);
|
|
174
|
+
|
|
175
|
+
yTo = validateSnapTo({
|
|
176
|
+
snapTo: getClosest(snapInverse, currentY),
|
|
177
|
+
sheetHeight,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// This happens before calling `animate` but it doesn't really matter
|
|
181
|
+
onSnap?.(snapIndex);
|
|
181
182
|
} else if (currentY / sheetHeight > dragCloseThreshold) {
|
|
182
183
|
// Close if dragged over enough far
|
|
183
|
-
|
|
184
|
+
yTo = sheetHeight;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
snapTo = validateSnapTo({ snapTo, sheetHeight });
|
|
187
|
-
|
|
188
187
|
// Update the spring value so that the sheet is animated to the snap point
|
|
189
|
-
animate(y,
|
|
188
|
+
animate(y, yTo, animationOptions);
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
getClosest(snapPoints, snapValue)
|
|
195
|
-
);
|
|
196
|
-
onSnap(snapIndex);
|
|
190
|
+
// +1px for imprecision tolerance
|
|
191
|
+
if (yTo + 1 >= sheetHeight) {
|
|
192
|
+
onClose();
|
|
197
193
|
}
|
|
198
|
-
|
|
199
|
-
const roundedSheetHeight = Math.round(sheetHeight);
|
|
200
|
-
const shouldClose = snapTo + 2 >= roundedSheetHeight; // 2px tolerance
|
|
201
|
-
|
|
202
|
-
if (shouldClose) onClose();
|
|
203
194
|
}
|
|
204
195
|
|
|
205
196
|
// Reset indicator rotation after dragging
|
|
@@ -208,40 +199,74 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
208
199
|
|
|
209
200
|
// Trigger onSnap callback when sheet is opened or closed
|
|
210
201
|
useEffect(() => {
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
202
|
+
if (snapPointsProp && onSnap) {
|
|
203
|
+
onSnap(isOpen ? initialSnap : snapPointsProp.length - 1);
|
|
204
|
+
}
|
|
205
|
+
}, [isOpen]);
|
|
206
|
+
|
|
207
|
+
// Animate the sheet open
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!isOpen) return; // let `AnimatePresence` handle the exit animation
|
|
210
|
+
|
|
211
|
+
let yTo = 0;
|
|
212
|
+
|
|
213
|
+
if (snapPointsProp) {
|
|
214
|
+
const snapIndex = initialSnap;
|
|
215
|
+
const sheetHeight = getSheetHeight(sheetRef);
|
|
216
|
+
const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight });
|
|
217
|
+
const ySnap = getSnapY({ sheetHeight, snapPoints, snapIndex });
|
|
218
|
+
|
|
219
|
+
if (ySnap !== null) {
|
|
220
|
+
yTo = ySnap;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
animate(y, yTo, animationOptions);
|
|
214
225
|
}, [isOpen]);
|
|
215
226
|
|
|
216
227
|
useImperativeHandle(ref, () => ({
|
|
217
228
|
y,
|
|
218
229
|
snapTo: (snapIndex: number) => {
|
|
219
|
-
|
|
230
|
+
if (!snapPointsProp) {
|
|
231
|
+
console.warn('Snapping is not possible without `snapPoints` prop.');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
220
234
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const snapTo = validateSnapTo({
|
|
225
|
-
snapTo: sheetHeight - snapPoint,
|
|
226
|
-
sheetHeight,
|
|
227
|
-
});
|
|
235
|
+
const sheetHeight = getSheetHeight(sheetRef);
|
|
236
|
+
const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight });
|
|
237
|
+
const ySnap = getSnapY({ sheetHeight, snapPoints, snapIndex });
|
|
228
238
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
239
|
+
if (ySnap !== null) {
|
|
240
|
+
animate(y, ySnap, animationOptions);
|
|
241
|
+
onSnap?.(snapIndex);
|
|
242
|
+
|
|
243
|
+
// +1px for imprecision tolerance
|
|
244
|
+
if (ySnap + 1 >= sheetHeight) {
|
|
245
|
+
onClose();
|
|
246
|
+
}
|
|
232
247
|
}
|
|
233
248
|
},
|
|
234
249
|
}));
|
|
235
250
|
|
|
251
|
+
if (rootId) {
|
|
252
|
+
console.warn(
|
|
253
|
+
'The `rootId` prop is deprecated and will be removed in the next major version. Use `modalEffectRootId` instead.'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
236
257
|
useModalEffect({
|
|
237
258
|
y,
|
|
238
|
-
rootId,
|
|
239
259
|
sheetRef,
|
|
260
|
+
snapPointsProp,
|
|
261
|
+
rootId: rootId || modalEffectRootId,
|
|
262
|
+
startThreshold: modalEffectThreshold,
|
|
240
263
|
});
|
|
241
264
|
|
|
242
|
-
//
|
|
265
|
+
// Motion should handle body scroll locking but it's not working
|
|
243
266
|
// properly on iOS. Scroll locking from React Aria seems to work much better.
|
|
244
|
-
usePreventScroll({
|
|
267
|
+
usePreventScroll({
|
|
268
|
+
isDisabled: disableScrollLocking || !isOpen,
|
|
269
|
+
});
|
|
245
270
|
|
|
246
271
|
const dragProps = useMemo(() => {
|
|
247
272
|
const dragProps: SheetContextType['dragProps'] = {
|
|
@@ -262,7 +287,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
262
287
|
sheetRef,
|
|
263
288
|
isOpen,
|
|
264
289
|
initialSnap,
|
|
265
|
-
snapPoints,
|
|
266
290
|
detent,
|
|
267
291
|
indicatorRotation,
|
|
268
292
|
callbacks,
|
package/src/styles.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const styles: Record<string, Properties> = {
|
|
|
18
18
|
width: '100%',
|
|
19
19
|
height: '100%',
|
|
20
20
|
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
21
|
-
touchAction: 'none',
|
|
21
|
+
touchAction: 'none',
|
|
22
22
|
border: 'none',
|
|
23
23
|
WebkitTapHighlightColor: 'transparent',
|
|
24
24
|
},
|
package/src/types.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type MutableRefObject,
|
|
6
6
|
type ReactNode,
|
|
7
7
|
type RefAttributes,
|
|
8
|
+
type RefObject,
|
|
8
9
|
} from 'react';
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -39,7 +40,9 @@ export type SheetProps = {
|
|
|
39
40
|
isOpen: boolean;
|
|
40
41
|
children: ReactNode;
|
|
41
42
|
onClose: () => void;
|
|
43
|
+
/** @deprecated Use `modalEffectRootId` instead! */
|
|
42
44
|
rootId?: string;
|
|
45
|
+
modalEffectRootId?: string;
|
|
43
46
|
mountPoint?: Element;
|
|
44
47
|
snapPoints?: number[];
|
|
45
48
|
detent?: SheetDetent;
|
|
@@ -50,6 +53,7 @@ export type SheetProps = {
|
|
|
50
53
|
prefersReducedMotion?: boolean;
|
|
51
54
|
dragVelocityThreshold?: number;
|
|
52
55
|
dragCloseThreshold?: number;
|
|
56
|
+
modalEffectThreshold?: number;
|
|
53
57
|
} & SheetEvents &
|
|
54
58
|
ComponentPropsWithoutRef<typeof motion.div>;
|
|
55
59
|
|
|
@@ -95,13 +99,12 @@ export interface SheetDragProps {
|
|
|
95
99
|
|
|
96
100
|
export interface SheetContextType {
|
|
97
101
|
y: MotionValue<any>;
|
|
98
|
-
sheetRef:
|
|
102
|
+
sheetRef: RefObject<any>;
|
|
99
103
|
isOpen: boolean;
|
|
100
|
-
snapPoints: SheetProps['snapPoints'];
|
|
101
104
|
detent: SheetDetent;
|
|
102
105
|
initialSnap: SheetProps['initialSnap'];
|
|
103
106
|
indicatorRotation: MotionValue<number>;
|
|
104
|
-
callbacks:
|
|
107
|
+
callbacks: RefObject<SheetEvents>;
|
|
105
108
|
dragProps?: SheetDragProps;
|
|
106
109
|
windowHeight: number;
|
|
107
110
|
animationOptions: Transition;
|