Masked date input
canonDate typed straight into a dd.mm.rrrr mask with inline validation; no leading zero required, ISO out only once complete and valid.
Merged to origin/main 15 September 2026 (MR !3540), approved by the product owner as part of the New person dialog work (`.claude/rules/design-system.md` §"Vstupy a mobil", 14 Sep 2026: a structured value is a mask, never free text). Sibling pattern to `MaskedPhoneInput` (input + muted ghost-template) but that component has no card of its own yet in this registry — it is only listed under `record-rail/rail`'s files — so this is the mask pattern's first documented entry, filed under Text field for lack of a dedicated family. Code correction against the design brief: `inputMode` is `"decimal"`, not `"numeric"`. The component's only call site (`NewContactDialog`, the New person dialog) is not merged yet, and it wires no calendar next to the field — "the calendar is a complement" is the rule this component exists to satisfy, not yet an observed behaviour. No stories: lives in `apps/web/src/components/multivalue/`, which `packages/ui` must not import. Registry repaired 17 Oct 2026 (review F1, F10): now exported from @acrm/ui (packages/ui/src/components/MaskedDateInput.tsx, lib/date-mask.ts); the apps/web paths and the "lives in apps/web" caveat are outdated.
Used in 2 files · 2 areas · 3× — components/person, components/relations
Keyboard
- Native
<input>: typing edits the value in place, Tab moves focus in and out; there is no keyboard shortcut and no calendar to tab into or escape from (the component renders no calendar). - Backspace over an auto-inserted separator deletes the digit before it too, so deleting never gets stuck on a character the user did not type — covered by
components/multivalue/__tests__/masked-date-input.test.tsx. - No arrow-key segment navigation (day → month → year): the caret moves through the mask like an ordinary text field; only the separator characters are special-cased.
ARIA
- The accessible name comes straight from
ariaLabel, rendered as a nativearia-labelon the<input>itself — not through<Field>, whose<label>is not linked to its control (seetext-field/input.a11y.md). The component's own test finds the field withscreen.getByLabelText(...), confirming the name resolves correctly. aria-invalidis set only wheninvalidis true (aria-invalid={invalid || undefined}): a valid or empty field carries noaria-invalidattribute at all, rather than"false".- Gap — the error text is not linked to the input. The only current caller (
NewContactDialog) rendersdateMaskError's message as a plain<span class="fl-err">sibling of the row, with noid/aria-describedbypair to the input. A screen reader user hears that the field is invalid but not why, unless they navigate to the error text separately. - The ghost template (
.fl-mask-ghost) isaria-hidden="true"— it visually echoes the input's own value plus the remaining template, and exposing it too would read the date twice.
Focus
- The input has no visible border or box of its own —
.fl-mask-inis transparent and absolutely positioned over the ghost — so it relies on the surrounding row's focus treatment (formless-dialog's.fl-row:has(input:focus)row tint) rather than drawing a dedicated ring.dialog-formless.csshas no separate:focusrule for.fl-mask-in.
Contrast
Computed from packages/ui/src/tokens/tokens.css — the same tokens already measured in dialog/formless-dialog.a11y.md, reused here because the mask sits inside that same dialog body:
- Typed text (valid)
--fgon--surface-100— 17.59:1 light / 14.53:1 dark. - Typed text (invalid) and the error line both use
--critical-ink— 5.37:1 / 5.80:1, passes AA. - Ghost template
.rest--fg-muted— 2.85:1 / 3.63:1, below AA for text; it is a placeholder echo of the template, not content — the same system-wide--fg-mutedlimit as every other muted placeholder in this family.
Known limits
- The error message has no
aria-describedbylink to the input (see ARIA). - No calendar complement exists in the shipped code, so there is no alternative input method for someone who cannot type the mask —
.claude/rules/design-system.mdtreats the calendar as a required complement, not an optional one; this field currently has none at all. - No live region and no "character rejected" feedback: a dropped keystroke (e.g. a letter) simply does not appear, with nothing announced to a screen reader user.