Skip to content

Commit 7091b17

Browse files
Copilothotlong
andcommitted
feat: add SwipeableRow component for swipe-to-reveal actions in list views
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent c4f2bfa commit 7091b17

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React, { useRef } from "react";
2+
import { View, Text, Pressable, Animated } from "react-native";
3+
import { Swipeable } from "react-native-gesture-handler";
4+
import { Edit, Trash2 } from "lucide-react-native";
5+
6+
/* ------------------------------------------------------------------ */
7+
/* Props */
8+
/* ------------------------------------------------------------------ */
9+
10+
export interface SwipeableRowProps {
11+
/** Content rendered inside the swipeable row */
12+
children: React.ReactNode;
13+
/** Called when the Edit action is tapped */
14+
onEdit?: () => void;
15+
/** Called when the Delete action is tapped */
16+
onDelete?: () => void;
17+
}
18+
19+
/* ------------------------------------------------------------------ */
20+
/* Constants */
21+
/* ------------------------------------------------------------------ */
22+
23+
const ACTION_WIDTH = 80;
24+
25+
/* ------------------------------------------------------------------ */
26+
/* SwipeableRow */
27+
/* ------------------------------------------------------------------ */
28+
29+
/**
30+
* Wraps children in a swipeable container that reveals Edit and Delete
31+
* action buttons on left-swipe.
32+
*
33+
* Actions are only rendered when at least one callback is provided.
34+
*
35+
* Usage:
36+
* ```tsx
37+
* <SwipeableRow onEdit={() => edit(id)} onDelete={() => remove(id)}>
38+
* <MyListItem />
39+
* </SwipeableRow>
40+
* ```
41+
*/
42+
export function SwipeableRow({
43+
children,
44+
onEdit,
45+
onDelete,
46+
}: SwipeableRowProps) {
47+
const swipeableRef = useRef<Swipeable>(null);
48+
49+
const hasActions = !!(onEdit || onDelete);
50+
51+
/* ---- Close helper ---- */
52+
const close = () => swipeableRef.current?.close();
53+
54+
/* ---- Right actions ---- */
55+
const renderRightActions = (
56+
_progress: Animated.AnimatedInterpolation<number>,
57+
dragX: Animated.AnimatedInterpolation<number>,
58+
) => {
59+
const actionCount = (onEdit ? 1 : 0) + (onDelete ? 1 : 0);
60+
const totalWidth = actionCount * ACTION_WIDTH;
61+
62+
const translateX = dragX.interpolate({
63+
inputRange: [-totalWidth, 0],
64+
outputRange: [0, totalWidth],
65+
extrapolate: "clamp",
66+
});
67+
68+
return (
69+
<Animated.View
70+
style={{ flexDirection: "row", transform: [{ translateX }] }}
71+
>
72+
{onEdit && (
73+
<Pressable
74+
onPress={() => {
75+
close();
76+
onEdit();
77+
}}
78+
className="items-center justify-center bg-blue-600"
79+
style={{ width: ACTION_WIDTH }}
80+
>
81+
<Edit size={20} color="#ffffff" />
82+
<Text className="mt-1 text-xs font-medium text-white">Edit</Text>
83+
</Pressable>
84+
)}
85+
86+
{onDelete && (
87+
<Pressable
88+
onPress={() => {
89+
close();
90+
onDelete();
91+
}}
92+
className="items-center justify-center bg-red-600"
93+
style={{ width: ACTION_WIDTH }}
94+
>
95+
<Trash2 size={20} color="#ffffff" />
96+
<Text className="mt-1 text-xs font-medium text-white">Delete</Text>
97+
</Pressable>
98+
)}
99+
</Animated.View>
100+
);
101+
};
102+
103+
/* ---- Render ---- */
104+
if (!hasActions) {
105+
return <View>{children}</View>;
106+
}
107+
108+
return (
109+
<Swipeable
110+
ref={swipeableRef}
111+
friction={2}
112+
rightThreshold={40}
113+
renderRightActions={renderRightActions}
114+
overshootRight={false}
115+
>
116+
{children}
117+
</Swipeable>
118+
);
119+
}

components/renderers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export type { DetailViewRendererProps } from "./DetailViewRenderer";
1515
export { DashboardViewRenderer } from "./DashboardViewRenderer";
1616
export type { DashboardViewRendererProps, WidgetDataPayload } from "./DashboardViewRenderer";
1717

18+
// Swipeable row
19+
export { SwipeableRow } from "./SwipeableRow";
20+
export type { SwipeableRowProps } from "./SwipeableRow";
21+
1822
// Filter drawer
1923
export { FilterDrawer, FilterButton } from "./FilterDrawer";
2024
export type { FilterDrawerProps, FilterButtonProps } from "./FilterDrawer";

0 commit comments

Comments
 (0)