From 0b46b89b626f1662916c3f4b84f87a735e67cf6b Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 10 Nov 2026 00:11:50 +0100 Subject: [PATCH] fix(mobile): swipe actions are rounded tiles beside the row, icon only; expenses keep their own buttons The actions under a swiped row were square blocks to the row's edge, unlike the rounded cards around them. They are now tiles of their own with the card's radius and a gap, inset within a grouped card such as the packing list, and show only their icon (the label names them for a screen reader). An expense already shows edit and delete beside its card, so it no longer swipes to a second, larger pair of the same buttons. --- .../src/mobile/components/MSwipeRow.test.tsx | 10 +++---- client/src/mobile/components/MSwipeRow.tsx | 27 ++++++++++++++----- .../mobile/screens/trip/tabs/MCostsTab.tsx | 15 +++-------- .../screens/trip/tabs/MPackingListTab.tsx | 1 + 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/client/src/mobile/components/MSwipeRow.test.tsx b/client/src/mobile/components/MSwipeRow.test.tsx index 79592b3b9..acb6f512f 100644 --- a/client/src/mobile/components/MSwipeRow.test.tsx +++ b/client/src/mobile/components/MSwipeRow.test.tsx @@ -35,8 +35,8 @@ function setup() { describe('MSwipeRow', () => { it('FE-MOB-SWIPE-001: shows no action buttons until the row is swiped', () => { setup(); - expect(screen.queryByText('Delete')).toBeNull(); - expect(screen.queryByText('Done')).toBeNull(); + expect(screen.queryByLabelText('Delete')).toBeNull(); + expect(screen.queryByLabelText('Done')).toBeNull(); }); it('FE-MOB-SWIPE-002: a swipe towards the start opens the trailing action, a tap on it runs it', () => { @@ -45,7 +45,7 @@ describe('MSwipeRow', () => { touch(row, 'touchmove', 160); touch(row, 'touchmove', 120); touch(row, 'touchend'); - fireEvent.click(screen.getByText('Delete')); + fireEvent.click(screen.getByLabelText('Delete')); expect(onDelete).toHaveBeenCalledOnce(); }); @@ -54,7 +54,7 @@ describe('MSwipeRow', () => { touch(row, 'touchstart', 200, 100); touch(row, 'touchmove', 190, 160); touch(row, 'touchend'); - expect(screen.queryByText('Delete')).toBeNull(); + expect(screen.queryByLabelText('Delete')).toBeNull(); }); it('FE-MOB-SWIPE-004: a tap on an open row closes it instead of opening the row', () => { @@ -72,7 +72,7 @@ describe('MSwipeRow', () => { touch(row, 'touchstart', 10); touch(row, 'touchmove', 120); touch(row, 'touchend'); - expect(screen.queryByText('Done')).toBeNull(); + expect(screen.queryByLabelText('Done')).toBeNull(); }); it('FE-MOB-SWIPE-006: swiped through past most of the row, the first action runs at once', () => { diff --git a/client/src/mobile/components/MSwipeRow.tsx b/client/src/mobile/components/MSwipeRow.tsx index ce47a2a8f..c8e491274 100644 --- a/client/src/mobile/components/MSwipeRow.tsx +++ b/client/src/mobile/components/MSwipeRow.tsx @@ -34,10 +34,18 @@ interface MSwipeRowProps { className?: string; /** The row's own background: it covers the buttons until the row moves, so it must match what is around it. */ surface?: string; + /** + * How far the buttons sit in from the row's top and bottom edge. A row that is a + * card of its own leaves it at 0, so the buttons are as tall as the card; a row + * inside a grouped card insets them, so they do not touch its dividers. + */ + inset?: number; children: ReactNode; } -const BUTTON_W = 76; +/** One button, and the gap before it: the buttons are cards of their own beside the row. */ +const BUTTON_W = 70; +const GAP = 6; const EDGE_PX = 24; const CLAIM_PX = 10; const AXIS_BIAS = 1.5; @@ -59,6 +67,7 @@ export default function MSwipeRow({ disabled = false, className = '', surface = 'bg-[color:var(--m-card)]', + inset = 0, children, }: MSwipeRowProps) { const rowRef = useRef(null); @@ -91,7 +100,11 @@ export default function MSwipeRow({ // Positive offset reveals the leading side in LTR; RTL mirrors it. const leadSign = () => (rtl() ? -1 : 1); const openWidth = (which: 'leading' | 'trailing') => - which === 'leading' ? (latest.current.leading ? BUTTON_W : 0) : latest.current.trailing.length * BUTTON_W; + which === 'leading' + ? latest.current.leading + ? BUTTON_W + GAP + : 0 + : latest.current.trailing.length * (BUTTON_W + GAP); const close = () => { place(0, true); @@ -194,17 +207,18 @@ export default function MSwipeRow({ action.onAction(); }; const shown = open ?? side; + // The icon alone, as in Mail; the label names the button for a screen reader. const button = (action: SwipeAction) => ( ); @@ -213,7 +227,8 @@ export default function MSwipeRow({ {shown && (
{shown === 'leading' ? leading && button(leading) : trailing.map(button)}
diff --git a/client/src/mobile/screens/trip/tabs/MCostsTab.tsx b/client/src/mobile/screens/trip/tabs/MCostsTab.tsx index deaa063a2..9eba7d1cd 100644 --- a/client/src/mobile/screens/trip/tabs/MCostsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MCostsTab.tsx @@ -15,7 +15,6 @@ import { useExpenseLinks } from '../../../../components/Budget/useExpenseLinks' import { finalBudgetFor, finalBudgetSources, formerParticipantRows, paidByUser, readUserNote } from '../../../../components/Budget/CostsPanel.helpers' import { catMeta, COST_CAT_META } from '../../../../components/Budget/costsCategories' import MConfirmSheet from '../../settings/MConfirmSheet' -import MSwipeRow, { type SwipeAction } from '../../../components/MSwipeRow' import { CountPill, TabPanes } from './tabChrome' import { AddPaymentSheet, MemberAvatar } from './MCostsPayment' import { STATUS_COLOR, type MTabScreenProps } from './tabModel' @@ -595,17 +594,10 @@ function ExpenseRow({ item, ctx, base, locale, t, canEdit, onEdit, onDelete, onT const members = item.members || [] const note = readUserNote(item) - // A swipe offers what the buttons beside the card do; delete still asks first. - const swipe: SwipeAction[] = canEdit - ? [ - { label: t('common.delete'), icon: Trash2, tone: 'danger', onAction: onDelete }, - { label: t('common.edit'), icon: Pencil, tone: 'neutral', onAction: onEdit }, - ] - : [] - + // No swipe here: edit and delete stand beside the card already, and a swipe + // put a second, larger pair of the same buttons next to them. return ( - -
+
)}
- ) } diff --git a/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx b/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx index dd5d431f4..925569fe4 100644 --- a/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx @@ -714,6 +714,7 @@ function PackingItemRow({ item, planner, currentUserId, editMode, canEdit, bagTr leading={swipeLeading} trailing={swipeTrailing} surface="bg-m-sheetop" + inset={4} className="border-t border-[color:var(--m-rowbr)] first:border-t-0" >