react-modal-sheet 4.0.2 → 4.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/utils.ts CHANGED
@@ -1,5 +1,53 @@
1
- import { type ForwardedRef, type RefCallback } from 'react';
1
+ import { type ForwardedRef, type RefCallback, type RefObject } from 'react';
2
+ import { IS_SSR } from './constants';
2
3
 
4
+ /**
5
+ * Get the rounded height of the sheet element and log a warning if the
6
+ * element is not mounted yet but the height is requested.
7
+ */
8
+ export function getSheetHeight(sheetRef: RefObject<HTMLDivElement | null>) {
9
+ const sheetEl = sheetRef.current;
10
+
11
+ if (!sheetEl) {
12
+ console.warn(
13
+ 'Sheet height is not available because the sheet element is not mounted yet.'
14
+ );
15
+ return 0;
16
+ }
17
+
18
+ return Math.round(sheetEl.getBoundingClientRect().height);
19
+ }
20
+
21
+ /**
22
+ * Convert negative / percentage snap points to absolute values
23
+ */
24
+ export function getSnapPoints({
25
+ snapPointsProp,
26
+ sheetHeight,
27
+ }: {
28
+ snapPointsProp: number[];
29
+ sheetHeight: number;
30
+ }) {
31
+ const snapPointValues = snapPointsProp.map((point) => {
32
+ // Percentage values e.g. between 0.0 and 1.0
33
+ if (point > 0 && point <= 1) {
34
+ return Math.round(point * sheetHeight);
35
+ }
36
+
37
+ return point < 0 ? sheetHeight + point : point; // negative values
38
+ });
39
+
40
+ console.assert(
41
+ inDescendingOrder(snapPointValues) || sheetHeight === 0,
42
+ `Snap points need to be in descending order got: [${snapPointsProp.join(', ')}]`
43
+ );
44
+
45
+ return snapPointValues;
46
+ }
47
+
48
+ /**
49
+ * Get the closest number to the goal from the array of numbers.
50
+ */
3
51
  export function getClosest(nums: number[], goal: number) {
4
52
  let closest = nums[0];
5
53
  let minDifference = Math.abs(nums[0] - goal);
@@ -15,6 +63,10 @@ export function getClosest(nums: number[], goal: number) {
15
63
  return closest;
16
64
  }
17
65
 
66
+ /**
67
+ * Check if the array is in descending order.
68
+ * This is used to validate the snap points.
69
+ */
18
70
  export function inDescendingOrder(arr: number[]) {
19
71
  for (let i = 0; i < arr.length; i++) {
20
72
  if (arr[i + 1] > arr[i]) return false;
@@ -23,6 +75,37 @@ export function inDescendingOrder(arr: number[]) {
23
75
  return true;
24
76
  }
25
77
 
78
+ /**
79
+ * Get the Y value of the given snap point. This can be passed to the `animate`
80
+ * function to animate the sheet to the given snap point.
81
+ */
82
+ export function getSnapY({
83
+ sheetHeight,
84
+ snapPoints,
85
+ snapIndex,
86
+ }: {
87
+ sheetHeight: number;
88
+ snapPoints: number[];
89
+ snapIndex: number;
90
+ }) {
91
+ const snapPoint = snapPoints[snapIndex];
92
+
93
+ if (snapPoint === undefined) {
94
+ console.warn(
95
+ `Invalid snap index ${snapIndex}. Snap points are: [${snapPoints.join(', ')}]`
96
+ );
97
+ return null;
98
+ }
99
+
100
+ const y = validateSnapTo({ snapTo: sheetHeight - snapPoint, sheetHeight });
101
+
102
+ return y;
103
+ }
104
+
105
+ /**
106
+ * Make sure the snap point is not out of bounds.
107
+ * Snap points cannot be negative - `0` means the sheet is fully open.
108
+ */
26
109
  export function validateSnapTo({
27
110
  snapTo,
28
111
  sheetHeight,
@@ -32,9 +115,7 @@ export function validateSnapTo({
32
115
  }) {
33
116
  if (snapTo < 0) {
34
117
  console.warn(
35
- `Snap point is out of bounds. Sheet height is ${sheetHeight} but snap point is ${
36
- sheetHeight + Math.abs(snapTo)
37
- }.`
118
+ `Snap point is out of bounds. Sheet height is ${sheetHeight} but snap point is ${sheetHeight + Math.abs(snapTo)}.`
38
119
  );
39
120
  }
40
121
 
@@ -54,7 +135,7 @@ export function mergeRefs<T = any>(refs: ForwardedRef<T>[]): RefCallback<T> {
54
135
  }
55
136
 
56
137
  export function isTouchDevice() {
57
- if (typeof window === 'undefined') return false;
138
+ if (IS_SSR) return false;
58
139
  return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
59
140
  }
60
141
 
@@ -86,11 +167,8 @@ const isIPhone = cached(function () {
86
167
  });
87
168
 
88
169
  const isIPad = cached(function () {
89
- return (
90
- testPlatform(/^iPad/i) ||
91
- // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
92
- (isMac() && navigator.maxTouchPoints > 1)
93
- );
170
+ // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
171
+ return testPlatform(/^iPad/i) || (isMac() && navigator.maxTouchPoints > 1);
94
172
  });
95
173
 
96
174
  export const isIOS = cached(function () {
package/src/hooks.tsx DELETED
@@ -1,197 +0,0 @@
1
- import {
2
- type RefObject,
3
- useCallback,
4
- useEffect,
5
- useLayoutEffect,
6
- useRef,
7
- useState,
8
- } from 'react';
9
-
10
- import { type MotionValue, type BoundingBox, transform } from 'motion/react';
11
-
12
- import { IS_SSR } from './constants';
13
- import { type SheetEvents } from './types';
14
-
15
- export const useIsomorphicLayoutEffect = IS_SSR ? useEffect : useLayoutEffect;
16
-
17
- export function useModalEffect({
18
- y,
19
- rootId,
20
- sheetRef,
21
- }: {
22
- y: MotionValue<number>;
23
- rootId?: string;
24
- sheetRef: RefObject<HTMLDivElement | null>;
25
- }) {
26
- const heightRef = useRef(IS_SSR ? 0 : window.innerHeight);
27
-
28
- function setup() {
29
- const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
30
- const body = document.querySelector('body') as HTMLBodyElement;
31
- if (!root) return;
32
-
33
- body.style.backgroundColor = '#000';
34
- root.style.overflow = 'hidden';
35
- root.style.transitionTimingFunction = 'cubic-bezier(0.32, 0.72, 0, 1)';
36
- root.style.transitionProperty = 'transform, border-radius';
37
- root.style.transitionDuration = '0.5s';
38
- root.style.transformOrigin = 'center top';
39
- }
40
-
41
- function cleanup() {
42
- const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
43
- const body = document.querySelector('body') as HTMLBodyElement;
44
- if (!root) return;
45
-
46
- setTimeout(() => {
47
- body.style.removeProperty('background-color');
48
- root.style.removeProperty('overflow');
49
- root.style.removeProperty('transition-timing-function');
50
- root.style.removeProperty('transition-property');
51
- root.style.removeProperty('transition-duration');
52
- root.style.removeProperty('transform-origin');
53
- root.style.removeProperty('transform');
54
- root.style.removeProperty('border-top-right-radius');
55
- root.style.removeProperty('border-top-left-radius');
56
- }, 100);
57
- }
58
-
59
- useIsomorphicLayoutEffect(() => {
60
- return () => {
61
- if (rootId) cleanup();
62
- };
63
- }, []);
64
-
65
- useEffect(() => {
66
- const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
67
- if (!root) return;
68
-
69
- function onCompleted() {
70
- if (y.get() - 10 >= heightRef.current) cleanup();
71
- }
72
-
73
- const removeStartListener = y.on('animationStart', () => {
74
- heightRef.current = sheetRef.current?.offsetHeight || window.innerHeight;
75
- setup();
76
- });
77
-
78
- const removeChangeListener = y.on('change', (value) => {
79
- if (root) {
80
- const progress = Math.max(0, 1 - value / heightRef.current);
81
- const pageWidth = window.innerWidth;
82
- const scale = (pageWidth - 16) / pageWidth;
83
- const ty = transform(progress, [0, 1], [0, 24]);
84
- const s = transform(progress, [0, 1], [1, scale]);
85
- const borderRadius = transform(progress, [0, 1], [0, 10]);
86
- const inset = 'env(safe-area-inset-top)';
87
-
88
- root.style.transform = `scale(${s}) translate3d(0, calc(${inset} + ${ty}px), 0)`; // prettier-ignore
89
- root.style.borderTopRightRadius = `${borderRadius}px`;
90
- root.style.borderTopLeftRadius = `${borderRadius}px`;
91
- }
92
- });
93
-
94
- const removeCompleteListener = y.on('animationComplete', onCompleted);
95
- const removeCancelListener = y.on('animationCancel', onCompleted);
96
-
97
- return () => {
98
- removeStartListener();
99
- removeChangeListener();
100
- removeCompleteListener();
101
- removeCancelListener();
102
- };
103
- }, [y, rootId]);
104
- }
105
-
106
- export function useEventCallbacks(
107
- isOpen: boolean,
108
- callbacks: RefObject<SheetEvents>
109
- ) {
110
- const prevOpen = usePrevious(isOpen);
111
- const didOpen = useRef(false);
112
-
113
- // Because of AnimatePrecence we don't have access to latest isOpen value
114
- // so we need to read and write to a ref to determine if we are
115
- // opening or closing the sheet
116
- const handleAnimationComplete = useCallback(() => {
117
- if (!didOpen.current) {
118
- callbacks.current.onOpenEnd?.();
119
- didOpen.current = true;
120
- } else {
121
- callbacks.current.onCloseEnd?.();
122
- didOpen.current = false;
123
- }
124
- }, [isOpen, prevOpen]);
125
-
126
- useEffect(() => {
127
- if (!prevOpen && isOpen) {
128
- callbacks.current.onOpenStart?.();
129
- } else if (!isOpen && prevOpen) {
130
- callbacks.current.onCloseStart?.();
131
- }
132
- }, [isOpen, prevOpen]);
133
-
134
- return { handleAnimationComplete };
135
- }
136
-
137
- export function useDimensions() {
138
- const [dimensions, setDimensions] = useState({
139
- height: !IS_SSR ? window.innerHeight : 0,
140
- width: !IS_SSR ? window.innerWidth : 0,
141
- });
142
-
143
- useIsomorphicLayoutEffect(() => {
144
- function handler() {
145
- setDimensions({
146
- height: window.innerHeight,
147
- width: window.innerWidth,
148
- });
149
- }
150
-
151
- handler();
152
-
153
- window.addEventListener('resize', handler);
154
-
155
- return () => {
156
- window.removeEventListener('resize', handler);
157
- };
158
- }, []);
159
-
160
- return dimensions;
161
- }
162
-
163
- export function usePrevious<T>(state: T): T | undefined {
164
- const ref = useRef<T>(undefined);
165
-
166
- useEffect(() => {
167
- ref.current = state;
168
- });
169
-
170
- return ref.current;
171
- }
172
-
173
- // Userland version of the `useEffectEvent` React hook:
174
- // RFC: https://react.dev/reference/react/experimental_useEffectEvent
175
- export function useEffectEvent<T extends (...args: any[]) => any>(handler: T) {
176
- const handlerRef = useRef<T>(undefined);
177
-
178
- useIsomorphicLayoutEffect(() => {
179
- handlerRef.current = handler;
180
- });
181
-
182
- return useCallback((...args: any[]) => {
183
- const fn = handlerRef.current;
184
- return fn?.(...args);
185
- }, []) as T;
186
- }
187
-
188
- // This is a hacky way to fix a bug in motion/react where the drag
189
- // constraints are not updated when window is resized.
190
- // https://github.com/framer/motion/issues/1659
191
- const constraints: BoundingBox = { bottom: 0, top: 0, left: 0, right: 0 };
192
-
193
- export function useDragConstraints() {
194
- const constraintsRef = useRef<any>(null);
195
- const onMeasureDragConstraints = useCallback(() => constraints, []);
196
- return { constraintsRef, onMeasureDragConstraints };
197
- }