mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
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.
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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<HTMLDivElement>(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) => (
|
||||
<button
|
||||
key={action.label}
|
||||
type="button"
|
||||
data-swipe-action=""
|
||||
tabIndex={open ? 0 : -1}
|
||||
aria-label={action.label}
|
||||
onClick={() => run(action)}
|
||||
className={`flex h-full w-[76px] flex-none flex-col items-center justify-center gap-1 text-caption font-semibold ${TONE[action.tone]}`}
|
||||
className={`flex h-full w-[70px] flex-none items-center justify-center ${inset ? 'rounded-xl' : 'rounded-2xl'} ${TONE[action.tone]}`}
|
||||
>
|
||||
<action.icon size={18} strokeWidth={2.2} aria-hidden="true" />
|
||||
{action.label}
|
||||
<action.icon size={16} strokeWidth={2.2} aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -213,7 +227,8 @@ export default function MSwipeRow({
|
||||
{shown && (
|
||||
<div
|
||||
aria-hidden={!open}
|
||||
className={`absolute inset-0 flex ${shown === 'leading' ? 'justify-start' : 'justify-end'}`}
|
||||
className={`absolute inset-x-0 flex gap-[6px] ${shown === 'leading' ? 'justify-start pe-[6px]' : 'justify-end ps-[6px]'}`}
|
||||
style={{ top: inset, bottom: inset }}
|
||||
>
|
||||
{shown === 'leading' ? leading && button(leading) : trailing.map(button)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<MSwipeRow trailing={swipe} disabled={!canEdit} surface="bg-[color:var(--m-bg)]" className="mt-2 rounded-2xl">
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<div className="mt-2 flex items-center gap-[6px]">
|
||||
<div className="relative min-w-0 flex-1 rounded-2xl bg-m-card px-3 pb-[10px] pt-[22px]" style={{ border: `1.5px solid ${borderColor}` }}>
|
||||
<span
|
||||
className="absolute -start-[1.5px] -top-[1.5px] flex items-center gap-1 rounded-es-none rounded-ee-[12px] rounded-ss-[15px] rounded-se-none px-[11px] pb-[4px] pt-[3px] font-geist text-[0.5625rem] font-extrabold uppercase tracking-[.05em] text-white"
|
||||
@@ -688,7 +680,6 @@ function ExpenseRow({ item, ctx, base, locale, t, canEdit, onEdit, onDelete, onT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MSwipeRow>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<div className="flex items-center gap-[9px] py-2">
|
||||
|
||||
Reference in New Issue
Block a user