react-native-swipeable-actions 0.1.11 → 1.0.0-rc.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/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +377 -207
- package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +39 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +24 -10
- package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +804 -420
- package/build/Swipeable.d.ts +3 -8
- package/build/Swipeable.d.ts.map +1 -1
- package/build/Swipeable.js +87 -33
- package/build/Swipeable.js.map +1 -1
- package/build/Swipeable.types.d.ts +59 -9
- package/build/Swipeable.types.d.ts.map +1 -1
- package/build/SwipeableActions.d.ts +1 -1
- package/build/SwipeableActions.d.ts.map +1 -1
- package/build/SwipeableActions.js +4 -2
- package/build/SwipeableActions.js.map +1 -1
- package/build/SwipeableView.d.ts +11 -2
- package/build/SwipeableView.d.ts.map +1 -1
- package/build/SwipeableView.js.map +1 -1
- package/build/constants.d.ts +0 -1
- package/build/constants.d.ts.map +1 -1
- package/build/constants.js +0 -1
- package/build/constants.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/nativeModuleUtils.d.ts +3 -1
- package/build/nativeModuleUtils.d.ts.map +1 -1
- package/build/nativeModuleUtils.js +20 -9
- package/build/nativeModuleUtils.js.map +1 -1
- package/build/resolveSwipeableActions.d.ts +18 -0
- package/build/resolveSwipeableActions.d.ts.map +1 -0
- package/build/resolveSwipeableActions.js +26 -0
- package/build/resolveSwipeableActions.js.map +1 -0
- package/docs/migration-v1.md +191 -0
- package/docs/support.md +90 -0
- package/ios/SwipeAnimationConfig.swift +32 -0
- package/ios/SwipePhysics.swift +418 -0
- package/ios/SwipeableModule.swift +34 -12
- package/ios/SwipeableView.swift +1061 -600
- package/jestSetup.js +17 -5
- package/package.json +11 -5
- package/src/Swipeable.tsx +147 -54
- package/src/Swipeable.types.ts +50 -9
- package/src/SwipeableActions.tsx +6 -3
- package/src/SwipeableView.tsx +10 -3
- package/src/constants.ts +0 -1
- package/src/index.ts +7 -0
- package/src/nativeModuleUtils.ts +19 -8
- package/src/resolveSwipeableActions.ts +37 -0
package/jestSetup.js
CHANGED
|
@@ -7,16 +7,21 @@ jest.mock('./src/SwipeableView.tsx', () => {
|
|
|
7
7
|
closeByKey: jest.fn(),
|
|
8
8
|
cancelByKey: jest.fn(),
|
|
9
9
|
closeAll: jest.fn(),
|
|
10
|
-
isOpenByKey: jest.fn().mockReturnValue(true)
|
|
10
|
+
isOpenByKey: jest.fn().mockReturnValue(true),
|
|
11
|
+
openByKeyOnSide: jest.fn(),
|
|
12
|
+
getOpenSideByKey: jest.fn(() => null)
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const SwipeableView = React.forwardRef((props, ref) => {
|
|
16
|
+
// The native event props (onSwipeStart, onSwipeEnd and the others) stay on the
|
|
17
|
+
// host View, so a test can fire native-shaped events with fireEvent.
|
|
14
18
|
const {
|
|
15
19
|
children,
|
|
16
|
-
|
|
20
|
+
onActionsNeeded,
|
|
17
21
|
testID,
|
|
18
22
|
...rest
|
|
19
23
|
} = props
|
|
24
|
+
const { recyclingKey } = props
|
|
20
25
|
|
|
21
26
|
React.useImperativeHandle(ref, () => ({
|
|
22
27
|
close: jest.fn(),
|
|
@@ -25,8 +30,15 @@ jest.mock('./src/SwipeableView.tsx', () => {
|
|
|
25
30
|
}), [])
|
|
26
31
|
|
|
27
32
|
React.useEffect(() => {
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
// Match the native event shape: Swipeable reads the binding key from nativeEvent.
|
|
34
|
+
const nativeEvent = (extra) => ({
|
|
35
|
+
nativeEvent: { ...(recyclingKey != null ? { recyclingKey } : {}), ...extra }
|
|
36
|
+
})
|
|
37
|
+
// Mount both action rails so tests can find action content. This internal
|
|
38
|
+
// request calls no consumer callback. A mount never starts a swipe.
|
|
39
|
+
onActionsNeeded?.(nativeEvent({ side: 'left' }))
|
|
40
|
+
onActionsNeeded?.(nativeEvent({ side: 'right' }))
|
|
41
|
+
}, [onActionsNeeded, recyclingKey])
|
|
30
42
|
|
|
31
43
|
return React.createElement(
|
|
32
44
|
View,
|
|
@@ -44,4 +56,4 @@ jest.mock('./src/SwipeableView.tsx', () => {
|
|
|
44
56
|
SwipeableModule,
|
|
45
57
|
SwipeableView
|
|
46
58
|
}
|
|
47
|
-
})
|
|
59
|
+
})
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-swipeable-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
4
|
"description": "High-performance native swipeable row component for React Native using Expo Modules",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
+
"CHANGELOG.md",
|
|
9
|
+
"docs/migration-v1.md",
|
|
10
|
+
"docs/support.md",
|
|
8
11
|
"build",
|
|
9
12
|
"src",
|
|
10
13
|
"!src/__tests__",
|
|
11
14
|
"!src/__mocks__",
|
|
15
|
+
"!src/__type_tests__",
|
|
12
16
|
"ios",
|
|
13
17
|
"!ios/Tests",
|
|
14
18
|
"!ios/.build",
|
|
19
|
+
"!ios/build",
|
|
15
20
|
"android/src/main",
|
|
16
21
|
"android/build.gradle",
|
|
17
22
|
"expo-module.config.json",
|
|
@@ -33,6 +38,8 @@
|
|
|
33
38
|
"format:check": "biome format src example e2e",
|
|
34
39
|
"check": "biome check src example e2e",
|
|
35
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
+
"typecheck:fixtures": "tsc -p tsconfig.type-tests.json",
|
|
42
|
+
"test:package": "node --test scripts/package-artifact.test.mjs",
|
|
36
43
|
"android": "expo run:android",
|
|
37
44
|
"ios": "expo run:ios",
|
|
38
45
|
"e2e": "cd e2e && npm run test",
|
|
@@ -59,10 +66,9 @@
|
|
|
59
66
|
"author": "chocky335",
|
|
60
67
|
"license": "MIT",
|
|
61
68
|
"peerDependencies": {
|
|
62
|
-
"expo": ">=
|
|
63
|
-
"
|
|
64
|
-
"react": ">=
|
|
65
|
-
"react-native": ">=0.73.0"
|
|
69
|
+
"expo": ">=54.0.0",
|
|
70
|
+
"react": ">=19.1.0",
|
|
71
|
+
"react-native": ">=0.81.0"
|
|
66
72
|
},
|
|
67
73
|
"devDependencies": {
|
|
68
74
|
"@biomejs/biome": "2.4.9",
|
package/src/Swipeable.tsx
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Fragment,
|
|
3
|
+
type ForwardRefExoticComponent,
|
|
4
|
+
type RefAttributes,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState
|
|
12
|
+
} from 'react'
|
|
2
13
|
import { Platform, StyleSheet, View } from 'react-native'
|
|
3
14
|
import { SWIPE_DEFAULTS } from './constants'
|
|
4
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
cancelByKey,
|
|
17
|
+
closeAll,
|
|
18
|
+
closeByKey,
|
|
19
|
+
getOpenSideByKey,
|
|
20
|
+
openByKey,
|
|
21
|
+
openByKeyOnSide
|
|
22
|
+
} from './nativeModuleUtils'
|
|
23
|
+
import { resolveSwipeableActions } from './resolveSwipeableActions'
|
|
5
24
|
import type {
|
|
25
|
+
ActionsNeededEvent,
|
|
6
26
|
NativeSwipeableRef,
|
|
27
|
+
SwipeSide,
|
|
28
|
+
SwipeableComponentProps,
|
|
7
29
|
SwipeableMethods,
|
|
8
|
-
|
|
30
|
+
SwipeableStatic,
|
|
9
31
|
SwipeProgressEvent,
|
|
32
|
+
SwipeStartEvent,
|
|
10
33
|
SwipeStateEvent
|
|
11
34
|
} from './Swipeable.types'
|
|
12
35
|
import { SwipeableActions } from './SwipeableActions'
|
|
@@ -23,12 +46,17 @@ function normalizeRecyclingKey(key: string | number | undefined): string | undef
|
|
|
23
46
|
return undefined
|
|
24
47
|
}
|
|
25
48
|
|
|
26
|
-
const Swipeable = forwardRef<SwipeableMethods,
|
|
49
|
+
const Swipeable = forwardRef<SwipeableMethods, SwipeableComponentProps>(function Swipeable(
|
|
27
50
|
{
|
|
28
51
|
children,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
leftActions,
|
|
53
|
+
leftActionsWidth,
|
|
54
|
+
rightActions,
|
|
55
|
+
rightActionsWidth,
|
|
56
|
+
onLeftSwipeEnd,
|
|
57
|
+
onRightSwipeEnd,
|
|
58
|
+
appearance = 'default',
|
|
59
|
+
hapticFeedback,
|
|
32
60
|
friction = SWIPE_DEFAULTS.FRICTION,
|
|
33
61
|
threshold = SWIPE_DEFAULTS.THRESHOLD,
|
|
34
62
|
gestureEnabled = SWIPE_DEFAULTS.GESTURE_ENABLED,
|
|
@@ -46,28 +74,48 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(function Swipeabl
|
|
|
46
74
|
ref
|
|
47
75
|
) {
|
|
48
76
|
const nativeRef = useRef<NativeSwipeableRef>(null)
|
|
77
|
+
const resolved = useMemo(
|
|
78
|
+
() =>
|
|
79
|
+
resolveSwipeableActions({
|
|
80
|
+
leftActions,
|
|
81
|
+
...(leftActionsWidth !== undefined ? { leftActionsWidth } : {}),
|
|
82
|
+
rightActions,
|
|
83
|
+
...(rightActionsWidth !== undefined ? { rightActionsWidth } : {})
|
|
84
|
+
}),
|
|
85
|
+
[leftActions, leftActionsWidth, rightActions, rightActionsWidth]
|
|
86
|
+
)
|
|
49
87
|
|
|
50
88
|
// Normalize recyclingKey before hooks that depend on it
|
|
51
89
|
const normalizedKey = normalizeRecyclingKey(recyclingKey)
|
|
52
90
|
const recyclingKeyRef = useRef<string | undefined>(undefined)
|
|
53
91
|
recyclingKeyRef.current = normalizedKey
|
|
54
92
|
|
|
55
|
-
//
|
|
56
|
-
// This handles FlatList remounting components during data reorder - React state is lost
|
|
57
|
-
// but native cache preserves the open state.
|
|
58
|
-
const [hasActionsRendered, setHasActionsRendered] = useState(() => {
|
|
59
|
-
return normalizedKey != null && isOpenByKey(normalizedKey)
|
|
60
|
-
})
|
|
93
|
+
// Hydrate the cached physical edge, including after a keyed list remount.
|
|
61
94
|
const swipeStartedRef = useRef(false)
|
|
95
|
+
const cachedSide = useMemo(
|
|
96
|
+
() => (normalizedKey != null ? getOpenSideByKey(normalizedKey) : null),
|
|
97
|
+
[normalizedKey]
|
|
98
|
+
)
|
|
99
|
+
const initialSides = useMemo(
|
|
100
|
+
() => ({
|
|
101
|
+
key: normalizedKey,
|
|
102
|
+
left: cachedSide === 'left',
|
|
103
|
+
right: cachedSide === 'right'
|
|
104
|
+
}),
|
|
105
|
+
[normalizedKey, cachedSide]
|
|
106
|
+
)
|
|
107
|
+
const [renderedSides, setRenderedSides] = useState(initialSides)
|
|
108
|
+
const sides = renderedSides.key === normalizedKey ? renderedSides : initialSides
|
|
62
109
|
|
|
63
|
-
// When recyclingKey changes (FlatList reorder/recycle), sync hasActionsRendered
|
|
64
|
-
// with the native cached open state. FlatList reuses component instances without
|
|
65
|
-
// remounting, so useState initializer doesn't re-run.
|
|
66
110
|
useEffect(() => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
111
|
+
setRenderedSides((previous) => (previous.key === normalizedKey ? previous : initialSides))
|
|
112
|
+
swipeStartedRef.current = false
|
|
113
|
+
}, [initialSides, normalizedKey])
|
|
114
|
+
|
|
115
|
+
const warningText = resolved.warnings.join('\n')
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (__DEV__ && warningText) console.warn(`[Swipeable] ${warningText}`)
|
|
118
|
+
}, [warningText])
|
|
71
119
|
|
|
72
120
|
// Development-time validation
|
|
73
121
|
if (__DEV__) {
|
|
@@ -90,10 +138,11 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(function Swipeabl
|
|
|
90
138
|
console.warn('[Swipeable] close() called without recyclingKey - method has no effect')
|
|
91
139
|
}
|
|
92
140
|
},
|
|
93
|
-
open: () => {
|
|
141
|
+
open: (side?: SwipeSide) => {
|
|
94
142
|
const key = recyclingKeyRef.current
|
|
95
143
|
if (key) {
|
|
96
|
-
openByKey(key)
|
|
144
|
+
if (side === undefined) openByKey(key)
|
|
145
|
+
else openByKeyOnSide(key, side)
|
|
97
146
|
} else if (__DEV__) {
|
|
98
147
|
console.warn('[Swipeable] open() called without recyclingKey - method has no effect')
|
|
99
148
|
}
|
|
@@ -114,47 +163,80 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(function Swipeabl
|
|
|
114
163
|
[]
|
|
115
164
|
)
|
|
116
165
|
|
|
117
|
-
const handleSwipeStart = useCallback(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
const handleSwipeStart = useCallback(
|
|
167
|
+
(event: SwipeStartEvent) => {
|
|
168
|
+
// Missing metadata identifies the unkeyed native binding. It must not
|
|
169
|
+
// bypass identity checks after recycling into a keyed row.
|
|
170
|
+
if (event.nativeEvent.recyclingKey !== (normalizedKey || undefined)) return
|
|
171
|
+
if (!swipeStartedRef.current) {
|
|
172
|
+
swipeStartedRef.current = true
|
|
173
|
+
onSwipeStart?.()
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[normalizedKey, onSwipeStart]
|
|
177
|
+
)
|
|
121
178
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
179
|
+
const handleGestureCancelled = useCallback(
|
|
180
|
+
(event: { nativeEvent: { recyclingKey?: string } }) => {
|
|
181
|
+
if (event.nativeEvent.recyclingKey !== (normalizedKey || undefined)) return
|
|
182
|
+
swipeStartedRef.current = false
|
|
183
|
+
},
|
|
184
|
+
[normalizedKey]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const handleActionsNeeded = useCallback(
|
|
188
|
+
(event: ActionsNeededEvent) => {
|
|
189
|
+
const side = event.nativeEvent.side
|
|
190
|
+
if (event.nativeEvent.recyclingKey !== (normalizedKey || undefined)) return
|
|
191
|
+
if ((side !== 'left' && side !== 'right') || resolved[side].width <= 0) return
|
|
192
|
+
setRenderedSides((previous) => {
|
|
193
|
+
const current = previous.key === normalizedKey ? previous : initialSides
|
|
194
|
+
return current[side] ? current : { ...current, [side]: true }
|
|
195
|
+
})
|
|
196
|
+
},
|
|
197
|
+
[initialSides, normalizedKey, resolved]
|
|
198
|
+
)
|
|
127
199
|
|
|
128
200
|
const handleSwipeProgress = useCallback(
|
|
129
201
|
(event: SwipeProgressEvent) => {
|
|
202
|
+
if (event.nativeEvent.recyclingKey !== (normalizedKey || undefined)) return
|
|
130
203
|
const newProgress = event.nativeEvent.progress
|
|
131
204
|
const clamped = Math.max(0, Math.min(1, newProgress))
|
|
132
205
|
onProgress?.(clamped)
|
|
133
206
|
},
|
|
134
|
-
[onProgress]
|
|
207
|
+
[normalizedKey, onProgress]
|
|
135
208
|
)
|
|
136
209
|
|
|
137
210
|
const handleSwipeStateChange = useCallback(
|
|
138
211
|
(event: SwipeStateEvent) => {
|
|
212
|
+
if (event.nativeEvent.recyclingKey !== (normalizedKey || undefined)) return
|
|
139
213
|
onSwipeStateChange?.(event.nativeEvent.state)
|
|
140
214
|
},
|
|
141
|
-
[onSwipeStateChange]
|
|
215
|
+
[normalizedKey, onSwipeStateChange]
|
|
142
216
|
)
|
|
143
217
|
|
|
144
218
|
const handleSwipeEnd = useCallback(
|
|
145
219
|
(event: SwipeStateEvent) => {
|
|
220
|
+
if (event.nativeEvent.recyclingKey !== (normalizedKey || undefined)) return
|
|
146
221
|
swipeStartedRef.current = false
|
|
147
222
|
onSwipeEnd?.(event.nativeEvent.state)
|
|
223
|
+
const side = event.nativeEvent.side
|
|
224
|
+
if (side === 'left') onLeftSwipeEnd?.(event.nativeEvent.state)
|
|
225
|
+
if (side === 'right') onRightSwipeEnd?.(event.nativeEvent.state)
|
|
148
226
|
},
|
|
149
|
-
[onSwipeEnd]
|
|
227
|
+
[normalizedKey, onLeftSwipeEnd, onRightSwipeEnd, onSwipeEnd]
|
|
150
228
|
)
|
|
151
229
|
|
|
152
230
|
return (
|
|
153
231
|
<SwipeableView
|
|
154
232
|
ref={nativeRef}
|
|
155
233
|
style={[styles.container, style]}
|
|
156
|
-
|
|
157
|
-
|
|
234
|
+
leftActionsWidth={resolved.left.width}
|
|
235
|
+
rightActionsWidth={resolved.right.width}
|
|
236
|
+
onActionsNeeded={handleActionsNeeded}
|
|
237
|
+
{...(Platform.OS === 'ios'
|
|
238
|
+
? { appearance, hapticFeedback: hapticFeedback ?? appearance === 'ios26' }
|
|
239
|
+
: {})}
|
|
158
240
|
friction={friction}
|
|
159
241
|
threshold={threshold}
|
|
160
242
|
gestureEnabled={gestureEnabled}
|
|
@@ -164,20 +246,32 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(function Swipeabl
|
|
|
164
246
|
{...(normalizedKey ? { recyclingKey: normalizedKey } : {})}
|
|
165
247
|
{...(testID ? { testID } : {})}
|
|
166
248
|
onSwipeStart={handleSwipeStart}
|
|
249
|
+
onGestureCancelled={handleGestureCancelled}
|
|
167
250
|
onSwipeProgress={handleSwipeProgress}
|
|
168
251
|
onSwipeStateChange={handleSwipeStateChange}
|
|
169
252
|
onSwipeEnd={handleSwipeEnd}
|
|
170
253
|
>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<
|
|
254
|
+
<SwipeableActions
|
|
255
|
+
key='left-slot'
|
|
256
|
+
actionsPosition='left'
|
|
257
|
+
{...(testID ? { testID: `${testID}-left-actions` } : {})}
|
|
258
|
+
>
|
|
259
|
+
<Fragment key={normalizedKey ?? ''}>
|
|
260
|
+
{sides.left && resolved.left.width > 0 ? resolved.left.node : null}
|
|
261
|
+
</Fragment>
|
|
262
|
+
</SwipeableActions>
|
|
263
|
+
<SwipeableActions
|
|
264
|
+
key='right-slot'
|
|
265
|
+
actionsPosition='right'
|
|
266
|
+
{...(testID ? { testID: `${testID}-right-actions` } : {})}
|
|
267
|
+
>
|
|
268
|
+
<Fragment key={normalizedKey ?? ''}>
|
|
269
|
+
{sides.right && resolved.right.width > 0 ? resolved.right.node : null}
|
|
270
|
+
</Fragment>
|
|
271
|
+
</SwipeableActions>
|
|
272
|
+
<View key='content' style={styles.content} collapsable={false}>
|
|
273
|
+
{children}
|
|
274
|
+
</View>
|
|
181
275
|
</SwipeableView>
|
|
182
276
|
)
|
|
183
277
|
})
|
|
@@ -196,21 +290,20 @@ const styles = StyleSheet.create({
|
|
|
196
290
|
Swipeable.displayName = 'Swipeable'
|
|
197
291
|
|
|
198
292
|
// Static methods for controlling Swipeables by recyclingKey
|
|
199
|
-
type SwipeableWithStatic =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
closeAll: (animated?: boolean) => void
|
|
204
|
-
}
|
|
293
|
+
type SwipeableWithStatic = ForwardRefExoticComponent<
|
|
294
|
+
SwipeableComponentProps & RefAttributes<SwipeableMethods>
|
|
295
|
+
> &
|
|
296
|
+
SwipeableStatic
|
|
205
297
|
|
|
206
298
|
const SwipeableExport = Swipeable as SwipeableWithStatic
|
|
207
299
|
|
|
208
|
-
SwipeableExport.open = (recyclingKey: string) => {
|
|
300
|
+
SwipeableExport.open = (recyclingKey: string, side?: SwipeSide) => {
|
|
209
301
|
if (__DEV__ && (!recyclingKey || typeof recyclingKey !== 'string')) {
|
|
210
302
|
console.warn('[Swipeable.open] recyclingKey must be a non-empty string')
|
|
211
303
|
return
|
|
212
304
|
}
|
|
213
|
-
openByKey(recyclingKey)
|
|
305
|
+
if (side === undefined) openByKey(recyclingKey)
|
|
306
|
+
else openByKeyOnSide(recyclingKey, side)
|
|
214
307
|
}
|
|
215
308
|
|
|
216
309
|
SwipeableExport.close = (recyclingKey: string, animated?: boolean) => {
|
package/src/Swipeable.types.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import type { ReactNode, Ref } from 'react'
|
|
2
2
|
import type { StyleProp, ViewProps, ViewStyle } from 'react-native'
|
|
3
3
|
|
|
4
|
+
export type SwipeSide = 'left' | 'right'
|
|
5
|
+
export type SwipeAppearance = 'default' | 'ios26'
|
|
6
|
+
|
|
7
|
+
/** Internal native identity; the consumer's onSwipeStart remains zero-argument. */
|
|
8
|
+
export interface SwipeStartEvent {
|
|
9
|
+
nativeEvent: { recyclingKey?: string; side?: SwipeSide }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ActionsNeededEvent {
|
|
13
|
+
nativeEvent: { side: SwipeSide; recyclingKey?: string }
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
export interface SwipeProgressEvent {
|
|
5
17
|
nativeEvent: {
|
|
6
18
|
progress: number
|
|
7
19
|
translationX: number
|
|
20
|
+
side?: SwipeSide
|
|
21
|
+
recyclingKey?: string
|
|
8
22
|
}
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
export interface SwipeStateEvent {
|
|
12
26
|
nativeEvent: {
|
|
13
27
|
state: 'open' | 'closed'
|
|
28
|
+
side?: SwipeSide
|
|
29
|
+
recyclingKey?: string
|
|
14
30
|
}
|
|
15
31
|
}
|
|
16
32
|
|
|
@@ -20,16 +36,20 @@ export interface NativeSwipeableRef {
|
|
|
20
36
|
cancel: () => Promise<void>
|
|
21
37
|
}
|
|
22
38
|
|
|
39
|
+
/** Native child order is always left slot, right slot, then content. */
|
|
23
40
|
export interface SwipeableViewProps extends ViewProps {
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
leftActionsWidth?: number
|
|
42
|
+
rightActionsWidth?: number
|
|
43
|
+
onActionsNeeded?: (event: ActionsNeededEvent) => void
|
|
44
|
+
appearance?: SwipeAppearance
|
|
45
|
+
hapticFeedback?: boolean
|
|
26
46
|
friction?: number
|
|
27
47
|
threshold?: number
|
|
28
48
|
gestureEnabled?: boolean
|
|
29
49
|
dragOffsetFromEdge?: number
|
|
30
50
|
recyclingKey?: string
|
|
31
51
|
onSwipeProgress?: (event: SwipeProgressEvent) => void
|
|
32
|
-
onSwipeStart?: () => void
|
|
52
|
+
onSwipeStart?: (event: SwipeStartEvent) => void
|
|
33
53
|
onSwipeStateChange?: (event: SwipeStateEvent) => void
|
|
34
54
|
onSwipeEnd?: (event: SwipeStateEvent) => void
|
|
35
55
|
autoClose?: boolean
|
|
@@ -40,11 +60,13 @@ export interface SwipeableViewProps extends ViewProps {
|
|
|
40
60
|
testID?: string
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
/** Props that every side configuration shares. Wrappers that emit declarations name it. */
|
|
64
|
+
export interface SwipeableCommonProps {
|
|
44
65
|
children: ReactNode
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
onLeftSwipeEnd?: (state: 'open' | 'closed') => void
|
|
67
|
+
onRightSwipeEnd?: (state: 'open' | 'closed') => void
|
|
68
|
+
appearance?: SwipeAppearance
|
|
69
|
+
hapticFeedback?: boolean
|
|
48
70
|
friction?: number
|
|
49
71
|
threshold?: number
|
|
50
72
|
gestureEnabled?: boolean
|
|
@@ -63,13 +85,32 @@ export interface SwipeableProps {
|
|
|
63
85
|
|
|
64
86
|
export interface SwipeableMethods {
|
|
65
87
|
close: (animated?: boolean) => void
|
|
66
|
-
|
|
88
|
+
/** Open the requested physical edge; without a side, prefer enabled right, then left. */
|
|
89
|
+
open: (side?: SwipeSide) => void
|
|
67
90
|
cancel: () => void
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
export interface SwipeableStatic {
|
|
71
|
-
open: (recyclingKey: string) => void
|
|
94
|
+
open: (recyclingKey: string, side?: SwipeSide) => void
|
|
72
95
|
close: (recyclingKey: string, animated?: boolean) => void
|
|
73
96
|
cancel: (recyclingKey: string) => void
|
|
74
97
|
closeAll: (animated?: boolean) => void
|
|
75
98
|
}
|
|
99
|
+
|
|
100
|
+
type LeftActionsConfiguration =
|
|
101
|
+
| { leftActions: ReactNode; leftActionsWidth: number }
|
|
102
|
+
| { leftActions?: null | false | undefined; leftActionsWidth?: never }
|
|
103
|
+
|
|
104
|
+
type RightActionsConfiguration =
|
|
105
|
+
| { rightActions: ReactNode; rightActionsWidth: number }
|
|
106
|
+
| { rightActions?: null | false | undefined; rightActionsWidth?: never }
|
|
107
|
+
|
|
108
|
+
/** The sole action API: explicit physical-edge pairs, including single-sided rows. */
|
|
109
|
+
export type SwipeableProps = SwipeableCommonProps &
|
|
110
|
+
LeftActionsConfiguration &
|
|
111
|
+
RightActionsConfiguration &
|
|
112
|
+
({ leftActions: ReactNode } | { rightActions: ReactNode })
|
|
113
|
+
|
|
114
|
+
/** Descriptive alias for the same explicit-side API, not a separate implementation. */
|
|
115
|
+
export type TwoSidedSwipeableProps = SwipeableProps
|
|
116
|
+
export type SwipeableComponentProps = SwipeableProps
|
package/src/SwipeableActions.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
-
import { StyleSheet, View } from 'react-native'
|
|
2
|
+
import { I18nManager, StyleSheet, View } from 'react-native'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Props for SwipeableActions component
|
|
@@ -16,14 +16,17 @@ export interface SwipeableActionsProps {
|
|
|
16
16
|
/**
|
|
17
17
|
* SwipeableActions - Internal layout component for swipeable actions
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* Provides a stable physical-edge slot while action content mounts lazily.
|
|
20
20
|
*/
|
|
21
21
|
export function SwipeableActions({ children, actionsPosition, testID }: SwipeableActionsProps) {
|
|
22
|
+
// Side names are physical edges; flex row start/end reverse in RTL.
|
|
23
|
+
const leading = I18nManager.isRTL ? actionsPosition === 'right' : actionsPosition === 'left'
|
|
22
24
|
return (
|
|
23
25
|
<View
|
|
24
|
-
style={
|
|
26
|
+
style={leading ? styles.actionsLeading : styles.actionsTrailing}
|
|
25
27
|
pointerEvents={children ? 'auto' : 'none'}
|
|
26
28
|
testID={testID}
|
|
29
|
+
collapsable={false}
|
|
27
30
|
>
|
|
28
31
|
{children}
|
|
29
32
|
</View>
|
package/src/SwipeableView.tsx
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { requireNativeModule, requireNativeViewManager } from 'expo-modules-core'
|
|
2
2
|
import { type ComponentProps, createElement, forwardRef } from 'react'
|
|
3
|
-
import type { NativeSwipeableRef, SwipeableViewProps } from './Swipeable.types'
|
|
3
|
+
import type { NativeSwipeableRef, SwipeableViewProps, SwipeSide } from './Swipeable.types'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type NativeViewProps = SwipeableViewProps & {
|
|
6
|
+
/** Internal lifecycle notification; never forwarded as a consumer completion. */
|
|
7
|
+
onGestureCancelled?: (event: { nativeEvent: { recyclingKey?: string } }) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const NativeView = requireNativeViewManager<NativeViewProps>('Swipeable')
|
|
6
11
|
|
|
7
12
|
// Module-level functions interface
|
|
8
13
|
interface SwipeableNativeModule {
|
|
@@ -11,11 +16,13 @@ interface SwipeableNativeModule {
|
|
|
11
16
|
cancelByKey: (key: string) => void
|
|
12
17
|
closeAll: (animated?: boolean) => void
|
|
13
18
|
isOpenByKey: (key: string) => boolean
|
|
19
|
+
openByKeyOnSide: (key: string, side: SwipeSide) => void
|
|
20
|
+
getOpenSideByKey: (key: string) => SwipeSide | null
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export const SwipeableModule = requireNativeModule<SwipeableNativeModule>('Swipeable')
|
|
17
24
|
|
|
18
|
-
export const SwipeableView = forwardRef<NativeSwipeableRef,
|
|
25
|
+
export const SwipeableView = forwardRef<NativeSwipeableRef, NativeViewProps>(
|
|
19
26
|
function SwipeableView(props, ref) {
|
|
20
27
|
return createElement(NativeView, { ...props, ref } as ComponentProps<typeof NativeView>)
|
|
21
28
|
}
|
package/src/constants.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,12 +4,19 @@ export type { NativeModuleError, SwipeableErrorHandler } from './nativeModuleUti
|
|
|
4
4
|
export { setGlobalErrorHandler } from './nativeModuleUtils'
|
|
5
5
|
export { default as Swipeable } from './Swipeable'
|
|
6
6
|
export type {
|
|
7
|
+
ActionsNeededEvent,
|
|
7
8
|
NativeSwipeableRef,
|
|
9
|
+
SwipeAppearance,
|
|
10
|
+
SwipeSide,
|
|
11
|
+
SwipeableCommonProps,
|
|
12
|
+
SwipeableComponentProps,
|
|
13
|
+
TwoSidedSwipeableProps,
|
|
8
14
|
SwipeableMethods,
|
|
9
15
|
SwipeableProps,
|
|
10
16
|
SwipeableStatic,
|
|
11
17
|
SwipeableViewProps,
|
|
12
18
|
SwipeProgressEvent,
|
|
19
|
+
SwipeStartEvent,
|
|
13
20
|
SwipeStateEvent
|
|
14
21
|
} from './Swipeable.types'
|
|
15
22
|
export { SwipeableView } from './SwipeableView'
|
package/src/nativeModuleUtils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SwipeableModule } from './SwipeableView'
|
|
2
|
+
import type { SwipeSide } from './Swipeable.types'
|
|
2
3
|
|
|
3
4
|
export interface NativeModuleError {
|
|
4
5
|
code: string
|
|
@@ -40,6 +41,24 @@ export function openByKey(key: string): void {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
export function openByKeyOnSide(key: string, side: SwipeSide): void {
|
|
45
|
+
try {
|
|
46
|
+
SwipeableModule.openByKeyOnSide(key, side)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
handleNativeError(error, 'openByKeyOnSide', key)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getOpenSideByKey(key: string): SwipeSide | null {
|
|
53
|
+
try {
|
|
54
|
+
const side = SwipeableModule.getOpenSideByKey(key)
|
|
55
|
+
return side === 'left' || side === 'right' ? side : null
|
|
56
|
+
} catch (error) {
|
|
57
|
+
handleNativeError(error, 'getOpenSideByKey', key)
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
export function closeByKey(key: string, animated?: boolean): void {
|
|
44
63
|
try {
|
|
45
64
|
SwipeableModule.closeByKey(key, animated)
|
|
@@ -63,11 +82,3 @@ export function closeAll(animated?: boolean): void {
|
|
|
63
82
|
handleNativeError(error, 'closeAll')
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
|
-
|
|
67
|
-
export function isOpenByKey(key: string): boolean {
|
|
68
|
-
try {
|
|
69
|
-
return SwipeableModule.isOpenByKey(key)
|
|
70
|
-
} catch {
|
|
71
|
-
return false
|
|
72
|
-
}
|
|
73
|
-
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { SwipeSide } from './Swipeable.types'
|
|
3
|
+
|
|
4
|
+
interface ActionsInput {
|
|
5
|
+
leftActions?: ReactNode
|
|
6
|
+
leftActionsWidth?: number
|
|
7
|
+
rightActions?: ReactNode
|
|
8
|
+
rightActionsWidth?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResolvedActions {
|
|
12
|
+
node: ReactNode
|
|
13
|
+
width: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isContent = (node: ReactNode) => node != null && typeof node !== 'boolean'
|
|
17
|
+
const isWidth = (width: number | undefined): width is number =>
|
|
18
|
+
width !== undefined && Number.isFinite(width) && width > 0
|
|
19
|
+
|
|
20
|
+
export function resolveSwipeableActions(input: ActionsInput) {
|
|
21
|
+
const warnings: string[] = []
|
|
22
|
+
const resolve = (side: SwipeSide, node: ReactNode, width?: number): ResolvedActions => {
|
|
23
|
+
if (node !== undefined) {
|
|
24
|
+
if (!isContent(node)) return { node: null, width: 0 }
|
|
25
|
+
if (isWidth(width)) return { node, width }
|
|
26
|
+
warnings.push(side + 'Actions requires a finite, positive ' + side + 'ActionsWidth.')
|
|
27
|
+
return { node: null, width: 0 }
|
|
28
|
+
}
|
|
29
|
+
if (width !== undefined) {
|
|
30
|
+
warnings.push(side + 'ActionsWidth is ignored without explicit ' + side + 'Actions.')
|
|
31
|
+
}
|
|
32
|
+
return { node: null, width: 0 }
|
|
33
|
+
}
|
|
34
|
+
const left = resolve('left', input.leftActions, input.leftActionsWidth)
|
|
35
|
+
const right = resolve('right', input.rightActions, input.rightActionsWidth)
|
|
36
|
+
return { left, right, warnings }
|
|
37
|
+
}
|