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