Update date picker examples and behaviour - #82
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.