refactor(vacay): draw the phone user picker list with one component

The invite and the share sheet each carried the same unfolded user list. Both now render MUserPickerList with the users, the picked id and the pick handler from useVacayUserPicker. The markup is unchanged.
This commit is contained in:
Maurice
2026-10-09 15:44:13 +02:00
parent 201de0bd5b
commit 69db17b5dd
4 changed files with 71 additions and 32 deletions
@@ -0,0 +1,39 @@
// FE-COMP-MUSERPICK-001 to FE-COMP-MUSERPICK-003
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import MUserPickerList from './MUserPickerList';
const users = [
{ id: 2, username: 'bob' },
{ id: 3, username: 'carol' },
];
describe('MUserPickerList', () => {
it('FE-COMP-MUSERPICK-001: renders one button per user and tints none while nothing is picked', () => {
render(<MUserPickerList users={users} selected={null} onPick={vi.fn()} />);
const rows = screen.getAllByRole('button');
expect(rows.map((b) => b.textContent)).toEqual(['bob', 'carol']);
for (const row of rows) {
expect(row).toHaveAttribute('type', 'button');
expect(row.className).not.toContain('bg-[color:var(--m-ic)]');
}
});
it('FE-COMP-MUSERPICK-002: tints only the picked user', () => {
render(<MUserPickerList users={users} selected={3} onPick={vi.fn()} />);
expect(screen.getByRole('button', { name: 'carol' }).className).toContain('bg-[color:var(--m-ic)]');
expect(screen.getByRole('button', { name: 'bob' }).className).not.toContain('bg-[color:var(--m-ic)]');
});
it('FE-COMP-MUSERPICK-003: hands the tapped user id to onPick', () => {
const onPick = vi.fn();
render(<MUserPickerList users={users} selected={null} onPick={onPick} />);
fireEvent.click(screen.getByRole('button', { name: 'bob' }));
expect(onPick).toHaveBeenCalledWith(2);
});
});
@@ -0,0 +1,28 @@
interface MUserPickerListProps {
users: { id: number; username: string }[];
selected: number | null;
onPick: (id: number) => void;
}
/**
* The unfolded user list under a phone sheet's user picker (the vacay invite
* and share sheets): one row per user, the picked one tinted.
*/
export default function MUserPickerList({ users, selected, onPick }: MUserPickerListProps) {
return (
<div className="mt-[6px] max-h-[180px] overflow-y-auto rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheetop)] p-[6px]">
{users.map((u) => (
<button
key={u.id}
type="button"
onClick={() => onPick(u.id)}
className={`flex w-full items-center gap-[9px] rounded-[10px] px-[10px] py-[9px] text-start text-[0.8125rem] font-semibold ${
u.id === selected ? 'bg-[color:var(--m-ic)]' : ''
}`}
>
<span className="min-w-0 flex-1 truncate">{u.username}</span>
</button>
))}
</div>
);
}
@@ -1,6 +1,7 @@
import { ChevronDown, Clock, Loader2, X } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MUserPickerList from '../../components/MUserPickerList'
import { useVacayStore } from '../../../store/vacayStore'
import { useTranslation } from '../../../i18n'
import { type VacayUser } from '../../../types'
@@ -59,22 +60,7 @@ export default function MVacayInviteSheet({ open, onClose }: MVacayInviteSheetPr
</span>
<ChevronDown size={14} strokeWidth={2} className="flex-none text-m-faint" />
</button>
{pickerOpen && (
<div className="mt-[6px] max-h-[180px] overflow-y-auto rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheetop)] p-[6px]">
{available.map(u => (
<button
key={u.id}
type="button"
onClick={() => pick(u.id)}
className={`flex w-full items-center gap-[9px] rounded-[10px] px-[10px] py-[9px] text-start text-[0.8125rem] font-semibold ${
u.id === selected ? 'bg-[color:var(--m-ic)]' : ''
}`}
>
<span className="min-w-0 flex-1 truncate">{u.username}</span>
</button>
))}
</div>
)}
{pickerOpen && <MUserPickerList users={available} selected={selected} onPick={pick} />}
</>
)}
@@ -1,6 +1,7 @@
import { ChevronDown, Eye, EyeOff, Loader2, Share2, X } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MUserPickerList from '../../components/MUserPickerList'
import { useVacayStore } from '../../../store/vacayStore'
import { useTranslation } from '../../../i18n'
import { useVacayShareActions } from '../../../components/Vacay/useVacayShareActions'
@@ -71,22 +72,7 @@ export default function MVacayShareSheet({ open, onClose }: MVacayShareSheetProp
{t('vacay.share')}
</button>
</div>
{pickerOpen && (
<div className="mt-[6px] max-h-[180px] overflow-y-auto rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheetop)] p-[6px]">
{available.map(u => (
<button
key={u.id}
type="button"
onClick={() => pick(u.id)}
className={`flex w-full items-center gap-[9px] rounded-[10px] px-[10px] py-[9px] text-start text-[0.8125rem] font-semibold ${
u.id === selected ? 'bg-[color:var(--m-ic)]' : ''
}`}
>
<span className="min-w-0 flex-1 truncate">{u.username}</span>
</button>
))}
</div>
)}
{pickerOpen && <MUserPickerList users={available} selected={selected} onPick={pick} />}
</>
)}