fix(person): block submit while a life-date input is partial
A partial date (e.g. "14.03.") left the hidden ISO input empty, so saving the edit form silently cleared a stored date. PersonLifeDateField now delegates to the shared DateInput primitive (inline format error, calendar validation) and sets a custom validity while the error is present, so the browser blocks native submission for both person forms. A full clear stays submittable - that is the intentional clear path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { isoToGerman, handleGermanDateInput } from '$lib/shared/utils/date';
|
||||
import DateInput from '$lib/shared/primitives/DateInput.svelte';
|
||||
import type { DatePrecision } from '$lib/shared/utils/documentDate';
|
||||
|
||||
// Only DAY / MONTH / YEAR are offered for life dates: RANGE and SEASON make no
|
||||
@@ -26,8 +26,9 @@ let {
|
||||
initialPrecision?: string | null;
|
||||
} = $props();
|
||||
|
||||
let display = $state('');
|
||||
let iso = $state('');
|
||||
let errorMessage = $state<string | null>(null);
|
||||
let inputEl = $state<HTMLInputElement | undefined>();
|
||||
let precision = $state<DatePrecision>('DAY');
|
||||
|
||||
// Seed once at mount (WhoWhenSection pattern): a later load() rerun must not
|
||||
@@ -35,7 +36,6 @@ let precision = $state<DatePrecision>('DAY');
|
||||
onMount(() => {
|
||||
if (initialIso) {
|
||||
iso = initialIso;
|
||||
display = isoToGerman(initialIso);
|
||||
}
|
||||
const offered = PERSON_DATE_PRECISIONS.some((p) => p.value === initialPrecision);
|
||||
if (offered) {
|
||||
@@ -47,11 +47,11 @@ onMount(() => {
|
||||
}
|
||||
});
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const result = handleGermanDateInput(e);
|
||||
display = result.display;
|
||||
iso = result.iso;
|
||||
}
|
||||
// A partial date leaves the hidden ISO empty — submitting then would silently
|
||||
// clear a stored date. Block native submission until completed or fully emptied.
|
||||
$effect(() => {
|
||||
inputEl?.setCustomValidity(errorMessage ?? '');
|
||||
});
|
||||
|
||||
const controlCls =
|
||||
'block min-h-[44px] w-full rounded border border-line px-3 py-2 font-serif text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring';
|
||||
@@ -63,18 +63,20 @@ const controlCls =
|
||||
</legend>
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
<DateInput
|
||||
bind:value={iso}
|
||||
bind:errorMessage={errorMessage}
|
||||
bind:inputEl={inputEl}
|
||||
name={name}
|
||||
id={name}
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
placeholder="TT.MM.JJJJ"
|
||||
maxlength="10"
|
||||
aria-label={legend}
|
||||
value={display}
|
||||
oninput={handleInput}
|
||||
ariaLabel={legend}
|
||||
ariaDescribedby={errorMessage ? `${name}-error` : undefined}
|
||||
class={controlCls}
|
||||
/>
|
||||
<input type="hidden" name={name} value={iso} />
|
||||
{#if errorMessage}
|
||||
<p id="{name}-error" class="mt-1 font-sans text-xs text-red-600">{errorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<select
|
||||
|
||||
64
frontend/src/lib/person/PersonLifeDateField.svelte.spec.ts
Normal file
64
frontend/src/lib/person/PersonLifeDateField.svelte.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import PersonLifeDateField from './PersonLifeDateField.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const baseProps = {
|
||||
name: 'birthDate',
|
||||
legend: 'Geburtsdatum',
|
||||
precisionLabel: 'Genauigkeit'
|
||||
};
|
||||
|
||||
const visibleInput = () => page.getByLabelText('Geburtsdatum', { exact: true });
|
||||
const hiddenIso = () => document.querySelector<HTMLInputElement>('input[name="birthDate"]');
|
||||
|
||||
describe('PersonLifeDateField', () => {
|
||||
it('shows the format error and flags the input invalid for a partial date', async () => {
|
||||
render(PersonLifeDateField, { props: baseProps });
|
||||
|
||||
await userEvent.fill(visibleInput(), '14.03.');
|
||||
|
||||
await expect.element(page.getByText(m.form_date_error())).toBeVisible();
|
||||
const input = (await visibleInput().element()) as HTMLInputElement;
|
||||
expect(input.checkValidity()).toBe(false);
|
||||
expect(hiddenIso()?.value).toBe('');
|
||||
});
|
||||
|
||||
it('clears error and custom validity when the input is fully emptied', async () => {
|
||||
render(PersonLifeDateField, { props: { ...baseProps, initialIso: '1899-03-14' } });
|
||||
|
||||
await userEvent.fill(visibleInput(), '');
|
||||
|
||||
await expect.element(page.getByText(m.form_date_error())).not.toBeInTheDocument();
|
||||
const input = (await visibleInput().element()) as HTMLInputElement;
|
||||
expect(input.checkValidity()).toBe(true);
|
||||
// Empty hidden ISO is the intentional clear path — the server action omits the pair.
|
||||
expect(hiddenIso()?.value).toBe('');
|
||||
});
|
||||
|
||||
it('keeps a stored date submittable when untouched', async () => {
|
||||
render(PersonLifeDateField, {
|
||||
props: { ...baseProps, initialIso: '1899-03-14', initialPrecision: 'DAY' }
|
||||
});
|
||||
|
||||
await expect.element(visibleInput()).toHaveValue('14.03.1899');
|
||||
const input = (await visibleInput().element()) as HTMLInputElement;
|
||||
expect(input.checkValidity()).toBe(true);
|
||||
expect(hiddenIso()?.value).toBe('1899-03-14');
|
||||
});
|
||||
|
||||
it('becomes valid again once the partial date is completed', async () => {
|
||||
render(PersonLifeDateField, { props: baseProps });
|
||||
|
||||
await userEvent.fill(visibleInput(), '14.03.');
|
||||
await userEvent.fill(visibleInput(), '14.03.1899');
|
||||
|
||||
await expect.element(page.getByText(m.form_date_error())).not.toBeInTheDocument();
|
||||
const input = (await visibleInput().element()) as HTMLInputElement;
|
||||
expect(input.checkValidity()).toBe(true);
|
||||
expect(hiddenIso()?.value).toBe('1899-03-14');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user