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/README.md
CHANGED
|
@@ -1,278 +1,448 @@
|
|
|
1
1
|
# react-native-swipeable-actions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/react-native-swipeable-actions)
|
|
4
|
+
[](https://github.com/chocky335/react-native-swipeable-actions/actions/workflows/ci.yml)
|
|
5
|
+
[](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
|
-
```
|
|
49
|
-
|
|
26
|
+
```sh
|
|
27
|
+
npx expo install react-native-swipeable-actions
|
|
50
28
|
```
|
|
51
29
|
|
|
52
|
-
|
|
30
|
+
This is an [Expo module](https://docs.expo.dev/modules/overview/) with native code.
|
|
53
31
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
42
|
+
See [compatibility](#compatibility) for the configurations that were tested.
|
|
64
43
|
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
79
|
-
const
|
|
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={
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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={
|
|
89
|
-
<Text>Swipe
|
|
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
|
-
|
|
79
|
+
Each side is a pair: content plus its width in layout units.
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
swipeableRef.current?.open()
|
|
107
|
+
## Lists and recycling
|
|
136
108
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
112
|
+
```tsx
|
|
113
|
+
import { FlashList } from "@shopify/flash-list";
|
|
145
114
|
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
Swipeable.open('row-1')
|
|
133
|
+
## Swipe to reply
|
|
153
134
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
Swipeable
|
|
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
|
-
##
|
|
155
|
+
## Swipe to go back
|
|
167
156
|
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
174
|
+
if (state !== "open") return;
|
|
175
|
+
if (hasUnsavedDraft()) screen.current?.close();
|
|
176
|
+
else navigation.goBack();
|
|
182
177
|
}}
|
|
183
178
|
>
|
|
184
|
-
<
|
|
179
|
+
<ChatScreen />
|
|
185
180
|
</Swipeable>
|
|
186
181
|
```
|
|
187
182
|
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
import { FlashList } from '@shopify/flash-list'
|
|
197
|
+
## Commands
|
|
194
198
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
252
|
+
## RTL
|
|
246
253
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
## iOS appearance and haptics
|
|
255
264
|
|
|
256
265
|
```tsx
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|