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/README.md +332 -188
- package/dist/index.d.mts +51 -39
- package/dist/index.d.ts +51 -39
- package/dist/index.js +854 -585
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +847 -579
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/SheetBackdrop.tsx +19 -6
- package/src/SheetContainer.tsx +34 -25
- package/src/SheetContent.tsx +77 -14
- package/src/SheetDragIndicator.tsx +52 -0
- package/src/SheetHeader.tsx +30 -30
- 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 +14 -10
- package/src/sheet.tsx +247 -209
- package/src/snap.ts +226 -0
- package/src/styles.ts +94 -53
- package/src/types.tsx +83 -79
- package/src/utils.ts +8 -149
- package/src/SheetScroller.tsx +0 -96
- package/src/hooks/use-event-callbacks.ts +0 -44
package/src/utils.ts
CHANGED
|
@@ -1,161 +1,20 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CSSProperties, type ForwardedRef, type RefCallback } from 'react';
|
|
2
2
|
import { IS_SSR } from './constants';
|
|
3
|
-
import type { SheetDetent } from './types';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
4
|
+
export function applyStyles(
|
|
5
|
+
styles: { base: CSSProperties; decorative: CSSProperties },
|
|
6
|
+
unstyled: boolean
|
|
7
|
+
) {
|
|
8
|
+
return unstyled ? styles.base : { ...styles.base, ...styles.decorative };
|
|
47
9
|
}
|
|
48
10
|
|
|
49
|
-
export function
|
|
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[]) {
|
|
11
|
+
export function isAscendingOrder(arr: number[]) {
|
|
105
12
|
for (let i = 0; i < arr.length; i++) {
|
|
106
|
-
if (arr[i + 1]
|
|
13
|
+
if (arr[i + 1] < arr[i]) return false;
|
|
107
14
|
}
|
|
108
|
-
|
|
109
15
|
return true;
|
|
110
16
|
}
|
|
111
17
|
|
|
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
18
|
export function mergeRefs<T = any>(refs: ForwardedRef<T>[]): RefCallback<T> {
|
|
160
19
|
return (value: any) => {
|
|
161
20
|
refs.forEach((ref: any) => {
|
package/src/SheetScroller.tsx
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import React, { type TouchEvent, type UIEvent, forwardRef } from 'react';
|
|
2
|
-
import { motion, type MotionStyle } from 'motion/react';
|
|
3
|
-
|
|
4
|
-
import { useSheetContext, useSheetScrollerContext } from './context';
|
|
5
|
-
import { type SheetScrollerProps } from './types';
|
|
6
|
-
import { isTouchDevice } from './utils';
|
|
7
|
-
import { styles } from './styles';
|
|
8
|
-
|
|
9
|
-
export const SheetScroller = forwardRef<any, SheetScrollerProps>(
|
|
10
|
-
(
|
|
11
|
-
{
|
|
12
|
-
autoPadding = true,
|
|
13
|
-
disableScroll = false,
|
|
14
|
-
draggableAt = 'top',
|
|
15
|
-
children,
|
|
16
|
-
style: styleProp,
|
|
17
|
-
className = '',
|
|
18
|
-
...rest
|
|
19
|
-
},
|
|
20
|
-
ref
|
|
21
|
-
) => {
|
|
22
|
-
const { y } = useSheetContext();
|
|
23
|
-
const { setDragEnabled, setDragDisabled } = useSheetScrollerContext();
|
|
24
|
-
|
|
25
|
-
function determineDragState(element: HTMLDivElement) {
|
|
26
|
-
/**
|
|
27
|
-
* If the caller is managing the scrollability of the element,
|
|
28
|
-
* enable the dragging of the sheet content.
|
|
29
|
-
*/
|
|
30
|
-
if (disableScroll) {
|
|
31
|
-
setDragEnabled();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
36
|
-
const isScrollable = scrollHeight > clientHeight;
|
|
37
|
-
|
|
38
|
-
if (!isScrollable) return;
|
|
39
|
-
|
|
40
|
-
const isAtTop = scrollTop <= 0;
|
|
41
|
-
const isAtBottom = scrollHeight - scrollTop === clientHeight;
|
|
42
|
-
|
|
43
|
-
const shouldEnable =
|
|
44
|
-
(draggableAt === 'top' && isAtTop) ||
|
|
45
|
-
(draggableAt === 'bottom' && isAtBottom) ||
|
|
46
|
-
(draggableAt === 'both' && (isAtTop || isAtBottom));
|
|
47
|
-
|
|
48
|
-
if (shouldEnable) {
|
|
49
|
-
setDragEnabled();
|
|
50
|
-
} else {
|
|
51
|
-
setDragDisabled();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onScroll(event: UIEvent<HTMLDivElement>) {
|
|
56
|
-
determineDragState(event.currentTarget);
|
|
57
|
-
if (rest.onScroll) rest.onScroll(event);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function onTouchStart(event: TouchEvent<HTMLDivElement>) {
|
|
61
|
-
determineDragState(event.currentTarget);
|
|
62
|
-
if (rest.onTouchStart) rest.onTouchStart(event);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const scrollProps = isTouchDevice()
|
|
66
|
-
? { onScroll, onTouchStart }
|
|
67
|
-
: undefined;
|
|
68
|
-
|
|
69
|
-
const style: MotionStyle = {
|
|
70
|
-
...styles.scroller,
|
|
71
|
-
...styleProp,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
if (autoPadding) {
|
|
75
|
-
style.paddingBottom = y;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (disableScroll) {
|
|
79
|
-
style.overflowY = 'hidden';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<motion.div
|
|
84
|
-
{...rest}
|
|
85
|
-
ref={ref}
|
|
86
|
-
className={`react-modal-sheet-scroller ${className}`}
|
|
87
|
-
style={style}
|
|
88
|
-
{...scrollProps}
|
|
89
|
-
>
|
|
90
|
-
{children}
|
|
91
|
-
</motion.div>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
SheetScroller.displayName = 'SheetScroller';
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { type RefObject, useCallback, useEffect, useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
import { type SheetEvents } from '../types';
|
|
4
|
-
|
|
5
|
-
export function useEventCallbacks(
|
|
6
|
-
isOpen: boolean,
|
|
7
|
-
callbacks: RefObject<SheetEvents>
|
|
8
|
-
) {
|
|
9
|
-
const prevOpen = usePrevious(isOpen);
|
|
10
|
-
const didOpen = useRef(false);
|
|
11
|
-
|
|
12
|
-
// Because of AnimatePrecence we don't have access to latest isOpen value
|
|
13
|
-
// so we need to read and write to a ref to determine if we are
|
|
14
|
-
// opening or closing the sheet
|
|
15
|
-
const handleAnimationComplete = useCallback(() => {
|
|
16
|
-
if (!didOpen.current) {
|
|
17
|
-
callbacks.current.onOpenEnd?.();
|
|
18
|
-
didOpen.current = true;
|
|
19
|
-
} else {
|
|
20
|
-
callbacks.current.onCloseEnd?.();
|
|
21
|
-
didOpen.current = false;
|
|
22
|
-
}
|
|
23
|
-
}, [isOpen, prevOpen]);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (!prevOpen && isOpen) {
|
|
27
|
-
callbacks.current.onOpenStart?.();
|
|
28
|
-
} else if (!isOpen && prevOpen) {
|
|
29
|
-
callbacks.current.onCloseStart?.();
|
|
30
|
-
}
|
|
31
|
-
}, [isOpen, prevOpen]);
|
|
32
|
-
|
|
33
|
-
return { handleAnimationComplete };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function usePrevious<T>(state: T): T | undefined {
|
|
37
|
-
const ref = useRef<T>(undefined);
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
ref.current = state;
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return ref.current;
|
|
44
|
-
}
|