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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.3.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": "16.9.35",
54
- "@types/react-dom": "16.9.8",
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.1.3",
57
+ "framer-motion": "5.4.4",
58
58
  "husky": "4.2.5",
59
- "react": "16.13.1",
60
- "react-dom": "16.13.1",
61
- "styled-components": "5.2.1",
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.1.5"
64
+ "typescript": "4.5.2"
65
65
  },
66
66
  "dependencies": {
67
67
  "react-merge-refs": "1.1.0"
@@ -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 }}
@@ -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: window.innerHeight }}
35
- animate={{ y: initialY, transition: { type: 'tween' } }}
36
- exit={{ y: window.innerHeight }}
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
- useSpring,
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, isBrowser } from './utils';
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 sheetDragY = useMotionValue(isBrowser ? window.innerHeight : 0);
43
- const sheetSpringY = useSpring(sheetDragY, springConfig);
44
- const y = isDragging ? sheetDragY : sheetSpringY;
45
- const pointerEvents = isOpen ? 'auto' : 'none';
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 = sheetDragY.getVelocity();
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
- sheetDragY.set(Math.max(sheetDragY.get() + delta.y, 0));
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
- snapPoints.map(p => contentHeight - p),
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
- sheetSpringY.set(snapTo);
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 || !mounted) return;
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
- sheetSpringY.set(snapTo);
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
- return mounted ? (
167
- ReactDOM.createPortal(
168
- <SheetContext.Provider value={context}>
169
- <div {...wrapperProps}>
170
- <AnimatePresence>
171
- {/* NOTE: AnimatePresence requires us to set keys to children */}
172
- {isOpen
173
- ? React.Children.map(children, (child: any, i) =>
174
- React.cloneElement(child, { key: `sheet-child-${i}` })
175
- )
176
- : null}
177
- </AnimatePresence>
178
- </div>
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
- export const isBrowser = typeof window !== 'undefined';
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
+ };