react-modal-sheet 1.6.2 → 1.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 +46 -15
- package/dist/SheetBackdrop.d.ts +1 -1
- package/dist/SheetContent.d.ts +1 -1
- package/dist/SheetHeader.d.ts +1 -1
- package/dist/constants.d.ts +9 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/react-modal-sheet.cjs.development.js +169 -119
- package/dist/react-modal-sheet.cjs.development.js.map +1 -1
- package/dist/react-modal-sheet.cjs.production.min.js +1 -1
- package/dist/react-modal-sheet.cjs.production.min.js.map +1 -1
- package/dist/react-modal-sheet.esm.js +171 -121
- package/dist/react-modal-sheet.esm.js.map +1 -1
- package/dist/sheet.d.ts +1 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +6 -4
- package/package.json +2 -2
- package/src/SheetContainer.tsx +12 -8
- package/src/SheetContent.tsx +3 -3
- package/src/SheetHeader.tsx +2 -2
- package/src/constants.ts +13 -0
- package/src/hooks.tsx +35 -2
- package/src/sheet.tsx +109 -66
- package/src/types.tsx +5 -2
- package/src/utils.ts +21 -23
package/src/sheet.tsx
CHANGED
|
@@ -4,33 +4,47 @@ import { usePreventScroll } from '@react-aria/overlays';
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
animate,
|
|
7
|
-
useMotionValue,
|
|
8
|
-
PanInfo,
|
|
9
7
|
AnimatePresence,
|
|
8
|
+
PanInfo,
|
|
9
|
+
useMotionValue,
|
|
10
10
|
} from 'framer-motion';
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
useModalEffect,
|
|
14
|
+
useWindowHeight,
|
|
15
|
+
useIsomorphicLayoutEffect,
|
|
16
|
+
useEvent,
|
|
17
|
+
} from './hooks';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_SPRING_CONFIG,
|
|
21
|
+
DRAG_CLOSE_THRESHOLD,
|
|
22
|
+
DRAG_VELOCITY_THRESHOLD,
|
|
23
|
+
IS_SSR,
|
|
24
|
+
} from './constants';
|
|
25
|
+
|
|
12
26
|
import { SheetContextType, SheetProps } from './types';
|
|
13
|
-
import { getClosest, inDescendingOrder, isSSR, useWindowHeight } from './utils';
|
|
14
27
|
import { SheetContext } from './context';
|
|
15
|
-
import {
|
|
28
|
+
import { getClosest, inDescendingOrder, validateSnapTo } from './utils';
|
|
16
29
|
import styles from './styles';
|
|
17
30
|
|
|
18
31
|
const Sheet = React.forwardRef<any, SheetProps>(
|
|
19
32
|
(
|
|
20
33
|
{
|
|
21
|
-
children,
|
|
22
|
-
isOpen,
|
|
23
|
-
onClose,
|
|
24
34
|
onOpenStart,
|
|
25
35
|
onOpenEnd,
|
|
36
|
+
onClose,
|
|
26
37
|
onCloseStart,
|
|
27
38
|
onCloseEnd,
|
|
28
39
|
onSnap,
|
|
40
|
+
children,
|
|
41
|
+
isOpen,
|
|
29
42
|
snapPoints,
|
|
43
|
+
detent,
|
|
30
44
|
initialSnap = 0,
|
|
31
45
|
rootId,
|
|
32
46
|
mountPoint,
|
|
33
|
-
springConfig =
|
|
47
|
+
springConfig = DEFAULT_SPRING_CONFIG,
|
|
34
48
|
disableDrag = false,
|
|
35
49
|
style,
|
|
36
50
|
...rest
|
|
@@ -38,7 +52,6 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
38
52
|
ref
|
|
39
53
|
) => {
|
|
40
54
|
const sheetRef = React.useRef<any>(null);
|
|
41
|
-
const callbacks = React.useRef({ onOpenStart, onOpenEnd, onCloseStart, onCloseEnd }); // prettier-ignore
|
|
42
55
|
const indicatorRotation = useMotionValue(0);
|
|
43
56
|
const windowHeight = useWindowHeight();
|
|
44
57
|
|
|
@@ -47,10 +60,29 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
47
60
|
// and after that it is driven by the gestures and/or snapping
|
|
48
61
|
const y = useMotionValue(0);
|
|
49
62
|
|
|
63
|
+
// Keep the callback fns up-to-date so that they can be accessed inside
|
|
64
|
+
// the effect without including them to the dependencies array
|
|
65
|
+
const callbacks = React.useRef({
|
|
66
|
+
onOpenStart,
|
|
67
|
+
onOpenEnd,
|
|
68
|
+
onCloseStart,
|
|
69
|
+
onCloseEnd,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useIsomorphicLayoutEffect(() => {
|
|
73
|
+
callbacks.current = {
|
|
74
|
+
onOpenStart,
|
|
75
|
+
onOpenEnd,
|
|
76
|
+
onCloseStart,
|
|
77
|
+
onCloseEnd,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
50
81
|
if (snapPoints) {
|
|
51
82
|
// Convert negative / percentage snap points to absolute values
|
|
52
83
|
snapPoints = snapPoints.map(point => {
|
|
53
|
-
|
|
84
|
+
// Percentage values e.g. between 0.0 and 1.0
|
|
85
|
+
if (point > 0 && point <= 1) return Math.round(point * windowHeight);
|
|
54
86
|
return point < 0 ? windowHeight + point : point; // negative values
|
|
55
87
|
});
|
|
56
88
|
|
|
@@ -60,54 +92,61 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
60
92
|
);
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
const
|
|
95
|
+
const onDrag = useEvent((_, { delta }: PanInfo) => {
|
|
64
96
|
// Update drag indicator rotation based on drag velocity
|
|
65
97
|
const velocity = y.getVelocity();
|
|
98
|
+
|
|
66
99
|
if (velocity > 0) indicatorRotation.set(10);
|
|
67
100
|
if (velocity < 0) indicatorRotation.set(-10);
|
|
68
101
|
|
|
69
102
|
// Make sure user cannot drag beyond the top of the sheet
|
|
70
103
|
y.set(Math.max(y.get() + delta.y, 0));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const handleDragEnd = React.useCallback(
|
|
74
|
-
(_, { velocity }: PanInfo) => {
|
|
75
|
-
if (velocity.y > 500) {
|
|
76
|
-
// User flicked the sheet down
|
|
77
|
-
onClose();
|
|
78
|
-
} else {
|
|
79
|
-
const sheetEl = sheetRef.current as HTMLDivElement;
|
|
80
|
-
const contentHeight = sheetEl.getBoundingClientRect().height;
|
|
81
|
-
const snapTo = Math.round(
|
|
82
|
-
snapPoints
|
|
83
|
-
? getClosest(snapPoints.map(p => contentHeight - p), y.get()) // prettier-ignore
|
|
84
|
-
: y.get() / contentHeight > 0.6 // Close if dragged over 60%
|
|
85
|
-
? contentHeight
|
|
86
|
-
: 0
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Update the spring value so that the sheet is animated to the snap point
|
|
90
|
-
animate(y, snapTo, { type: 'spring', ...springConfig });
|
|
104
|
+
});
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
const onDragEnd = useEvent((_, { velocity }: PanInfo) => {
|
|
107
|
+
if (velocity.y > DRAG_VELOCITY_THRESHOLD) {
|
|
108
|
+
// User flicked the sheet down
|
|
109
|
+
onClose();
|
|
110
|
+
} else {
|
|
111
|
+
const sheetEl = sheetRef.current as HTMLDivElement;
|
|
112
|
+
const sheetHeight = sheetEl.getBoundingClientRect().height;
|
|
113
|
+
const currentY = y.get();
|
|
114
|
+
|
|
115
|
+
let snapTo = 0;
|
|
116
|
+
|
|
117
|
+
if (snapPoints) {
|
|
118
|
+
const snapToValues = snapPoints
|
|
119
|
+
.map(p => sheetHeight - p)
|
|
120
|
+
.filter(p => p >= 0); // negative values can occur with `content-height` detent
|
|
121
|
+
|
|
122
|
+
// Allow snapping to the top of the sheet if detent is set to `content-height`
|
|
123
|
+
if (detent === 'content-height' && !snapToValues.includes(0)) {
|
|
124
|
+
snapToValues.unshift(0);
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
|
|
127
|
+
// Get the closest snap point
|
|
128
|
+
snapTo = getClosest(snapToValues, currentY);
|
|
129
|
+
} else if (currentY / sheetHeight > DRAG_CLOSE_THRESHOLD) {
|
|
130
|
+
// Close if dragged over enough far
|
|
131
|
+
snapTo = sheetHeight;
|
|
99
132
|
}
|
|
100
133
|
|
|
101
|
-
|
|
102
|
-
indicatorRotation.set(0);
|
|
103
|
-
},
|
|
104
|
-
[onClose, onSnap] // eslint-disable-line
|
|
105
|
-
);
|
|
134
|
+
snapTo = validateSnapTo({ snapTo, sheetHeight });
|
|
106
135
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
// Update the spring value so that the sheet is animated to the snap point
|
|
137
|
+
animate(y, snapTo, { type: 'spring', ...springConfig });
|
|
138
|
+
|
|
139
|
+
if (snapPoints && onSnap) {
|
|
140
|
+
const snapValue = Math.abs(Math.round(snapPoints[0] - snapTo));
|
|
141
|
+
const snapIndex = snapPoints.indexOf(getClosest(snapPoints, snapValue)); // prettier-ignore
|
|
142
|
+
onSnap(snapIndex);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (snapTo >= sheetHeight) onClose();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reset indicator rotation after dragging
|
|
149
|
+
indicatorRotation.set(0);
|
|
111
150
|
});
|
|
112
151
|
|
|
113
152
|
// Trigger onSnap callback when sheet is opened or closed
|
|
@@ -121,16 +160,23 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
121
160
|
y,
|
|
122
161
|
snapTo: (snapIndex: number) => {
|
|
123
162
|
const sheetEl = sheetRef.current as HTMLDivElement | null;
|
|
163
|
+
|
|
124
164
|
if (
|
|
125
165
|
snapPoints &&
|
|
126
166
|
snapPoints[snapIndex] !== undefined &&
|
|
127
167
|
sheetEl !== null
|
|
128
168
|
) {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
169
|
+
const sheetHeight = sheetEl.getBoundingClientRect().height;
|
|
170
|
+
const snapPoint = snapPoints[snapIndex];
|
|
171
|
+
const snapTo = validateSnapTo({
|
|
172
|
+
snapTo: sheetHeight - snapPoint,
|
|
173
|
+
sheetHeight,
|
|
174
|
+
});
|
|
175
|
+
|
|
131
176
|
animate(y, snapTo, { type: 'spring', ...springConfig });
|
|
177
|
+
|
|
132
178
|
if (onSnap) onSnap(snapIndex);
|
|
133
|
-
if (snapTo >=
|
|
179
|
+
if (snapTo >= sheetHeight) onClose();
|
|
134
180
|
}
|
|
135
181
|
},
|
|
136
182
|
}));
|
|
@@ -141,16 +187,18 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
141
187
|
// properly on iOS. Scroll locking from React Aria seems to work much better.
|
|
142
188
|
usePreventScroll({ isDisabled: !isOpen });
|
|
143
189
|
|
|
144
|
-
const dragProps =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
const dragProps = React.useMemo(() => {
|
|
191
|
+
const dragProps: SheetContextType['dragProps'] = {
|
|
192
|
+
drag: 'y',
|
|
193
|
+
dragElastic: 0,
|
|
194
|
+
dragConstraints: { top: 0, bottom: 0 },
|
|
195
|
+
dragMomentum: false,
|
|
196
|
+
onDrag,
|
|
197
|
+
onDragEnd,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return disableDrag ? undefined : dragProps;
|
|
201
|
+
}, [disableDrag]); // eslint-disable-line
|
|
154
202
|
|
|
155
203
|
const context: SheetContextType = {
|
|
156
204
|
y,
|
|
@@ -158,6 +206,7 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
158
206
|
isOpen,
|
|
159
207
|
initialSnap,
|
|
160
208
|
snapPoints,
|
|
209
|
+
detent,
|
|
161
210
|
indicatorRotation,
|
|
162
211
|
callbacks,
|
|
163
212
|
dragProps,
|
|
@@ -165,15 +214,9 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
165
214
|
springConfig,
|
|
166
215
|
};
|
|
167
216
|
|
|
168
|
-
const wrapperProps = {
|
|
169
|
-
...rest,
|
|
170
|
-
ref,
|
|
171
|
-
style: { ...styles.wrapper, ...style },
|
|
172
|
-
};
|
|
173
|
-
|
|
174
217
|
const sheet = (
|
|
175
218
|
<SheetContext.Provider value={context}>
|
|
176
|
-
<div {...
|
|
219
|
+
<div {...rest} ref={ref} style={{ ...styles.wrapper, ...style }}>
|
|
177
220
|
<AnimatePresence>
|
|
178
221
|
{/* NOTE: AnimatePresence requires us to set keys to children */}
|
|
179
222
|
{isOpen
|
|
@@ -186,7 +229,7 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
186
229
|
</SheetContext.Provider>
|
|
187
230
|
);
|
|
188
231
|
|
|
189
|
-
if (
|
|
232
|
+
if (IS_SSR) return sheet;
|
|
190
233
|
|
|
191
234
|
return ReactDOM.createPortal(sheet, mountPoint ?? document.body);
|
|
192
235
|
}
|
package/src/types.tsx
CHANGED
|
@@ -13,6 +13,8 @@ export type SheetEvents = {
|
|
|
13
13
|
onSnap?: (index: number) => void;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type SheetDetent = 'full-height' | 'content-height';
|
|
17
|
+
|
|
16
18
|
export type SheetProps = {
|
|
17
19
|
isOpen: boolean;
|
|
18
20
|
children: React.ReactNode;
|
|
@@ -20,6 +22,7 @@ export type SheetProps = {
|
|
|
20
22
|
rootId?: string;
|
|
21
23
|
mountPoint?: Element;
|
|
22
24
|
snapPoints?: number[];
|
|
25
|
+
detent?: SheetDetent;
|
|
23
26
|
initialSnap?: number; // index of snap points array
|
|
24
27
|
springConfig?: Parameters<typeof useSpring>[1];
|
|
25
28
|
disableDrag?: boolean;
|
|
@@ -58,7 +61,6 @@ export type SheetDragProps = {
|
|
|
58
61
|
dragConstraints: any;
|
|
59
62
|
dragMomentum: boolean;
|
|
60
63
|
onDrag: DragHandlers['onDrag'];
|
|
61
|
-
onDragStart: DragHandlers['onDragStart'];
|
|
62
64
|
onDragEnd: DragHandlers['onDragEnd'];
|
|
63
65
|
};
|
|
64
66
|
|
|
@@ -67,10 +69,11 @@ export type SheetContextType = {
|
|
|
67
69
|
sheetRef: React.MutableRefObject<any>;
|
|
68
70
|
isOpen: boolean;
|
|
69
71
|
snapPoints: SheetProps['snapPoints'];
|
|
72
|
+
detent?: SheetDetent;
|
|
70
73
|
initialSnap: SheetProps['initialSnap'];
|
|
71
74
|
indicatorRotation: MotionValue<number>;
|
|
72
75
|
callbacks: React.MutableRefObject<SheetEvents>;
|
|
73
|
-
dragProps
|
|
76
|
+
dragProps?: SheetDragProps;
|
|
74
77
|
windowHeight: number;
|
|
75
78
|
springConfig: Parameters<typeof useSpring>[1];
|
|
76
79
|
};
|
package/src/utils.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const isSSR = typeof window === 'undefined';
|
|
4
|
-
|
|
5
|
-
export const getClosest = (nums: number[], goal: number) => {
|
|
1
|
+
export function getClosest(nums: number[], goal: number) {
|
|
6
2
|
return nums.reduce((prev, curr) => {
|
|
7
3
|
return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev;
|
|
8
4
|
});
|
|
9
|
-
}
|
|
5
|
+
}
|
|
10
6
|
|
|
11
7
|
const highlightId = 'react-modal-sheet-highlight';
|
|
12
8
|
|
|
@@ -70,25 +66,27 @@ export function cleanupRootStyles(rootId: string) {
|
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
export function useWindowHeight() {
|
|
76
|
-
const [windowHeight, setWindowHeight] = useState(0);
|
|
77
|
-
|
|
78
|
-
useIsomorphicLayoutEffect(() => {
|
|
79
|
-
const updateHeight = () => setWindowHeight(window.innerHeight);
|
|
80
|
-
window.addEventListener('resize', updateHeight);
|
|
81
|
-
updateHeight();
|
|
82
|
-
return () => window.removeEventListener('resize', updateHeight);
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
return windowHeight;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export const inDescendingOrder = (arr: number[]) => {
|
|
69
|
+
export function inDescendingOrder(arr: number[]) {
|
|
89
70
|
for (let i = 0; i < arr.length; i++) {
|
|
90
71
|
if (arr[i + 1] > arr[i]) return false;
|
|
91
72
|
}
|
|
92
73
|
|
|
93
74
|
return true;
|
|
94
|
-
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function validateSnapTo({
|
|
78
|
+
snapTo,
|
|
79
|
+
sheetHeight,
|
|
80
|
+
}: {
|
|
81
|
+
snapTo: number;
|
|
82
|
+
sheetHeight: number;
|
|
83
|
+
}) {
|
|
84
|
+
if (snapTo < 0) {
|
|
85
|
+
console.warn(
|
|
86
|
+
`Snap point is out of bounds. Sheet height is ${sheetHeight} but snap point is ${sheetHeight +
|
|
87
|
+
Math.abs(snapTo)}.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Math.max(Math.round(snapTo), 0);
|
|
92
|
+
}
|