react-modal-sheet 1.3.1 → 1.4.2
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 +15 -15
- package/dist/SheetBackdrop.d.ts +1 -1
- package/dist/SheetContainer.d.ts +1 -1
- package/dist/react-modal-sheet.cjs.development.js +78 -50
- 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 +80 -52
- package/dist/react-modal-sheet.esm.js.map +1 -1
- package/dist/sheet.d.ts +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/utils.d.ts +3 -1
- package/package.json +9 -9
- package/src/SheetBackdrop.tsx +2 -1
- package/src/SheetContainer.tsx +8 -3
- package/src/sheet.tsx +48 -60
- package/src/styles.ts +2 -0
- package/src/types.tsx +2 -1
- package/src/utils.ts +26 -1
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.4.2",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "react-modal-sheet",
|
|
5
5
|
"description": "Flexible bottom sheet component for your React apps",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"release": "np"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"framer-motion": "2.x || 3.x || 4.x",
|
|
35
|
+
"framer-motion": "2.x || 3.x || 4.x || 5.x",
|
|
36
36
|
"react": ">=16"
|
|
37
37
|
},
|
|
38
38
|
"husky": {
|
|
@@ -50,18 +50,18 @@
|
|
|
50
50
|
"yarn": true
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@types/react": "
|
|
54
|
-
"@types/react-dom": "
|
|
53
|
+
"@types/react": "17.0.37",
|
|
54
|
+
"@types/react-dom": "17.0.11",
|
|
55
55
|
"@types/styled-components": "5.1.7",
|
|
56
56
|
"csstype": "3.0.6",
|
|
57
|
-
"framer-motion": "4.
|
|
57
|
+
"framer-motion": "5.4.4",
|
|
58
58
|
"husky": "4.2.5",
|
|
59
|
-
"react": "
|
|
60
|
-
"react-dom": "
|
|
61
|
-
"styled-components": "5.
|
|
59
|
+
"react": "17.0.2",
|
|
60
|
+
"react-dom": "17.0.2",
|
|
61
|
+
"styled-components": "5.3.3",
|
|
62
62
|
"tsdx": "0.14.1",
|
|
63
63
|
"tslib": "2.1.0",
|
|
64
|
-
"typescript": "4.
|
|
64
|
+
"typescript": "4.5.2"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"react-merge-refs": "1.1.0"
|
package/src/SheetBackdrop.tsx
CHANGED
|
@@ -9,13 +9,14 @@ const isClickable = (props: any) => !!props.onClick || !!props.onTap;
|
|
|
9
9
|
const SheetBackdrop = React.forwardRef<any, SheetBackdropProps>(
|
|
10
10
|
({ style = {}, ...rest }, ref) => {
|
|
11
11
|
const Comp = isClickable(rest) ? motion.button : motion.div;
|
|
12
|
+
const pointerEvents = isClickable(rest) ? 'auto' : 'none';
|
|
12
13
|
|
|
13
14
|
return (
|
|
14
15
|
<Comp
|
|
15
16
|
{...rest}
|
|
16
17
|
ref={ref}
|
|
17
18
|
className="react-modal-sheet-backdrop"
|
|
18
|
-
style={{ ...styles.backdrop, ...style }}
|
|
19
|
+
style={{ ...styles.backdrop, ...style, pointerEvents }}
|
|
19
20
|
initial={{ opacity: 0 }}
|
|
20
21
|
animate={{ opacity: 1 }}
|
|
21
22
|
exit={{ opacity: 0 }}
|
package/src/SheetContainer.tsx
CHANGED
|
@@ -18,6 +18,8 @@ const SheetContainer = React.forwardRef<any, SheetContainerProps>(
|
|
|
18
18
|
snapPoints,
|
|
19
19
|
initialSnap = 0,
|
|
20
20
|
sheetRef,
|
|
21
|
+
windowHeight,
|
|
22
|
+
springConfig,
|
|
21
23
|
} = useSheetContext();
|
|
22
24
|
|
|
23
25
|
const { handleAnimationComplete } = useEventCallbacks(isOpen, callbacks);
|
|
@@ -31,9 +33,12 @@ const SheetContainer = React.forwardRef<any, SheetContainerProps>(
|
|
|
31
33
|
ref={mergeRefs([sheetRef, ref])}
|
|
32
34
|
className="react-modal-sheet-container"
|
|
33
35
|
style={{ ...styles.container, height: sheetHeight, ...style, y }}
|
|
34
|
-
initial={{ y:
|
|
35
|
-
animate={{
|
|
36
|
-
|
|
36
|
+
initial={{ y: windowHeight }}
|
|
37
|
+
animate={{
|
|
38
|
+
y: initialY,
|
|
39
|
+
transition: { type: 'spring', ...springConfig },
|
|
40
|
+
}}
|
|
41
|
+
exit={{ y: windowHeight }}
|
|
37
42
|
onAnimationComplete={handleAnimationComplete}
|
|
38
43
|
>
|
|
39
44
|
{children}
|
package/src/sheet.tsx
CHANGED
|
@@ -2,14 +2,14 @@ import * as React from 'react';
|
|
|
2
2
|
import * as ReactDOM from 'react-dom';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
animate,
|
|
6
6
|
useMotionValue,
|
|
7
|
-
AnimatePresence,
|
|
8
7
|
PanInfo,
|
|
8
|
+
AnimatePresence,
|
|
9
9
|
} from 'framer-motion';
|
|
10
10
|
|
|
11
|
-
import { SheetProps } from './types';
|
|
12
|
-
import { getClosest,
|
|
11
|
+
import { SheetContextType, SheetProps } from './types';
|
|
12
|
+
import { getClosest, inDescendingOrder, isSSR, useWindowHeight } from './utils';
|
|
13
13
|
import { SheetContext } from './context';
|
|
14
14
|
import { useModalEffect } from './hooks';
|
|
15
15
|
import styles from './styles';
|
|
@@ -34,30 +34,33 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
34
34
|
},
|
|
35
35
|
ref
|
|
36
36
|
) => {
|
|
37
|
-
const [mounted, setMounted] = React.useState(false);
|
|
38
|
-
const [isDragging, setDragging] = React.useState(false);
|
|
39
37
|
const sheetRef = React.useRef<any>(null);
|
|
40
38
|
const callbacks = React.useRef({ onOpenStart, onOpenEnd, onCloseStart, onCloseEnd }); // prettier-ignore
|
|
41
39
|
const indicatorRotation = useMotionValue(0);
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const windowHeight = useWindowHeight();
|
|
41
|
+
const y = useMotionValue(isOpen ? windowHeight : 0);
|
|
42
|
+
|
|
43
|
+
if (snapPoints) {
|
|
44
|
+
// Convert negative / percentage snap points to absolute values
|
|
45
|
+
snapPoints = snapPoints.map(point => {
|
|
46
|
+
if (point > 0 && point <= 1) return point * windowHeight; // percentage values e.g. between 0.0 and 1.0
|
|
47
|
+
return point < 0 ? windowHeight + point : point; // negative values
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.assert(
|
|
51
|
+
inDescendingOrder(snapPoints) || windowHeight === 0,
|
|
52
|
+
`Snap points need to be in descending order got: [${snapPoints}]`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
const handleDrag = React.useCallback((_, { delta }: PanInfo) => {
|
|
48
57
|
// Update drag indicator rotation based on drag velocity
|
|
49
|
-
const velocity =
|
|
58
|
+
const velocity = y.getVelocity();
|
|
50
59
|
if (velocity > 0) indicatorRotation.set(10);
|
|
51
60
|
if (velocity < 0) indicatorRotation.set(-10);
|
|
52
61
|
|
|
53
62
|
// Make sure user cannot drag beyond the top of the sheet
|
|
54
|
-
|
|
55
|
-
}, []); // eslint-disable-line
|
|
56
|
-
|
|
57
|
-
const handleDragStart = React.useCallback(() => {
|
|
58
|
-
// Sync the drag value with the spring value so that dragging start at the correct y position
|
|
59
|
-
sheetDragY.set(sheetSpringY.get());
|
|
60
|
-
setDragging(true);
|
|
63
|
+
y.set(Math.max(y.get() + delta.y, 0));
|
|
61
64
|
}, []); // eslint-disable-line
|
|
62
65
|
|
|
63
66
|
const handleDragEnd = React.useCallback(
|
|
@@ -69,16 +72,13 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
69
72
|
const sheetEl = sheetRef.current as HTMLDivElement;
|
|
70
73
|
const contentHeight = sheetEl.getBoundingClientRect().height;
|
|
71
74
|
const snapTo = snapPoints
|
|
72
|
-
? getClosest(
|
|
73
|
-
|
|
74
|
-
sheetDragY.get()
|
|
75
|
-
)
|
|
76
|
-
: sheetDragY.get() / contentHeight > 0.6 // Close if dragged over 60%
|
|
75
|
+
? getClosest(snapPoints.map(p => contentHeight - p), y.get()) // prettier-ignore
|
|
76
|
+
: y.get() / contentHeight > 0.6 // Close if dragged over 60%
|
|
77
77
|
? contentHeight
|
|
78
78
|
: 0;
|
|
79
79
|
|
|
80
80
|
// Update the spring value so that the sheet is animated to the snap point
|
|
81
|
-
|
|
81
|
+
animate(y, snapTo, { type: 'spring', ...springConfig });
|
|
82
82
|
|
|
83
83
|
if (snapPoints && onSnap) {
|
|
84
84
|
const snapValue = Math.abs(Math.round(snapPoints[0] - snapTo));
|
|
@@ -87,8 +87,6 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
if (snapTo >= contentHeight) onClose();
|
|
90
|
-
|
|
91
|
-
setDragging(false);
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
// Reset indicator rotation after dragging
|
|
@@ -103,16 +101,12 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
103
101
|
callbacks.current = { onOpenStart, onOpenEnd, onCloseStart, onCloseEnd };
|
|
104
102
|
});
|
|
105
103
|
|
|
106
|
-
React.useEffect(() => {
|
|
107
|
-
setMounted(true);
|
|
108
|
-
}, []);
|
|
109
|
-
|
|
110
104
|
// Trigger onSnap callback when sheet is opened or closed
|
|
111
105
|
React.useEffect(() => {
|
|
112
|
-
if (!snapPoints || !onSnap
|
|
106
|
+
if (!snapPoints || !onSnap) return;
|
|
113
107
|
const snapIndex = isOpen ? initialSnap : snapPoints.length - 1;
|
|
114
108
|
onSnap(snapIndex);
|
|
115
|
-
}, [isOpen]);
|
|
109
|
+
}, [isOpen]); // eslint-disable-line
|
|
116
110
|
|
|
117
111
|
React.useImperativeHandle(ref, () => ({
|
|
118
112
|
snapTo: (snapIndex: number) => {
|
|
@@ -121,7 +115,7 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
121
115
|
const contentHeight = sheetEl.getBoundingClientRect().height;
|
|
122
116
|
const snapTo = contentHeight - snapPoints[snapIndex];
|
|
123
117
|
|
|
124
|
-
|
|
118
|
+
animate(y, snapTo, { type: 'spring', ...springConfig });
|
|
125
119
|
if (onSnap) onSnap(snapIndex);
|
|
126
120
|
if (snapTo >= contentHeight) onClose();
|
|
127
121
|
}
|
|
@@ -138,52 +132,46 @@ const Sheet = React.forwardRef<any, SheetProps>(
|
|
|
138
132
|
dragConstraints: { top: 0, bottom: 0 },
|
|
139
133
|
dragMomentum: false,
|
|
140
134
|
onDrag: handleDrag,
|
|
141
|
-
onDragStart: handleDragStart,
|
|
142
135
|
onDragEnd: handleDragEnd,
|
|
143
136
|
};
|
|
144
137
|
|
|
145
|
-
const context = {
|
|
138
|
+
const context: SheetContextType = {
|
|
146
139
|
y,
|
|
147
140
|
sheetRef,
|
|
148
141
|
isOpen,
|
|
149
|
-
isDragging,
|
|
150
142
|
initialSnap,
|
|
151
143
|
snapPoints,
|
|
152
144
|
indicatorRotation,
|
|
153
145
|
callbacks,
|
|
154
146
|
dragProps,
|
|
147
|
+
windowHeight,
|
|
148
|
+
springConfig,
|
|
155
149
|
};
|
|
156
150
|
|
|
157
151
|
const wrapperProps = {
|
|
158
152
|
...rest,
|
|
159
153
|
ref,
|
|
160
|
-
style:
|
|
161
|
-
...styles.wrapper,
|
|
162
|
-
pointerEvents,
|
|
163
|
-
} as any,
|
|
154
|
+
style: styles.wrapper,
|
|
164
155
|
};
|
|
165
156
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
</SheetContext.Provider>,
|
|
180
|
-
document.body
|
|
181
|
-
)
|
|
182
|
-
) : (
|
|
183
|
-
// NOTE: return the wrapper div so that SSR frameworks don't get confused
|
|
184
|
-
// with the HTML structure ("Expected server HTML to contain a matching <div>...")
|
|
185
|
-
<div {...wrapperProps} />
|
|
157
|
+
const sheet = (
|
|
158
|
+
<SheetContext.Provider value={context}>
|
|
159
|
+
<div {...wrapperProps}>
|
|
160
|
+
<AnimatePresence>
|
|
161
|
+
{/* NOTE: AnimatePresence requires us to set keys to children */}
|
|
162
|
+
{isOpen
|
|
163
|
+
? React.Children.map(children, (child: any, i) =>
|
|
164
|
+
React.cloneElement(child, { key: `sheet-child-${i}` })
|
|
165
|
+
)
|
|
166
|
+
: null}
|
|
167
|
+
</AnimatePresence>
|
|
168
|
+
</div>
|
|
169
|
+
</SheetContext.Provider>
|
|
186
170
|
);
|
|
171
|
+
|
|
172
|
+
if (isSSR) return sheet;
|
|
173
|
+
|
|
174
|
+
return ReactDOM.createPortal(sheet, document.body);
|
|
187
175
|
}
|
|
188
176
|
);
|
|
189
177
|
|
package/src/styles.ts
CHANGED
|
@@ -9,6 +9,7 @@ const styles: { [key: string]: CSS.Properties } = {
|
|
|
9
9
|
right: 0,
|
|
10
10
|
zIndex: 9999999,
|
|
11
11
|
overflow: 'hidden',
|
|
12
|
+
pointerEvents: 'none',
|
|
12
13
|
},
|
|
13
14
|
backdrop: {
|
|
14
15
|
zIndex: 1,
|
|
@@ -32,6 +33,7 @@ const styles: { [key: string]: CSS.Properties } = {
|
|
|
32
33
|
boxShadow: '0px -2px 16px rgba(0, 0, 0, 0.3)',
|
|
33
34
|
display: 'flex',
|
|
34
35
|
flexDirection: 'column',
|
|
36
|
+
pointerEvents: 'auto',
|
|
35
37
|
},
|
|
36
38
|
headerWrapper: {
|
|
37
39
|
width: '100%',
|
package/src/types.tsx
CHANGED
|
@@ -59,11 +59,12 @@ export type SheetDragProps = {
|
|
|
59
59
|
export type SheetContextType = {
|
|
60
60
|
y: MotionValue<any>;
|
|
61
61
|
sheetRef: React.MutableRefObject<any>;
|
|
62
|
-
isDragging: boolean;
|
|
63
62
|
isOpen: boolean;
|
|
64
63
|
snapPoints: SheetProps['snapPoints'];
|
|
65
64
|
initialSnap: SheetProps['initialSnap'];
|
|
66
65
|
indicatorRotation: MotionValue<number>;
|
|
67
66
|
callbacks: React.MutableRefObject<SheetEvents>;
|
|
68
67
|
dragProps: SheetDragProps;
|
|
68
|
+
windowHeight: number;
|
|
69
|
+
springConfig: Parameters<typeof useSpring>[1];
|
|
69
70
|
};
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const isSSR = typeof window === 'undefined';
|
|
2
4
|
|
|
3
5
|
export const getClosest = (nums: number[], goal: number) => {
|
|
4
6
|
return nums.reduce((prev, curr) => {
|
|
@@ -67,3 +69,26 @@ export function cleanupRootStyles(rootId: string) {
|
|
|
67
69
|
root.addEventListener('transitionend', onTransitionEnd);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect;
|
|
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[]) => {
|
|
89
|
+
for (let i = 0; i < arr.length; i++) {
|
|
90
|
+
if (arr[i + 1] > arr[i]) return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
};
|