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