Masked date input — accessibility
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.