@swmansion/react-native-bottom-sheet 0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/lib/module/BottomSheet.js +8 -0
- package/lib/module/BottomSheet.js.map +1 -0
- package/lib/module/BottomSheetBase.js +309 -0
- package/lib/module/BottomSheetBase.js.map +1 -0
- package/lib/module/BottomSheetContext.js +13 -0
- package/lib/module/BottomSheetContext.js.map +1 -0
- package/lib/module/BottomSheetFlatList.js +29 -0
- package/lib/module/BottomSheetFlatList.js.map +1 -0
- package/lib/module/BottomSheetProvider.js +73 -0
- package/lib/module/BottomSheetProvider.js.map +1 -0
- package/lib/module/BottomSheetScrollView.js +29 -0
- package/lib/module/BottomSheetScrollView.js.map +1 -0
- package/lib/module/ModalBottomSheet.js +13 -0
- package/lib/module/ModalBottomSheet.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/useBottomSheetScrollable.js +56 -0
- package/lib/module/useBottomSheetScrollable.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BottomSheet.d.ts +4 -0
- package/lib/typescript/src/BottomSheet.d.ts.map +1 -0
- package/lib/typescript/src/BottomSheetBase.d.ts +18 -0
- package/lib/typescript/src/BottomSheetBase.d.ts.map +1 -0
- package/lib/typescript/src/BottomSheetContext.d.ts +17 -0
- package/lib/typescript/src/BottomSheetContext.d.ts.map +1 -0
- package/lib/typescript/src/BottomSheetFlatList.d.ts +3 -0
- package/lib/typescript/src/BottomSheetFlatList.d.ts.map +1 -0
- package/lib/typescript/src/BottomSheetProvider.d.ts +8 -0
- package/lib/typescript/src/BottomSheetProvider.d.ts.map +1 -0
- package/lib/typescript/src/BottomSheetScrollView.d.ts +3 -0
- package/lib/typescript/src/BottomSheetScrollView.d.ts.map +1 -0
- package/lib/typescript/src/ModalBottomSheet.d.ts +8 -0
- package/lib/typescript/src/ModalBottomSheet.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/useBottomSheetScrollable.d.ts +10 -0
- package/lib/typescript/src/useBottomSheetScrollable.d.ts.map +1 -0
- package/package.json +117 -0
- package/src/BottomSheet.tsx +8 -0
- package/src/BottomSheetBase.tsx +378 -0
- package/src/BottomSheetContext.tsx +30 -0
- package/src/BottomSheetFlatList.tsx +24 -0
- package/src/BottomSheetProvider.tsx +83 -0
- package/src/BottomSheetScrollView.tsx +24 -0
- package/src/ModalBottomSheet.tsx +16 -0
- package/src/index.tsx +8 -0
- package/src/useBottomSheetScrollable.ts +62 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
2
|
+
import type { FlatListPropsWithLayout } from 'react-native-reanimated';
|
|
3
|
+
import Animated from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
import { useBottomSheetScrollable } from './useBottomSheetScrollable';
|
|
6
|
+
|
|
7
|
+
export const BottomSheetFlatList = <T,>(
|
|
8
|
+
props: Omit<FlatListPropsWithLayout<T>, 'onScroll'>
|
|
9
|
+
) => {
|
|
10
|
+
const { scrollEnabled, ...rest } = props;
|
|
11
|
+
const { scrollHandler, scrollableRef, nativeGesture, animatedProps } =
|
|
12
|
+
useBottomSheetScrollable(scrollEnabled);
|
|
13
|
+
return (
|
|
14
|
+
<GestureDetector gesture={nativeGesture}>
|
|
15
|
+
<Animated.FlatList
|
|
16
|
+
{...rest}
|
|
17
|
+
animatedProps={animatedProps}
|
|
18
|
+
ref={scrollableRef}
|
|
19
|
+
onScroll={scrollHandler}
|
|
20
|
+
scrollEventThrottle={16}
|
|
21
|
+
/>
|
|
22
|
+
</GestureDetector>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useReducer,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
import { StyleSheet, View } from 'react-native';
|
|
11
|
+
|
|
12
|
+
interface PortalContextType {
|
|
13
|
+
addPortal: (key: string, element: ReactNode) => void;
|
|
14
|
+
removePortal: (key: string) => void;
|
|
15
|
+
subscribe: (callback: () => void) => () => void;
|
|
16
|
+
getPortals: () => Map<string, ReactNode>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PortalContext = createContext<PortalContextType | null>(null);
|
|
20
|
+
|
|
21
|
+
const PortalHost = () => {
|
|
22
|
+
const context = useContext(PortalContext)!;
|
|
23
|
+
const [, forceRender] = useReducer((x: number) => x + 1, 0);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
return context.subscribe(forceRender);
|
|
26
|
+
}, [context]);
|
|
27
|
+
return Array.from(context.getPortals().entries()).map(([key, element]) => (
|
|
28
|
+
<View key={key} style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
29
|
+
{element}
|
|
30
|
+
</View>
|
|
31
|
+
));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
|
|
35
|
+
const [context] = useState<PortalContextType>(() => {
|
|
36
|
+
const portals = new Map<string, ReactNode>();
|
|
37
|
+
const subscribers = new Set<() => void>();
|
|
38
|
+
const notify = () => {
|
|
39
|
+
subscribers.forEach((subscriber) => subscriber());
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
addPortal: (key, element) => {
|
|
43
|
+
portals.set(key, element);
|
|
44
|
+
notify();
|
|
45
|
+
},
|
|
46
|
+
removePortal: (key) => {
|
|
47
|
+
portals.delete(key);
|
|
48
|
+
notify();
|
|
49
|
+
},
|
|
50
|
+
subscribe: (callback) => {
|
|
51
|
+
subscribers.add(callback);
|
|
52
|
+
return () => {
|
|
53
|
+
subscribers.delete(callback);
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
getPortals: () => portals,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
return (
|
|
60
|
+
<PortalContext.Provider value={context}>
|
|
61
|
+
{children}
|
|
62
|
+
<PortalHost />
|
|
63
|
+
</PortalContext.Provider>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Portal = ({ children }: { children: ReactNode }) => {
|
|
68
|
+
const context = useContext(PortalContext);
|
|
69
|
+
if (context === null) {
|
|
70
|
+
throw new Error('`Portal` must be used within `BottomSheetProvider`.');
|
|
71
|
+
}
|
|
72
|
+
const { addPortal, removePortal } = context;
|
|
73
|
+
const id = useId();
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
addPortal(id, children);
|
|
76
|
+
}, [id, children, addPortal]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
return () => {
|
|
79
|
+
removePortal(id);
|
|
80
|
+
};
|
|
81
|
+
}, [id, removePortal]);
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ScrollViewProps } from 'react-native';
|
|
2
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
3
|
+
import Animated from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
import { useBottomSheetScrollable } from './useBottomSheetScrollable';
|
|
6
|
+
|
|
7
|
+
export const BottomSheetScrollView = (
|
|
8
|
+
props: Omit<ScrollViewProps, 'onScroll'>
|
|
9
|
+
) => {
|
|
10
|
+
const { scrollEnabled, ...rest } = props;
|
|
11
|
+
const { scrollHandler, scrollableRef, nativeGesture, animatedProps } =
|
|
12
|
+
useBottomSheetScrollable(scrollEnabled);
|
|
13
|
+
return (
|
|
14
|
+
<GestureDetector gesture={nativeGesture}>
|
|
15
|
+
<Animated.ScrollView
|
|
16
|
+
{...rest}
|
|
17
|
+
animatedProps={animatedProps}
|
|
18
|
+
ref={scrollableRef}
|
|
19
|
+
onScroll={scrollHandler}
|
|
20
|
+
scrollEventThrottle={16}
|
|
21
|
+
/>
|
|
22
|
+
</GestureDetector>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
import type { BottomSheetCommonProps } from './BottomSheetBase';
|
|
5
|
+
import { BottomSheetBase } from './BottomSheetBase';
|
|
6
|
+
|
|
7
|
+
export interface ModalBottomSheetProps extends BottomSheetCommonProps {
|
|
8
|
+
scrim?: (progress: SharedValue<number>) => ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ModalBottomSheet = ({
|
|
12
|
+
scrim,
|
|
13
|
+
...props
|
|
14
|
+
}: ModalBottomSheetProps) => (
|
|
15
|
+
<BottomSheetBase {...props} modal renderScrim={scrim} />
|
|
16
|
+
);
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { BottomSheet } from './BottomSheet';
|
|
2
|
+
export type { BottomSheetProps } from './BottomSheet';
|
|
3
|
+
export { ModalBottomSheet } from './ModalBottomSheet';
|
|
4
|
+
export type { ModalBottomSheetProps } from './ModalBottomSheet';
|
|
5
|
+
export { BottomSheetProvider } from './BottomSheetProvider';
|
|
6
|
+
export { BottomSheetFlatList } from './BottomSheetFlatList';
|
|
7
|
+
export { BottomSheetScrollView } from './BottomSheetScrollView';
|
|
8
|
+
export type { Detent } from './BottomSheetBase';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
+
import {
|
|
4
|
+
type SharedValue,
|
|
5
|
+
useAnimatedProps,
|
|
6
|
+
useAnimatedScrollHandler,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
|
|
9
|
+
import { useBottomSheetContext } from './BottomSheetContext';
|
|
10
|
+
|
|
11
|
+
export const useBottomSheetScrollable = (
|
|
12
|
+
baseScrollEnabled: boolean | SharedValue<boolean | undefined> = true
|
|
13
|
+
) => {
|
|
14
|
+
const {
|
|
15
|
+
scrollOffset,
|
|
16
|
+
scrollableRef,
|
|
17
|
+
hasScrollable,
|
|
18
|
+
isScrollableGestureActive,
|
|
19
|
+
isScrollableLocked,
|
|
20
|
+
panGesture,
|
|
21
|
+
} = useBottomSheetContext();
|
|
22
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
23
|
+
onScroll: (event) => {
|
|
24
|
+
'worklet';
|
|
25
|
+
scrollOffset.set(Math.max(0, event.contentOffset.y));
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const nativeGesture = Gesture.Native()
|
|
29
|
+
.simultaneousWithExternalGesture(panGesture)
|
|
30
|
+
.onStart(() => {
|
|
31
|
+
'worklet';
|
|
32
|
+
isScrollableGestureActive.set(true);
|
|
33
|
+
})
|
|
34
|
+
.onFinalize(() => {
|
|
35
|
+
'worklet';
|
|
36
|
+
isScrollableGestureActive.set(false);
|
|
37
|
+
});
|
|
38
|
+
const animatedProps = useAnimatedProps(() => {
|
|
39
|
+
const resolvedScrollEnabled =
|
|
40
|
+
typeof baseScrollEnabled === 'object' && baseScrollEnabled !== null
|
|
41
|
+
? baseScrollEnabled.value ?? true
|
|
42
|
+
: baseScrollEnabled ?? true;
|
|
43
|
+
return {
|
|
44
|
+
scrollEnabled: resolvedScrollEnabled && !isScrollableLocked.value,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (__DEV__ && hasScrollable.value) {
|
|
49
|
+
console.warn(
|
|
50
|
+
'Multiple scrollables within a single bottom sheet. Only one `BottomSheetScrollView` ' +
|
|
51
|
+
'or `BottomSheetFlatList` is supported per bottom sheet.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
hasScrollable.set(true);
|
|
55
|
+
return () => {
|
|
56
|
+
hasScrollable.set(false);
|
|
57
|
+
isScrollableGestureActive.set(false);
|
|
58
|
+
isScrollableLocked.set(false);
|
|
59
|
+
};
|
|
60
|
+
}, [hasScrollable, isScrollableGestureActive, isScrollableLocked]);
|
|
61
|
+
return { scrollHandler, scrollableRef, nativeGesture, animatedProps };
|
|
62
|
+
};
|