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.
Files changed (55) hide show
  1. package/README.md +250 -0
  2. package/android/build.gradle +99 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/expo/modules/swipeable/SwipeConstants.kt +29 -0
  5. package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +66 -0
  6. package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +69 -0
  7. package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +951 -0
  8. package/build/Swipeable.d.ts +10 -0
  9. package/build/Swipeable.d.ts.map +1 -0
  10. package/build/Swipeable.js +114 -0
  11. package/build/Swipeable.js.map +1 -0
  12. package/build/Swipeable.types.d.ts +64 -0
  13. package/build/Swipeable.types.d.ts.map +1 -0
  14. package/build/Swipeable.types.js +3 -0
  15. package/build/Swipeable.types.js.map +1 -0
  16. package/build/SwipeableActions.d.ts +17 -0
  17. package/build/SwipeableActions.d.ts.map +1 -0
  18. package/build/SwipeableActions.js +31 -0
  19. package/build/SwipeableActions.js.map +1 -0
  20. package/build/SwipeableView.d.ts +10 -0
  21. package/build/SwipeableView.d.ts.map +1 -0
  22. package/build/SwipeableView.js +11 -0
  23. package/build/SwipeableView.js.map +1 -0
  24. package/build/constants.d.ts +11 -0
  25. package/build/constants.d.ts.map +1 -0
  26. package/build/constants.js +14 -0
  27. package/build/constants.js.map +1 -0
  28. package/build/index.d.ts +9 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +21 -0
  31. package/build/index.js.map +1 -0
  32. package/build/nativeModuleUtils.d.ts +11 -0
  33. package/build/nativeModuleUtils.d.ts.map +1 -0
  34. package/build/nativeModuleUtils.js +49 -0
  35. package/build/nativeModuleUtils.js.map +1 -0
  36. package/build/swipeStateUtils.d.ts +16 -0
  37. package/build/swipeStateUtils.d.ts.map +1 -0
  38. package/build/swipeStateUtils.js +37 -0
  39. package/build/swipeStateUtils.js.map +1 -0
  40. package/expo-module.config.json +9 -0
  41. package/ios/Package.swift +28 -0
  42. package/ios/SwipeAnimationConfig.swift +27 -0
  43. package/ios/SwipePhysics.swift +66 -0
  44. package/ios/SwipeableModule.swift +84 -0
  45. package/ios/SwipeableView.swift +845 -0
  46. package/ios/react-native-swipeable-actions.podspec +22 -0
  47. package/package.json +85 -0
  48. package/src/Swipeable.tsx +195 -0
  49. package/src/Swipeable.types.ts +70 -0
  50. package/src/SwipeableActions.tsx +41 -0
  51. package/src/SwipeableView.tsx +20 -0
  52. package/src/constants.ts +11 -0
  53. package/src/index.ts +22 -0
  54. package/src/nativeModuleUtils.ts +57 -0
  55. 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
+ )
@@ -0,0 +1,11 @@
1
+ export const SWIPE_PROGRESS = {
2
+ OPEN_THRESHOLD: 0.99,
3
+ CLOSED_THRESHOLD: 0.01
4
+ } as const
5
+
6
+ export const SWIPE_DEFAULTS = {
7
+ FRICTION: 1,
8
+ THRESHOLD: 0.4,
9
+ DRAG_OFFSET_FROM_EDGE: 0,
10
+ ACTIONS_POSITION: 'right'
11
+ } as const
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
+ }