From ae7350dae6ec83713a99a57b18d648fc3b383bfe Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 2 Jul 2026 00:52:50 -0400 Subject: [PATCH] Prevent swipe rows from opening items --- app/(app)/entities/index.tsx | 59 ++++++++-------- app/(app)/invoices/index.tsx | 46 ++++++------ app/(app)/more/expenses/index.tsx | 113 ++++++++++++++---------------- components/SwipeableRow.tsx | 79 +++++++++++++++++++-- 4 files changed, 180 insertions(+), 117 deletions(-) diff --git a/app/(app)/entities/index.tsx b/app/(app)/entities/index.tsx index 0778aba..75e0fc3 100644 --- a/app/(app)/entities/index.tsx +++ b/app/(app)/entities/index.tsx @@ -2,7 +2,6 @@ import { router } from "expo-router"; import { useState } from "react"; import { Alert, - Pressable, RefreshControl, ScrollView, StyleSheet, @@ -155,23 +154,22 @@ export default function EntitiesScreen() { onPress: () => confirmDelete(client.id, client.name), }, ]} + onPress={() => router.push(`/(app)/entities/clients/${client.id}`)} > - router.push(`/(app)/entities/clients/${client.id}`)}> - - - {client.name} - {client.email ? ( - {client.email} - ) : null} - {client.defaultHourlyRate != null ? ( - - {formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")} - /hr - - ) : null} - - - + + + {client.name} + {client.email ? ( + {client.email} + ) : null} + {client.defaultHourlyRate != null ? ( + + {formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")} + /hr + + ) : null} + + )) ) @@ -205,23 +203,22 @@ export default function EntitiesScreen() { onPress: () => confirmDelete(business.id, business.name), }, ]} + onPress={() => router.push(`/(app)/entities/businesses/${business.id}`)} > - router.push(`/(app)/entities/businesses/${business.id}`)}> - - - - {business.name} - {business.isDefault ? ( - Default - ) : null} - - {business.nickname ? ( - {business.nickname} + + + + {business.name} + {business.isDefault ? ( + Default ) : null} - {business.email ? {business.email} : null} - - + {business.nickname ? ( + {business.nickname} + ) : null} + {business.email ? {business.email} : null} + + )) )} diff --git a/app/(app)/invoices/index.tsx b/app/(app)/invoices/index.tsx index f83a9d2..cc6ff6e 100644 --- a/app/(app)/invoices/index.tsx +++ b/app/(app)/invoices/index.tsx @@ -2,7 +2,6 @@ import { router } from "expo-router"; import { useState } from "react"; import { Alert, - Pressable, RefreshControl, ScrollView, StyleSheet, @@ -205,31 +204,32 @@ export default function InvoicesScreen() { ]; return ( - - router.push(`/(app)/invoices/${invoice.id}`)} - onLongPress={() => promptStatusChange(invoice.id, status)} - > - - - - - {label} - - {invoice.client?.name ?? "Client"} - - - - {formatCurrency(invoice.totalAmount, invoice.currency)} + router.push(`/(app)/invoices/${invoice.id}`)} + onLongPress={() => promptStatusChange(invoice.id, status)} + > + + + + + {label} + + {invoice.client?.name ?? "Client"} - - Due {formatDate(invoice.dueDate)} - - + + {formatCurrency(invoice.totalAmount, invoice.currency)} + - - + + Due {formatDate(invoice.dueDate)} + + + + ); }) diff --git a/app/(app)/more/expenses/index.tsx b/app/(app)/more/expenses/index.tsx index 190ddbc..dd5eb74 100644 --- a/app/(app)/more/expenses/index.tsx +++ b/app/(app)/more/expenses/index.tsx @@ -2,7 +2,6 @@ import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import { useMemo, useState } from "react"; import { - Pressable, RefreshControl, ScrollView, StyleSheet, @@ -225,6 +224,12 @@ function ExpenseRow({ expense, onDelete }: { expense: Expense; onDelete: () => v return ( [ + styles.row, + { borderColor: colors.border }, + pressed && styles.rowPressed, + ]} + onPress={() => router.push(`/(app)/more/expenses/${expense.id}` as never)} actions={[ { key: "open", @@ -244,67 +249,57 @@ function ExpenseRow({ expense, onDelete }: { expense: Expense; onDelete: () => v }, ]} > - router.push(`/(app)/more/expenses/${expense.id}` as never)} - style={({ pressed }) => [ - styles.row, - { borderColor: colors.border }, - pressed && styles.rowPressed, - ]} - > - - + + + + + + + {expense.description} + + {expense.receiptCount ? ( + + + + {expense.receiptCount} + + + ) : null} - - - - {expense.description} + + {formatDate(expense.date)} + {expense.category ? ` · ${expense.category}` : ""} + {expense.client?.name ? ` · ${expense.client.name}` : ""} + + + {expense.billable ? ( + + Billable - {expense.receiptCount ? ( - - - - {expense.receiptCount} - - - ) : null} - - - {formatDate(expense.date)} - {expense.category ? ` · ${expense.category}` : ""} - {expense.client?.name ? ` · ${expense.client.name}` : ""} - - - {expense.billable ? ( - - Billable - - ) : null} - {expense.reimbursable ? ( - - Reimbursable - - ) : null} - {expense.taxDeductible ? ( - - Tax - - ) : null} - + ) : null} + {expense.reimbursable ? ( + + Reimbursable + + ) : null} + {expense.taxDeductible ? ( + + Tax + + ) : null} - - - {formatCurrency(expense.amount, expense.currency)} - - - - + + + + {formatCurrency(expense.amount, expense.currency)} + + + ); } diff --git a/components/SwipeableRow.tsx b/components/SwipeableRow.tsx index f686c9c..f7fa2fa 100644 --- a/components/SwipeableRow.tsx +++ b/components/SwipeableRow.tsx @@ -1,6 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import { ReactNode, useRef } from "react"; -import { Pressable, StyleSheet, Text, View } from "react-native"; +import { Pressable, type PressableProps, StyleSheet, Text, View } from "react-native"; import Swipeable, { type SwipeableMethods, } from "react-native-gesture-handler/ReanimatedSwipeable"; @@ -24,6 +24,9 @@ type SwipeableRowProps = { actions: SwipeAction[]; enabled?: boolean; backgroundColor?: string; + onPress?: () => void; + onLongPress?: () => void; + contentStyle?: PressableProps["style"]; }; export function SwipeableRow({ @@ -31,11 +34,63 @@ export function SwipeableRow({ actions, enabled = true, backgroundColor, + onPress, + onLongPress, + contentStyle, }: SwipeableRowProps) { const { colors } = useAppTheme(); const styles = useThemedStyles(createSwipeableRowStyles); const rowBackground = backgroundColor ?? colors.background; const swipeRef = useRef(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 ( + + {children} + + ); + } + + return ( + [ + styles.row, + { backgroundColor: rowBackground }, + typeof contentStyle === "function" ? contentStyle(state) : contentStyle, + ]} + > + {children} + + ); + } function renderRightActions() { return ( @@ -45,7 +100,9 @@ export function SwipeableRow({ key={action.key} style={[styles.actionButton, { backgroundColor: action.backgroundColor }]} onPress={() => { + suppressContentPress(); swipeRef.current?.close(); + rowOpenRef.current = false; action.onPress(); }} > @@ -58,12 +115,26 @@ export function SwipeableRow({ } if (!enabled || actions.length === 0) { - return {children}; + return renderContent(); } return ( - - {children} + { + suppressContentPress(); + rowOpenRef.current = true; + }} + onSwipeableWillClose={suppressContentPress} + onSwipeableClose={() => { + rowOpenRef.current = false; + }} + > + {renderContent()} ); }