@hero-design/rn 8.128.3 → 8.129.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/CHANGELOG.md +12 -0
- package/es/index.js +335 -127
- package/lib/index.js +334 -126
- package/package.json +1 -1
- package/src/components/BottomSheet/StyledBottomSheet.tsx +1 -1
- package/src/components/Drawer/DragableDrawer/DragableDrawerContext.ts +32 -0
- package/src/components/Drawer/DragableDrawer/DragableScrollView.tsx +113 -0
- package/src/components/Drawer/DragableDrawer/index.tsx +51 -144
- package/src/components/Drawer/DragableDrawer/useDragablePan.ts +223 -0
- package/src/components/Drawer/StyledDrawer.tsx +1 -1
- package/src/components/Drawer/index.tsx +2 -0
- package/types/components/BottomSheet/StyledBottomSheet.d.ts +3 -3
- package/types/components/Drawer/DragableDrawer/DragableDrawerContext.d.ts +18 -0
- package/types/components/Drawer/DragableDrawer/DragableScrollView.d.ts +18 -0
- package/types/components/Drawer/DragableDrawer/useDragablePan.d.ts +23 -0
- package/types/components/Drawer/index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
Animated,
|
|
6
6
|
KeyboardAvoidingView,
|
|
7
7
|
Pressable,
|
|
8
|
-
SafeAreaView,
|
|
9
8
|
StyleSheet,
|
|
10
9
|
TouchableOpacity,
|
|
11
10
|
View,
|
|
12
11
|
} from 'react-native';
|
|
12
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
13
13
|
|
|
14
14
|
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
15
15
|
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
interface DragableDrawerContextValue {
|
|
5
|
+
/** Whether the drawer has settled at maximum height (offset === 0). */
|
|
6
|
+
isAtMaxHeight: boolean;
|
|
7
|
+
/** Current vertical scroll offset reported by DragableScrollView. */
|
|
8
|
+
scrollYRef: MutableRefObject<number>;
|
|
9
|
+
/** Report scroll offset changes from DragableScrollView. */
|
|
10
|
+
onScrollY: (y: number) => void;
|
|
11
|
+
/** Begin a drawer pan gesture (stops any running animation). */
|
|
12
|
+
beginPan: () => void;
|
|
13
|
+
/** Move the drawer by dy pixels during an active pan. */
|
|
14
|
+
movePan: (dy: number) => void;
|
|
15
|
+
/** End pan and snap to the nearest snap point. */
|
|
16
|
+
releasePan: (dy: number, vy: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const noop = () => ({});
|
|
20
|
+
|
|
21
|
+
const DragableDrawerContext = createContext<DragableDrawerContextValue>({
|
|
22
|
+
isAtMaxHeight: false,
|
|
23
|
+
scrollYRef: { current: 0 },
|
|
24
|
+
onScrollY: noop,
|
|
25
|
+
beginPan: noop,
|
|
26
|
+
movePan: noop,
|
|
27
|
+
releasePan: noop,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const useDragableDrawerContext = () => useContext(DragableDrawerContext);
|
|
31
|
+
|
|
32
|
+
export default DragableDrawerContext;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { PanResponder, ScrollView, View } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
NativeScrollEvent,
|
|
5
|
+
NativeSyntheticEvent,
|
|
6
|
+
ScrollViewProps,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import type { ReactElement } from 'react';
|
|
9
|
+
|
|
10
|
+
import { useDragableDrawerContext } from './DragableDrawerContext';
|
|
11
|
+
|
|
12
|
+
export type DragableScrollViewProps = ScrollViewProps;
|
|
13
|
+
|
|
14
|
+
// scrollY tolerance: treat anything within this many pixels of the top as
|
|
15
|
+
// "at scroll top" to handle fractional pixels and throttled scroll events.
|
|
16
|
+
const SCROLL_TOP_THRESHOLD_PX = 5;
|
|
17
|
+
// Minimum downward finger movement before the pull-down collapse is triggered.
|
|
18
|
+
const PULL_DOWN_THRESHOLD_PX = 10;
|
|
19
|
+
const DEFAULT_SCROLL_EVENT_THROTTLE = 16;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A ScrollView that coordinates with a parent DragableDrawer.
|
|
23
|
+
*
|
|
24
|
+
* - Drawer below max height: all gestures move the drawer; scroll is locked.
|
|
25
|
+
* - Drawer at max height: native scrolling works normally.
|
|
26
|
+
* - Drawer at max + scrolled to top + pull down: drawer collapses.
|
|
27
|
+
*
|
|
28
|
+
* The native ScrollView with bounces={false}/overScrollMode="never" will not
|
|
29
|
+
* intercept a downward gesture at scrollY=0 (nothing left to scroll), so
|
|
30
|
+
* onMoveShouldSetPanResponderCapture fires cleanly on both platforms.
|
|
31
|
+
* onScrollEndDrag + onMomentumScrollEnd ensure scrollYRef is up-to-date
|
|
32
|
+
* before the next gesture starts.
|
|
33
|
+
*/
|
|
34
|
+
const DragableScrollView = ({
|
|
35
|
+
onScroll,
|
|
36
|
+
onScrollEndDrag: onScrollEndDragProp,
|
|
37
|
+
onMomentumScrollEnd: onMomentumScrollEndProp,
|
|
38
|
+
scrollEventThrottle = DEFAULT_SCROLL_EVENT_THROTTLE,
|
|
39
|
+
children,
|
|
40
|
+
...props
|
|
41
|
+
}: DragableScrollViewProps): ReactElement => {
|
|
42
|
+
const {
|
|
43
|
+
isAtMaxHeight,
|
|
44
|
+
scrollYRef,
|
|
45
|
+
onScrollY,
|
|
46
|
+
beginPan,
|
|
47
|
+
movePan,
|
|
48
|
+
releasePan,
|
|
49
|
+
} = useDragableDrawerContext();
|
|
50
|
+
|
|
51
|
+
// Mirror context value into a ref so PanResponder closures always read
|
|
52
|
+
// the latest value (PanResponder is created once and closures are frozen).
|
|
53
|
+
const isAtMaxHeightRef = useRef(isAtMaxHeight);
|
|
54
|
+
isAtMaxHeightRef.current = isAtMaxHeight;
|
|
55
|
+
|
|
56
|
+
const panResponder = useRef(
|
|
57
|
+
PanResponder.create({
|
|
58
|
+
// Drawer below max: capture on start so the native ScrollView never
|
|
59
|
+
// gets the touch and scroll is completely locked.
|
|
60
|
+
onStartShouldSetPanResponderCapture: () => !isAtMaxHeightRef.current,
|
|
61
|
+
|
|
62
|
+
// Drawer at max + scroll at top + pull down: recapture for collapse.
|
|
63
|
+
// bounces={false} / overScrollMode="never" means the native ScrollView
|
|
64
|
+
// has nothing to do here, so it will not intercept the gesture and this
|
|
65
|
+
// capture fires reliably before any scroll activity begins.
|
|
66
|
+
onMoveShouldSetPanResponderCapture: (_, gesture) =>
|
|
67
|
+
isAtMaxHeightRef.current &&
|
|
68
|
+
scrollYRef.current <= SCROLL_TOP_THRESHOLD_PX &&
|
|
69
|
+
gesture.dy > PULL_DOWN_THRESHOLD_PX,
|
|
70
|
+
|
|
71
|
+
onPanResponderGrant: () => beginPan(),
|
|
72
|
+
onPanResponderMove: (_, gesture) => movePan(gesture.dy),
|
|
73
|
+
onPanResponderRelease: (_, gesture) => releasePan(gesture.dy, gesture.vy),
|
|
74
|
+
})
|
|
75
|
+
).current;
|
|
76
|
+
|
|
77
|
+
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
78
|
+
onScrollY(e.nativeEvent.contentOffset.y);
|
|
79
|
+
onScroll?.(e);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Keep scrollYRef accurate at every scroll boundary so the next gesture
|
|
83
|
+
// checks the correct position even when onScroll is throttled.
|
|
84
|
+
const handleScrollEndDrag = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
85
|
+
onScrollY(e.nativeEvent.contentOffset.y);
|
|
86
|
+
onScrollEndDragProp?.(e);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleMomentumScrollEnd = (
|
|
90
|
+
e: NativeSyntheticEvent<NativeScrollEvent>
|
|
91
|
+
) => {
|
|
92
|
+
onScrollY(e.nativeEvent.contentOffset.y);
|
|
93
|
+
onMomentumScrollEndProp?.(e);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<View {...panResponder.panHandlers} style={{ flex: 1 }}>
|
|
98
|
+
<ScrollView
|
|
99
|
+
{...props}
|
|
100
|
+
scrollEnabled={isAtMaxHeight}
|
|
101
|
+
onScroll={handleScroll}
|
|
102
|
+
onScrollEndDrag={handleScrollEndDrag}
|
|
103
|
+
onMomentumScrollEnd={handleMomentumScrollEnd}
|
|
104
|
+
scrollEventThrottle={scrollEventThrottle}
|
|
105
|
+
overScrollMode="always"
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</ScrollView>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default DragableScrollView;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { Animated, PanResponder, Easing, Platform } from 'react-native';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
3
2
|
|
|
4
3
|
import type { ReactElement, ReactNode } from 'react';
|
|
5
4
|
import {
|
|
@@ -8,12 +7,8 @@ import {
|
|
|
8
7
|
StyledHandler,
|
|
9
8
|
StyledHandlerContainer,
|
|
10
9
|
} from '../StyledDrawer';
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
calculateSnapPointsData,
|
|
14
|
-
getOffset,
|
|
15
|
-
} from './helpers';
|
|
16
|
-
import type { SnapPointsData } from './helpers';
|
|
10
|
+
import DragableDrawerContext from './DragableDrawerContext';
|
|
11
|
+
import useDragablePan from './useDragablePan';
|
|
17
12
|
|
|
18
13
|
export interface DragableDrawerProps {
|
|
19
14
|
/**
|
|
@@ -56,148 +51,60 @@ const DragableDrawer = ({
|
|
|
56
51
|
testID,
|
|
57
52
|
}: DragableDrawerProps): ReactElement => {
|
|
58
53
|
const [height, setHeight] = useState(0);
|
|
59
|
-
const baseHeightForMeasure = useRef(0);
|
|
60
|
-
const snapPointsData = useRef<SnapPointsData>({
|
|
61
|
-
list: [],
|
|
62
|
-
minHeightOffset: 0,
|
|
63
|
-
maxHeightOffset: 0,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Track drag
|
|
67
|
-
const pan = useRef(new Animated.Value(0)).current;
|
|
68
|
-
const offset = useRef(0);
|
|
69
|
-
const offsetBeforePan = useRef(0);
|
|
70
|
-
const [animatedToValue, setAnimatedToValue] = useState<number>(-1);
|
|
71
|
-
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const id = pan.addListener(({ value }) => {
|
|
74
|
-
offset.current = value;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return () => pan.removeListener(id);
|
|
78
|
-
}, []);
|
|
79
|
-
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (height > 0) {
|
|
82
|
-
const initialOffset = getOffset(
|
|
83
|
-
height,
|
|
84
|
-
initialHeightPercentage || minimumHeightPercentage
|
|
85
|
-
);
|
|
86
|
-
setAnimatedToValue(initialOffset);
|
|
87
|
-
}
|
|
88
|
-
}, [height]);
|
|
89
|
-
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
if (height > 0) {
|
|
92
|
-
pan.setValue(height);
|
|
93
|
-
offset.current = height;
|
|
94
|
-
|
|
95
|
-
baseHeightForMeasure.current = height;
|
|
96
|
-
|
|
97
|
-
// Calculate snap points information
|
|
98
|
-
snapPointsData.current = calculateSnapPointsData(
|
|
99
|
-
minimumHeightPercentage,
|
|
100
|
-
height,
|
|
101
|
-
snapPoints
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}, [height, minimumHeightPercentage]);
|
|
105
54
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return () => animation.stop();
|
|
126
|
-
}
|
|
127
|
-
}, [animatedToValue]);
|
|
128
|
-
|
|
129
|
-
const panResponder = useRef(
|
|
130
|
-
PanResponder.create({
|
|
131
|
-
onMoveShouldSetPanResponder: () => true,
|
|
132
|
-
onPanResponderGrant: () => {
|
|
133
|
-
offsetBeforePan.current = offset.current;
|
|
134
|
-
pan.setOffset(offset.current);
|
|
135
|
-
pan.setValue(0);
|
|
136
|
-
},
|
|
137
|
-
onPanResponderMove: (_, gesture) => {
|
|
138
|
-
const panDistance = gesture.dy;
|
|
139
|
-
|
|
140
|
-
// Moving toward top, stop at highest snap point
|
|
141
|
-
if (offsetBeforePan.current + panDistance < 0) {
|
|
142
|
-
pan.setValue(-offsetBeforePan.current);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Moving toward bottom, stop at lowest snap point
|
|
147
|
-
if (
|
|
148
|
-
offsetBeforePan.current + panDistance >
|
|
149
|
-
snapPointsData.current?.minHeightOffset
|
|
150
|
-
) {
|
|
151
|
-
pan.setValue(
|
|
152
|
-
baseHeightForMeasure.current -
|
|
153
|
-
baseHeightForMeasure.current * (minimumHeightPercentage / 100) -
|
|
154
|
-
offsetBeforePan.current
|
|
155
|
-
);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
pan.setValue(panDistance);
|
|
160
|
-
},
|
|
161
|
-
onPanResponderRelease: (_, gesture) => {
|
|
162
|
-
pan.flattenOffset();
|
|
163
|
-
|
|
164
|
-
// Attach to nearest snappoint
|
|
165
|
-
const panDistance = gesture.dy;
|
|
166
|
-
const offsetAfterPan = offsetBeforePan.current + panDistance;
|
|
167
|
-
const animatedValue = calculateAnimatedToValue(
|
|
168
|
-
offsetAfterPan,
|
|
169
|
-
snapPointsData.current.list
|
|
170
|
-
);
|
|
55
|
+
const {
|
|
56
|
+
pan,
|
|
57
|
+
isAtMaxHeight,
|
|
58
|
+
scrollYRef,
|
|
59
|
+
onScrollY,
|
|
60
|
+
beginPan,
|
|
61
|
+
movePan,
|
|
62
|
+
releasePan,
|
|
63
|
+
panHandlers,
|
|
64
|
+
} = useDragablePan({
|
|
65
|
+
height,
|
|
66
|
+
initialHeightPercentage,
|
|
67
|
+
minimumHeightPercentage,
|
|
68
|
+
snapPoints,
|
|
69
|
+
onExpanded,
|
|
70
|
+
onCollapsed,
|
|
71
|
+
});
|
|
171
72
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
73
|
+
const contextValue = useMemo(
|
|
74
|
+
() => ({
|
|
75
|
+
isAtMaxHeight,
|
|
76
|
+
scrollYRef,
|
|
77
|
+
onScrollY,
|
|
78
|
+
beginPan,
|
|
79
|
+
movePan,
|
|
80
|
+
releasePan,
|
|
81
|
+
}),
|
|
82
|
+
[isAtMaxHeight, onScrollY, beginPan, movePan, releasePan]
|
|
83
|
+
);
|
|
176
84
|
|
|
177
85
|
return (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
pointerEvents="box-none"
|
|
182
|
-
>
|
|
183
|
-
<StyledDragableDrawerContainer
|
|
86
|
+
<DragableDrawerContext.Provider value={contextValue}>
|
|
87
|
+
<StyledDragableContainer
|
|
88
|
+
testID={testID}
|
|
184
89
|
enableShadow
|
|
185
|
-
|
|
186
|
-
transform: [
|
|
187
|
-
{ scaleY: baseHeightForMeasure.current > 0 ? 1 : 0 },
|
|
188
|
-
{ translateY: pan },
|
|
189
|
-
],
|
|
190
|
-
}}
|
|
191
|
-
onLayout={({ nativeEvent }) => {
|
|
192
|
-
setHeight(nativeEvent.layout.height);
|
|
193
|
-
}}
|
|
90
|
+
pointerEvents="box-none"
|
|
194
91
|
>
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
92
|
+
<StyledDragableDrawerContainer
|
|
93
|
+
enableShadow
|
|
94
|
+
style={{
|
|
95
|
+
transform: [{ scaleY: height > 0 ? 1 : 0 }, { translateY: pan }],
|
|
96
|
+
}}
|
|
97
|
+
onLayout={({ nativeEvent }) => {
|
|
98
|
+
setHeight(nativeEvent.layout.height);
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<StyledHandlerContainer {...panHandlers}>
|
|
102
|
+
<StyledHandler />
|
|
103
|
+
</StyledHandlerContainer>
|
|
104
|
+
{children}
|
|
105
|
+
</StyledDragableDrawerContainer>
|
|
106
|
+
</StyledDragableContainer>
|
|
107
|
+
</DragableDrawerContext.Provider>
|
|
201
108
|
);
|
|
202
109
|
};
|
|
203
110
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
3
|
+
import { Animated, Easing, PanResponder, Platform } from 'react-native';
|
|
4
|
+
import type { GestureResponderHandlers } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
calculateAnimatedToValue,
|
|
8
|
+
calculateSnapPointsData,
|
|
9
|
+
getOffset,
|
|
10
|
+
} from './helpers';
|
|
11
|
+
import type { SnapPointsData } from './helpers';
|
|
12
|
+
|
|
13
|
+
const FLICK_VELOCITY_THRESHOLD = 0.3;
|
|
14
|
+
const SHORT_DRAG_THRESHOLD_PX = 20;
|
|
15
|
+
const VELOCITY_PROJECTION_FACTOR = 200;
|
|
16
|
+
|
|
17
|
+
interface UseDragablePanOptions {
|
|
18
|
+
height: number;
|
|
19
|
+
initialHeightPercentage: number | undefined;
|
|
20
|
+
minimumHeightPercentage: number;
|
|
21
|
+
snapPoints: number[];
|
|
22
|
+
onExpanded?: () => void;
|
|
23
|
+
onCollapsed?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface UseDragablePanResult {
|
|
27
|
+
pan: Animated.Value;
|
|
28
|
+
isAtMaxHeight: boolean;
|
|
29
|
+
scrollYRef: MutableRefObject<number>;
|
|
30
|
+
onScrollY: (y: number) => void;
|
|
31
|
+
beginPan: () => void;
|
|
32
|
+
movePan: (dy: number) => void;
|
|
33
|
+
releasePan: (dy: number, vy: number) => void;
|
|
34
|
+
panHandlers: GestureResponderHandlers;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const useDragablePan = ({
|
|
38
|
+
height,
|
|
39
|
+
initialHeightPercentage,
|
|
40
|
+
minimumHeightPercentage,
|
|
41
|
+
snapPoints,
|
|
42
|
+
onExpanded,
|
|
43
|
+
onCollapsed,
|
|
44
|
+
}: UseDragablePanOptions): UseDragablePanResult => {
|
|
45
|
+
const baseHeightForMeasure = useRef(0);
|
|
46
|
+
const snapPointsData = useRef<SnapPointsData>({
|
|
47
|
+
list: [],
|
|
48
|
+
minHeightOffset: 0,
|
|
49
|
+
maxHeightOffset: 0,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const pan = useRef(new Animated.Value(0)).current;
|
|
53
|
+
const offset = useRef(0);
|
|
54
|
+
const offsetBeforePan = useRef(0);
|
|
55
|
+
const [animatedToValue, setAnimatedToValue] = useState<number>(-1);
|
|
56
|
+
|
|
57
|
+
const [isAtMaxHeight, setIsAtMaxHeight] = useState(false);
|
|
58
|
+
const scrollYRef = useRef(0);
|
|
59
|
+
const minimumHeightPercentageRef = useRef(minimumHeightPercentage);
|
|
60
|
+
minimumHeightPercentageRef.current = minimumHeightPercentage;
|
|
61
|
+
|
|
62
|
+
const onScrollY = useCallback((y: number) => {
|
|
63
|
+
scrollYRef.current = y;
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const id = pan.addListener(({ value }) => {
|
|
68
|
+
offset.current = value;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return () => pan.removeListener(id);
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (height > 0) {
|
|
76
|
+
const initialOffset = getOffset(
|
|
77
|
+
height,
|
|
78
|
+
initialHeightPercentage || minimumHeightPercentage
|
|
79
|
+
);
|
|
80
|
+
setAnimatedToValue(initialOffset);
|
|
81
|
+
}
|
|
82
|
+
}, [height, initialHeightPercentage, minimumHeightPercentage]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (height > 0) {
|
|
86
|
+
pan.setValue(height);
|
|
87
|
+
offset.current = height;
|
|
88
|
+
|
|
89
|
+
baseHeightForMeasure.current = height;
|
|
90
|
+
|
|
91
|
+
snapPointsData.current = calculateSnapPointsData(
|
|
92
|
+
minimumHeightPercentage,
|
|
93
|
+
height,
|
|
94
|
+
snapPoints
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}, [height, minimumHeightPercentage]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (animatedToValue >= 0) {
|
|
101
|
+
const animation = Animated.timing(pan, {
|
|
102
|
+
toValue: animatedToValue,
|
|
103
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
104
|
+
easing: Easing.inOut(Easing.cubic),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
animation.start(({ finished }) => {
|
|
108
|
+
if (finished) {
|
|
109
|
+
if (animatedToValue === 0) {
|
|
110
|
+
setIsAtMaxHeight(true);
|
|
111
|
+
onExpanded?.();
|
|
112
|
+
} else {
|
|
113
|
+
setIsAtMaxHeight(false);
|
|
114
|
+
if (
|
|
115
|
+
animatedToValue === getOffset(height, minimumHeightPercentage)
|
|
116
|
+
) {
|
|
117
|
+
onCollapsed?.();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
setAnimatedToValue(-1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return () => animation.stop();
|
|
125
|
+
}
|
|
126
|
+
}, [
|
|
127
|
+
animatedToValue,
|
|
128
|
+
onExpanded,
|
|
129
|
+
onCollapsed,
|
|
130
|
+
height,
|
|
131
|
+
minimumHeightPercentage,
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const beginPan = useCallback(() => {
|
|
135
|
+
pan.stopAnimation();
|
|
136
|
+
setIsAtMaxHeight(false);
|
|
137
|
+
offsetBeforePan.current = offset.current;
|
|
138
|
+
pan.setOffset(offset.current);
|
|
139
|
+
pan.setValue(0);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const movePan = useCallback((dy: number) => {
|
|
143
|
+
// Moving toward top, stop at highest snap point
|
|
144
|
+
if (offsetBeforePan.current + dy < 0) {
|
|
145
|
+
pan.setValue(-offsetBeforePan.current);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Moving toward bottom, stop at lowest snap point
|
|
150
|
+
if (
|
|
151
|
+
offsetBeforePan.current + dy >
|
|
152
|
+
snapPointsData.current?.minHeightOffset
|
|
153
|
+
) {
|
|
154
|
+
pan.setValue(
|
|
155
|
+
baseHeightForMeasure.current -
|
|
156
|
+
baseHeightForMeasure.current *
|
|
157
|
+
(minimumHeightPercentageRef.current / 100) -
|
|
158
|
+
offsetBeforePan.current
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pan.setValue(dy);
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const releasePan = useCallback((dy: number, vy: number) => {
|
|
167
|
+
pan.flattenOffset();
|
|
168
|
+
|
|
169
|
+
const offsetAfterPan = offsetBeforePan.current + dy;
|
|
170
|
+
|
|
171
|
+
// Flick or short downward drag: snap to the next lower snap point.
|
|
172
|
+
if (vy > FLICK_VELOCITY_THRESHOLD || dy > SHORT_DRAG_THRESHOLD_PX) {
|
|
173
|
+
const lowerPoints = snapPointsData.current.list
|
|
174
|
+
.filter((p) => p > offsetBeforePan.current)
|
|
175
|
+
.sort((a, b) => a - b);
|
|
176
|
+
if (lowerPoints.length > 0) {
|
|
177
|
+
setAnimatedToValue(lowerPoints[0]);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Upward flick or drag: project the intended landing position using
|
|
183
|
+
// velocity, then snap to the nearest higher snap point to that projection.
|
|
184
|
+
// This allows fast flicks to skip intermediate snap points.
|
|
185
|
+
if (vy < -FLICK_VELOCITY_THRESHOLD || dy < -SHORT_DRAG_THRESHOLD_PX) {
|
|
186
|
+
const projected = offsetAfterPan + vy * VELOCITY_PROJECTION_FACTOR;
|
|
187
|
+
const higherPoints = snapPointsData.current.list
|
|
188
|
+
.filter((p) => p < offsetBeforePan.current)
|
|
189
|
+
.sort((a, b) => b - a);
|
|
190
|
+
if (higherPoints.length > 0) {
|
|
191
|
+
setAnimatedToValue(calculateAnimatedToValue(projected, higherPoints));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Otherwise: snap to nearest.
|
|
197
|
+
setAnimatedToValue(
|
|
198
|
+
calculateAnimatedToValue(offsetAfterPan, snapPointsData.current.list)
|
|
199
|
+
);
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
const panResponder = useRef(
|
|
203
|
+
PanResponder.create({
|
|
204
|
+
onMoveShouldSetPanResponder: () => true,
|
|
205
|
+
onPanResponderGrant: () => beginPan(),
|
|
206
|
+
onPanResponderMove: (_, gesture) => movePan(gesture.dy),
|
|
207
|
+
onPanResponderRelease: (_, gesture) => releasePan(gesture.dy, gesture.vy),
|
|
208
|
+
})
|
|
209
|
+
).current;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
pan,
|
|
213
|
+
isAtMaxHeight,
|
|
214
|
+
scrollYRef,
|
|
215
|
+
onScrollY,
|
|
216
|
+
beginPan,
|
|
217
|
+
movePan,
|
|
218
|
+
releasePan,
|
|
219
|
+
panHandlers: panResponder.panHandlers,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export default useDragablePan;
|
|
@@ -57,7 +57,7 @@ const StyledDragableDrawerContainer = styled(Animated.View)<{
|
|
|
57
57
|
backgroundColor: theme.__hd__.drawer.colors.background,
|
|
58
58
|
elevation: enableShadow ? theme.__hd__.drawer.shadows.elevation : undefined,
|
|
59
59
|
overflow: 'hidden',
|
|
60
|
-
|
|
60
|
+
height: '100%',
|
|
61
61
|
}));
|
|
62
62
|
|
|
63
63
|
const StyledHandlerContainer = styled(View)<ViewProps>(({ theme }) => ({
|
|
@@ -2,6 +2,7 @@ import type { ReactElement, ReactNode } from 'react';
|
|
|
2
2
|
import React, { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Animated, Dimensions, Easing } from 'react-native';
|
|
4
4
|
import DragableDrawer from './DragableDrawer';
|
|
5
|
+
import DragableScrollView from './DragableDrawer/DragableScrollView';
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
StyledBackdrop,
|
|
@@ -97,4 +98,5 @@ const Drawer = ({
|
|
|
97
98
|
|
|
98
99
|
export default Object.assign(Drawer, {
|
|
99
100
|
Dragable: DragableDrawer,
|
|
101
|
+
DragableScrollView,
|
|
100
102
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ViewProps, KeyboardAvoidingViewProps } from 'react-native';
|
|
2
|
-
import { Animated, KeyboardAvoidingView,
|
|
2
|
+
import { Animated, KeyboardAvoidingView, View } from 'react-native';
|
|
3
3
|
declare const StyledWrapper: import("@emotion/native").StyledComponent<ViewProps & {
|
|
4
4
|
theme?: import("@emotion/react").Theme;
|
|
5
5
|
as?: React.ElementType;
|
|
@@ -12,7 +12,7 @@ declare const StyledKeyboardAvoidingView: import("@emotion/native").StyledCompon
|
|
|
12
12
|
}, {}, {
|
|
13
13
|
ref?: import("react").Ref<KeyboardAvoidingView> | undefined;
|
|
14
14
|
}>;
|
|
15
|
-
declare const StyledFloatingWrapper: import("@emotion/native").StyledComponent<Animated.AnimatedProps<
|
|
15
|
+
declare const StyledFloatingWrapper: import("@emotion/native").StyledComponent<Animated.AnimatedProps<import("react-native-safe-area-context").NativeSafeAreaViewProps & import("react").RefAttributes<import("react").Component<import("react-native-safe-area-context/lib/typescript/src/specs/NativeSafeAreaView").NativeProps, {}, any> & import("react-native").NativeMethods>> & {
|
|
16
16
|
theme?: import("@emotion/react").Theme;
|
|
17
17
|
as?: React.ElementType;
|
|
18
18
|
}, {}, {}>;
|
|
@@ -20,7 +20,7 @@ declare const StyledFloatingBottomSheet: import("@emotion/native").StyledCompone
|
|
|
20
20
|
theme?: import("@emotion/react").Theme;
|
|
21
21
|
as?: React.ElementType;
|
|
22
22
|
}, {}, {}>;
|
|
23
|
-
declare const StyledBottomSheet: import("@emotion/native").StyledComponent<Animated.AnimatedProps<
|
|
23
|
+
declare const StyledBottomSheet: import("@emotion/native").StyledComponent<Animated.AnimatedProps<import("react-native-safe-area-context").NativeSafeAreaViewProps & import("react").RefAttributes<import("react").Component<import("react-native-safe-area-context/lib/typescript/src/specs/NativeSafeAreaView").NativeProps, {}, any> & import("react-native").NativeMethods>> & {
|
|
24
24
|
theme?: import("@emotion/react").Theme;
|
|
25
25
|
as?: React.ElementType;
|
|
26
26
|
}, {}, {}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
interface DragableDrawerContextValue {
|
|
3
|
+
/** Whether the drawer has settled at maximum height (offset === 0). */
|
|
4
|
+
isAtMaxHeight: boolean;
|
|
5
|
+
/** Current vertical scroll offset reported by DragableScrollView. */
|
|
6
|
+
scrollYRef: MutableRefObject<number>;
|
|
7
|
+
/** Report scroll offset changes from DragableScrollView. */
|
|
8
|
+
onScrollY: (y: number) => void;
|
|
9
|
+
/** Begin a drawer pan gesture (stops any running animation). */
|
|
10
|
+
beginPan: () => void;
|
|
11
|
+
/** Move the drawer by dy pixels during an active pan. */
|
|
12
|
+
movePan: (dy: number) => void;
|
|
13
|
+
/** End pan and snap to the nearest snap point. */
|
|
14
|
+
releasePan: (dy: number, vy: number) => void;
|
|
15
|
+
}
|
|
16
|
+
declare const DragableDrawerContext: import("react").Context<DragableDrawerContextValue>;
|
|
17
|
+
export declare const useDragableDrawerContext: () => DragableDrawerContextValue;
|
|
18
|
+
export default DragableDrawerContext;
|