react-native-swipeable-actions 0.1.12 → 1.0.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 (50) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +377 -207
  4. package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +39 -0
  5. package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +24 -10
  6. package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +804 -420
  7. package/build/Swipeable.d.ts +3 -8
  8. package/build/Swipeable.d.ts.map +1 -1
  9. package/build/Swipeable.js +87 -33
  10. package/build/Swipeable.js.map +1 -1
  11. package/build/Swipeable.types.d.ts +59 -9
  12. package/build/Swipeable.types.d.ts.map +1 -1
  13. package/build/SwipeableActions.d.ts +1 -1
  14. package/build/SwipeableActions.d.ts.map +1 -1
  15. package/build/SwipeableActions.js +4 -2
  16. package/build/SwipeableActions.js.map +1 -1
  17. package/build/SwipeableView.d.ts +11 -2
  18. package/build/SwipeableView.d.ts.map +1 -1
  19. package/build/SwipeableView.js.map +1 -1
  20. package/build/constants.d.ts +0 -1
  21. package/build/constants.d.ts.map +1 -1
  22. package/build/constants.js +0 -1
  23. package/build/constants.js.map +1 -1
  24. package/build/index.d.ts +1 -1
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js.map +1 -1
  27. package/build/nativeModuleUtils.d.ts +3 -1
  28. package/build/nativeModuleUtils.d.ts.map +1 -1
  29. package/build/nativeModuleUtils.js +20 -9
  30. package/build/nativeModuleUtils.js.map +1 -1
  31. package/build/resolveSwipeableActions.d.ts +18 -0
  32. package/build/resolveSwipeableActions.d.ts.map +1 -0
  33. package/build/resolveSwipeableActions.js +26 -0
  34. package/build/resolveSwipeableActions.js.map +1 -0
  35. package/docs/migration-v1.md +191 -0
  36. package/docs/support.md +90 -0
  37. package/ios/SwipeAnimationConfig.swift +32 -0
  38. package/ios/SwipePhysics.swift +418 -0
  39. package/ios/SwipeableModule.swift +34 -12
  40. package/ios/SwipeableView.swift +1062 -613
  41. package/jestSetup.js +17 -5
  42. package/package.json +11 -5
  43. package/src/Swipeable.tsx +147 -54
  44. package/src/Swipeable.types.ts +50 -9
  45. package/src/SwipeableActions.tsx +6 -3
  46. package/src/SwipeableView.tsx +10 -3
  47. package/src/constants.ts +0 -1
  48. package/src/index.ts +7 -0
  49. package/src/nativeModuleUtils.ts +19 -8
  50. package/src/resolveSwipeableActions.ts +37 -0
package/README.md CHANGED
@@ -1,278 +1,448 @@
1
1
  # react-native-swipeable-actions
2
2
 
3
- A high-performance native swipeable row component for React Native. Native alternative to `react-native-gesture-handler/Swipeable` - no `react-native-reanimated` required.
4
-
5
- ## Features
6
-
7
- - Pure native gestures - no JS thread blocking
8
- - 60fps spring animations on both iOS and Android
9
- - No `react-native-reanimated` dependency
10
- - Built-in FlashList/virtualized list support via `recyclingKey`
11
- - AutoClose pattern for chat-style swipe-to-reply
12
- - Lazy action rendering for optimal performance
13
-
14
- ## Performance
15
-
16
- Benchmark comparison on Android 16, Pixel 8 Pro, GrapheneOS — List Demo with 100 items:
17
-
18
- <table>
19
- <tr>
20
- <th>react-native-gesture-handler/Swipeable</th>
21
- <th>react-native-swipeable-actions</th>
22
- </tr>
23
- <tr>
24
- <td>
25
-
26
- https://github.com/user-attachments/assets/6d7cf1e6-c719-4044-8b47-205e94a38d10
27
-
28
- </td>
29
- <td>
30
-
31
- https://github.com/user-attachments/assets/f59ef788-caf4-4985-93bc-8d098122fe1a
32
-
33
- </td></tr>
34
- <tr>
35
- <td align="center">Reanimated</td>
36
- <td align="center">Pure native</td>
37
- </tr>
38
- <tr>
39
- <td align="center"><b>Avg 60.9</b> FPS (Min: 56.7 / Max: 65.7)</td>
40
- <td align="center"><b>Avg 116.8</b> FPS (Min: 109.9 / Max: 120.3)</td>
41
- </tr>
42
- </table>
43
-
44
- > Videos not loading? View on [GitHub](https://github.com/chocky335/react-native-swipeable-actions#performance)
3
+ [![npm](https://img.shields.io/npm/v/react-native-swipeable-actions)](https://www.npmjs.com/package/react-native-swipeable-actions)
4
+ [![CI](https://github.com/chocky335/react-native-swipeable-actions/actions/workflows/ci.yml/badge.svg)](https://github.com/chocky335/react-native-swipeable-actions/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/react-native-swipeable-actions)](LICENSE)
6
+
7
+ Native swipeable rows for React Native. Gestures, springs and progress run in
8
+ Swift and Kotlin, so a busy JavaScript thread does not drop swipe frames. It is a
9
+ native alternative to `react-native-gesture-handler/Swipeable` and needs no
10
+ `react-native-reanimated`.
11
+
12
+ - Independent left and right actions on the same row, each with its own width
13
+ - Action content mounts on demand, on the first swipe or programmatic open
14
+ - Open state survives FlatList and FlashList recycling through `recyclingKey`
15
+ - Ref and static commands: open a chosen side, close, cancel, close all
16
+ - Auto-close for chat-style swipe-to-reply
17
+ - Opt-in iOS-style action motion and native iOS haptics
18
+ - Physical left/right sides that behave the same in RTL layouts
19
+
20
+ > Upgrading from 0.x? Version 1.0 replaces `actions`, `actionsWidth` and
21
+ > `actionsPosition` with explicit left/right props. Read the
22
+ > [migration guide](docs/migration-v1.md).
45
23
 
46
24
  ## Installation
47
25
 
48
- ```bash
49
- npm install react-native-swipeable-actions
26
+ ```sh
27
+ npx expo install react-native-swipeable-actions
50
28
  ```
51
29
 
52
- ### Peer Dependencies
30
+ This is an [Expo module](https://docs.expo.dev/modules/overview/) with native code.
53
31
 
54
- ```json
55
- {
56
- "expo": ">=50.0.0",
57
- "expo-modules-core": ">=1.0.0",
58
- "react": ">=18.0.0",
59
- "react-native": ">=0.73.0"
60
- }
61
- ```
32
+ - Requirements: Expo SDK 54 or newer, React Native 0.81 or newer, React 19.1 or
33
+ newer, and the New Architecture (Fabric). The legacy architecture (Paper) is
34
+ not supported.
35
+ - Expo: use a [development build](https://docs.expo.dev/develop/development-builds/introduction/)
36
+ or `npx expo run:ios` / `npx expo run:android`. Expo Go cannot load it.
37
+ - Bare React Native: set up Expo Modules first with `npx install-expo-modules@latest`,
38
+ then run `pod install`.
39
+ - Rebuild the iOS and Android apps after you install or upgrade this package.
40
+ A JavaScript reload or an OTA update cannot change the native code.
62
41
 
63
- ## Basic Usage
42
+ See [compatibility](#compatibility) for the configurations that were tested.
64
43
 
65
- ```tsx
66
- import { Swipeable, SwipeableMethods } from 'react-native-swipeable-actions'
67
- import { useRef } from 'react'
68
- import { View, Text, TouchableOpacity } from 'react-native'
44
+ ## Quick start
69
45
 
70
- function DeleteAction({ onPress }: { onPress: () => void }) {
71
- return (
72
- <TouchableOpacity onPress={onPress} style={styles.deleteAction}>
73
- <Text style={styles.actionText}>Delete</Text>
74
- </TouchableOpacity>
75
- )
76
- }
46
+ ```tsx
47
+ import { useRef } from "react";
48
+ import { Pressable, Text, View } from "react-native";
49
+ import { Swipeable, type SwipeableMethods } from "react-native-swipeable-actions";
77
50
 
78
- function MyRow() {
79
- const swipeableRef = useRef<SwipeableMethods>(null)
51
+ export function MessageRow({ id, onArchive }: { id: string; onArchive: () => void }) {
52
+ const row = useRef<SwipeableMethods>(null);
80
53
 
81
54
  return (
82
55
  <Swipeable
83
- ref={swipeableRef}
84
- actions={<DeleteAction onPress={() => console.log('Delete!')} />}
85
- actionsWidth={80}
86
- onSwipeEnd={(state) => console.log('Swipe ended:', state)}
56
+ ref={row}
57
+ recyclingKey={id}
58
+ rightActions={
59
+ <Pressable
60
+ onPress={() => {
61
+ onArchive();
62
+ row.current?.close();
63
+ }}
64
+ style={{ width: 88, height: "100%", alignItems: "center", justifyContent: "center", backgroundColor: "#d33" }}
65
+ >
66
+ <Text style={{ color: "#fff" }}>Archive</Text>
67
+ </Pressable>
68
+ }
69
+ rightActionsWidth={88}
87
70
  >
88
- <View style={styles.row}>
89
- <Text>Swipe me left</Text>
71
+ <View style={{ minHeight: 64, padding: 16, backgroundColor: "#fff" }}>
72
+ <Text>Swipe left to archive</Text>
90
73
  </View>
91
74
  </Swipeable>
92
- )
75
+ );
93
76
  }
94
77
  ```
95
78
 
96
- ## Props
79
+ Each side is a pair: content plus its width in layout units.
97
80
 
98
- | Prop | Type | Default | Description |
99
- |------|------|---------|-------------|
100
- | `children` | `ReactNode` | required | Row content |
101
- | `actions` | `ReactNode` | required | Action buttons revealed on swipe |
102
- | `actionsWidth` | `number` | required | Width of actions container in pixels |
103
- | `actionsPosition` | `'left' \| 'right'` | `'right'` | Position of actions (`'left'` = swipe right to reveal) |
104
- | `friction` | `number` | `1` | Drag damping factor (0-1). Lower = more resistance |
105
- | `threshold` | `number` | `0.4` | Snap-to threshold as percentage of actionsWidth (0-1) |
106
- | `gestureEnabled` | `boolean` | `true` | Enables or disables the swipe gesture |
107
- | `dragOffsetFromEdge` | `number` | `0` | Minimum drag distance before gesture starts |
108
- | `autoClose` | `boolean` | `false` | Auto-close after swipe release (for swipe-to-reply) |
109
- | `autoCloseTimeout` | `number` | `0` | Delay in ms before auto-closing |
110
- | `recyclingKey` | `string \| number` | - | Unique key for FlashList/recycling support |
111
- | `style` | `StyleProp<ViewStyle>` | - | Container style |
112
- | `testID` | `string` | - | Test ID for e2e testing |
113
-
114
- ## Callbacks
115
-
116
- | Callback | Type | Description |
117
- |----------|------|-------------|
118
- | `onSwipeStart` | `() => void` | Called when swipe gesture begins |
119
- | `onSwipeStateChange` | `(state: 'open' \| 'closed') => void` | Called when gesture ends (before animation) |
120
- | `onSwipeEnd` | `(state: 'open' \| 'closed') => void` | Called when animation completes |
121
- | `onProgress` | `(progress: number) => void` | Called on each frame (0 = closed, 1 = fully open) |
122
-
123
- ## Ref Methods
124
-
125
- Access via ref:
81
+ - The width is the full reveal distance, including padding and gaps. The library
82
+ does not measure action content.
83
+ - The action rail spans the row. Give the action content its own width and height.
84
+ - Omit a side, or pass `null` or `false`, to disable it. Omit its width too.
85
+ - A disabled side never falls back to the other side.
86
+
87
+ ## Two-sided rows
126
88
 
127
89
  ```tsx
128
- const swipeableRef = useRef<SwipeableMethods>(null)
90
+ <Swipeable
91
+ recyclingKey={item.id}
92
+ leftActions={<ReplyAction />}
93
+ leftActionsWidth={72}
94
+ rightActions={<DeleteAndMoreActions />}
95
+ rightActionsWidth={160}
96
+ onLeftSwipeEnd={(state) => state === "open" && markRead(item.id)}
97
+ >
98
+ <Message item={item} />
99
+ </Swipeable>
100
+ ```
129
101
 
130
- // Close the row (with optional animation)
131
- swipeableRef.current?.close() // animated
132
- swipeableRef.current?.close(false) // instant
102
+ - Drag right to reveal `leftActions`. Drag left to reveal `rightActions`.
103
+ - One gesture stays on one side. A drag that crosses the closed position does not
104
+ open the other side until the next gesture.
105
+ - Content and widths can change while the row is mounted.
133
106
 
134
- // Open the row
135
- swipeableRef.current?.open()
107
+ ## Lists and recycling
136
108
 
137
- // Cancel an in-flight gesture and snap back to closed.
138
- // Use from `onSwipeStart` to abort a swipe before it can complete —
139
- // e.g. when the parent screen has unsaved state and wants to show a
140
- // confirm dialog instead of letting the swipe-to-pop proceed.
141
- swipeableRef.current?.cancel()
142
- ```
109
+ Give every row a stable, unique `recyclingKey`. The native view caches the open
110
+ side per key, so a recycled cell shows the correct state for its new item.
143
111
 
144
- ## Static Methods
112
+ ```tsx
113
+ import { FlashList } from "@shopify/flash-list";
145
114
 
146
- Control swipeables globally by their `recyclingKey`:
115
+ <FlashList
116
+ data={messages}
117
+ renderItem={({ item }) => (
118
+ <Swipeable recyclingKey={item.id} rightActions={<DeleteAction id={item.id} />} rightActionsWidth={80}>
119
+ <MessageRow message={item} />
120
+ </Swipeable>
121
+ )}
122
+ />;
123
+ ```
147
124
 
148
- ```tsx
149
- import { Swipeable } from 'react-native-swipeable-actions'
125
+ - The key identifies the item, not its index or its current language.
126
+ - A row needs a `recyclingKey` if its open state must survive recycling or a
127
+ screen change.
128
+ - If a cached side is now disabled, the row restores closed.
129
+ - If a side's width changed, the row restores at the new width.
130
+ - Rows do not close their siblings automatically. Call `Swipeable.closeAll()` or
131
+ `Swipeable.close(key)` when your UI needs it.
150
132
 
151
- // Open a specific row
152
- Swipeable.open('row-1')
133
+ ## Swipe to reply
153
134
 
154
- // Close a specific row
155
- Swipeable.close('row-1') // animated
156
- Swipeable.close('row-1', false) // instant
135
+ With `autoClose`, a completed swipe snaps back closed and reports
136
+ `onSwipeEnd("open")` once. The `"open"` state means the swipe passed the threshold.
137
+ It does not mean the row stays open.
157
138
 
158
- // Close all open rows
159
- Swipeable.closeAll() // animated
160
- Swipeable.closeAll(false) // instant
139
+ `autoClose` applies to swipes only. A programmatic `open()` on an `autoClose` row
140
+ stays open and does not report `onSwipeEnd`.
161
141
 
162
- // Cancel an in-flight gesture on a specific row
163
- Swipeable.cancel('row-1')
142
+ ```tsx
143
+ <Swipeable
144
+ recyclingKey={message.id}
145
+ leftActions={<ReplyIcon />}
146
+ leftActionsWidth={60}
147
+ autoClose
148
+ threshold={0.6}
149
+ onSwipeEnd={(state) => state === "open" && startReply(message)}
150
+ >
151
+ <ChatBubble message={message} />
152
+ </Swipeable>
164
153
  ```
165
154
 
166
- ## AutoClose Pattern (Swipe-to-Reply)
155
+ ## Swipe to go back
167
156
 
168
- For chat-style swipe-to-reply where the row should automatically close:
157
+ Wrap a whole screen to slide it right with the finger. A full swipe goes back, and
158
+ nothing shows behind the screen. Give the left side empty content as wide as the
159
+ window. Do not pass `null`, because `null` disables the side.
169
160
 
170
161
  ```tsx
162
+ const { width } = useWindowDimensions();
163
+ const screen = useRef<SwipeableMethods>(null);
164
+ const key = useId();
165
+
171
166
  <Swipeable
172
- actions={<ReplyIndicator />}
173
- actionsWidth={60}
174
- actionsPosition="right"
175
- autoClose={true}
176
- threshold={0.6}
167
+ ref={screen}
168
+ recyclingKey={key}
169
+ leftActions={<View />}
170
+ leftActionsWidth={width}
171
+ dragOffsetFromEdge={20}
172
+ onSwipeStart={() => hasUnsavedDraft() && screen.current?.cancel()}
177
173
  onSwipeEnd={(state) => {
178
- if (state === 'open') {
179
- // Trigger reply action
180
- onReply(message)
181
- }
174
+ if (state !== "open") return;
175
+ if (hasUnsavedDraft()) screen.current?.close();
176
+ else navigation.goBack();
182
177
  }}
183
178
  >
184
- <ChatMessage message={message} />
179
+ <ChatScreen />
185
180
  </Swipeable>
186
181
  ```
187
182
 
188
- ## FlashList Integration
183
+ - `onSwipeStart` is an optional guard. `cancel()` closes the row at once, and the
184
+ swipe does not complete.
185
+ - `onSwipeStart` reaches JavaScript asynchronously. If the swipe settles open before
186
+ your `cancel()` runs, `onSwipeEnd("open")` has already been sent. A busy
187
+ JavaScript thread makes this more likely. Check the guard again in `onSwipeEnd`,
188
+ and call `close()` instead of navigating.
189
+ - `cancel()` and `close()` need a `recyclingKey`. Use a new key for each mount of the screen.
190
+ Then a later mount does not restore the open state of a screen that you swiped away.
191
+ - If the navigator has its own back gesture on that screen, turn it off. For a
192
+ native stack, set `gestureEnabled: false`.
189
193
 
190
- For virtualized lists, use `recyclingKey` to persist swipe state across recycling:
194
+ The example app has a small reusable wrapper,
195
+ [`SwipeToGoBack`](https://github.com/chocky335/react-native-swipeable-actions/blob/main/example/components/SwipeToGoBack.tsx).
191
196
 
192
- ```tsx
193
- import { FlashList } from '@shopify/flash-list'
197
+ ## Commands
194
198
 
195
- function MessageList({ messages }) {
196
- return (
197
- <FlashList
198
- data={messages}
199
- renderItem={({ item }) => (
200
- <Swipeable
201
- recyclingKey={item.id}
202
- actions={<DeleteAction />}
203
- actionsWidth={80}
204
- >
205
- <MessageRow message={item} />
206
- </Swipeable>
207
- )}
208
- estimatedItemSize={60}
209
- />
210
- )
211
- }
199
+ ```tsx
200
+ const row = useRef<SwipeableMethods>(null);
201
+
202
+ row.current?.open(); // Right if enabled, else left. No-op if neither is enabled.
203
+ row.current?.open("left"); // No-op if left is disabled. Never opens right instead.
204
+ row.current?.close(); // Animated.
205
+ row.current?.close(false); // Instant. Also clears the cached open state.
206
+ row.current?.cancel(); // Closes at once. A pending swipe does not complete.
207
+
208
+ Swipeable.open("message-42", "right");
209
+ Swipeable.close("message-42", false);
210
+ Swipeable.cancel("message-42");
211
+ Swipeable.closeAll();
212
212
  ```
213
213
 
214
- ## Architecture
214
+ Ref commands need a nonempty `recyclingKey` on the row. Numeric keys become strings,
215
+ so `Swipeable.close("7")` reaches the row with `recyclingKey={7}`.
215
216
 
216
- ```
217
- ┌─────────────────────────────────────────────────────────────┐
218
- │ JS Layer (React) │
219
- │ ┌─────────────────┐ ┌──────────────────────────────────┐ │
220
- │ │ <Swipeable> │ │ Actions (ReactNode) │ │
221
- │ │ - renderActions│──│ - Rendered lazily on first swipe│ │
222
- │ │ - ref methods │ │ - Any React components │ │
223
- │ └────────┬────────┘ └──────────────────────────────────┘ │
224
- └───────────│─────────────────────────────────────────────────┘
225
- │ Expo Modules Bridge
226
- ┌───────────▼─────────────────────────────────────────────────┐
227
- │ Native Layer (Kotlin/Swift) │
228
- │ ┌─────────────────────────────────────────────────────────┐│
229
- │ │ SwipeableView ││
230
- │ │ - UIPanGestureRecognizer (iOS) / OnTouchListener (And) ││
231
- │ │ - Spring animations (UIView.animate / DynamicAnimation)││
232
- │ │ - Progress events @ 60fps (CADisplayLink / Choreograph)││
233
- │ │ - LRU cache for recycling state persistence ││
234
- │ └─────────────────────────────────────────────────────────┘│
235
- └─────────────────────────────────────────────────────────────┘
217
+ ## Accessibility
218
+
219
+ A swipe is not the only way to reach row actions. Users of screen readers and
220
+ switch control need a path without a swipe. Give the row content
221
+ `accessibilityActions`, and call the same business callbacks as the action buttons.
222
+
223
+ ```tsx
224
+ <Swipeable
225
+ recyclingKey={item.id}
226
+ rightActions={<ArchiveButton onPress={() => archive(item.id)} />}
227
+ rightActionsWidth={88}
228
+ >
229
+ <Pressable
230
+ onPress={() => openItem(item.id)}
231
+ accessibilityActions={[{ name: "archive", label: "Archive" }]}
232
+ onAccessibilityAction={(event) => {
233
+ if (event.nativeEvent.actionName === "archive") archive(item.id);
234
+ }}
235
+ >
236
+ <ItemRow item={item} />
237
+ </Pressable>
238
+ </Swipeable>
236
239
  ```
237
240
 
238
- **Key design decisions:**
241
+ - React Native gives these actions to VoiceOver and TalkBack as custom actions.
242
+ This package has no screen reader tests.
243
+ - A closed row hides its action rails from accessibility. The content actions are
244
+ then the only path to the row actions.
245
+ - The action widths are yours. The library does not measure action content. With
246
+ a large text size, a label can need more width than you declared. Make the
247
+ width bigger as the font scale grows (`useWindowDimensions().fontScale`), or use
248
+ short labels or icons with an `accessibilityLabel`.
239
249
 
240
- - Gestures handled entirely in native code for 60fps performance
241
- - Actions rendered lazily on first swipe gesture (performance optimization)
242
- - Recycling key enables state persistence in virtualized lists
243
- - No SharedValue dependency - progress synced via native events
250
+ The example list row uses the same pattern.
244
251
 
245
- ## Platform Support
252
+ ## RTL
246
253
 
247
- | Platform | Version |
248
- |----------|---------|
249
- | iOS | 12.0+ |
250
- | Android | API 21+ |
254
+ `left` and `right` are physical edges in every layout direction. RTL does not swap
255
+ action content, widths, commands or side callbacks. If your app thinks in logical
256
+ start/end actions, map them to left/right yourself.
251
257
 
252
- ## TypeScript
258
+ - Close open rows before you swap which actions sit on which physical side.
259
+ - A translated label can need a new width. Update the width with the label.
260
+ - A change to React Native's global RTL setting (`I18nManager.forceRTL`) needs an
261
+ app restart.
253
262
 
254
- Full TypeScript support with exported types:
263
+ ## iOS appearance and haptics
255
264
 
256
265
  ```tsx
257
- import {
258
- Swipeable,
259
- SwipeableProps,
260
- SwipeableMethods,
261
- SwipeableStatic,
262
- SwipeProgressEvent,
263
- SwipeStateEvent,
264
- } from 'react-native-swipeable-actions'
266
+ <Swipeable appearance="ios26" rightActions={actions} rightActionsWidth={144}>
267
+ {content}
268
+ </Swipeable>
265
269
  ```
266
270
 
267
- ## Testing
271
+ - `appearance="ios26"` adds iOS-style action motion. The default is `"default"`.
272
+ The OS version does not turn it on. It does not need the iOS 26 SDK. It is not
273
+ Apple's system swipe control.
274
+ - `hapticFeedback` controls the native threshold haptic on iOS. If you omit it,
275
+ it follows the preset: on for `ios26`, off for `default`.
276
+ - Reduce Motion removes the action scaling. It does not change the haptic setting.
277
+ The row still slides open and closed with its normal animation.
278
+ - Both options have no effect on Android.
279
+ - Rounded or inset buttons and multiple actions are your own action content.
280
+
281
+ ## API
282
+
283
+ ### Props
284
+
285
+ | Prop | Type | Default | Description |
286
+ | --- | --- | --- | --- |
287
+ | `children` | `ReactNode` | required | Row content. |
288
+ | `leftActions` / `rightActions` | `ReactNode` | - | Action content for that physical side. `null`/`false` disables the side. At least one side is required. |
289
+ | `leftActionsWidth` / `rightActionsWidth` | `number` | - | Required with its content. Finite and positive. |
290
+ | `recyclingKey` | `string \| number` | - | Stable item key. Required for ref commands, static commands and list state restoration. |
291
+ | `threshold` | `number` | `0.4` | Fraction (0-1) of the active side's width a release must pass to open. |
292
+ | `friction` | `number` | `1` | Drag damping (0-1). Lower values add resistance. |
293
+ | `dragOffsetFromEdge` | `number` | `0` | Horizontal distance before the swipe starts. |
294
+ | `gestureEnabled` | `boolean` | `true` | Turns the gesture off. Commands still work. |
295
+ | `autoClose` | `boolean` | `false` | Snap back after a completed swipe. |
296
+ | `autoCloseTimeout` | `number` | `0` | Delay in ms before the auto-close. |
297
+ | `appearance` | `'default' \| 'ios26'` | `'default'` | iOS action motion preset. |
298
+ | `hapticFeedback` | `boolean` | from `appearance` | iOS threshold haptic. |
299
+ | `style` | `StyleProp<ViewStyle>` | - | Container style. |
300
+ | `testID` | `string` | - | Test identifier. |
301
+
302
+ ### Callbacks
303
+
304
+ | Callback | Type | When |
305
+ | --- | --- | --- |
306
+ | `onSwipeStart` | `() => void` | A swipe gesture starts. |
307
+ | `onSwipeStateChange` | `(state: 'open' \| 'closed') => void` | The release target crosses the threshold during a drag. |
308
+ | `onSwipeEnd` | `(state: 'open' \| 'closed') => void` | A swipe or a command animation settles. |
309
+ | `onLeftSwipeEnd` / `onRightSwipeEnd` | `(state: 'open' \| 'closed') => void` | Same as `onSwipeEnd`, for that side only. Runs after `onSwipeEnd`. |
310
+ | `onProgress` | `(progress: number) => void` | Every frame of movement. `0` is closed, `1` is fully open. |
311
+
312
+ If you handle the same action in `onSwipeEnd` and in a side callback, it runs twice.
313
+ Do not use these callbacks to detect cache restoration or width updates. Those are
314
+ not swipes.
315
+
316
+ ### Methods
317
+
318
+ | Ref method | Static method | Description |
319
+ | --- | --- | --- |
320
+ | `open(side?)` | `Swipeable.open(key, side?)` | Open a side. Without a side: right if enabled, else left. |
321
+ | `close(animated = true)` | `Swipeable.close(key, animated = true)` | Close the row. |
322
+ | `cancel()` | `Swipeable.cancel(key)` | Close at once. A gesture in progress or a pending open does not complete. |
323
+ | - | `Swipeable.closeAll(animated = true)` | Close all rows, including rows that are not mounted (scrolled off or on a covered screen). Those rows come back closed. |
324
+
325
+ ### Types
326
+
327
+ ```ts
328
+ import type {
329
+ SwipeableProps, // Canonical props type
330
+ SwipeableCommonProps, // Props that every side configuration shares
331
+ SwipeableMethods, // Ref methods
332
+ SwipeableStatic, // Static methods
333
+ SwipeSide, // 'left' | 'right'
334
+ SwipeAppearance, // 'default' | 'ios26'
335
+ } from "react-native-swipeable-actions";
336
+ ```
268
337
 
269
- Jest mock is included for testing components that use Swipeable:
338
+ `SwipeableProps` is a union of valid side configurations. To add wrapper props,
339
+ use an intersection instead of `interface extends`:
340
+
341
+ ```ts
342
+ type MessageSwipeProps = SwipeableProps & { itemId: string };
343
+ ```
344
+
345
+ `TwoSidedSwipeableProps` and `SwipeableComponentProps` are aliases of `SwipeableProps`.
346
+
347
+ ## Testing with Jest
348
+
349
+ The package ships a Jest mock for the native view:
270
350
 
271
351
  ```js
272
352
  // jest.config.js
273
- import 'react-native-swipeable-actions/jestSetup.js'
353
+ module.exports = {
354
+ setupFiles: ["react-native-swipeable-actions/jestSetup.js"],
355
+ };
356
+ ```
357
+
358
+ The package ships TypeScript source, so keep it in your `transformIgnorePatterns`
359
+ allowlist, as you do for other Expo modules.
360
+
361
+ - The mock renders both action rails on mount, so your tests can find action
362
+ content.
363
+ - A mount calls none of your callbacks. The mock never starts a swipe by itself.
364
+ - The ref and static commands are `jest.fn()` mocks. They do not move the row or
365
+ send events.
366
+ - To test a callback, fire a native-shaped event on the row's `testID`. Put the
367
+ row's `recyclingKey` (as a string) in `nativeEvent`. Leave it out for a row
368
+ without a key.
369
+
370
+ ```tsx
371
+ import { fireEvent, screen } from "@testing-library/react-native";
372
+
373
+ const row = screen.getByTestId("message-42");
374
+ fireEvent(row, "swipeStart", { nativeEvent: { recyclingKey: "42", side: "right" } });
375
+ fireEvent(row, "swipeEnd", {
376
+ nativeEvent: { state: "open", side: "right", recyclingKey: "42" },
377
+ });
378
+ // onSwipeStart() ran once, then onSwipeEnd("open") and onRightSwipeEnd("open").
379
+ ```
380
+
381
+ The other native events are `swipeStateChange` (`{ state, side }`) and
382
+ `swipeProgress` (`{ progress, translationX, side }`).
383
+
384
+ ## Compatibility
385
+
386
+ | | Tested for 1.0 |
387
+ | --- | --- |
388
+ | Expo / React Native / React | Expo SDK 54, React Native 0.81, React 19.1 |
389
+ | Architecture | New Architecture (Fabric) |
390
+ | iOS | iOS Simulator 26, built with Xcode 26 |
391
+ | Android | API 34 emulator |
392
+
393
+ The package requires the New Architecture. The legacy architecture (Paper) is not
394
+ supported. The peer ranges (`expo >=54`, `react-native >=0.81`, `react >=19.1`) and
395
+ the minimum targets (iOS 13.4, Android API 23) are what the package declares. They
396
+ are not a tested matrix. [docs/support.md](docs/support.md) lists what was tested
397
+ and what was not.
398
+
399
+ An app built with the iOS 27 SDK (Xcode 27) stops at launch on iOS 27 if it does not
400
+ adopt the UIScene lifecycle. iOS sets this requirement for the app. The Expo SDK 54
401
+ app template does not meet it, and this package does not affect it. The example app adopts the UIScene lifecycle. Built with Xcode 27,
402
+ it launched on a physical iPhone with iOS 27.0.1. Only the launch was checked.
403
+
404
+ ## Performance
405
+
406
+ Android 16, Pixel 8 Pro, list demo with 100 items. Measured with 0.1.x.
407
+
408
+ <table>
409
+ <tr><th>react-native-gesture-handler/Swipeable</th><th>react-native-swipeable-actions</th></tr>
410
+ <tr>
411
+ <td>
412
+
413
+ https://github.com/user-attachments/assets/6d7cf1e6-c719-4044-8b47-205e94a38d10
414
+
415
+ </td>
416
+ <td>
417
+
418
+ https://github.com/user-attachments/assets/f59ef788-caf4-4985-93bc-8d098122fe1a
419
+
420
+ </td>
421
+ </tr>
422
+ <tr><td align="center">Reanimated</td><td align="center">Native</td></tr>
423
+ <tr><td align="center"><b>Avg 60.9</b> FPS (min 56.7 / max 65.7)</td><td align="center"><b>Avg 116.8</b> FPS (min 109.9 / max 120.3)</td></tr>
424
+ </table>
425
+
426
+ ## How it works
427
+
428
+ `<Swipeable>` renders one native view with three children: the left action rail,
429
+ the right action rail and your content. A pan recognizer on iOS and a touch
430
+ handler on Android move the content with spring animations and send progress
431
+ events to JavaScript. The native side keeps a per-key cache of the open side for
432
+ recycled list cells. Action rails mount in React only when a side is about to show.
433
+
434
+ ## Contributing
435
+
436
+ The `example/` app demonstrates every option and runs the E2E suites
437
+ (WebdriverIO + Appium, plus Maestro). See [e2e/README.md](https://github.com/chocky335/react-native-swipeable-actions/blob/main/e2e/README.md)
438
+ and the [release checklist](https://github.com/chocky335/react-native-swipeable-actions/blob/main/docs/releasing.md).
439
+
440
+ ```sh
441
+ npm test # Jest
442
+ npm run test:ios # Swift unit tests: helpers and state logic, no UIKit
443
+ npm run test:android
274
444
  ```
275
445
 
276
446
  ## License
277
447
 
278
- MIT
448
+ [MIT](LICENSE)