@impulse-ui-native/flyout 1.0.10 → 2.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.
@@ -1,333 +0,0 @@
1
- import { useEventCallback } from "@impulse-ui-native/core";
2
- import { SafeAreaView } from "@impulse-ui-native/safe-area-view";
3
- import { AppTheme, useColors, useTheme } from "@impulse-ui-native/theme";
4
- import { View } from "@impulse-ui-native/view";
5
- import {
6
- Fragment,
7
- memo,
8
- PropsWithChildren,
9
- useEffect,
10
- useMemo,
11
- useRef,
12
- useState,
13
- } from "react";
14
- import {
15
- Dimensions,
16
- LayoutChangeEvent,
17
- Platform,
18
- Pressable,
19
- StyleSheet,
20
- } from "react-native";
21
- import {
22
- Gesture,
23
- GestureDetector,
24
- GestureStateChangeEvent,
25
- GestureUpdateEvent,
26
- PanGestureHandlerEventPayload,
27
- } from "react-native-gesture-handler";
28
- import Animated, {
29
- Easing,
30
- useAnimatedScrollHandler,
31
- useAnimatedStyle,
32
- useSharedValue,
33
- withSpring,
34
- withTiming,
35
- } from "react-native-reanimated";
36
- import { Edge } from "react-native-safe-area-context";
37
- import { scheduleOnRN } from "react-native-worklets";
38
-
39
- import { FlyoutProps } from "../types";
40
-
41
- import { FlyoutHandle } from "./flyout-handle";
42
- import { FlyoutTitle } from "./flyout-title";
43
-
44
- const EnterAnimationConfig = {
45
- damping: 50,
46
- stiffness: 300,
47
- mass: 1,
48
- overshootClamping: true,
49
- };
50
-
51
- const ExitAnimationConfig = {
52
- duration: 150,
53
- easing: Easing.in(Easing.quad),
54
- };
55
-
56
- const StickyHeaderIndices = [0];
57
- const ScreenHeight = Dimensions.get("screen").height;
58
- const MaxHeight = ScreenHeight * 0.9;
59
-
60
- const AndroidOverScroll =
61
- Platform.OS === "android" ? { overScrollMode: "always" as const } : {};
62
-
63
- export const Flyout = memo(function Flyout({
64
- children,
65
- placement = "bottom",
66
- ...props
67
- }: PropsWithChildren<FlyoutProps>) {
68
- const colors = useColors();
69
- const theme = useTheme();
70
-
71
- const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);
72
- const [bounceEnabled, setBounceEnabled] = useState(placement !== "bottom");
73
- const [viewportHeight, setViewportHeight] = useState(0);
74
- const [contentHeight, setContentHeight] = useState(0);
75
-
76
- const didOpen = useRef(false);
77
- const canDismiss = useSharedValue(placement === "bottom");
78
- const scrollY = useSharedValue(0);
79
- const maxScrollY = useSharedValue(0);
80
-
81
- const translateY = useSharedValue(
82
- placement === "top" ? -ScreenHeight : ScreenHeight
83
- );
84
- const opacity = useSharedValue(0);
85
-
86
- const isScrollable = useMemo(
87
- () => contentHeight > viewportHeight,
88
- [contentHeight, viewportHeight]
89
- );
90
-
91
- const onScroll = useAnimatedScrollHandler({
92
- onScroll(event) {
93
- scrollY.value = event.contentOffset.y;
94
- maxScrollY.value = Math.max(
95
- 0,
96
- event.contentSize.height - event.layoutMeasurement.height
97
- );
98
- },
99
- onMomentumEnd(event) {
100
- if (placement === "bottom" && event.contentOffset.y <= 0) {
101
- scheduleOnRN(setBounceEnabled, false);
102
- canDismiss.value = true;
103
- }
104
-
105
- if (placement === "top" && scrollY.value >= maxScrollY.value - 1) {
106
- scheduleOnRN(setBounceEnabled, false);
107
- canDismiss.value = true;
108
- }
109
- },
110
- });
111
-
112
- const onLayout = useEventCallback((event: LayoutChangeEvent) => {
113
- if (!didOpen.current && measuredHeight === null) {
114
- const rawHeight = event.nativeEvent.layout.height;
115
- const clampedHeight = Math.min(rawHeight, MaxHeight);
116
- setMeasuredHeight(clampedHeight);
117
- translateY.value = placement === "top" ? -clampedHeight : clampedHeight;
118
- }
119
- });
120
-
121
- const onLayoutScrollview = useEventCallback((event: LayoutChangeEvent) => {
122
- setViewportHeight(event.nativeEvent.layout.height);
123
- });
124
-
125
- const onContentSizeChange = useEventCallback(
126
- (_contentWidth: number, contentHeight: number) => {
127
- setContentHeight(contentHeight);
128
- }
129
- );
130
-
131
- const animateOut = useEventCallback(() => {
132
- if (measuredHeight) {
133
- const target = placement === "top" ? -measuredHeight : measuredHeight;
134
-
135
- translateY.value = withTiming(target, ExitAnimationConfig, (finished) => {
136
- if (finished && props.onCloseFinished) {
137
- scheduleOnRN(props.onCloseFinished, props.id);
138
- }
139
- });
140
-
141
- opacity.value = withTiming(0, ExitAnimationConfig);
142
- didOpen.current = false;
143
- }
144
- });
145
-
146
- const onPanGestureEnd = useEventCallback(
147
- (event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
148
- if (!measuredHeight) return;
149
-
150
- let shouldClose = false;
151
-
152
- if (placement === "bottom") {
153
- shouldClose =
154
- event.translationY > measuredHeight * 0.5 || event.velocityY > 1500;
155
- }
156
-
157
- if (placement === "top") {
158
- shouldClose =
159
- event.translationY < -measuredHeight * 0.5 || event.velocityY < -1500;
160
- }
161
-
162
- if (shouldClose && (canDismiss.value || !isScrollable)) {
163
- animateOut();
164
- } else {
165
- translateY.value = withSpring(0, EnterAnimationConfig);
166
- opacity.value = withSpring(0.4, EnterAnimationConfig);
167
- }
168
- }
169
- );
170
-
171
- const onPanGestureHandleEnd = useEventCallback(
172
- (event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
173
- onPanGestureEnd(event);
174
-
175
- if (placement === "bottom" && scrollY.value > 0) {
176
- canDismiss.value = false;
177
- }
178
-
179
- if (placement === "top" && scrollY.value < maxScrollY.value - 1) {
180
- canDismiss.value = false;
181
- }
182
- }
183
- );
184
-
185
- const onPanGestureUpdate = (
186
- event: GestureUpdateEvent<PanGestureHandlerEventPayload>
187
- ) => {
188
- if (placement === "bottom" && event.translationY < 0) {
189
- scheduleOnRN(setBounceEnabled, true);
190
- canDismiss.value = false;
191
- }
192
-
193
- if (placement === "top" && event.translationY > 0) {
194
- scheduleOnRN(setBounceEnabled, true);
195
- canDismiss.value = false;
196
- }
197
-
198
- if (
199
- (placement === "bottom" && event.translationY < 0) ||
200
- (placement === "top" && event.translationY > 0)
201
- ) {
202
- return;
203
- }
204
-
205
- if (canDismiss.value || !isScrollable) {
206
- translateY.value = event.translationY;
207
- }
208
- };
209
-
210
- const onPanGestureHandleUpdate = (
211
- event: GestureUpdateEvent<PanGestureHandlerEventPayload>
212
- ) => {
213
- canDismiss.value = true;
214
-
215
- if (
216
- (placement === "bottom" && event.translationY < 0) ||
217
- (placement === "top" && event.translationY > 0)
218
- ) {
219
- return;
220
- }
221
-
222
- translateY.value = event.translationY;
223
- };
224
-
225
- const panGesture = Gesture.Pan()
226
- .onUpdate(onPanGestureUpdate)
227
- .onEnd((event) => scheduleOnRN(onPanGestureEnd, event));
228
-
229
- const gesture = Gesture.Simultaneous(Gesture.Native(), panGesture);
230
-
231
- const handleGesture = Gesture.Pan()
232
- .onUpdate(onPanGestureHandleUpdate)
233
- .onEnd((event) => scheduleOnRN(onPanGestureHandleEnd, event));
234
-
235
- const animatedStyle = useAnimatedStyle(() => ({
236
- transform: [{ translateY: translateY.value }],
237
- zIndex: 100 + props.layer,
238
- }));
239
-
240
- const overlayStyle = useAnimatedStyle(() => ({
241
- opacity: opacity.value,
242
- zIndex: 100 + props.layer,
243
- backgroundColor: colors.black,
244
- }));
245
-
246
- const edges = useMemo<Edge[]>(() => {
247
- return [placement, "left", "right"];
248
- }, [placement]);
249
-
250
- const styles = useMemo(
251
- () => themedStyles(theme, placement),
252
- [theme, placement]
253
- );
254
-
255
- useEffect(() => {
256
- if (measuredHeight === null || didOpen.current || !props.open) return;
257
-
258
- translateY.value = withSpring(0, EnterAnimationConfig);
259
- opacity.value = withSpring(0.4, EnterAnimationConfig);
260
- didOpen.current = true;
261
- }, [measuredHeight, props.open]);
262
-
263
- return (
264
- <Fragment>
265
- <Animated.View style={[StyleSheet.absoluteFill, overlayStyle]}>
266
- <Pressable onPress={animateOut} style={StyleSheet.absoluteFillObject} />
267
- </Animated.View>
268
- <GestureDetector gesture={gesture}>
269
- <Animated.View style={[styles.container, animatedStyle]}>
270
- <SafeAreaView
271
- edges={edges}
272
- style={styles.safeAreaView}
273
- onLayout={onLayout}
274
- >
275
- {placement === "bottom" ? (
276
- <GestureDetector gesture={handleGesture}>
277
- <FlyoutHandle placement={placement} />
278
- </GestureDetector>
279
- ) : null}
280
-
281
- <Animated.ScrollView
282
- onLayout={onLayoutScrollview}
283
- onContentSizeChange={onContentSizeChange}
284
- onScroll={onScroll}
285
- scrollEventThrottle={16}
286
- scrollEnabled={isScrollable}
287
- bounces={isScrollable && bounceEnabled}
288
- style={styles.scrollview}
289
- stickyHeaderIndices={
290
- props.title ? StickyHeaderIndices : undefined
291
- }
292
- showsHorizontalScrollIndicator={false}
293
- showsVerticalScrollIndicator={false}
294
- {...AndroidOverScroll}
295
- >
296
- {props.title && <FlyoutTitle title={props.title} />}
297
- <View padding={theme.spacing.sm}>{children}</View>
298
- </Animated.ScrollView>
299
-
300
- {placement === "top" ? (
301
- <GestureDetector gesture={handleGesture}>
302
- <FlyoutHandle placement={placement} />
303
- </GestureDetector>
304
- ) : null}
305
- </SafeAreaView>
306
- </Animated.View>
307
- </GestureDetector>
308
- </Fragment>
309
- );
310
- });
311
-
312
- const themedStyles = function (theme: AppTheme, placement: "top" | "bottom") {
313
- return StyleSheet.create({
314
- container: {
315
- position: "absolute",
316
- [placement]: 0,
317
- left: 0,
318
- right: 0,
319
- backgroundColor: theme.colors.background.secondary,
320
- borderTopLeftRadius: placement === "bottom" ? theme.radii.xl : 0,
321
- borderTopRightRadius: placement === "bottom" ? theme.radii.xl : 0,
322
- borderBottomLeftRadius: placement === "top" ? theme.radii.xl : 0,
323
- borderBottomRightRadius: placement === "top" ? theme.radii.xl : 0,
324
- },
325
- scrollview: {
326
- borderTopLeftRadius: placement === "bottom" ? theme.radii.xl : 0,
327
- borderTopRightRadius: placement === "bottom" ? theme.radii.xl : 0,
328
- borderBottomLeftRadius: placement === "top" ? theme.radii.xl : 0,
329
- borderBottomRightRadius: placement === "top" ? theme.radii.xl : 0,
330
- },
331
- safeAreaView: { maxHeight: MaxHeight },
332
- });
333
- };
@@ -1 +0,0 @@
1
- export { Flyout } from "./flyout";
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./components";
2
- export * from "./types";
@@ -1,6 +0,0 @@
1
- import { LayerComponentBaseProps } from "@impulse-ui-native/layer-manager-types";
2
-
3
- export interface FlyoutProps extends LayerComponentBaseProps {
4
- open?: boolean;
5
- placement?: "top" | "bottom";
6
- }
@@ -1 +0,0 @@
1
- export * from "./flyout.types";