Skip to content

Update date picker examples and behaviour - #82

Merged
LBU4SH merged 13 commits into
mainfrom
update-date-picker-examples-and-behaviour
Aug 24, 2026
Merged

Update date picker examples and behaviour#82
LBU4SH merged 13 commits into
mainfrom
update-date-picker-examples-and-behaviour

Conversation

@LBU4SH

@LBU4SH LBU4SH commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

LBU4SH added 13 commits August 24, 2026 10:56
…m/aaaa

The actual regression: the readonly trigger's default display (no
allowInput) formats via Intl dateStyle:'medium' on the resolved locale,
which falls back to en-US in Storybook (no LOCALE_ID configured) — so
most demos silently rendered "Jul 8, 2026" instead of "08/07/2026",
even though the placeholder/docs advertise jj/mm/aaaa. This also
affected stories that start empty but format a date once one is picked
interactively (Default, Required, Error, ButtonBar, DisabledWeekends,
DisabledDates, IconTemplate, SmartPosition).

Give every demo an explicit numeric dateFormat (with a time-aware
variant for the showTime stories) so they render 08/07/2026 (08/07/2026
14:30 with time) regardless of the ambient locale — verified both on
the stories' initial value and by picking a date interactively. Drop
the AutoFormattedInputEnUs demo (mm/dd/yyyy) and keep CustomFormat as
the single deliberate exception ("Jul 8, 2026", en-US) showing that
dateFormat/parseDate can support any display format.

Left untouched: AutoFormattedInputMonthPicker (adding a default
dateFormat there would disable its own numeric mm/aaaa masking, gated
on dateFormat being absent) and TimeOnly (the timeOnly branch ignores
dateFormat and hourFormat entirely and always renders a locale AM/PM
time — a component behavior issue, out of scope here).
…meOnly

displayValue's timeOnly branch formatted directly via Intl
'timeStyle: short' on the resolved locale, bypassing hourFormat and
dateFormat entirely: hourFormat="24" (the default) had no effect —
the clock followed whatever AM/PM-vs-24h convention the locale
defaulted to (en-US shows AM/PM) — and a custom dateFormat had no way
to reach this mode at all.

Add formatTime(), symmetric with formatDate()/formatMonth(): honors
dateFormat when provided, otherwise forces hour12 from hourFormat
instead of leaving it to the locale.
Chained onto the native input's aria-describedby alongside the helper/
error message id (never replacing it) — a plain [attr.aria-describedby]
override on a composite host (e.g. ui-datepicker's own format hint)
would otherwise clobber whichever of the two lands last.
…ments

autoFormatSegments() re-derives the masked text from the flat digit
stream on every keystroke. The bounds check (acceptsMaskChar) exists to
reject an invalid *new* leading digit while typing forward (e.g. '8'
can never start a valid 1-31 day, so it's skipped) — applied instead
to the digits left over after a deletion, that same skip can discard a
still-valid residual digit and reassign every following segment by one
position (day steals month's slice, month steals year's...).

Add an enforceBounds option (default true, unchanged for typing/
pasting) and disable it in ui-datepicker's onTriggerInput whenever the
new data is shorter than before (a shrink = a deletion). Segments can
show a transient out-of-range value until the final blur/Enter parse
(finalizeParsed already validates and reverts), but digits are never
reassigned to the wrong segment anymore.

True in-place segment editing (rewriting just the day while leaving
month/year untouched, wherever the caret is) remains a bigger, separate
effort — this only fixes the cross-segment corruption.
A calendar grid alone forces a screen-reader user through ~30 cells to
pick a date; typing is far faster. allowInput now defaults to true
(single mode, unchanged for multiple/range/timeOnly).

showClear also defaults to true, but only takes effect when showIcon
is false: the calendar/clock toggle otherwise always wins the
trigger's single icon slot, so there's always a click target to reopen
the panel and change the date directly, without ever losing that
affordance to the clear cross. Clearing then goes through the keyboard
instead (select the text, delete it) — consistent with allowInput
being on by default. showClearButton is gated accordingly.

Also adds an aria-describedby format hint (e.g. 'Format attendu :
jj/mm/aaaa'), chained onto the trigger's existing helper/error message
rather than replacing it (ui-input's new ariaDescribedBy input) —
the placeholder alone is an unreliable, disappearing-on-input signal
across screen readers. New formatHintLabel input to override/disable
it.

Updates the affected stories (meta defaults, Clearable now toggles
showIcon to actually demonstrate the cross, IconTemplate shows both
icon states side by side) and the MDX doc.
…tuck

Reported via screen recording: typing/committing a full date (e.g.
20/08/2020), then Backspacing from the end, correctly shrinks down to
"20/08/" (day+month complete) and then stops responding — every
further Backspace looks like a no-op.

Root cause: autoFormatSegments() eagerly appends a segment's trailing
'/' as soon as it's complete, and the caret was placed at text.length,
i.e. right after that separator. A Backspace there deletes the
separator, not a digit — and the very next render silently
re-inserts it (still no data past it), so the field appears frozen.

autoFormatSegments() now also returns dataEnd, the position right
after the last actual data character (never past a dangling
separator). onTriggerInput uses it as the caret target specifically
for a deletion whose caret sat at/past all remaining data (editing at
the tail — the common case); a genuine mid-string deletion still falls
back to the existing tokenIndices-based placement, unchanged.

Verified live (simulated native input events, not just unit tests):
20/08/2020 -> 20/08/202 -> ... -> 20/08/ -> 20/0 -> 20/ -> 2 -> '',
with no stall at any step.
Per review: the previous two fixes (bounds-skip disabled on deletion,
caret parked before a dangling separator) still didn't cover the
reported case — clicking into an already-valid date to fix one segment
(e.g. just the month) still shifted everything after it, and repeated
Backspace from the end could still stall. Root cause is structural:
re-deriving the ENTIRE text from a flat digit stream on every keystroke
only ever behaves well for *constructing* a date from nothing
(sequential forward typing, or backspacing from the end) — it has no
notion of "this segment was already valid, leave it alone".

typingSlots() now also returns null once hasValue() is true, routing
onTriggerInput to the existing plain-passthrough branch: no live
auto-slash, no re-derivation, so no way to shift or corrupt a segment
— just ordinary text editing, parsed on blur/Enter as before (already
tolerant of arbitrary separators via defaultParse). The mask re-arms
on its own once the field is cleared and hasValue() goes back to
false, so the very next fresh date still gets the auto-"/" guidance.

Verified live against the exact reported scenario (native input events
against a running instance, hasValue()/typingSlots() inspected via
ng.getComponent): editing 20/08/2020 in place now behaves as plain
text (delete/retype any character anywhere, e.g. just the month's '8'
-> "20/0/2020" with day and year untouched, then retype -> commits
correctly on blur), and clearing the field flips typingSlots() back
to non-null for the next entry.
hasValue() only flips to false on a real commit (blur/Enter, or the
clear cross) — clearing the field by hand and typing straight back in,
with no blur in between, left it masked off for the whole next entry
too, since nothing had actually told the model the value was gone.

onTriggerInput's passthrough branch (mask off) now calls clear() the
instant the raw text reads empty, instead of waiting for commitTyped
on blur. hasValue() genuinely flips to false right there, so
typingSlots() re-arms on the very next keystroke and stays armed for
the whole fresh entry - not just the one instant the field happens to
be empty (an earlier attempt at this, checking typedValue() === '' in
typingSlots itself, only held for that single keystroke: hasValue()
was still stale-true, so the mask flipped off again the moment the
first new character landed).

Verified live: clearing 08/07/2026 by hand (no blur) flips hasValue()
to false and typingSlots() to non-null immediately; typing
'01011999' right after auto-slashes correctly the whole way to
01/01/1999, with no blur anywhere in the sequence.
Extends allowInput to range and multiple, as a complement to the grid
(clicking a day keeps working exactly as before, both paths feed the
same model). Deliberately no live auto-"/" mask for either — that
engine only ever models one date, and single mode already showed how
fiddly it gets; range/multiple stay plain-text, parsed on blur/Enter:

- range: both dates in the same field, joined by " - " (e.g.
  "08/07/2026 - 18/07/2026"), reordered chronologically if typed
  backwards (mirrors the grid's own reordering in selectDay).
- multiple: a ", "-joined list, any count, duplicates collapsed
  (mirrors the grid's click-to-toggle).

Implementation: triggerReadonly() no longer hardcodes single mode;
typingSlots() explicitly opts back out for range/multiple (never runs
the live mask for them, regardless of hasValue()). New
parseTypedValue()/parseTypedMulti() split typed text on the mode's
separator and parse each part with the existing single-date
parseTyped() (so a custom parseDate applies per part, symmetric with
dateFormat already applying per date via formatDate()). An unparseable
or disabled part fails the whole thing - never a partial commit.
resolvedPlaceholder() composes the single-date token into
"jj/mm/aaaa - jj/mm/aaaa" / "jj/mm/aaaa, ..." when not overridden.

Not covered: showTime combined with range/multiple typed entry (typed
dates always land at startOfDay; the grid still handles time for these
modes) - a further chantier of its own.

Verified live against a running instance (real typed input + blur,
inspected via ng.getComponent): range reorders end-before-start input
and highlights correctly in the grid; multiple collapses a typed
duplicate and highlights all three dates; an incomplete range reverts
on blur; grid clicks and typed entry compose (typing a range then
clicking a third day promotes it to multiple's existing 3-date
array).
…rmat

singleDatePlaceholder always built a locale-numeric token ("jj/mm/aaaa")
regardless of a custom dateFormat, so a consumer using e.g. "Jul 8,
2026" style formatting still saw a placeholder describing a format
the field neither displays nor accepts (and that a matching custom
parseDate would reject outright).

With dateFormat set, the placeholder (and the aria-describedby hint
derived from it) now comes from that same formatter applied to an
illustrative date, instead of the locale-numeric token. CustomFormat
story updated to demonstrate it (placeholder: '' to let it auto-derive)
instead of masking the bug behind meta.args' default placeholder.
TestBed spec (follows ui-select.spec.ts/ui-autocomplete.spec.ts: host
component + native input events dispatched directly on the trigger's
<input>, mirroring a real keystroke) for the three behaviors chased
down across this ticket:

- hasValue()-gated mask on/off (single mode): auto-formats while
  constructing a date from empty, plain text once a value exists.
- enforceBounds/dataEnd deletion fixes: mid-string delete no longer
  scrambles segments, backspacing through a completed segment no
  longer stalls (regression test pinned to the exact sequence that
  used to freeze at "20/08/").
- mask re-arms the instant the field reads empty, no blur needed.

Sanity-checked these actually catch a regression, not just pass by
construction: temporarily disabled the hasValue() gate in
typingSlots() and confirmed the corresponding test failed with the
expected diff (masked '01/01/1999' instead of plain '01011999'),
then restored the fix and reran to confirm green again.
- showTime mask: unranged year segment never triggered its own
  trailing literal (mask-engine.ts atSegmentEnd), so the first hour
  digit typed after the year glued straight onto it; previewTyped now
  also waits for a complete time before committing a live preview.
- range/multiple typed parsing: splitting on a literal separator broke
  when a custom dateFormat's own text contained it (ISO dash, comma
  format); splitTypedSegments now only accepts a boundary once the
  text up to it parses as a complete date.
- range display separator: displayValue reused the typing separator
  (plain hyphen) instead of the pre-existing en dash, silently
  changing the look for every non-typing range consumer.
- grid focus on open: moving triggerReadonly's hardcoded non-single
  read-only out dropped the implicit rove-into-grid on icon-click open
  for range/multiple; restored explicitly.

All four found and verified during the branch review requested for
update-date-picker-examples-and-behaviour.
range now gets the same auto-"/" mask as single: both dates plus
their " - " separator build up as you type, gated on hasValue() the
same way single already is (mask off once a complete range exists,
edit as plain text, re-arms on clear).

multiple stays plain-text-on-blur only — an unbounded date count
doesn't fit a fixed mask template, a bigger chantier of its own.

mask-engine.ts: autoFormatSegments only auto-inserted the FIRST
literal character right after a completed segment, which is enough
for every single-char separator ("/", ":", " ") but silently drops
the rest of a multi-char one like range's " - " (three literal slots
in a row). Now appends every consecutive literal in one go.
@LBU4SH
LBU4SH merged commit 12ae03e into main Aug 24, 2026
2 of 3 checks passed
@LBU4SH
LBU4SH deleted the update-date-picker-examples-and-behaviour branch August 24, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant