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')}