Files

167 lines
4.2 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { ReactNode, useRef } from "react";
import { Pressable, type PressableProps, StyleSheet, Text, View } from "react-native";
import Swipeable, {
type SwipeableMethods,
} from "react-native-gesture-handler/ReanimatedSwipeable";
import { fonts, radii, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
export type SwipeAction = {
key: string;
label: string;
icon: keyof typeof Ionicons.glyphMap;
color: string;
backgroundColor: string;
onPress: () => void;
};
type SwipeableRowProps = {
children: ReactNode;
actions: SwipeAction[];
enabled?: boolean;
backgroundColor?: string;
onPress?: () => void;
onLongPress?: () => void;
contentStyle?: PressableProps["style"];
};
export function SwipeableRow({
children,
actions,
enabled = true,
backgroundColor,
onPress,
onLongPress,
contentStyle,
}: SwipeableRowProps) {
const { colors } = useAppTheme();
const styles = useThemedStyles(createSwipeableRowStyles);
const rowBackground = backgroundColor ?? colors.background;
const swipeRef = useRef<SwipeableMethods>(null);
const rowOpenRef = useRef(false);
const suppressPressUntilRef = useRef(0);
function suppressContentPress() {
suppressPressUntilRef.current = Date.now() + 350;
}
function handleContentPress() {
if (!onPress) return;
if (rowOpenRef.current || Date.now() < suppressPressUntilRef.current) {
swipeRef.current?.close();
rowOpenRef.current = false;
return;
}
onPress();
}
function renderContent() {
if (!onPress && !onLongPress) {
return (
<View
style={[
styles.row,
{ backgroundColor: rowBackground },
typeof contentStyle === "function" ? undefined : contentStyle,
]}
>
{children}
</View>
);
}
return (
<Pressable
accessibilityRole="button"
onPress={handleContentPress}
onLongPress={onLongPress}
style={(state) => [
styles.row,
{ backgroundColor: rowBackground },
typeof contentStyle === "function" ? contentStyle(state) : contentStyle,
]}
>
{children}
</Pressable>
);
}
function renderRightActions() {
return (
<View style={styles.actions}>
{actions.map((action) => (
<Pressable
key={action.key}
style={[styles.actionButton, { backgroundColor: action.backgroundColor }]}
onPress={() => {
suppressContentPress();
swipeRef.current?.close();
rowOpenRef.current = false;
action.onPress();
}}
>
<Ionicons name={action.icon} size={20} color={action.color} />
<Text style={[styles.actionLabel, { color: action.color }]}>{action.label}</Text>
</Pressable>
))}
</View>
);
}
if (!enabled || actions.length === 0) {
return renderContent();
}
return (
<Swipeable
ref={swipeRef}
renderRightActions={renderRightActions}
overshootRight={false}
onSwipeableOpenStartDrag={suppressContentPress}
onSwipeableCloseStartDrag={suppressContentPress}
onSwipeableWillOpen={() => {
suppressContentPress();
rowOpenRef.current = true;
}}
onSwipeableWillClose={suppressContentPress}
onSwipeableClose={() => {
rowOpenRef.current = false;
}}
>
{renderContent()}
</Swipeable>
);
}
const createSwipeableRowStyles = (colors: ThemeColors) =>
StyleSheet.create({
row: {
backgroundColor: colors.background,
borderRadius: radii.lg,
overflow: "hidden",
marginBottom: spacing.xs,
},
actions: {
flexDirection: "row",
alignItems: "stretch",
},
actionButton: {
width: 80,
alignItems: "center",
justifyContent: "center",
gap: spacing.xs,
borderRadius: radii.md,
marginLeft: spacing.xs,
},
actionLabel: {
fontFamily: fonts.bodyMedium,
fontSize: 11,
},
});