react-native-swipeable-actions 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/README.md +250 -0
- package/android/build.gradle +99 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeConstants.kt +29 -0
- package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +66 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +69 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +951 -0
- package/build/Swipeable.d.ts +10 -0
- package/build/Swipeable.d.ts.map +1 -0
- package/build/Swipeable.js +114 -0
- package/build/Swipeable.js.map +1 -0
- package/build/Swipeable.types.d.ts +64 -0
- package/build/Swipeable.types.d.ts.map +1 -0
- package/build/Swipeable.types.js +3 -0
- package/build/Swipeable.types.js.map +1 -0
- package/build/SwipeableActions.d.ts +17 -0
- package/build/SwipeableActions.d.ts.map +1 -0
- package/build/SwipeableActions.js +31 -0
- package/build/SwipeableActions.js.map +1 -0
- package/build/SwipeableView.d.ts +10 -0
- package/build/SwipeableView.d.ts.map +1 -0
- package/build/SwipeableView.js +11 -0
- package/build/SwipeableView.js.map +1 -0
- package/build/constants.d.ts +11 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +14 -0
- package/build/constants.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +21 -0
- package/build/index.js.map +1 -0
- package/build/nativeModuleUtils.d.ts +11 -0
- package/build/nativeModuleUtils.d.ts.map +1 -0
- package/build/nativeModuleUtils.js +49 -0
- package/build/nativeModuleUtils.js.map +1 -0
- package/build/swipeStateUtils.d.ts +16 -0
- package/build/swipeStateUtils.d.ts.map +1 -0
- package/build/swipeStateUtils.js +37 -0
- package/build/swipeStateUtils.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Package.swift +28 -0
- package/ios/SwipeAnimationConfig.swift +27 -0
- package/ios/SwipePhysics.swift +66 -0
- package/ios/SwipeableModule.swift +84 -0
- package/ios/SwipeableView.swift +845 -0
- package/ios/react-native-swipeable-actions.podspec +22 -0
- package/package.json +85 -0
- package/src/Swipeable.tsx +195 -0
- package/src/Swipeable.types.ts +70 -0
- package/src/SwipeableActions.tsx +41 -0
- package/src/SwipeableView.tsx +20 -0
- package/src/constants.ts +11 -0
- package/src/index.ts +22 -0
- package/src/nativeModuleUtils.ts +57 -0
- package/src/swipeStateUtils.ts +55 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'react-native-swipeable-actions'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['repository']['url']
|
|
13
|
+
s.platforms = { :ios => '13.4' }
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
s.source_files = '*.{h,m,mm,swift}'
|
|
21
|
+
s.exclude_files = 'Package.swift'
|
|
22
|
+
end
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-swipeable-actions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance native swipeable row component for React Native using Expo Modules",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"build",
|
|
9
|
+
"src",
|
|
10
|
+
"!src/__tests__",
|
|
11
|
+
"!src/__mocks__",
|
|
12
|
+
"ios",
|
|
13
|
+
"!ios/Tests",
|
|
14
|
+
"!ios/.build",
|
|
15
|
+
"android/src/main",
|
|
16
|
+
"android/build.gradle",
|
|
17
|
+
"expo-module.config.json"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"clean": "rm -rf build",
|
|
22
|
+
"prepare": "npm run clean && npm run build",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"test:ios": "cd ios && swift test",
|
|
25
|
+
"test:android": "cd example/android && ./gradlew :react-native-swipeable-actions:test --quiet",
|
|
26
|
+
"test:all": "npm test && npm run test:ios && npm run test:android",
|
|
27
|
+
"test:watch": "jest --watch",
|
|
28
|
+
"test:coverage": "jest --coverage",
|
|
29
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
30
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
31
|
+
"format": "prettier --write src",
|
|
32
|
+
"format:check": "prettier --check src",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"android": "expo run:android",
|
|
35
|
+
"ios": "expo run:ios",
|
|
36
|
+
"e2e": "cd e2e && npm run test",
|
|
37
|
+
"e2e:android": "cd e2e && npm run test:android",
|
|
38
|
+
"e2e:perf": "cd e2e && npm run test:perf"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react-native",
|
|
42
|
+
"expo",
|
|
43
|
+
"swipeable",
|
|
44
|
+
"gesture",
|
|
45
|
+
"native",
|
|
46
|
+
"ios",
|
|
47
|
+
"android"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/chocky335/react-native-swipeable-actions"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/chocky335/react-native-swipeable-actions#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/chocky335/react-native-swipeable-actions/issues"
|
|
56
|
+
},
|
|
57
|
+
"author": "chocky335",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"expo": ">=50.0.0",
|
|
61
|
+
"expo-modules-core": ">=1.0.0",
|
|
62
|
+
"react": ">=18.0.0",
|
|
63
|
+
"react-native": ">=0.73.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@testing-library/react-native": "^12.4.0",
|
|
67
|
+
"@types/jest": "^29.5.0",
|
|
68
|
+
"@types/react": "19.1.1",
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
70
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
71
|
+
"eslint": "^8.57.0",
|
|
72
|
+
"eslint-config-prettier": "^9.1.0",
|
|
73
|
+
"eslint-plugin-react": "^7.34.0",
|
|
74
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
75
|
+
"expo-modules-core": "^3.0.26",
|
|
76
|
+
"jest": "^29.7.0",
|
|
77
|
+
"prettier": "^3.2.0",
|
|
78
|
+
"prettier-config-holepunch": "^2.0.0",
|
|
79
|
+
"react": "19.1.1",
|
|
80
|
+
"react-native": "0.82.1",
|
|
81
|
+
"react-test-renderer": "19.1.1",
|
|
82
|
+
"ts-jest": "^29.1.0",
|
|
83
|
+
"typescript": "^5.9.3"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { forwardRef, useImperativeHandle, useRef, useCallback, useState } from 'react'
|
|
2
|
+
import { StyleSheet, View, Platform } from 'react-native'
|
|
3
|
+
|
|
4
|
+
import { SwipeableView } from './SwipeableView'
|
|
5
|
+
import { SwipeableActions } from './SwipeableActions'
|
|
6
|
+
import {
|
|
7
|
+
SwipeableProps,
|
|
8
|
+
SwipeableMethods,
|
|
9
|
+
SwipeProgressEvent,
|
|
10
|
+
SwipeStateEvent,
|
|
11
|
+
NativeSwipeableRef
|
|
12
|
+
} from './Swipeable.types'
|
|
13
|
+
import { SWIPE_DEFAULTS } from './constants'
|
|
14
|
+
import { openByKey, closeByKey, closeAll } from './nativeModuleUtils'
|
|
15
|
+
|
|
16
|
+
function normalizeRecyclingKey(key: string | number | undefined): string | undefined {
|
|
17
|
+
if (key == null) return undefined
|
|
18
|
+
if (typeof key === 'string') return key
|
|
19
|
+
if (typeof key === 'number' && Number.isFinite(key)) return String(key)
|
|
20
|
+
|
|
21
|
+
if (__DEV__) {
|
|
22
|
+
console.warn(`[Swipeable] Invalid recyclingKey: ${key}. Expected string or finite number.`)
|
|
23
|
+
}
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(function Swipeable(
|
|
28
|
+
{
|
|
29
|
+
children,
|
|
30
|
+
actions,
|
|
31
|
+
actionsWidth,
|
|
32
|
+
actionsPosition = SWIPE_DEFAULTS.ACTIONS_POSITION,
|
|
33
|
+
friction = SWIPE_DEFAULTS.FRICTION,
|
|
34
|
+
threshold = SWIPE_DEFAULTS.THRESHOLD,
|
|
35
|
+
dragOffsetFromEdge = SWIPE_DEFAULTS.DRAG_OFFSET_FROM_EDGE,
|
|
36
|
+
style,
|
|
37
|
+
testID,
|
|
38
|
+
onSwipeStart,
|
|
39
|
+
onSwipeStateChange,
|
|
40
|
+
onSwipeEnd,
|
|
41
|
+
onProgress,
|
|
42
|
+
autoClose = false,
|
|
43
|
+
autoCloseTimeout = 0,
|
|
44
|
+
recyclingKey
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) {
|
|
48
|
+
const nativeRef = useRef<NativeSwipeableRef>(null)
|
|
49
|
+
const [hasActionsRendered, setHasActionsRendered] = useState(false)
|
|
50
|
+
const swipeStartedRef = useRef(false)
|
|
51
|
+
|
|
52
|
+
// Normalize and sync recyclingKey to ref for use in useImperativeHandle
|
|
53
|
+
// (set during render to ensure it's always current before callbacks fire)
|
|
54
|
+
const normalizedKey = normalizeRecyclingKey(recyclingKey)
|
|
55
|
+
const recyclingKeyRef = useRef<string | undefined>(undefined)
|
|
56
|
+
recyclingKeyRef.current = normalizedKey
|
|
57
|
+
|
|
58
|
+
// Development-time validation
|
|
59
|
+
if (__DEV__) {
|
|
60
|
+
if (autoClose && recyclingKey == null) {
|
|
61
|
+
console.warn(
|
|
62
|
+
'[Swipeable] autoClose is enabled but recyclingKey is not set. ' +
|
|
63
|
+
'This may cause issues in virtualized lists. Consider adding a recyclingKey prop.'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
useImperativeHandle(
|
|
69
|
+
ref,
|
|
70
|
+
() => ({
|
|
71
|
+
close: (animated?: boolean) => {
|
|
72
|
+
const key = recyclingKeyRef.current
|
|
73
|
+
if (key) {
|
|
74
|
+
closeByKey(key, animated)
|
|
75
|
+
} else if (__DEV__) {
|
|
76
|
+
console.warn('[Swipeable] close() called without recyclingKey - method has no effect')
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
open: () => {
|
|
80
|
+
const key = recyclingKeyRef.current
|
|
81
|
+
if (key) {
|
|
82
|
+
openByKey(key)
|
|
83
|
+
} else if (__DEV__) {
|
|
84
|
+
console.warn('[Swipeable] open() called without recyclingKey - method has no effect')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
[]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const handleSwipeStart = useCallback(() => {
|
|
92
|
+
if (!hasActionsRendered) {
|
|
93
|
+
setHasActionsRendered(true)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!swipeStartedRef.current) {
|
|
97
|
+
swipeStartedRef.current = true
|
|
98
|
+
onSwipeStart?.()
|
|
99
|
+
}
|
|
100
|
+
}, [hasActionsRendered, onSwipeStart])
|
|
101
|
+
|
|
102
|
+
const handleSwipeProgress = useCallback(
|
|
103
|
+
(event: SwipeProgressEvent) => {
|
|
104
|
+
const newProgress = event.nativeEvent.progress
|
|
105
|
+
const clamped = Math.max(0, Math.min(1, newProgress))
|
|
106
|
+
onProgress?.(clamped)
|
|
107
|
+
},
|
|
108
|
+
[onProgress]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const handleSwipeStateChange = useCallback(
|
|
112
|
+
(event: SwipeStateEvent) => {
|
|
113
|
+
onSwipeStateChange?.(event.nativeEvent.state)
|
|
114
|
+
},
|
|
115
|
+
[onSwipeStateChange]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const handleSwipeEnd = useCallback(
|
|
119
|
+
(event: SwipeStateEvent) => {
|
|
120
|
+
swipeStartedRef.current = false
|
|
121
|
+
onSwipeEnd?.(event.nativeEvent.state)
|
|
122
|
+
},
|
|
123
|
+
[onSwipeEnd]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<SwipeableView
|
|
128
|
+
ref={nativeRef}
|
|
129
|
+
style={[styles.container, style]}
|
|
130
|
+
actionsWidth={actionsWidth}
|
|
131
|
+
actionsPosition={actionsPosition}
|
|
132
|
+
friction={friction}
|
|
133
|
+
threshold={threshold}
|
|
134
|
+
dragOffsetFromEdge={dragOffsetFromEdge}
|
|
135
|
+
autoClose={autoClose}
|
|
136
|
+
autoCloseTimeout={autoCloseTimeout}
|
|
137
|
+
{...(normalizedKey ? { recyclingKey: normalizedKey } : {})}
|
|
138
|
+
{...(testID ? { testID } : {})}
|
|
139
|
+
onSwipeStart={handleSwipeStart}
|
|
140
|
+
onSwipeProgress={handleSwipeProgress}
|
|
141
|
+
onSwipeStateChange={handleSwipeStateChange}
|
|
142
|
+
onSwipeEnd={handleSwipeEnd}
|
|
143
|
+
>
|
|
144
|
+
{hasActionsRendered && (
|
|
145
|
+
<SwipeableActions actionsPosition={actionsPosition}>{actions}</SwipeableActions>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<View style={styles.content}>{children}</View>
|
|
149
|
+
</SwipeableView>
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const styles = StyleSheet.create({
|
|
154
|
+
container: {
|
|
155
|
+
// iOS: overflow hidden works with native clipsToBounds
|
|
156
|
+
// Android: overflow visible required, native handles clipping
|
|
157
|
+
overflow: Platform.OS === 'ios' ? 'hidden' : 'visible'
|
|
158
|
+
},
|
|
159
|
+
content: {
|
|
160
|
+
width: '100%'
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
Swipeable.displayName = 'Swipeable'
|
|
165
|
+
|
|
166
|
+
// Static methods for controlling Swipeables by recyclingKey
|
|
167
|
+
type SwipeableWithStatic = typeof Swipeable & {
|
|
168
|
+
open: (recyclingKey: string) => void
|
|
169
|
+
close: (recyclingKey: string, animated?: boolean) => void
|
|
170
|
+
closeAll: (animated?: boolean) => void
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const SwipeableExport = Swipeable as SwipeableWithStatic
|
|
174
|
+
|
|
175
|
+
SwipeableExport.open = (recyclingKey: string) => {
|
|
176
|
+
if (__DEV__ && (!recyclingKey || typeof recyclingKey !== 'string')) {
|
|
177
|
+
console.warn('[Swipeable.open] recyclingKey must be a non-empty string')
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
openByKey(recyclingKey)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
SwipeableExport.close = (recyclingKey: string, animated?: boolean) => {
|
|
184
|
+
if (__DEV__ && (!recyclingKey || typeof recyclingKey !== 'string')) {
|
|
185
|
+
console.warn('[Swipeable.close] recyclingKey must be a non-empty string')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
closeByKey(recyclingKey, animated)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
SwipeableExport.closeAll = (animated?: boolean) => {
|
|
192
|
+
closeAll(animated)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export default SwipeableExport
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ReactNode, Ref } from 'react'
|
|
2
|
+
import { ViewProps, StyleProp, ViewStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
export interface SwipeProgressEvent {
|
|
5
|
+
nativeEvent: {
|
|
6
|
+
progress: number
|
|
7
|
+
translationX: number
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SwipeStateEvent {
|
|
12
|
+
nativeEvent: {
|
|
13
|
+
state: 'open' | 'closed'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NativeSwipeableRef {
|
|
18
|
+
close: (animated?: boolean) => Promise<void>
|
|
19
|
+
open: () => Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SwipeableViewProps extends ViewProps {
|
|
23
|
+
actionsWidth: number
|
|
24
|
+
actionsPosition?: 'left' | 'right'
|
|
25
|
+
friction?: number
|
|
26
|
+
threshold?: number
|
|
27
|
+
dragOffsetFromEdge?: number
|
|
28
|
+
recyclingKey?: string
|
|
29
|
+
onSwipeProgress?: (event: SwipeProgressEvent) => void
|
|
30
|
+
onSwipeStart?: () => void
|
|
31
|
+
onSwipeStateChange?: (event: SwipeStateEvent) => void
|
|
32
|
+
onSwipeEnd?: (event: SwipeStateEvent) => void
|
|
33
|
+
autoClose?: boolean
|
|
34
|
+
autoCloseTimeout?: number
|
|
35
|
+
children?: ReactNode
|
|
36
|
+
style?: StyleProp<ViewStyle>
|
|
37
|
+
nativeRef?: Ref<NativeSwipeableRef>
|
|
38
|
+
testID?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SwipeableProps {
|
|
42
|
+
children: ReactNode
|
|
43
|
+
actions: ReactNode
|
|
44
|
+
actionsWidth: number
|
|
45
|
+
actionsPosition?: 'left' | 'right'
|
|
46
|
+
friction?: number
|
|
47
|
+
threshold?: number
|
|
48
|
+
dragOffsetFromEdge?: number
|
|
49
|
+
style?: StyleProp<ViewStyle>
|
|
50
|
+
testID?: string
|
|
51
|
+
ref?: Ref<SwipeableMethods>
|
|
52
|
+
onSwipeStart?: () => void
|
|
53
|
+
onSwipeStateChange?: (state: 'open' | 'closed') => void
|
|
54
|
+
onSwipeEnd?: (state: 'open' | 'closed') => void
|
|
55
|
+
onProgress?: (progress: number) => void
|
|
56
|
+
autoClose?: boolean
|
|
57
|
+
autoCloseTimeout?: number
|
|
58
|
+
recyclingKey?: string | number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SwipeableMethods {
|
|
62
|
+
close: (animated?: boolean) => void
|
|
63
|
+
open: () => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SwipeableStatic {
|
|
67
|
+
open: (recyclingKey: string) => void
|
|
68
|
+
close: (recyclingKey: string, animated?: boolean) => void
|
|
69
|
+
closeAll: (animated?: boolean) => void
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
|
+
import { StyleSheet, View } from 'react-native'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for SwipeableActions component
|
|
6
|
+
*/
|
|
7
|
+
export interface SwipeableActionsProps {
|
|
8
|
+
/** Action components to render */
|
|
9
|
+
children: ReactNode
|
|
10
|
+
/** Position of actions: 'left' or 'right' */
|
|
11
|
+
actionsPosition: 'left' | 'right'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SwipeableActions - Internal layout component for swipeable actions
|
|
16
|
+
*
|
|
17
|
+
* Handles entrance animation when children mount.
|
|
18
|
+
*/
|
|
19
|
+
export function SwipeableActions({ children, actionsPosition }: SwipeableActionsProps) {
|
|
20
|
+
return (
|
|
21
|
+
<View
|
|
22
|
+
style={actionsPosition === 'left' ? styles.actionsLeading : styles.actionsTrailing}
|
|
23
|
+
pointerEvents={children ? 'auto' : 'none'}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</View>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const styles = StyleSheet.create({
|
|
31
|
+
actionsTrailing: {
|
|
32
|
+
...StyleSheet.absoluteFillObject,
|
|
33
|
+
flexDirection: 'row',
|
|
34
|
+
justifyContent: 'flex-end'
|
|
35
|
+
},
|
|
36
|
+
actionsLeading: {
|
|
37
|
+
...StyleSheet.absoluteFillObject,
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
justifyContent: 'flex-start'
|
|
40
|
+
}
|
|
41
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { forwardRef, createElement, ComponentProps } from 'react'
|
|
2
|
+
import { requireNativeViewManager, requireNativeModule } from 'expo-modules-core'
|
|
3
|
+
import { SwipeableViewProps, NativeSwipeableRef } from './Swipeable.types'
|
|
4
|
+
|
|
5
|
+
const NativeView = requireNativeViewManager<SwipeableViewProps>('Swipeable')
|
|
6
|
+
|
|
7
|
+
// Module-level functions interface
|
|
8
|
+
interface SwipeableNativeModule {
|
|
9
|
+
openByKey: (key: string) => void
|
|
10
|
+
closeByKey: (key: string, animated?: boolean) => void
|
|
11
|
+
closeAll: (animated?: boolean) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SwipeableModule = requireNativeModule<SwipeableNativeModule>('Swipeable')
|
|
15
|
+
|
|
16
|
+
export const SwipeableView = forwardRef<NativeSwipeableRef, SwipeableViewProps>(
|
|
17
|
+
function SwipeableView(props, ref) {
|
|
18
|
+
return createElement(NativeView, { ...props, ref } as ComponentProps<typeof NativeView>)
|
|
19
|
+
}
|
|
20
|
+
)
|
package/src/constants.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { default as Swipeable } from './Swipeable'
|
|
2
|
+
export { SwipeableView } from './SwipeableView'
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
SwipeableProps,
|
|
6
|
+
SwipeableMethods,
|
|
7
|
+
SwipeableStatic,
|
|
8
|
+
SwipeableViewProps,
|
|
9
|
+
SwipeProgressEvent,
|
|
10
|
+
SwipeStateEvent,
|
|
11
|
+
NativeSwipeableRef
|
|
12
|
+
} from './Swipeable.types'
|
|
13
|
+
|
|
14
|
+
export { SWIPE_PROGRESS, SWIPE_DEFAULTS } from './constants'
|
|
15
|
+
|
|
16
|
+
export { isSwipeFullyOpen, isSwipeFullyClosed, calculateSwipeStateChange } from './swipeStateUtils'
|
|
17
|
+
|
|
18
|
+
export type { SwipeState, SwipeStateChange } from './swipeStateUtils'
|
|
19
|
+
|
|
20
|
+
// Error handling utilities
|
|
21
|
+
export { setGlobalErrorHandler } from './nativeModuleUtils'
|
|
22
|
+
export type { SwipeableErrorHandler, NativeModuleError } from './nativeModuleUtils'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { SwipeableModule } from './SwipeableView'
|
|
2
|
+
|
|
3
|
+
export interface NativeModuleError {
|
|
4
|
+
code: string
|
|
5
|
+
message: string
|
|
6
|
+
nativeStackTrace?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type SwipeableErrorHandler = (
|
|
10
|
+
error: NativeModuleError,
|
|
11
|
+
operation: string,
|
|
12
|
+
key?: string
|
|
13
|
+
) => void
|
|
14
|
+
|
|
15
|
+
let globalErrorHandler: SwipeableErrorHandler | undefined
|
|
16
|
+
|
|
17
|
+
export function setGlobalErrorHandler(handler: SwipeableErrorHandler | undefined): void {
|
|
18
|
+
globalErrorHandler = handler
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleNativeError(error: unknown, operation: string, key?: string): void {
|
|
22
|
+
const nativeError: NativeModuleError = {
|
|
23
|
+
code: 'SWIPEABLE_NATIVE_ERROR',
|
|
24
|
+
message: error instanceof Error ? error.message : String(error),
|
|
25
|
+
...(error instanceof Error && error.stack ? { nativeStackTrace: error.stack } : {})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (globalErrorHandler) {
|
|
29
|
+
globalErrorHandler(nativeError, operation, key)
|
|
30
|
+
} else if (__DEV__) {
|
|
31
|
+
console.error(`[Swipeable] ${operation} failed:`, nativeError.message)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function openByKey(key: string): void {
|
|
36
|
+
try {
|
|
37
|
+
SwipeableModule.openByKey(key)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
handleNativeError(error, 'openByKey', key)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function closeByKey(key: string, animated?: boolean): void {
|
|
44
|
+
try {
|
|
45
|
+
SwipeableModule.closeByKey(key, animated)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
handleNativeError(error, 'closeByKey', key)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function closeAll(animated?: boolean): void {
|
|
52
|
+
try {
|
|
53
|
+
SwipeableModule.closeAll(animated)
|
|
54
|
+
} catch (error) {
|
|
55
|
+
handleNativeError(error, 'closeAll')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SWIPE_PROGRESS } from './constants'
|
|
2
|
+
|
|
3
|
+
export interface SwipeState {
|
|
4
|
+
wasOpen: boolean
|
|
5
|
+
swipeStarted: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SwipeStateChange {
|
|
9
|
+
nextState: SwipeState
|
|
10
|
+
shouldEmitEnd: boolean
|
|
11
|
+
/** Whether the open state changed (for onOpenChange callback) */
|
|
12
|
+
openStateChanged?: boolean
|
|
13
|
+
/** The new open state (true = opened, false = closed) */
|
|
14
|
+
newOpenState?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isSwipeFullyOpen(progress: number): boolean {
|
|
18
|
+
return progress >= SWIPE_PROGRESS.OPEN_THRESHOLD
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isSwipeFullyClosed(progress: number): boolean {
|
|
22
|
+
return progress <= SWIPE_PROGRESS.CLOSED_THRESHOLD
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function calculateSwipeStateChange(
|
|
26
|
+
progress: number,
|
|
27
|
+
currentState: SwipeState
|
|
28
|
+
): SwipeStateChange {
|
|
29
|
+
const isOpen = isSwipeFullyOpen(progress)
|
|
30
|
+
const isClosed = isSwipeFullyClosed(progress)
|
|
31
|
+
|
|
32
|
+
if (!currentState.swipeStarted) {
|
|
33
|
+
return { nextState: currentState, shouldEmitEnd: false }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isOpen && !currentState.wasOpen) {
|
|
37
|
+
return {
|
|
38
|
+
nextState: { wasOpen: true, swipeStarted: false },
|
|
39
|
+
shouldEmitEnd: true,
|
|
40
|
+
openStateChanged: true,
|
|
41
|
+
newOpenState: true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isClosed && currentState.wasOpen) {
|
|
46
|
+
return {
|
|
47
|
+
nextState: { wasOpen: false, swipeStarted: false },
|
|
48
|
+
shouldEmitEnd: true,
|
|
49
|
+
openStateChanged: true,
|
|
50
|
+
newOpenState: false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { nextState: currentState, shouldEmitEnd: false }
|
|
55
|
+
}
|