@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 @@
|
|
|
1
|
+
{"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../../src/BottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEtD,eAAO,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAElD,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
|
|
3
|
+
export type Detent = number | 'max';
|
|
4
|
+
export interface BottomSheetCommonProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
detents?: Detent[];
|
|
7
|
+
index: number;
|
|
8
|
+
onIndexChange?: (index: number) => void;
|
|
9
|
+
position?: SharedValue<number>;
|
|
10
|
+
openAnimationConfig?: WithSpringConfig;
|
|
11
|
+
closeAnimationConfig?: WithSpringConfig;
|
|
12
|
+
}
|
|
13
|
+
export interface BottomSheetBaseProps extends BottomSheetCommonProps {
|
|
14
|
+
modal?: boolean;
|
|
15
|
+
renderScrim?: (progress: SharedValue<number>) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare const BottomSheetBase: ({ children, detents, index, onIndexChange, position: externalPosition, openAnimationConfig, closeAnimationConfig, modal, renderScrim, }: BottomSheetBaseProps) => import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=BottomSheetBase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetBase.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAiB7E,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAEpC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B,mBAAmB,CAAC,EAAE,gBAAgB,CAAC;IACvC,oBAAoB,CAAC,EAAE,gBAAgB,CAAC;CACzC;AAED,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;CAC5D;AA8CD,eAAO,MAAM,eAAe,GAAI,yIAU7B,oBAAoB,4CA6RtB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PanGesture } from 'react-native-gesture-handler';
|
|
2
|
+
import type { AnimatedRef, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
export interface BottomSheetContextType {
|
|
4
|
+
translateY: SharedValue<number>;
|
|
5
|
+
position: SharedValue<number>;
|
|
6
|
+
index: SharedValue<number>;
|
|
7
|
+
sheetHeight: SharedValue<number>;
|
|
8
|
+
scrollOffset: SharedValue<number>;
|
|
9
|
+
scrollableRef: AnimatedRef<any>;
|
|
10
|
+
hasScrollable: SharedValue<boolean>;
|
|
11
|
+
isScrollableGestureActive: SharedValue<boolean>;
|
|
12
|
+
isScrollableLocked: SharedValue<boolean>;
|
|
13
|
+
panGesture: PanGesture;
|
|
14
|
+
}
|
|
15
|
+
export declare const BottomSheetContextProvider: import("react").Provider<BottomSheetContextType | null>;
|
|
16
|
+
export declare const useBottomSheetContext: () => BottomSheetContextType;
|
|
17
|
+
//# sourceMappingURL=BottomSheetContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BottomSheetContext.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetContext.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAExE,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,aAAa,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,yBAAyB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,kBAAkB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,UAAU,EAAE,UAAU,CAAC;CACxB;AAID,eAAO,MAAM,0BAA0B,yDAA8B,CAAC;AAEtE,eAAO,MAAM,qBAAqB,8BAQjC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BottomSheetFlatList.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetFlatList.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAKvE,eAAO,MAAM,mBAAmB,GAAI,CAAC,EACnC,OAAO,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,4CAgBpD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare const BottomSheetProvider: ({ children }: {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare const Portal: ({ children }: {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}) => null;
|
|
8
|
+
//# sourceMappingURL=BottomSheetProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BottomSheetProvider.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAyBvC,eAAO,MAAM,mBAAmB,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CA+BxE,CAAC;AAEF,eAAO,MAAM,MAAM,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,SAgB3D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BottomSheetScrollView.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAMpD,eAAO,MAAM,qBAAqB,GAChC,OAAO,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,4CAgBzC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import type { BottomSheetCommonProps } from './BottomSheetBase';
|
|
4
|
+
export interface ModalBottomSheetProps extends BottomSheetCommonProps {
|
|
5
|
+
scrim?: (progress: SharedValue<number>) => ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare const ModalBottomSheet: ({ scrim, ...props }: ModalBottomSheetProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=ModalBottomSheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalBottomSheet.d.ts","sourceRoot":"","sources":["../../../src/ModalBottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,MAAM,WAAW,qBAAsB,SAAQ,sBAAsB;IACnE,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;CACtD;AAED,eAAO,MAAM,gBAAgB,GAAI,qBAG9B,qBAAqB,4CAEvB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
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';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SharedValue } from 'react-native-reanimated';
|
|
2
|
+
export declare const useBottomSheetScrollable: (baseScrollEnabled?: boolean | SharedValue<boolean | undefined>) => {
|
|
3
|
+
scrollHandler: import("react-native-reanimated").ScrollHandlerProcessed<Record<string, unknown>>;
|
|
4
|
+
scrollableRef: import("react-native-reanimated").AnimatedRef<any>;
|
|
5
|
+
nativeGesture: import("react-native-gesture-handler/lib/typescript/handlers/gestures/nativeGesture").NativeGesture;
|
|
6
|
+
animatedProps: Partial<{
|
|
7
|
+
scrollEnabled: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=useBottomSheetScrollable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBottomSheetScrollable.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetScrollable.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,WAAW,EAGjB,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,wBAAwB,GACnC,oBAAmB,OAAO,GAAG,WAAW,CAAC,OAAO,GAAG,SAAS,CAAQ;;;;;;;CAkDrE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@swmansion/react-native-bottom-sheet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Provides bottom-sheet components for React Native.",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace @swmansion/react-native-bottom-sheet-example",
|
|
36
|
+
"clean": "del-cli lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\""
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"react-native",
|
|
43
|
+
"android",
|
|
44
|
+
"ios"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/software-mansion-labs/react-native-bottom-sheet.git"
|
|
49
|
+
},
|
|
50
|
+
"author": "Software Mansion",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/software-mansion-labs/react-native-bottom-sheet/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/software-mansion-labs/react-native-bottom-sheet",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"registry": "https://registry.npmjs.org"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@eslint/compat": "1.4.1",
|
|
61
|
+
"@eslint/eslintrc": "3.3.3",
|
|
62
|
+
"@eslint/js": "9.39.2",
|
|
63
|
+
"@react-native/babel-preset": "0.83.0",
|
|
64
|
+
"@react-native/eslint-config": "0.83.0",
|
|
65
|
+
"@types/react": "19.2.14",
|
|
66
|
+
"del-cli": "6.0.0",
|
|
67
|
+
"eslint": "9.39.2",
|
|
68
|
+
"eslint-config-prettier": "10.1.8",
|
|
69
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
70
|
+
"prettier": "2.8.8",
|
|
71
|
+
"react": "19.1.0",
|
|
72
|
+
"react-native": "0.81.5",
|
|
73
|
+
"react-native-builder-bob": "0.40.18",
|
|
74
|
+
"react-native-gesture-handler": "2.28.0",
|
|
75
|
+
"react-native-reanimated": "4.1.6",
|
|
76
|
+
"react-native-safe-area-context": "5.6.2",
|
|
77
|
+
"react-native-worklets": "0.5.1",
|
|
78
|
+
"typescript": "5.9.3"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"react": ">=18.0.0",
|
|
82
|
+
"react-native": ">=0.76.0",
|
|
83
|
+
"react-native-gesture-handler": ">=2.14.0",
|
|
84
|
+
"react-native-reanimated": ">=3.16.0",
|
|
85
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
86
|
+
"react-native-worklets": ">=0.4.0"
|
|
87
|
+
},
|
|
88
|
+
"workspaces": [
|
|
89
|
+
"example"
|
|
90
|
+
],
|
|
91
|
+
"packageManager": "yarn@4.11.0",
|
|
92
|
+
"react-native-builder-bob": {
|
|
93
|
+
"source": "src",
|
|
94
|
+
"output": "lib",
|
|
95
|
+
"targets": [
|
|
96
|
+
[
|
|
97
|
+
"module",
|
|
98
|
+
{
|
|
99
|
+
"esm": true
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"typescript",
|
|
104
|
+
{
|
|
105
|
+
"project": "tsconfig.build.json"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"prettier": {
|
|
111
|
+
"quoteProps": "consistent",
|
|
112
|
+
"singleQuote": true,
|
|
113
|
+
"tabWidth": 2,
|
|
114
|
+
"trailingComma": "es5",
|
|
115
|
+
"useTabs": false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BottomSheetCommonProps } from './BottomSheetBase';
|
|
2
|
+
import { BottomSheetBase } from './BottomSheetBase';
|
|
3
|
+
|
|
4
|
+
export type BottomSheetProps = BottomSheetCommonProps;
|
|
5
|
+
|
|
6
|
+
export const BottomSheet = (props: BottomSheetProps) => (
|
|
7
|
+
<BottomSheetBase {...props} />
|
|
8
|
+
);
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
4
|
+
import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
5
|
+
import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
|
|
6
|
+
import Animated, {
|
|
7
|
+
measure,
|
|
8
|
+
scrollTo,
|
|
9
|
+
useAnimatedRef,
|
|
10
|
+
useAnimatedReaction,
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
useDerivedValue,
|
|
13
|
+
useSharedValue,
|
|
14
|
+
withSpring,
|
|
15
|
+
} from 'react-native-reanimated';
|
|
16
|
+
import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
17
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
18
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
|
+
import { Portal } from './BottomSheetProvider';
|
|
20
|
+
import { BottomSheetContextProvider } from './BottomSheetContext';
|
|
21
|
+
|
|
22
|
+
export type Detent = number | 'max';
|
|
23
|
+
|
|
24
|
+
export interface BottomSheetCommonProps {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
detents?: Detent[];
|
|
27
|
+
index: number;
|
|
28
|
+
onIndexChange?: (index: number) => void;
|
|
29
|
+
position?: SharedValue<number>;
|
|
30
|
+
openAnimationConfig?: WithSpringConfig;
|
|
31
|
+
closeAnimationConfig?: WithSpringConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BottomSheetBaseProps extends BottomSheetCommonProps {
|
|
35
|
+
modal?: boolean;
|
|
36
|
+
renderScrim?: (progress: SharedValue<number>) => ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_OPEN_ANIMATION_CONFIG: WithSpringConfig = {
|
|
40
|
+
dampingRatio: 1,
|
|
41
|
+
duration: 300,
|
|
42
|
+
overshootClamping: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CLOSE_ANIMATION_CONFIG: WithSpringConfig = {
|
|
46
|
+
dampingRatio: 1,
|
|
47
|
+
duration: 250,
|
|
48
|
+
overshootClamping: true,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const VELOCITY_THRESHOLD = 800;
|
|
52
|
+
|
|
53
|
+
const resolveDetent = (
|
|
54
|
+
detent: Detent,
|
|
55
|
+
contentHeight: number,
|
|
56
|
+
maxHeight: number
|
|
57
|
+
) => {
|
|
58
|
+
if (typeof detent === 'number') return detent;
|
|
59
|
+
if (detent === 'max') {
|
|
60
|
+
return contentHeight > 0 ? Math.min(contentHeight, maxHeight) : maxHeight;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Invalid detent: \`${detent}\`.`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const clampIndex = (index: number, detentCount: number) => {
|
|
66
|
+
if (detentCount <= 0) return 0;
|
|
67
|
+
return Math.min(Math.max(index, 0), detentCount - 1);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const DefaultScrim = ({ progress }: { progress: SharedValue<number> }) => {
|
|
71
|
+
const style = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
72
|
+
return (
|
|
73
|
+
<Animated.View
|
|
74
|
+
style={[
|
|
75
|
+
StyleSheet.absoluteFill,
|
|
76
|
+
{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)' },
|
|
77
|
+
style,
|
|
78
|
+
]}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const BottomSheetBase = ({
|
|
84
|
+
children,
|
|
85
|
+
detents = [0, 'max'],
|
|
86
|
+
index,
|
|
87
|
+
onIndexChange,
|
|
88
|
+
position: externalPosition,
|
|
89
|
+
openAnimationConfig = DEFAULT_OPEN_ANIMATION_CONFIG,
|
|
90
|
+
closeAnimationConfig = DEFAULT_CLOSE_ANIMATION_CONFIG,
|
|
91
|
+
modal = false,
|
|
92
|
+
renderScrim,
|
|
93
|
+
}: BottomSheetBaseProps) => {
|
|
94
|
+
const { height: screenHeight } = useWindowDimensions();
|
|
95
|
+
const insets = useSafeAreaInsets();
|
|
96
|
+
const maxHeight = screenHeight - insets.top;
|
|
97
|
+
const resolvedIndex = clampIndex(index, detents.length);
|
|
98
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
99
|
+
if (detents.length === 0) {
|
|
100
|
+
throw new Error('detents must include at least one value.');
|
|
101
|
+
}
|
|
102
|
+
const normalizedDetents = detents.map((point) => {
|
|
103
|
+
const resolved = resolveDetent(point, contentHeight, maxHeight);
|
|
104
|
+
return Math.max(0, Math.min(resolved, maxHeight));
|
|
105
|
+
});
|
|
106
|
+
const initialMaxSnap = Math.max(0, ...normalizedDetents);
|
|
107
|
+
const translateY = useSharedValue(initialMaxSnap);
|
|
108
|
+
const animationTarget = useSharedValue(NaN);
|
|
109
|
+
const sheetHeight = useSharedValue(initialMaxSnap);
|
|
110
|
+
const scrollOffset = useSharedValue(0);
|
|
111
|
+
const hasScrollable = useSharedValue(false);
|
|
112
|
+
const isScrollableGestureActive = useSharedValue(false);
|
|
113
|
+
const isScrollableLocked = useSharedValue(false);
|
|
114
|
+
const scrollableRef = useAnimatedRef();
|
|
115
|
+
const isDraggingSheet = useSharedValue(false);
|
|
116
|
+
const isDraggingFromScrollable = useSharedValue(false);
|
|
117
|
+
const panStartY = useSharedValue(0);
|
|
118
|
+
const panActivated = useSharedValue(false);
|
|
119
|
+
const dragStartTranslateY = useSharedValue(0);
|
|
120
|
+
const isTouchWithinScrollable = useSharedValue(false);
|
|
121
|
+
const detentsValue = useSharedValue(normalizedDetents);
|
|
122
|
+
const currentIndex = useSharedValue(resolvedIndex);
|
|
123
|
+
const internalPosition = useDerivedValue(() =>
|
|
124
|
+
Math.max(0, sheetHeight.value - translateY.value)
|
|
125
|
+
);
|
|
126
|
+
useAnimatedReaction(
|
|
127
|
+
() => internalPosition.value,
|
|
128
|
+
(value) => {
|
|
129
|
+
if (externalPosition !== undefined) externalPosition.set(value);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
const scrimProgress = useDerivedValue(() => {
|
|
133
|
+
const maxSnap = sheetHeight.value;
|
|
134
|
+
if (maxSnap <= 0) return 0;
|
|
135
|
+
const progress = internalPosition.value / maxSnap;
|
|
136
|
+
return Math.min(1, Math.max(0, progress));
|
|
137
|
+
});
|
|
138
|
+
const handleIndexChange = (nextIndex: number) => {
|
|
139
|
+
onIndexChange?.(nextIndex);
|
|
140
|
+
};
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const maxSnap = Math.max(0, ...normalizedDetents);
|
|
143
|
+
detentsValue.set(normalizedDetents);
|
|
144
|
+
sheetHeight.set(maxSnap);
|
|
145
|
+
}, [normalizedDetents, sheetHeight, detentsValue]);
|
|
146
|
+
const animateToIndex = useCallback(
|
|
147
|
+
(targetIndex: number, velocity?: number) => {
|
|
148
|
+
'worklet';
|
|
149
|
+
const maxSnap = sheetHeight.value;
|
|
150
|
+
const targetTranslate = maxSnap - (detentsValue.value[targetIndex] ?? 0);
|
|
151
|
+
if (animationTarget.value === targetTranslate && velocity === undefined) {
|
|
152
|
+
currentIndex.set(targetIndex);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
animationTarget.set(targetTranslate);
|
|
156
|
+
const isOpening = targetTranslate < translateY.value;
|
|
157
|
+
const baseConfig = isOpening ? openAnimationConfig : closeAnimationConfig;
|
|
158
|
+
const springConfig =
|
|
159
|
+
velocity === undefined ? baseConfig : { ...baseConfig, velocity };
|
|
160
|
+
translateY.set(withSpring(targetTranslate, springConfig));
|
|
161
|
+
currentIndex.set(targetIndex);
|
|
162
|
+
},
|
|
163
|
+
[
|
|
164
|
+
animationTarget,
|
|
165
|
+
closeAnimationConfig,
|
|
166
|
+
currentIndex,
|
|
167
|
+
detentsValue,
|
|
168
|
+
openAnimationConfig,
|
|
169
|
+
sheetHeight,
|
|
170
|
+
translateY,
|
|
171
|
+
]
|
|
172
|
+
);
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
scheduleOnUI(animateToIndex, resolvedIndex);
|
|
175
|
+
}, [animateToIndex, resolvedIndex, normalizedDetents]);
|
|
176
|
+
const panGesture = Gesture.Pan()
|
|
177
|
+
.manualActivation(true)
|
|
178
|
+
.onTouchesDown((event) => {
|
|
179
|
+
'worklet';
|
|
180
|
+
panActivated.set(false);
|
|
181
|
+
isDraggingSheet.set(false);
|
|
182
|
+
isDraggingFromScrollable.set(false);
|
|
183
|
+
isScrollableLocked.set(false);
|
|
184
|
+
isTouchWithinScrollable.set(false);
|
|
185
|
+
const touch = event.changedTouches[0] ?? event.allTouches[0];
|
|
186
|
+
if (touch) panStartY.set(touch.absoluteY);
|
|
187
|
+
if (touch && hasScrollable.value) {
|
|
188
|
+
const layout = measure(scrollableRef);
|
|
189
|
+
if (layout) {
|
|
190
|
+
const withinX =
|
|
191
|
+
touch.absoluteX >= layout.pageX &&
|
|
192
|
+
touch.absoluteX <= layout.pageX + layout.width;
|
|
193
|
+
const withinY =
|
|
194
|
+
touch.absoluteY >= layout.pageY &&
|
|
195
|
+
touch.absoluteY <= layout.pageY + layout.height;
|
|
196
|
+
isTouchWithinScrollable.set(withinX && withinY);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
.onTouchesMove((event, stateManager) => {
|
|
201
|
+
'worklet';
|
|
202
|
+
if (panActivated.value) return;
|
|
203
|
+
const touch = event.changedTouches[0] ?? event.allTouches[0];
|
|
204
|
+
if (!touch) return;
|
|
205
|
+
const deltaY = touch.absoluteY - panStartY.value;
|
|
206
|
+
if (
|
|
207
|
+
hasScrollable.value &&
|
|
208
|
+
scrollOffset.value > 0 &&
|
|
209
|
+
isTouchWithinScrollable.value
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (deltaY > 0 || translateY.value > 0) {
|
|
214
|
+
panActivated.set(true);
|
|
215
|
+
stateManager.activate();
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.onBegin(() => {
|
|
219
|
+
'worklet';
|
|
220
|
+
animationTarget.set(NaN);
|
|
221
|
+
isDraggingSheet.set(false);
|
|
222
|
+
isDraggingFromScrollable.set(false);
|
|
223
|
+
dragStartTranslateY.set(translateY.value);
|
|
224
|
+
})
|
|
225
|
+
.onUpdate((event) => {
|
|
226
|
+
'worklet';
|
|
227
|
+
if (isDraggingSheet.value) {
|
|
228
|
+
if (isDraggingFromScrollable.value) {
|
|
229
|
+
scrollTo(scrollableRef, 0, 0, false);
|
|
230
|
+
}
|
|
231
|
+
const nextTranslate = Math.min(
|
|
232
|
+
Math.max(dragStartTranslateY.value + event.translationY, 0),
|
|
233
|
+
sheetHeight.value
|
|
234
|
+
);
|
|
235
|
+
translateY.set(nextTranslate);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const isDraggingDown = event.translationY > 0;
|
|
239
|
+
const canStartDrag =
|
|
240
|
+
!hasScrollable.value ||
|
|
241
|
+
scrollOffset.value <= 0 ||
|
|
242
|
+
!isTouchWithinScrollable.value;
|
|
243
|
+
if (!canStartDrag || (!isDraggingDown && translateY.value <= 0)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const isScrollableActive =
|
|
247
|
+
hasScrollable.value && isScrollableGestureActive.value;
|
|
248
|
+
isDraggingSheet.set(true);
|
|
249
|
+
isDraggingFromScrollable.set(
|
|
250
|
+
isScrollableActive && isTouchWithinScrollable.value
|
|
251
|
+
);
|
|
252
|
+
isScrollableLocked.set(hasScrollable.value);
|
|
253
|
+
if (isTouchWithinScrollable.value && hasScrollable.value) {
|
|
254
|
+
scrollTo(scrollableRef, 0, 0, false);
|
|
255
|
+
}
|
|
256
|
+
const nextTranslate = Math.min(
|
|
257
|
+
Math.max(dragStartTranslateY.value + event.translationY, 0),
|
|
258
|
+
sheetHeight.value
|
|
259
|
+
);
|
|
260
|
+
translateY.set(nextTranslate);
|
|
261
|
+
})
|
|
262
|
+
.onEnd((event) => {
|
|
263
|
+
'worklet';
|
|
264
|
+
const wasDragging = isDraggingSheet.value;
|
|
265
|
+
isScrollableLocked.set(false);
|
|
266
|
+
isDraggingSheet.set(false);
|
|
267
|
+
if (!wasDragging) {
|
|
268
|
+
animateToIndex(currentIndex.value);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const maxSnap = sheetHeight.value;
|
|
272
|
+
const allPositions = detentsValue.value.map((point, snapIndex) => ({
|
|
273
|
+
index: snapIndex,
|
|
274
|
+
translateY: maxSnap - point,
|
|
275
|
+
}));
|
|
276
|
+
const currentTranslate = translateY.value;
|
|
277
|
+
const velocityY = event.velocityY;
|
|
278
|
+
let targetIndex = currentIndex.value;
|
|
279
|
+
let minDistance = Infinity;
|
|
280
|
+
for (const pos of allPositions) {
|
|
281
|
+
const distance = Math.abs(currentTranslate - pos.translateY);
|
|
282
|
+
if (distance < minDistance) {
|
|
283
|
+
minDistance = distance;
|
|
284
|
+
targetIndex = pos.index;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (Math.abs(velocityY) > VELOCITY_THRESHOLD) {
|
|
288
|
+
if (velocityY > 0) {
|
|
289
|
+
const lower = allPositions
|
|
290
|
+
.filter((pos) => pos.translateY > currentTranslate + 1)
|
|
291
|
+
.sort((a, b) => a.translateY - b.translateY);
|
|
292
|
+
if (lower.length > 0) targetIndex = lower[0]!.index;
|
|
293
|
+
} else {
|
|
294
|
+
const upper = allPositions
|
|
295
|
+
.filter((pos) => pos.translateY < currentTranslate - 1)
|
|
296
|
+
.sort((a, b) => b.translateY - a.translateY);
|
|
297
|
+
if (upper.length > 0) targetIndex = upper[0]!.index;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const hasIndexChanged = targetIndex !== currentIndex.value;
|
|
301
|
+
if (hasIndexChanged) scheduleOnRN(handleIndexChange, targetIndex);
|
|
302
|
+
const shouldApplyVelocity = hasIndexChanged && Number.isFinite(velocityY);
|
|
303
|
+
animateToIndex(targetIndex, shouldApplyVelocity ? velocityY : undefined);
|
|
304
|
+
});
|
|
305
|
+
const handleSentinelLayout = (event: LayoutChangeEvent) => {
|
|
306
|
+
setContentHeight(event.nativeEvent.layout.y);
|
|
307
|
+
};
|
|
308
|
+
const closedIndex = normalizedDetents.indexOf(0);
|
|
309
|
+
const handleScrimPress = () => {
|
|
310
|
+
if (closedIndex === -1 || resolvedIndex === closedIndex) return;
|
|
311
|
+
handleIndexChange(closedIndex);
|
|
312
|
+
scheduleOnUI(animateToIndex, closedIndex);
|
|
313
|
+
};
|
|
314
|
+
const wrapperStyle = useAnimatedStyle(() => ({
|
|
315
|
+
transform: [{ translateY: translateY.value }],
|
|
316
|
+
height: sheetHeight.value,
|
|
317
|
+
opacity: translateY.value >= sheetHeight.value ? 0 : 1,
|
|
318
|
+
}));
|
|
319
|
+
const isCollapsed = normalizedDetents[resolvedIndex] === 0;
|
|
320
|
+
const pointerEvents = modal ? (isCollapsed ? 'none' : 'auto') : 'box-none';
|
|
321
|
+
let scrimElement: ReactNode | null = null;
|
|
322
|
+
if (renderScrim) {
|
|
323
|
+
scrimElement = renderScrim(scrimProgress);
|
|
324
|
+
} else if (modal) {
|
|
325
|
+
scrimElement = <DefaultScrim progress={scrimProgress} />;
|
|
326
|
+
}
|
|
327
|
+
const sheetContent = (
|
|
328
|
+
<BottomSheetContextProvider
|
|
329
|
+
value={{
|
|
330
|
+
translateY,
|
|
331
|
+
position: internalPosition,
|
|
332
|
+
index: currentIndex,
|
|
333
|
+
sheetHeight,
|
|
334
|
+
scrollOffset,
|
|
335
|
+
scrollableRef,
|
|
336
|
+
hasScrollable,
|
|
337
|
+
isScrollableGestureActive,
|
|
338
|
+
isScrollableLocked,
|
|
339
|
+
panGesture,
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
<Animated.View
|
|
343
|
+
style={[
|
|
344
|
+
{
|
|
345
|
+
position: 'absolute',
|
|
346
|
+
bottom: 0,
|
|
347
|
+
left: 0,
|
|
348
|
+
right: 0,
|
|
349
|
+
},
|
|
350
|
+
wrapperStyle,
|
|
351
|
+
]}
|
|
352
|
+
pointerEvents="box-none"
|
|
353
|
+
>
|
|
354
|
+
<GestureDetector gesture={panGesture}>
|
|
355
|
+
<View style={{ flex: 1 }} pointerEvents="box-none">
|
|
356
|
+
{children}
|
|
357
|
+
<View onLayout={handleSentinelLayout} pointerEvents="none" />
|
|
358
|
+
</View>
|
|
359
|
+
</GestureDetector>
|
|
360
|
+
</Animated.View>
|
|
361
|
+
</BottomSheetContextProvider>
|
|
362
|
+
);
|
|
363
|
+
const sheetContainer = (
|
|
364
|
+
<Animated.View
|
|
365
|
+
style={StyleSheet.absoluteFill}
|
|
366
|
+
pointerEvents={pointerEvents}
|
|
367
|
+
>
|
|
368
|
+
{modal && scrimElement ? (
|
|
369
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={handleScrimPress}>
|
|
370
|
+
{scrimElement}
|
|
371
|
+
</Pressable>
|
|
372
|
+
) : null}
|
|
373
|
+
{sheetContent}
|
|
374
|
+
</Animated.View>
|
|
375
|
+
);
|
|
376
|
+
if (modal) return <Portal>{sheetContainer}</Portal>;
|
|
377
|
+
return sheetContainer;
|
|
378
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { PanGesture } from 'react-native-gesture-handler';
|
|
3
|
+
import type { AnimatedRef, SharedValue } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
export interface BottomSheetContextType {
|
|
6
|
+
translateY: SharedValue<number>;
|
|
7
|
+
position: SharedValue<number>;
|
|
8
|
+
index: SharedValue<number>;
|
|
9
|
+
sheetHeight: SharedValue<number>;
|
|
10
|
+
scrollOffset: SharedValue<number>;
|
|
11
|
+
scrollableRef: AnimatedRef<any>;
|
|
12
|
+
hasScrollable: SharedValue<boolean>;
|
|
13
|
+
isScrollableGestureActive: SharedValue<boolean>;
|
|
14
|
+
isScrollableLocked: SharedValue<boolean>;
|
|
15
|
+
panGesture: PanGesture;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BottomSheetContext = createContext<BottomSheetContextType | null>(null);
|
|
19
|
+
|
|
20
|
+
export const BottomSheetContextProvider = BottomSheetContext.Provider;
|
|
21
|
+
|
|
22
|
+
export const useBottomSheetContext = () => {
|
|
23
|
+
const context = useContext(BottomSheetContext);
|
|
24
|
+
if (context === null) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'`useBottomSheetContext` must be used within `BottomSheet`.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
};
|