fix(timeline): wire the server date error to the date field's aria-invalid
REQ-011 asks for both required-field errors via per-field aria-invalid, but the blank-date error was rendered as a standalone <p> after the date card while the date input's aria-invalid only reflected the client-side malformed-date cue. DatePrecisionField gains a `dateError` prop: a server error now marks the field aria-invalid and renders inline under the input (sharing the same error id), and EventForm drops its detached <p>. While here, migrate the field's two error texts from hard-coded text-red-600 to the semantic `text-danger` token so they keep ≥4.5:1 contrast in dark mode (the token remaps; #dc2626 was borderline) — this also fixes the contrast for WhoWhenSection, the other consumer. Component test asserts the date input gains aria-invalid on a server date error. Addresses PR #832 review (Requirements Engineer REQ-011; UI/UX dark-mode contrast). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ let {
|
||||
suggestedDateIso = '',
|
||||
dateLabel = m.form_label_date(),
|
||||
dateRequired = true,
|
||||
dateError = '',
|
||||
dateTestId = undefined,
|
||||
precisionTestId = undefined,
|
||||
endDateInnerTestId = undefined
|
||||
@@ -49,6 +50,8 @@ let {
|
||||
suggestedDateIso?: string;
|
||||
dateLabel?: string;
|
||||
dateRequired?: boolean;
|
||||
/** Server-side date error (e.g. blank required field) wired to the field's aria-invalid. */
|
||||
dateError?: string;
|
||||
dateTestId?: string;
|
||||
precisionTestId?: string;
|
||||
endDateInnerTestId?: string;
|
||||
@@ -83,6 +86,9 @@ onMount(() => {
|
||||
});
|
||||
|
||||
const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === '');
|
||||
// Either the client-side malformed-date cue or a server-provided required-field
|
||||
// error marks the field invalid (REQ-011 per-field aria-invalid).
|
||||
const dateFieldInvalid = $derived(dateInvalid || dateError.length > 0);
|
||||
|
||||
// Inline mirror of the server guard (#678). ISO YYYY-MM-DD strings compare
|
||||
// lexicographically, so no Date object is needed. Server stays the gate —
|
||||
@@ -128,17 +134,21 @@ $effect(() => {
|
||||
maxlength="10"
|
||||
aria-required={dateRequired ? 'true' : undefined}
|
||||
class="block min-h-[48px] w-full rounded border border-line px-2 py-3 text-sm shadow-sm
|
||||
{dateInvalid
|
||||
{dateFieldInvalid
|
||||
? 'border-red-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500'
|
||||
: 'focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'}"
|
||||
aria-invalid={dateInvalid ? 'true' : undefined}
|
||||
aria-describedby={dateInvalid ? `${dateInputName}-error` : undefined}
|
||||
aria-invalid={dateFieldInvalid ? 'true' : undefined}
|
||||
aria-describedby={dateFieldInvalid ? `${dateInputName}-error` : undefined}
|
||||
/>
|
||||
<input type="hidden" name={dateInputName} value={dateIso} />
|
||||
{#if dateInvalid}
|
||||
<p id="{dateInputName}-error" class="mt-1 text-xs text-red-600">
|
||||
<p id="{dateInputName}-error" class="mt-1 text-xs text-danger">
|
||||
<span aria-hidden="true">⚠ </span>{m.form_date_error()}
|
||||
</p>
|
||||
{:else if dateError}
|
||||
<p id="{dateInputName}-error" class="mt-1 text-xs text-danger">
|
||||
<span aria-hidden="true">⚠ </span>{dateError}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -183,7 +193,7 @@ $effect(() => {
|
||||
/>
|
||||
{#if endBeforeStart}
|
||||
<!-- Non-colour cue (WCAG 1.4.1): warning glyph + text, not red alone. -->
|
||||
<p id="{dateInputName}-end-error" class="mt-1 text-xs text-red-600">
|
||||
<p id="{dateInputName}-end-error" class="mt-1 text-xs text-danger">
|
||||
<span aria-hidden="true">⚠ </span>{m.error_invalid_date_range()}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user