diff --git a/client/src/components/Trips/TripFormModal.test.tsx b/client/src/components/Trips/TripFormModal.test.tsx index 25e92f2a9..9e671b26f 100644 --- a/client/src/components/Trips/TripFormModal.test.tsx +++ b/client/src/components/Trips/TripFormModal.test.tsx @@ -116,6 +116,16 @@ describe('TripFormModal', () => { expect(await screen.findByText('Title is required')).toBeInTheDocument(); }); + it('FE-COMP-TRIPFORM-009b: typing a title clears the title error (#2604)', async () => { + const user = userEvent.setup(); + render(); + const [submitBtn] = screen.getAllByRole('button', { name: 'Create New Trip' }).slice(-1); + await user.click(submitBtn as HTMLElement); + expect(await screen.findByText('Title is required')).toBeInTheDocument(); + await user.type(screen.getByPlaceholderText(/Summer in Japan/i), 'P'); + expect(screen.queryByText('Title is required')).not.toBeInTheDocument(); + }); + it('FE-COMP-TRIPFORM-010: typing title and submitting calls onSave', async () => { const user = userEvent.setup(); const onSave = vi.fn().mockResolvedValue({ trip: buildTrip({ id: 99 }) }); @@ -416,7 +426,7 @@ describe('TripFormModal', () => { // Changing the start of a dated trip must go through the date-shift choice step (#1288). const changeStartDate = async (user: ReturnType, iso: string) => { await user.click(screen.getAllByRole('button', { name: 'Enter date manually' })[0]); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: iso } }); fireEvent.keyDown(input, { key: 'Enter' }); }; @@ -931,7 +941,7 @@ describe('TripFormModal', () => { await user.type(screen.getByPlaceholderText(/Summer in Japan/i), 'Open Ended'); await user.click(screen.getAllByRole('button', { name: 'Enter date manually' })[0]); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '2026-04-10' } }); fireEvent.keyDown(input, { key: 'Enter' }); @@ -951,12 +961,12 @@ describe('TripFormModal', () => { await user.type(screen.getByPlaceholderText(/Summer in Japan/i), 'Backwards'); await user.click(screen.getAllByRole('button', { name: 'Enter date manually' })[1]); - const endInput = screen.getByPlaceholderText('DD.MM.YYYY'); + const endInput = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(endInput, { target: { value: '2026-03-01' } }); fireEvent.keyDown(endInput, { key: 'Enter' }); await user.click(screen.getAllByRole('button', { name: 'Enter date manually' })[0]); - const startInput = screen.getByPlaceholderText('DD.MM.YYYY'); + const startInput = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(startInput, { target: { value: '2026-01-01' } }); fireEvent.keyDown(startInput, { key: 'Enter' }); @@ -1283,7 +1293,7 @@ describe('TripFormModal', () => { render(); await user.click(screen.getAllByRole('button', { name: 'Enter date manually' })[1]); - const endInput = screen.getByPlaceholderText('DD.MM.YYYY'); + const endInput = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(endInput, { target: { value: '2036-01-01' } }); fireEvent.keyDown(endInput, { key: 'Enter' }); @@ -1326,7 +1336,7 @@ describe('TripFormModal', () => { }; const changeEndDate = async (user: ReturnType, iso: string) => { await user.click(screen.getAllByRole('button', { name: 'Enter date manually' })[1]); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: iso } }); fireEvent.keyDown(input, { key: 'Enter' }); }; diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx index 024f171e3..7e5bd31ce 100644 --- a/client/src/components/Trips/TripFormModal.tsx +++ b/client/src/components/Trips/TripFormModal.tsx @@ -223,7 +223,9 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp } } - const update = (field, value) => setFormData(prev => { + // An edit answers whatever the last save complained about, so the complaint + // goes with it rather than standing over the fixed field until the next save. + const update = (field, value) => { setError(''); setFormData(prev => { const next = { ...prev, [field]: value } if (field === 'start_date' && value) { if (!prev.end_date || prev.end_date < value) { @@ -235,7 +237,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp } } return next - }) + }) } /* The dashboard's shapes, in the app's own tokens. `.trek-dash` deliberately scopes its palette to that page, so none of its diff --git a/client/src/components/shared/CustomDateTimePicker.test.tsx b/client/src/components/shared/CustomDateTimePicker.test.tsx index 7b258a6d5..288555cd5 100644 --- a/client/src/components/shared/CustomDateTimePicker.test.tsx +++ b/client/src/components/shared/CustomDateTimePicker.test.tsx @@ -104,14 +104,14 @@ describe('CustomDatePicker', () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - expect(screen.getByPlaceholderText('DD.MM.YYYY')).toBeTruthy(); + expect(screen.getByRole('textbox', { name: 'Enter date manually' })).toBeTruthy(); }); it('FE-COMP-DATEPICKER-012: text input accepts ISO format YYYY-MM-DD', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '2026-07-04' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).toHaveBeenCalledWith('2026-07-04'); @@ -121,7 +121,7 @@ describe('CustomDatePicker', () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '17.07.2026' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).toHaveBeenCalledWith('2026-07-17'); @@ -131,7 +131,7 @@ describe('CustomDatePicker', () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '31.02.2026' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).not.toHaveBeenCalled(); @@ -141,7 +141,7 @@ describe('CustomDatePicker', () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '29.02.2024' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).toHaveBeenCalledWith('2024-02-29'); @@ -151,9 +151,9 @@ describe('CustomDatePicker', () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.keyDown(input, { key: 'Escape' }); - expect(screen.queryByPlaceholderText('DD.MM.YYYY')).toBeNull(); + expect(screen.queryByRole('textbox', { name: 'Enter date manually' })).toBeNull(); expect(screen.getAllByRole('button').length).toBeGreaterThan(0); }); @@ -183,7 +183,7 @@ describe('CustomDatePicker', () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '2026-03-25' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).not.toHaveBeenCalled(); @@ -503,22 +503,22 @@ describe('CustomDatePicker branches', () => { openCalendar(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - expect(screen.getByPlaceholderText('DD.MM.YYYY')).toHaveProperty('value', ''); + expect(screen.getByRole('textbox', { name: 'Enter date manually' })).toHaveProperty('value', ''); }); it('FE-W5DP-010: submitting an empty manual entry does nothing', () => { render(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - fireEvent.blur(screen.getByPlaceholderText('DD.MM.YYYY')); + fireEvent.blur(screen.getByRole('textbox', { name: 'Enter date manually' })); expect(onChange).not.toHaveBeenCalled(); - expect(screen.queryByPlaceholderText('DD.MM.YYYY')).not.toBeInTheDocument(); + expect(screen.queryByRole('textbox', { name: 'Enter date manually' })).not.toBeInTheDocument(); }); it('FE-W5DP-011: a manual entry without three number groups is ignored', () => { render(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '17.07' } }); fireEvent.keyDown(input, { key: 'Enter' }); @@ -528,7 +528,7 @@ describe('CustomDatePicker branches', () => { it('FE-W5DP-012: a manual entry already in locale order is taken as typed', () => { render(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '07/17/2026' } }); fireEvent.keyDown(input, { key: 'Enter' }); @@ -538,7 +538,7 @@ describe('CustomDatePicker branches', () => { it('FE-W5DP-013: a two-digit year is expanded into the 2000s', () => { render(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '07/17/26' } }); fireEvent.keyDown(input, { key: 'Enter' }); @@ -548,7 +548,7 @@ describe('CustomDatePicker branches', () => { it('FE-W5DP-014: an impossible day/month combination is rejected', () => { render(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '13/45/2026' } }); fireEvent.keyDown(input, { key: 'Enter' }); @@ -558,7 +558,7 @@ describe('CustomDatePicker branches', () => { it('FE-W5DP-015: a localised manual entry outside [min,max] is rejected', () => { render(); fireEvent.click(screen.getByRole('button', { name: /enter date manually/i })); - const input = screen.getByPlaceholderText('DD.MM.YYYY'); + const input = screen.getByRole('textbox', { name: 'Enter date manually' }); fireEvent.change(input, { target: { value: '03/25/2026' } }); fireEvent.keyDown(input, { key: 'Enter' }); @@ -743,7 +743,7 @@ describe('CustomDatePicker branches', () => { fireEvent.click(keyboard); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(screen.getByPlaceholderText('DD.MM.YYYY')).toHaveProperty('value', '03/15/2026'); + expect(screen.getByRole('textbox', { name: 'Enter date manually' })).toHaveProperty('value', '03/15/2026'); }); }); diff --git a/client/src/components/shared/CustomDateTimePicker.tsx b/client/src/components/shared/CustomDateTimePicker.tsx index 03997564b..f218d9a90 100644 --- a/client/src/components/shared/CustomDateTimePicker.tsx +++ b/client/src/components/shared/CustomDateTimePicker.tsx @@ -3,6 +3,7 @@ import { localIsoDate } from '../../utils/localDate'; import React, { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from '../../i18n'; +import { dateInputPattern } from '../../utils/dateInputPattern'; import { useRemeasureSignal } from '../../hooks/useAnchoredPosition'; import { useWeekStartDay } from '../../hooks/useWeekStartDay'; import { leadingBlanks, weekdayLabels } from '../../utils/calendarWeek'; @@ -276,8 +277,8 @@ export function CustomDatePicker({ if (e.key === 'Escape') setIsTyping(false); }} onBlur={handleTextSubmit} - placeholder="DD.MM.YYYY" - aria-label="Enter date manually" + placeholder={dateInputPattern(locale)} + aria-label={t('common.datepicker.enterManually')} style={{ width: '100%', padding: '8px 14px', diff --git a/client/src/mobile/screens/dashboard/MNewTripSheet.tsx b/client/src/mobile/screens/dashboard/MNewTripSheet.tsx index ce8d4fb5d..d0e4b93dd 100644 --- a/client/src/mobile/screens/dashboard/MNewTripSheet.tsx +++ b/client/src/mobile/screens/dashboard/MNewTripSheet.tsx @@ -76,8 +76,10 @@ export default function MNewTripSheet({ open, trip, onClose, onSave, onCoverUpda setPendingRemoval(null) }, [trip, open]) - // Moving the start keeps the trip length (same rule as TripFormModal). + // Moving the start keeps the trip length (same rule as TripFormModal). Every + // edit clears the last save's complaint, as on the desktop form. const changeStart = (value: string) => { + setError('') setEndDate(endDateForNewStart(startDate, endDate, value)) setStartDate(value) } @@ -154,7 +156,7 @@ export default function MNewTripSheet({ open, trip, onClose, onSave, onCoverUpda {t('dashboard.tripTitle')} canEditTrip && setTitle(e.target.value)} + onChange={e => { if (canEditTrip) { setError(''); setTitle(e.target.value) } }} readOnly={!canEditTrip} placeholder={t('dashboard.tripTitlePlaceholder')} className={inputCls} @@ -165,7 +167,7 @@ export default function MNewTripSheet({ open, trip, onClose, onSave, onCoverUpda {t('dashboard.tripDescription')}