fix(web): a date field holding a valid RFC3339 timestamp shows its day instead of "Invalid Date", and the picker opens on it (BUG-3225) (#1560)

This commit is contained in:
xarmian
2026-09-26 00:59:29 -04:00
committed by GitHub
parent c31fb06c86
commit 09f9bfbbdd
2 changed files with 55 additions and 7 deletions
@@ -141,3 +141,37 @@ describe('date picker opens from a FOCUSED input (BUG-2858)', () => {
expect(document.activeElement).toBe(input);
});
});
// BUG-3225: an RFC3339 value, which the server accepts for a date field,
// rendered "Invalid Date" because a midnight was appended to the WHOLE string.
describe('a date field holding an RFC3339 timestamp (BUG-3225)', () => {
const label = (c: HTMLElement) => c.querySelector('.date-label')?.textContent?.trim();
const dateInput = (c: HTMLElement) => c.querySelector('input[type="date"]') as HTMLInputElement;
it('shows the day as written, and preselects it in the picker', () => {
const { container } = render(FieldEditor, {
props: { field, value: '2026-09-26T23:30:00-05:00', onchange: vi.fn(), itemId: 'item-A' },
});
expect(label(container)).toBe('Sep 26, 2026');
expect(dateInput(container).value).toBe('2026-09-26');
});
it('read-only shows the same day', () => {
const { container } = render(FieldEditor, {
props: { field, value: '2026-09-26T10:00:00Z', onchange: vi.fn(), itemId: 'item-A', readonly: true },
});
expect(container.textContent).toContain('Sep 26, 2026');
expect(container.textContent).not.toContain('Invalid Date');
});
it('control: a bare date is unchanged', () => {
const { container } = render(FieldEditor, { props: { field, value: '2026-09-26', onchange: vi.fn(), itemId: 'item-A' } });
expect(label(container)).toBe('Sep 26, 2026');
expect(dateInput(container).value).toBe('2026-09-26');
});
it('a string that is not a date shows as itself, not "Invalid Date"', () => {
const { container } = render(FieldEditor, { props: { field, value: 'next week', onchange: vi.fn(), itemId: 'item-A' } });
expect(label(container)).toBe('next week');
});
});
@@ -757,13 +757,27 @@ handlers — onchange is never called.
dateTriggerEl?.focus();
}
/**
* The calendar day a stored date names, as written: `YYYY-MM-DD` itself, or
* the day part of an RFC3339 timestamp, which the server also accepts for a
* date field (BUG-3225). No timezone conversion: `2026-09-26T23:30:00-05:00`
* names the 26th, the day its writer wrote. `null` when the string does not
* start with a day.
*/
function dateDay(dateStr: string): string | null {
const m = /^(\d{4}-\d{2}-\d{2})(?:$|T)/.exec(dateStr);
return m ? m[1] : null;
}
function formatDate(dateStr: string): string {
try {
const d = new Date(dateStr + 'T00:00:00');
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
} catch {
return dateStr;
}
// Appending a midnight to the whole string turned every RFC3339 value
// into "Invalid Date" (BUG-3225). And `toLocaleDateString` answers
// "Invalid Date" rather than throwing, so the old `catch` never ran: an
// unparseable string now shows as itself.
const day = dateDay(dateStr);
const d = day ? new Date(day + 'T00:00:00') : null;
if (!d || isNaN(d.getTime())) return dateStr;
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
// ── Select dropdown state ──────────────────────────────────────────────
@@ -1607,7 +1621,7 @@ handlers — onchange is never called.
bind:this={dateInputEl}
class="date-hidden-input"
type="date"
value={value ?? ''}
value={value ? (dateDay(value) ?? '') : ''}
onchange={handleDateInput}
onkeydown={handleDateKeydown}
onfocus={() => (dateInputFocused = true)}