react-modal-sheet 4.3.0 → 5.0.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 +259 -165
- package/dist/index.d.mts +40 -30
- package/dist/index.d.ts +40 -30
- package/dist/index.js +858 -664
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +794 -601
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/src/SheetBackdrop.tsx +7 -5
- package/src/SheetContainer.tsx +29 -22
- package/src/SheetContent.tsx +76 -14
- package/src/SheetHeader.tsx +15 -12
- package/src/constants.ts +5 -3
- package/src/context.tsx +2 -42
- package/src/hooks/use-dimensions.ts +4 -4
- package/src/hooks/use-drag-constraints.ts +3 -3
- package/src/hooks/use-modal-effect.ts +19 -23
- package/src/hooks/use-resize-observer.ts +31 -0
- package/src/hooks/use-scroll-position.ts +72 -0
- package/src/hooks/use-sheet-state.ts +63 -0
- package/src/hooks/use-virtual-keyboard.ts +153 -0
- package/src/index.tsx +10 -8
- package/src/sheet.tsx +239 -209
- package/src/snap.ts +226 -0
- package/src/styles.ts +3 -3
- package/src/types.tsx +56 -60
- package/src/utils.ts +2 -150
- package/src/SheetScroller.tsx +0 -84
- package/src/hooks/use-event-callbacks.ts +0 -44
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,7 +1,7 @@
|
|
|
1
1
|
import { type Properties } from 'csstype';
|
|
2
2
|
|
|
3
|
-
export const styles
|
|
4
|
-
|
|
3
|
+
export const styles = {
|
|
4
|
+
root: {
|
|
5
5
|
position: 'fixed',
|
|
6
6
|
top: 0,
|
|
7
7
|
bottom: 0,
|
|
@@ -66,4 +66,4 @@ export const styles: Record<string, Properties> = {
|
|
|
66
66
|
overflowY: 'auto',
|
|
67
67
|
overscrollBehaviorY: 'none',
|
|
68
68
|
},
|
|
69
|
-
}
|
|
69
|
+
} satisfies Record<string, Properties>;
|
package/src/types.tsx
CHANGED
|
@@ -1,61 +1,58 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ComponentPropsWithoutRef,
|
|
3
3
|
type ForwardRefExoticComponent,
|
|
4
|
-
type HTMLAttributes,
|
|
5
|
-
type MutableRefObject,
|
|
6
4
|
type ReactNode,
|
|
7
5
|
type RefAttributes,
|
|
8
6
|
type RefObject,
|
|
9
7
|
} from 'react';
|
|
10
8
|
|
|
11
9
|
import {
|
|
12
|
-
type
|
|
13
|
-
type MotionValue,
|
|
14
|
-
type MotionProps,
|
|
15
|
-
type motion,
|
|
10
|
+
type DragHandler,
|
|
16
11
|
type EasingDefinition,
|
|
12
|
+
type MotionProps,
|
|
13
|
+
type MotionValue,
|
|
17
14
|
type Transition,
|
|
15
|
+
type motion,
|
|
18
16
|
} from 'motion/react';
|
|
17
|
+
import type { RectReadOnly } from 'react-use-measure';
|
|
19
18
|
|
|
20
|
-
export
|
|
21
|
-
onOpenStart?: () => void;
|
|
22
|
-
onOpenEnd?: () => void;
|
|
23
|
-
onCloseStart?: () => void;
|
|
24
|
-
onCloseEnd?: () => void;
|
|
25
|
-
onSnap?: (index: number) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type SheetDetent = 'full-height' | 'content-height';
|
|
19
|
+
export type SheetDetent = 'default' | 'full' | 'content';
|
|
29
20
|
|
|
30
21
|
type CommonProps = MotionProps & {
|
|
31
22
|
className?: string;
|
|
32
23
|
};
|
|
33
24
|
|
|
25
|
+
type MotionDivProps = ComponentPropsWithoutRef<typeof motion.div>;
|
|
26
|
+
|
|
34
27
|
export interface SheetTweenConfig {
|
|
35
28
|
ease: EasingDefinition;
|
|
36
29
|
duration: number;
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
export type SheetProps = {
|
|
40
|
-
|
|
33
|
+
avoidKeyboard?: boolean;
|
|
41
34
|
children: ReactNode;
|
|
42
|
-
onClose: () => void;
|
|
43
|
-
/** @deprecated Use `modalEffectRootId` instead! */
|
|
44
|
-
rootId?: string;
|
|
45
|
-
modalEffectRootId?: string;
|
|
46
|
-
mountPoint?: Element;
|
|
47
|
-
snapPoints?: number[];
|
|
48
35
|
detent?: SheetDetent;
|
|
49
|
-
|
|
50
|
-
tweenConfig?: SheetTweenConfig;
|
|
36
|
+
disableDismiss?: boolean;
|
|
51
37
|
disableDrag?: boolean;
|
|
52
38
|
disableScrollLocking?: boolean;
|
|
53
|
-
prefersReducedMotion?: boolean;
|
|
54
|
-
dragVelocityThreshold?: number;
|
|
55
39
|
dragCloseThreshold?: number;
|
|
40
|
+
dragVelocityThreshold?: number;
|
|
41
|
+
initialSnap?: number; // index of snap points array
|
|
42
|
+
isOpen: boolean;
|
|
43
|
+
modalEffectRootId?: string;
|
|
56
44
|
modalEffectThreshold?: number;
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
mountPoint?: Element;
|
|
46
|
+
prefersReducedMotion?: boolean;
|
|
47
|
+
snapPoints?: number[];
|
|
48
|
+
tweenConfig?: SheetTweenConfig;
|
|
49
|
+
onClose: () => void;
|
|
50
|
+
onCloseEnd?: () => void;
|
|
51
|
+
onCloseStart?: () => void;
|
|
52
|
+
onOpenEnd?: () => void;
|
|
53
|
+
onOpenStart?: () => void;
|
|
54
|
+
onSnap?: (index: number) => void;
|
|
55
|
+
} & MotionDivProps;
|
|
59
56
|
|
|
60
57
|
export type SheetContainerProps = Omit<
|
|
61
58
|
CommonProps,
|
|
@@ -64,16 +61,7 @@ export type SheetContainerProps = Omit<
|
|
|
64
61
|
children: ReactNode;
|
|
65
62
|
};
|
|
66
63
|
|
|
67
|
-
export type SheetDraggableProps =
|
|
68
|
-
CommonProps,
|
|
69
|
-
| 'drag'
|
|
70
|
-
| 'dragElastic'
|
|
71
|
-
| 'dragConstraints'
|
|
72
|
-
| 'dragMomentum'
|
|
73
|
-
| 'onDrag'
|
|
74
|
-
| 'onDragStart'
|
|
75
|
-
| 'onDragEnd'
|
|
76
|
-
> & {
|
|
64
|
+
export type SheetDraggableProps = MotionDivProps & {
|
|
77
65
|
children?: ReactNode;
|
|
78
66
|
disableDrag?: boolean;
|
|
79
67
|
};
|
|
@@ -83,34 +71,43 @@ export type SheetBackdropProps = Omit<
|
|
|
83
71
|
'initial' | 'animate' | 'exit'
|
|
84
72
|
>;
|
|
85
73
|
|
|
86
|
-
export type SheetScrollerProps = HTMLAttributes<HTMLDivElement> & {
|
|
87
|
-
draggableAt?: 'top' | 'bottom' | 'both';
|
|
88
|
-
disableScroll?: boolean;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
74
|
export interface SheetDragProps {
|
|
92
75
|
drag: 'y';
|
|
93
76
|
dragElastic: number;
|
|
94
77
|
dragMomentum: boolean;
|
|
95
78
|
dragPropagation: boolean;
|
|
96
|
-
onDrag:
|
|
97
|
-
onDragStart:
|
|
98
|
-
onDragEnd:
|
|
79
|
+
onDrag: DragHandler;
|
|
80
|
+
onDragStart: DragHandler;
|
|
81
|
+
onDragEnd: DragHandler;
|
|
99
82
|
}
|
|
100
83
|
|
|
84
|
+
export type SheetStateInfo = {
|
|
85
|
+
scrollPosition?: 'top' | 'bottom' | 'middle';
|
|
86
|
+
currentSnap?: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type SheetContentScrollableProps = MotionDivProps & {
|
|
90
|
+
disableDrag?: boolean | ((args: SheetStateInfo) => boolean);
|
|
91
|
+
disableScroll?: boolean | ((args: SheetStateInfo) => boolean);
|
|
92
|
+
scrollRef?: RefObject<HTMLDivElement | null>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type SheetSnapPoint = {
|
|
96
|
+
snapIndex: number;
|
|
97
|
+
snapValue: number; // Absolute value from the bottom of the sheet
|
|
98
|
+
snapValueY: number; // Y value is inverted as `y = 0` means sheet is at the top
|
|
99
|
+
};
|
|
100
|
+
|
|
101
101
|
export interface SheetContextType {
|
|
102
|
-
|
|
103
|
-
sheetRef: RefObject<any>;
|
|
104
|
-
isOpen: boolean;
|
|
102
|
+
currentSnap?: number;
|
|
105
103
|
detent: SheetDetent;
|
|
106
|
-
initialSnap: SheetProps['initialSnap'];
|
|
107
|
-
indicatorRotation: MotionValue<number>;
|
|
108
|
-
callbacks: RefObject<SheetEvents>;
|
|
109
|
-
dragProps?: SheetDragProps;
|
|
110
|
-
windowHeight: number;
|
|
111
|
-
animationOptions: Transition;
|
|
112
|
-
reduceMotion: boolean;
|
|
113
104
|
disableDrag: boolean;
|
|
105
|
+
dragProps?: SheetDragProps;
|
|
106
|
+
indicatorRotation: MotionValue<number>;
|
|
107
|
+
avoidKeyboard: boolean;
|
|
108
|
+
sheetBoundsRef: (node: HTMLDivElement | null) => void;
|
|
109
|
+
sheetRef: RefObject<any>;
|
|
110
|
+
y: MotionValue<any>;
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
export interface SheetScrollerContextType {
|
|
@@ -135,16 +132,15 @@ type SheetComponent = ForwardRefExoticComponent<
|
|
|
135
132
|
SheetProps & RefAttributes<any>
|
|
136
133
|
>;
|
|
137
134
|
|
|
138
|
-
type
|
|
139
|
-
|
|
135
|
+
type ContentScrollableComponent = ForwardRefExoticComponent<
|
|
136
|
+
SheetContentScrollableProps & RefAttributes<any>
|
|
140
137
|
>;
|
|
141
138
|
|
|
142
139
|
interface SheetCompoundComponent {
|
|
143
140
|
Container: ContainerComponent;
|
|
144
141
|
Header: DraggableComponent;
|
|
145
|
-
Content:
|
|
142
|
+
Content: ContentScrollableComponent;
|
|
146
143
|
Backdrop: BackdropComponent;
|
|
147
|
-
Scroller: ScrollerComponent;
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
export type SheetCompound = SheetComponent & SheetCompoundComponent;
|
package/src/utils.ts
CHANGED
|
@@ -1,161 +1,13 @@
|
|
|
1
1
|
import { type ForwardedRef, type RefCallback, type RefObject } from 'react';
|
|
2
2
|
import { IS_SSR } from './constants';
|
|
3
|
-
import type { SheetDetent } from './types';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
* Get the rounded height of the sheet element and log a warning if the
|
|
7
|
-
* element is not mounted yet but the height is requested.
|
|
8
|
-
*/
|
|
9
|
-
export function getSheetHeight(sheetRef: RefObject<HTMLDivElement | null>) {
|
|
10
|
-
const sheetEl = sheetRef.current;
|
|
11
|
-
|
|
12
|
-
if (!sheetEl) {
|
|
13
|
-
console.warn(
|
|
14
|
-
'Sheet height is not available because the sheet element is not mounted yet.'
|
|
15
|
-
);
|
|
16
|
-
return 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return Math.round(sheetEl.getBoundingClientRect().height);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Convert negative / percentage snap points to absolute values
|
|
24
|
-
*/
|
|
25
|
-
export function getSnapPoints({
|
|
26
|
-
snapPointsProp,
|
|
27
|
-
sheetHeight,
|
|
28
|
-
}: {
|
|
29
|
-
snapPointsProp: number[];
|
|
30
|
-
sheetHeight: number;
|
|
31
|
-
}) {
|
|
32
|
-
const snapPointValues = snapPointsProp.map((point) => {
|
|
33
|
-
// Percentage values e.g. between 0.0 and 1.0
|
|
34
|
-
if (point > 0 && point <= 1) {
|
|
35
|
-
return Math.round(point * sheetHeight);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return point < 0 ? sheetHeight + point : point; // negative values
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
console.assert(
|
|
42
|
-
inDescendingOrder(snapPointValues) || sheetHeight === 0,
|
|
43
|
-
`Snap points need to be in descending order got: [${snapPointsProp.join(', ')}]`
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
return snapPointValues;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function getClosestSnapPoint({
|
|
50
|
-
snapPoints,
|
|
51
|
-
currentY,
|
|
52
|
-
sheetHeight,
|
|
53
|
-
detent,
|
|
54
|
-
}: {
|
|
55
|
-
snapPoints: number[];
|
|
56
|
-
currentY: number;
|
|
57
|
-
sheetHeight: number;
|
|
58
|
-
detent?: SheetDetent;
|
|
59
|
-
}) {
|
|
60
|
-
// Inverse values are the values that can be passed to `animate`
|
|
61
|
-
const snapInverse = snapPoints.map(
|
|
62
|
-
(p) => sheetHeight - Math.min(p, sheetHeight)
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Allow snapping to the top of the sheet if detent is set to `content-height`
|
|
66
|
-
if (detent === 'content-height' && !snapInverse.includes(0)) {
|
|
67
|
-
snapInverse.unshift(0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Get the closest snap point
|
|
71
|
-
const snapTo = getClosest(snapInverse, currentY);
|
|
72
|
-
const snapIndex = snapInverse.indexOf(snapTo);
|
|
73
|
-
|
|
74
|
-
const snapY = validateSnapTo({
|
|
75
|
-
snapTo: getClosest(snapInverse, currentY),
|
|
76
|
-
sheetHeight,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return { snapY, snapIndex };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get the closest number to the goal from the array of numbers.
|
|
84
|
-
*/
|
|
85
|
-
export function getClosest(nums: number[], goal: number) {
|
|
86
|
-
let closest = nums[0];
|
|
87
|
-
let minDifference = Math.abs(nums[0] - goal);
|
|
88
|
-
|
|
89
|
-
for (let i = 1; i < nums.length; i++) {
|
|
90
|
-
const difference = Math.abs(nums[i] - goal);
|
|
91
|
-
if (difference < minDifference) {
|
|
92
|
-
closest = nums[i];
|
|
93
|
-
minDifference = difference;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return closest;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Check if the array is in descending order.
|
|
102
|
-
* This is used to validate the snap points.
|
|
103
|
-
*/
|
|
104
|
-
export function inDescendingOrder(arr: number[]) {
|
|
4
|
+
export function isAscendingOrder(arr: number[]) {
|
|
105
5
|
for (let i = 0; i < arr.length; i++) {
|
|
106
|
-
if (arr[i + 1]
|
|
6
|
+
if (arr[i + 1] < arr[i]) return false;
|
|
107
7
|
}
|
|
108
|
-
|
|
109
8
|
return true;
|
|
110
9
|
}
|
|
111
10
|
|
|
112
|
-
/**
|
|
113
|
-
* Get the Y value of the given snap point. This can be passed to the `animate`
|
|
114
|
-
* function to animate the sheet to the given snap point.
|
|
115
|
-
*/
|
|
116
|
-
export function getSnapY({
|
|
117
|
-
sheetHeight,
|
|
118
|
-
snapPoints,
|
|
119
|
-
snapIndex,
|
|
120
|
-
}: {
|
|
121
|
-
sheetHeight: number;
|
|
122
|
-
snapPoints: number[];
|
|
123
|
-
snapIndex: number;
|
|
124
|
-
}) {
|
|
125
|
-
const snapPoint = snapPoints[snapIndex];
|
|
126
|
-
|
|
127
|
-
if (snapPoint === undefined) {
|
|
128
|
-
console.warn(
|
|
129
|
-
`Invalid snap index ${snapIndex}. Snap points are: [${snapPoints.join(', ')}]`
|
|
130
|
-
);
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const y = validateSnapTo({ snapTo: sheetHeight - snapPoint, sheetHeight });
|
|
135
|
-
|
|
136
|
-
return y;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Make sure the snap point is not out of bounds.
|
|
141
|
-
* Snap points cannot be negative - `0` means the sheet is fully open.
|
|
142
|
-
*/
|
|
143
|
-
export function validateSnapTo({
|
|
144
|
-
snapTo,
|
|
145
|
-
sheetHeight,
|
|
146
|
-
}: {
|
|
147
|
-
snapTo: number;
|
|
148
|
-
sheetHeight: number;
|
|
149
|
-
}) {
|
|
150
|
-
if (snapTo < 0) {
|
|
151
|
-
console.warn(
|
|
152
|
-
`Snap point is out of bounds. Sheet height is ${sheetHeight} but snap point is ${sheetHeight + Math.abs(snapTo)}.`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return Math.max(Math.round(snapTo), 0);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
11
|
export function mergeRefs<T = any>(refs: ForwardedRef<T>[]): RefCallback<T> {
|
|
160
12
|
return (value: any) => {
|
|
161
13
|
refs.forEach((ref: any) => {
|
package/src/SheetScroller.tsx
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import React, { type TouchEvent, type UIEvent, forwardRef } from 'react';
|
|
2
|
-
|
|
3
|
-
import { useSheetScrollerContext } from './context';
|
|
4
|
-
import { type SheetScrollerProps } from './types';
|
|
5
|
-
import { isTouchDevice } from './utils';
|
|
6
|
-
import { styles } from './styles';
|
|
7
|
-
|
|
8
|
-
export const SheetScroller = forwardRef<any, SheetScrollerProps>(
|
|
9
|
-
(
|
|
10
|
-
{
|
|
11
|
-
disableScroll = false,
|
|
12
|
-
draggableAt = 'top',
|
|
13
|
-
children,
|
|
14
|
-
style,
|
|
15
|
-
className = '',
|
|
16
|
-
...rest
|
|
17
|
-
},
|
|
18
|
-
ref
|
|
19
|
-
) => {
|
|
20
|
-
const { setDragEnabled, setDragDisabled } = useSheetScrollerContext();
|
|
21
|
-
|
|
22
|
-
function determineDragState(element: HTMLDivElement) {
|
|
23
|
-
/**
|
|
24
|
-
* If the caller is managing the scrollability of the element,
|
|
25
|
-
* enable the dragging of the sheet content.
|
|
26
|
-
*/
|
|
27
|
-
if (disableScroll) {
|
|
28
|
-
setDragEnabled();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
33
|
-
const isScrollable = scrollHeight > clientHeight;
|
|
34
|
-
|
|
35
|
-
if (!isScrollable) return;
|
|
36
|
-
|
|
37
|
-
const isAtTop = scrollTop <= 0;
|
|
38
|
-
const isAtBottom = scrollHeight - scrollTop === clientHeight;
|
|
39
|
-
|
|
40
|
-
const shouldEnable =
|
|
41
|
-
(draggableAt === 'top' && isAtTop) ||
|
|
42
|
-
(draggableAt === 'bottom' && isAtBottom) ||
|
|
43
|
-
(draggableAt === 'both' && (isAtTop || isAtBottom));
|
|
44
|
-
|
|
45
|
-
if (shouldEnable) {
|
|
46
|
-
setDragEnabled();
|
|
47
|
-
} else {
|
|
48
|
-
setDragDisabled();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function onScroll(event: UIEvent<HTMLDivElement>) {
|
|
53
|
-
determineDragState(event.currentTarget);
|
|
54
|
-
if (rest.onScroll) rest.onScroll(event);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function onTouchStart(event: TouchEvent<HTMLDivElement>) {
|
|
58
|
-
determineDragState(event.currentTarget);
|
|
59
|
-
if (rest.onTouchStart) rest.onTouchStart(event);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const scrollProps = isTouchDevice()
|
|
63
|
-
? { onScroll, onTouchStart }
|
|
64
|
-
: undefined;
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<div
|
|
68
|
-
{...rest}
|
|
69
|
-
ref={ref}
|
|
70
|
-
className={`react-modal-sheet-scroller ${className}`}
|
|
71
|
-
style={{
|
|
72
|
-
...styles.scroller,
|
|
73
|
-
...style,
|
|
74
|
-
overflowY: disableScroll ? 'hidden' : 'auto',
|
|
75
|
-
}}
|
|
76
|
-
{...scrollProps}
|
|
77
|
-
>
|
|
78
|
-
{children}
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
SheetScroller.displayName = 'SheetScroller';
|