react-modal-sheet 4.4.0 → 5.1.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/src/snap.ts ADDED
@@ -0,0 +1,226 @@
1
+ import type { SheetSnapPoint } from './types';
2
+ import { isAscendingOrder } from './utils';
3
+
4
+ /**
5
+ * Convert negative / percentage snap points to absolute values
6
+ *
7
+ * Example output:
8
+ *
9
+ * ```json
10
+ * [
11
+ * {
12
+ * "snapIndex": 0, // <-- bottom snap point
13
+ * "snapValue": 0,
14
+ * "snapValueY": 810
15
+ * },
16
+ * {
17
+ * "snapIndex": 1,
18
+ * "snapValue": 170,
19
+ * "snapValueY": 640
20
+ * },
21
+ * {
22
+ * "snapIndex": 2,
23
+ * "snapValue": 405,
24
+ * "snapValueY": 405
25
+ * },
26
+ * {
27
+ * "snapIndex": 3,
28
+ * "snapValue": 760,
29
+ * "snapValueY": 50
30
+ * },
31
+ * {
32
+ * "snapIndex": 4, // <-- top snap point
33
+ * "snapValue": 810,
34
+ * "snapValueY": 0
35
+ * }
36
+ * ]
37
+ * ```
38
+ */
39
+ export function computeSnapPoints({
40
+ snapPointsProp,
41
+ sheetHeight,
42
+ }: {
43
+ snapPointsProp: number[];
44
+ sheetHeight: number;
45
+ }): SheetSnapPoint[] {
46
+ if (sheetHeight <= 0) {
47
+ console.warn(
48
+ `Sheet height is ${sheetHeight}, cannot compute snap points. ` +
49
+ 'Make sure the sheet is mounted and has a valid height.'
50
+ );
51
+ return [];
52
+ }
53
+
54
+ const snapPointValues = snapPointsProp.map((point) => {
55
+ // Percentage values e.g. between 0.0 and 1.0
56
+ if (point > 0 && point <= 1) {
57
+ return Math.round(point * sheetHeight);
58
+ }
59
+
60
+ return point < 0 ? sheetHeight + point : point; // negative values
61
+ });
62
+
63
+ console.assert(
64
+ isAscendingOrder(snapPointValues),
65
+ `Snap points need to be in ascending order got: [${snapPointsProp.join(', ')}]`
66
+ );
67
+
68
+ // Make sure all snap points are within the sheet height
69
+ snapPointValues.forEach((snap) => {
70
+ if (snap < 0 || snap > sheetHeight) {
71
+ console.warn(
72
+ `Snap point ${snap} is outside of the sheet height ${sheetHeight}. ` +
73
+ 'This can cause unexpected behavior. Consider adjusting your snap points.'
74
+ );
75
+ }
76
+ });
77
+
78
+ if (!snapPointValues.includes(sheetHeight)) {
79
+ console.warn(
80
+ 'Snap points do not include the sheet height.' +
81
+ 'Please include `1` as the last snap point or it will be included automatically.' +
82
+ 'This is to ensure the sheet can be fully opened.'
83
+ );
84
+ snapPointValues.push(sheetHeight);
85
+ }
86
+
87
+ return snapPointValues.map((snap, index) => ({
88
+ snapIndex: index,
89
+ snapValue: snap, // Absolute value from the bottom of the sheet
90
+ snapValueY: sheetHeight - snap, // Y value is inverted as `y = 0` means sheet is at the top
91
+ }));
92
+ }
93
+
94
+ function findClosestSnapPoint({
95
+ snapPoints,
96
+ currentY,
97
+ }: {
98
+ snapPoints: SheetSnapPoint[];
99
+ currentY: number;
100
+ }) {
101
+ return snapPoints.reduce((closest, snap) =>
102
+ Math.abs(snap.snapValueY - currentY) <
103
+ Math.abs(closest.snapValueY - currentY)
104
+ ? snap
105
+ : closest
106
+ );
107
+ }
108
+
109
+ function findNextSnapPointInDirection({
110
+ y,
111
+ snapPoints,
112
+ dragDirection,
113
+ }: {
114
+ y: number;
115
+ snapPoints: SheetSnapPoint[];
116
+ dragDirection: 'up' | 'down';
117
+ }) {
118
+ // NOTE: lower Y means higher in the sheet position!
119
+ if (dragDirection === 'down') {
120
+ /**
121
+ * Example:
122
+ *
123
+ * [
124
+ * { snapIndex: 0, snapValueY: 810 },
125
+ * { snapIndex: 1, snapValueY: 640 },
126
+ * { snapIndex: 2, snapValueY: 405 }, <-- next down
127
+ * ------------- Y = 60 ------------
128
+ * { snapIndex: 3, snapValueY: 50 },
129
+ * { snapIndex: 4, snapValueY: 0 },
130
+ * ]
131
+ */
132
+ return snapPoints
133
+ .slice()
134
+ .reverse()
135
+ .find((s) => s.snapValueY > y);
136
+ } else {
137
+ /**
138
+ * Example:
139
+ * [
140
+ * { snapIndex: 0, snapValueY: 810 },
141
+ * { snapIndex: 1, snapValueY: 640 },
142
+ * { snapIndex: 2, snapValueY: 405 },
143
+ * ------------- Y = 60 ------------
144
+ * { snapIndex: 3, snapValueY: 50 }, <-- next up
145
+ * { snapIndex: 4, snapValueY: 0 },
146
+ * ]
147
+ */
148
+ return snapPoints.find((s) => s.snapValueY < y);
149
+ }
150
+ }
151
+ export function handleHighVelocityDrag({
152
+ dragDirection,
153
+ snapPoints,
154
+ }: {
155
+ dragDirection: 'up' | 'down';
156
+ snapPoints: SheetSnapPoint[];
157
+ }) {
158
+ // Go to either the last or the first snap point depending on the direction
159
+ const bottomSnapPoint = snapPoints[0];
160
+ const topSnapPoint = snapPoints[snapPoints.length - 1];
161
+
162
+ if (dragDirection === 'down') {
163
+ return {
164
+ yTo: bottomSnapPoint.snapValueY,
165
+ snapIndex: bottomSnapPoint.snapIndex,
166
+ };
167
+ }
168
+ return {
169
+ yTo: topSnapPoint.snapValueY,
170
+ snapIndex: topSnapPoint.snapIndex,
171
+ };
172
+ }
173
+
174
+ export function handleLowVelocityDrag({
175
+ currentSnapPoint,
176
+ currentY,
177
+ dragDirection,
178
+ snapPoints,
179
+ velocity,
180
+ }: {
181
+ currentSnapPoint: SheetSnapPoint;
182
+ currentY: number;
183
+ dragDirection: 'up' | 'down';
184
+ snapPoints: SheetSnapPoint[];
185
+ velocity: number;
186
+ }) {
187
+ const closestSnapRelativeToCurrentY = findClosestSnapPoint({
188
+ snapPoints,
189
+ currentY,
190
+ });
191
+
192
+ /**
193
+ * If velocity is very low the user has stopped the sheet to a specific
194
+ * position and we should snap to the closest snap point as there is no
195
+ * "momentum" that would push the sheet further to the given direction
196
+ */
197
+ if (Math.abs(velocity) < 20) {
198
+ return {
199
+ yTo: closestSnapRelativeToCurrentY.snapValueY,
200
+ snapIndex: closestSnapRelativeToCurrentY.snapIndex,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * If the dragging has a bit more velocity, we instead want to go to
206
+ * the next snap point in the given direction if it exists
207
+ */
208
+ const nextSnapInDirectionRelativeToCurrentY = findNextSnapPointInDirection({
209
+ y: currentY,
210
+ snapPoints,
211
+ dragDirection,
212
+ });
213
+
214
+ if (nextSnapInDirectionRelativeToCurrentY) {
215
+ return {
216
+ yTo: nextSnapInDirectionRelativeToCurrentY.snapValueY,
217
+ snapIndex: nextSnapInDirectionRelativeToCurrentY.snapIndex,
218
+ };
219
+ }
220
+
221
+ // No snap point down, stay at current
222
+ return {
223
+ yTo: currentSnapPoint.snapValueY,
224
+ snapIndex: currentSnapPoint.snapIndex,
225
+ };
226
+ }
package/src/styles.ts CHANGED
@@ -1,69 +1,110 @@
1
- import { type Properties } from 'csstype';
1
+ import type { CSSProperties } from 'react';
2
2
 
3
- export const styles: Record<string, Properties> = {
4
- wrapper: {
5
- position: 'fixed',
6
- top: 0,
7
- bottom: 0,
8
- left: 0,
9
- right: 0,
10
- overflow: 'hidden',
11
- pointerEvents: 'none',
3
+ export const styles = {
4
+ root: {
5
+ base: {
6
+ position: 'fixed',
7
+ top: 0,
8
+ bottom: 0,
9
+ left: 0,
10
+ right: 0,
11
+ overflow: 'hidden',
12
+ pointerEvents: 'none',
13
+ },
14
+ decorative: {},
12
15
  },
13
16
  backdrop: {
14
- zIndex: 1,
15
- position: 'fixed',
16
- top: 0,
17
- left: 0,
18
- width: '100%',
19
- height: '100%',
20
- backgroundColor: 'rgba(0, 0, 0, 0.2)',
21
- touchAction: 'none',
22
- border: 'none',
23
- userSelect: 'none',
24
- WebkitTapHighlightColor: 'transparent',
17
+ base: {
18
+ zIndex: 1,
19
+ position: 'fixed',
20
+ top: 0,
21
+ left: 0,
22
+ width: '100%',
23
+ height: '100%',
24
+ touchAction: 'none',
25
+ userSelect: 'none',
26
+ },
27
+ decorative: {
28
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
29
+ border: 'none',
30
+ WebkitTapHighlightColor: 'transparent',
31
+ },
25
32
  },
26
33
  container: {
27
- zIndex: 2,
28
- position: 'absolute',
29
- left: 0,
30
- bottom: 0,
31
- width: '100%',
32
- backgroundColor: '#fff',
33
- borderTopRightRadius: '8px',
34
- borderTopLeftRadius: '8px',
35
- boxShadow: '0px -2px 16px rgba(0, 0, 0, 0.3)',
36
- display: 'flex',
37
- flexDirection: 'column',
38
- pointerEvents: 'auto',
34
+ base: {
35
+ zIndex: 2,
36
+ position: 'absolute',
37
+ left: 0,
38
+ bottom: 0,
39
+ width: '100%',
40
+ pointerEvents: 'auto',
41
+ display: 'flex',
42
+ flexDirection: 'column',
43
+ },
44
+ decorative: {
45
+ backgroundColor: '#fff',
46
+ borderTopRightRadius: '8px',
47
+ borderTopLeftRadius: '8px',
48
+ boxShadow: '0px -2px 16px rgba(0, 0, 0, 0.3)',
49
+ },
39
50
  },
40
51
  headerWrapper: {
41
- width: '100%',
52
+ base: {
53
+ width: '100%',
54
+ },
55
+ decorative: {},
42
56
  },
43
57
  header: {
44
- height: '40px',
45
- width: '100%',
46
- position: 'relative',
47
- display: 'flex',
48
- alignItems: 'center',
49
- justifyContent: 'center',
58
+ base: {
59
+ width: '100%',
60
+ position: 'relative',
61
+ },
62
+ decorative: {
63
+ height: '40px',
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ },
68
+ },
69
+ indicatorWrapper: {
70
+ base: {
71
+ display: 'flex',
72
+ },
73
+ decorative: {},
50
74
  },
51
75
  indicator: {
52
- width: '18px',
53
- height: '4px',
54
- borderRadius: '99px',
55
- backgroundColor: '#ddd',
76
+ base: {
77
+ display: 'inline-block',
78
+ },
79
+ decorative: {
80
+ width: '18px',
81
+ height: '4px',
82
+ borderRadius: '99px',
83
+ backgroundColor: '#ddd',
84
+ },
56
85
  },
57
86
  content: {
58
- flexGrow: 1,
59
- display: 'flex',
60
- flexDirection: 'column',
61
- minHeight: '0px',
62
- position: 'relative',
87
+ base: {
88
+ minHeight: '0px',
89
+ position: 'relative',
90
+ flexGrow: 1,
91
+ display: 'flex',
92
+ flexDirection: 'column',
93
+ },
94
+ decorative: {},
63
95
  },
64
96
  scroller: {
65
- height: '100%',
66
- overflowY: 'auto',
67
- overscrollBehaviorY: 'none',
97
+ base: {
98
+ height: '100%',
99
+ overflowY: 'auto',
100
+ overscrollBehaviorY: 'none',
101
+ },
102
+ decorative: {},
68
103
  },
69
- };
104
+ } satisfies Record<
105
+ string,
106
+ {
107
+ base: CSSProperties;
108
+ decorative: CSSProperties;
109
+ }
110
+ >;
package/src/types.tsx CHANGED
@@ -1,34 +1,33 @@
1
1
  import {
2
+ type CSSProperties,
2
3
  type ComponentPropsWithoutRef,
3
4
  type ForwardRefExoticComponent,
5
+ type FunctionComponent,
6
+ type HTMLAttributes,
4
7
  type ReactNode,
5
8
  type RefAttributes,
6
9
  type RefObject,
7
10
  } from 'react';
8
11
 
9
12
  import {
10
- type DragHandlers,
11
- type MotionValue,
13
+ type DragHandler,
14
+ type EasingDefinition,
12
15
  type MotionProps,
16
+ type MotionValue,
13
17
  type motion,
14
- type EasingDefinition,
15
- type Transition,
16
18
  } from 'motion/react';
17
19
 
18
- export interface SheetEvents {
19
- onOpenStart?: () => void;
20
- onOpenEnd?: () => void;
21
- onCloseStart?: () => void;
22
- onCloseEnd?: () => void;
23
- onSnap?: (index: number) => void;
24
- }
25
-
26
- export type SheetDetent = 'full-height' | 'content-height';
20
+ export type SheetDetent = 'default' | 'full' | 'content';
27
21
 
28
- type CommonProps = MotionProps & {
22
+ type CommonProps = {
29
23
  className?: string;
24
+ style?: CSSProperties;
25
+ unstyled?: boolean;
30
26
  };
31
27
 
28
+ type MotionCommonProps = Omit<MotionProps, 'initial' | 'animate' | 'exit'> &
29
+ CommonProps;
30
+
32
31
  type MotionDivProps = ComponentPropsWithoutRef<typeof motion.div>;
33
32
 
34
33
  export interface SheetTweenConfig {
@@ -37,81 +36,86 @@ export interface SheetTweenConfig {
37
36
  }
38
37
 
39
38
  export type SheetProps = {
40
- isOpen: boolean;
39
+ unstyled?: boolean;
40
+ avoidKeyboard?: boolean;
41
41
  children: ReactNode;
42
- onClose: () => void;
43
- /** @deprecated Use `modalEffectRootId` instead! */
44
- rootId?: string;
45
- modalEffectRootId?: string;
46
- mountPoint?: Element;
47
- snapPoints?: number[];
48
42
  detent?: SheetDetent;
49
- initialSnap?: number; // index of snap points array
50
- tweenConfig?: SheetTweenConfig;
43
+ disableDismiss?: boolean;
51
44
  disableDrag?: boolean;
52
45
  disableScrollLocking?: boolean;
53
- prefersReducedMotion?: boolean;
54
- dragVelocityThreshold?: number;
55
46
  dragCloseThreshold?: number;
47
+ dragVelocityThreshold?: number;
48
+ initialSnap?: number; // index of snap points array
49
+ isOpen: boolean;
50
+ modalEffectRootId?: string;
56
51
  modalEffectThreshold?: number;
57
- } & SheetEvents &
58
- MotionDivProps;
52
+ mountPoint?: Element;
53
+ prefersReducedMotion?: boolean;
54
+ snapPoints?: number[];
55
+ tweenConfig?: SheetTweenConfig;
56
+ onClose: () => void;
57
+ onCloseEnd?: () => void;
58
+ onCloseStart?: () => void;
59
+ onOpenEnd?: () => void;
60
+ onOpenStart?: () => void;
61
+ onSnap?: (index: number) => void;
62
+ } & MotionDivProps;
59
63
 
60
- export type SheetContainerProps = Omit<
61
- CommonProps,
62
- 'initial' | 'animate' | 'exit' | 'onAnimationComplete'
63
- > & {
64
- children: ReactNode;
65
- };
64
+ export type SheetContainerProps = MotionCommonProps &
65
+ CommonProps & {
66
+ children: ReactNode;
67
+ };
66
68
 
67
- export type SheetDraggableProps = Omit<
68
- CommonProps,
69
- | 'drag'
70
- | 'dragElastic'
71
- | 'dragConstraints'
72
- | 'dragMomentum'
73
- | 'onDrag'
74
- | 'onDragStart'
75
- | 'onDragEnd'
76
- > & {
77
- children?: ReactNode;
78
- disableDrag?: boolean;
79
- };
69
+ export type SheetHeaderProps = MotionDivProps &
70
+ CommonProps & {
71
+ children?: ReactNode;
72
+ disableDrag?: boolean;
73
+ };
80
74
 
81
- export type SheetBackdropProps = Omit<
82
- CommonProps,
83
- 'initial' | 'animate' | 'exit'
84
- >;
75
+ export type SheetContentProps = MotionDivProps &
76
+ CommonProps & {
77
+ disableDrag?: boolean | ((args: SheetStateInfo) => boolean);
78
+ disableScroll?: boolean | ((args: SheetStateInfo) => boolean);
79
+ scrollRef?: RefObject<HTMLDivElement | null>;
80
+ };
85
81
 
86
- export type SheetScrollerProps = MotionDivProps & {
87
- draggableAt?: 'top' | 'bottom' | 'both';
88
- disableScroll?: boolean;
89
- autoPadding?: boolean;
90
- };
82
+ export type SheetBackdropProps = MotionCommonProps & CommonProps;
83
+
84
+ export type SheetDragIndicatorProps = HTMLAttributes<HTMLDivElement> &
85
+ CommonProps;
91
86
 
92
87
  export interface SheetDragProps {
93
88
  drag: 'y';
94
89
  dragElastic: number;
95
90
  dragMomentum: boolean;
96
91
  dragPropagation: boolean;
97
- onDrag: DragHandlers['onDrag'];
98
- onDragStart: DragHandlers['onDragStart'];
99
- onDragEnd: DragHandlers['onDragEnd'];
92
+ onDrag: DragHandler;
93
+ onDragStart: DragHandler;
94
+ onDragEnd: DragHandler;
100
95
  }
101
96
 
97
+ export type SheetStateInfo = {
98
+ scrollPosition?: 'top' | 'bottom' | 'middle';
99
+ currentSnap?: number;
100
+ };
101
+
102
+ export type SheetSnapPoint = {
103
+ snapIndex: number;
104
+ snapValue: number; // Absolute value from the bottom of the sheet
105
+ snapValueY: number; // Y value is inverted as `y = 0` means sheet is at the top
106
+ };
107
+
102
108
  export interface SheetContextType {
103
- y: MotionValue<any>;
104
- sheetRef: RefObject<any>;
105
- isOpen: boolean;
109
+ currentSnap?: number;
106
110
  detent: SheetDetent;
107
- initialSnap: SheetProps['initialSnap'];
108
- indicatorRotation: MotionValue<number>;
109
- callbacks: RefObject<SheetEvents>;
110
- dragProps?: SheetDragProps;
111
- windowHeight: number;
112
- animationOptions: Transition;
113
- reduceMotion: boolean;
114
111
  disableDrag: boolean;
112
+ dragProps?: SheetDragProps;
113
+ indicatorRotation: MotionValue<number>;
114
+ avoidKeyboard: boolean;
115
+ sheetBoundsRef: (node: HTMLDivElement | null) => void;
116
+ sheetRef: RefObject<any>;
117
+ unstyled: boolean;
118
+ y: MotionValue<any>;
115
119
  }
116
120
 
117
121
  export interface SheetScrollerContextType {
@@ -120,32 +124,32 @@ export interface SheetScrollerContextType {
120
124
  setDragEnabled: () => void;
121
125
  }
122
126
 
127
+ type SheetComponent = ForwardRefExoticComponent<
128
+ SheetProps & RefAttributes<any>
129
+ >;
130
+
123
131
  type ContainerComponent = ForwardRefExoticComponent<
124
132
  SheetContainerProps & RefAttributes<any>
125
133
  >;
126
134
 
127
- type DraggableComponent = ForwardRefExoticComponent<
128
- SheetDraggableProps & RefAttributes<any>
135
+ type HeaderComponent = ForwardRefExoticComponent<
136
+ SheetHeaderProps & RefAttributes<any>
129
137
  >;
130
138
 
131
139
  type BackdropComponent = ForwardRefExoticComponent<
132
140
  SheetBackdropProps & RefAttributes<any>
133
141
  >;
134
142
 
135
- type SheetComponent = ForwardRefExoticComponent<
136
- SheetProps & RefAttributes<any>
137
- >;
138
-
139
- type ScrollerComponent = ForwardRefExoticComponent<
140
- SheetScrollerProps & RefAttributes<any>
143
+ type ContentComponent = ForwardRefExoticComponent<
144
+ SheetContentProps & RefAttributes<any>
141
145
  >;
142
146
 
143
147
  interface SheetCompoundComponent {
144
148
  Container: ContainerComponent;
145
- Header: DraggableComponent;
146
- Content: DraggableComponent;
149
+ Header: HeaderComponent;
150
+ DragIndicator: FunctionComponent<SheetDragIndicatorProps>;
151
+ Content: ContentComponent;
147
152
  Backdrop: BackdropComponent;
148
- Scroller: ScrollerComponent;
149
153
  }
150
154
 
151
155
  export type SheetCompound = SheetComponent & SheetCompoundComponent;