Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr

- **La boîte de `ui-field` est désormais enveloppée dans un `.ui-field-control`** (FSHSP-157). C'est le contexte de positionnement du libellé flottant, et il est rendu dans les deux modes plutôt que conditionnellement, pour que le DOM d'un champ ne dépende pas de l'option. Aucun impact visuel ni sur les sélecteurs publics ; un consommateur qui aurait écrit du CSS sur l'enchaînement direct `.ui-field > .ui-field-box` doit passer par le descendant.
- **`ui-label` tronque son texte quand il est contraint** au lieu de déborder (`text-overflow: ellipsis` sur `.ui-label-text`, `max-width: 100%` sur la racine). Sans contrainte de largeur, le comportement est inchangé : le texte passe à la ligne comme avant.
- **`ui-datepicker` est saisissable au clavier par défaut** (`allowInput` passe de `false` à `true`, FSHSP-118). Le champ ne proposait la sélection qu'au calendrier dans la grande majorité des configurations ; pour un non-voyant, taper une date est bien plus rapide que naviguer une grille de ~30 cases au lecteur d'écran. Repasser `allowInput` à `false` restaure l'ancien comportement (grille seule). Sans effet en `multiple`/`range` (aucun parseur défini pour deux dates ou une liste — chantier séparé) ni en `timeOnly` (pas de parseur pour une heure seule) : ces modes restent lecture seule comme avant.
- **`ui-datepicker` est saisissable au clavier par défaut** (`allowInput` passe de `false` à `true`, FSHSP-118). Le champ ne proposait la sélection qu'au calendrier dans la grande majorité des configurations ; pour un non-voyant, taper une date est bien plus rapide que naviguer une grille de ~30 cases au lecteur d'écran. Repasser `allowInput` à `false` restaure l'ancien comportement (grille seule). `range`/`multiple` sont couverts aussi (voir plus haut) ; sans effet en `timeOnly` (pas de parseur pour une heure seule), qui reste lecture seule comme avant.
- **`showClear` passe de `false` à `true` par défaut, et sa priorité change face à l'icône calendrier** (FSHSP-118). Avant, la croix remplaçait systématiquement l'icône calendrier/horloge dès qu'une valeur était présente (si `showClear`) — au prix de perdre le seul déclencheur focusable capable de rouvrir le panneau. Désormais la croix ne prend le pas que si `showIcon` est à `false` : avec l'icône affichée (le défaut), elle reste cliquable pour changer la date directement, et l'effacement passe par le clavier (`allowInput`, sélectionner + supprimer le texte). Un consommateur qui utilisait déjà `showClear` avec `showIcon` à `true` verra donc la croix disparaître au profit de l'icône calendrier ; passer `showIcon` à `false` restaure son ancien comportement.

### Fixed
Expand Down
34 changes: 11 additions & 23 deletions projects/ui-kit/forms/src/lib/mask-engine.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,8 @@ describe('buildMaskSlots', () => {
});

it('still tracks pos/len for a segment with no matching range (±Infinity, no-op validation)', () => {
// FSHSP-118 code-review fix: `.bound` used to stay entirely undefined for an unranged
// segment, which also meant `atSegmentEnd` (driven by `bound.pos`/`bound.len`) could never
// fire for it — an unranged 4-digit segment (e.g. ui-datepicker's year) never triggered its
// own trailing literal once fully typed. `pos`/`len` are now always attached; only the
// min/max become a no-op sentinel.
// FSHSP-118: `.bound` used to stay undefined for an unranged segment, so `atSegmentEnd`
// never fired for it either (e.g. ui-datepicker's year never triggered its trailing literal).
const slots = buildMaskSlots('99', []);
expect(slots[0].bound).toEqual({ min: -Infinity, max: Infinity, pos: 0, len: 2 });
expect(slots[1].bound).toEqual({ min: -Infinity, max: Infinity, pos: 1, len: 2 });
Expand Down Expand Up @@ -196,14 +193,10 @@ describe('autoFormatSegments', () => {
return buildMaskSlots(DATE_MASK, [{ min: 1, max: 31 }, { min: 1, max: 12 }, null]);
}

// Deleting the day's leading digit of "08/07/2026" (raw value "8/07/2026" once the browser
// removes it) leaves the residual digit stream "8072026". Re-deriving the mask with bounds
// enforced (the default — meant to reject an invalid *new* leading digit while typing forward)
// instead SKIPS "8" (no valid 1-31 day starts with it) and reassigns the digits meant for
// month/year across the segment boundaries, producing a value with no relation to what was on
// screen. `enforceBounds: false` keeps each segment to its own positional slice of the stream
// instead — segments can show a transient out-of-range value (caught by the final blur/Enter
// parse, see `finalizeParsed`), but digits are never stolen from one segment by another.
// Deleting the day's leading "0" of "08/07/2026" leaves "8072026". Enforcing bounds (meant to
// reject an invalid new leading digit while typing forward) skips the "8" (no 1-31 day starts
// with it) and shifts every digit after it into the wrong segment. `enforceBounds: false` keeps
// each segment to its own positional slice instead — never stealing digits across segments.
it('without enforceBounds, a deletion can steal digits across segment boundaries', () => {
const result = autoFormatSegments(dayMonthYearSlots(), '8072026');
expect(result.text).toBe('07/02/6'); // day/month/year no longer match ANY sensible edit
Expand All @@ -214,12 +207,9 @@ describe('autoFormatSegments', () => {
expect(result.text).toBe('80/72/026'); // each segment keeps its own slice of the stream
});

// FSHSP-118 code-review fix: an unranged segment (year) used to never trigger its own
// trailing literal, because `atSegmentEnd` (mask-engine.ts) required a real bound to have been
// attached at all — so the space before a showTime segment never auto-inserted once a bare
// 4-digit year was typed, and the next digit typed (the hour) landed glued straight onto the
// year with no separator (e.g. ui-datepicker's "08/07/2026" + "10" typed next used to become
// "08/07/202610", which a later parse misreads as a single corrupted year).
// FSHSP-118: an unranged year used to never trigger its own trailing literal (the space before
// showTime), so the next digit (the hour) glued straight onto it — "08/07/2026" + "10" typed
// became "08/07/202610", misread as a corrupted year.
function dateTimeSlots() {
return buildMaskSlots('99/99/9999 99:99', [
{ min: 1, max: 31 },
Expand All @@ -241,10 +231,8 @@ describe('autoFormatSegments', () => {
expect(result.text).toBe('08/07/2026 10:30');
});

// FSHSP-118 follow-up: `ui-datepicker`'s `range` mode reuses the single-date mask twice,
// joined by its three-character typing separator (" - "). Auto-inserting only the FIRST
// literal right after a completed segment (the original behavior) would leave the "-" and
// trailing space forever stranded — the fix appends every consecutive literal in one go.
// FSHSP-118: ui-datepicker's `range` mode reuses the single-date mask twice, joined by its
// 3-char separator (" - ") — needs every consecutive literal appended, not just the first.
function rangeSlots() {
return buildMaskSlots('99/99/9999 - 99/99/9999', [
{ min: 1, max: 31 },
Expand Down
21 changes: 6 additions & 15 deletions projects/ui-kit/forms/src/lib/mask-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,9 @@ export function buildMaskSlots(
}
}

// Every digit segment gets a `.bound` (position tracking is needed regardless of whether the
// segment has a real min/max), even one with no entry in `bounds` — an unranged segment (e.g.
// ui-datepicker's year, deliberately left unbounded so its 2-digit shortcut keeps working)
// still uses ±Infinity as a no-op range: `acceptsMaskChar`'s scale check always passes, but
// `pos`/`len` become available for `atSegmentEnd` to detect the segment's last slot. Without
// this, an unranged segment never triggers its OWN following literal (e.g. the space before a
// showTime segment never auto-inserts once a bare 4-digit year is typed — code-review fix,
// FSHSP-118) because that check used to require a real range to have been attached at all.
// Every segment gets a `.bound`, even unranged ones (±Infinity = no-op range): `pos`/`len` are
// needed for `atSegmentEnd` regardless, else an unranged segment (e.g. ui-datepicker's year)
// never auto-inserts its own following literal (FSHSP-118).
segments.forEach((seg, i) => {
const range = bounds[i] ?? { min: -Infinity, max: Infinity };
seg.forEach((slot, pos) => (slot.bound = { ...range, pos, len: seg.length }));
Expand Down Expand Up @@ -188,13 +183,9 @@ export function autoFormatSegments(

for (const slot of slots) {
if (slot.char !== null) {
// Keep appending EVERY consecutive literal right after a just-completed segment, not only
// the first — `atSegmentEnd` is deliberately left untouched here; the next digit slot below
// always overwrites it before it's read again (or the loop ends, so a stale value here is
// never read at all). A single-character separator ("/", ":") never told the two paths
// apart; a multi-character one (`ui-datepicker`'s range " - ", three literal slots in a
// row) needs all of them auto-inserted in one go, exactly like a single one (code-review
// follow-up, FSHSP-118: `range` gets a live mask too now).
// Append EVERY consecutive literal after a completed segment, not just the first — needed
// for a multi-char separator like range's " - " (FSHSP-118). `atSegmentEnd` is left as-is
// here; the next digit slot always resets it before it's read again.
if (atSegmentEnd) text += slot.char;
continue;
}
Expand Down
17 changes: 6 additions & 11 deletions projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
/**
* TestBed spec for `ui-datepicker`'s keyboard-entry masking (FSHSP-118). Follows the pattern
* from `ui-select.spec.ts`/`ui-autocomplete.spec.ts`: a minimal host component +
* `TestBed.configureTestingModule`, native `input` events dispatched directly on the trigger's
* `<input>` — set the raw value + caret, dispatch `input`, flush CD — mirroring exactly what a
* real keystroke does (`ui-datepicker` reads `nativeInputElement().value`/`.selectionStart`
* itself in `onTriggerInput`, not anything carried on the event).
* TestBed spec for `ui-datepicker`'s keyboard-entry masking (FSHSP-118), pattern from
* `ui-select.spec.ts`: native `input` events dispatched on the trigger's `<input>` — set value +
* caret, dispatch, flush CD — mirroring a real keystroke.
*
* Scope: the three behaviors chased down (and initially mis-fixed) across FSHSP-118 —
* `hasValue()`-gated mask on/off, the `enforceBounds`/`dataEnd` deletion fixes, and re-arming the
* mask on a manual clear — plus `range`'s own live mask (added later, same gating). Not covered
* here: `multiple` typed parsing (no live mask — unbounded date count, see the component doc) or
* the format-hint/placeholder derivation.
* Covers: `hasValue()`-gated mask on/off (`single` and `range`), the `enforceBounds`/`dataEnd`
* deletion fixes, re-arming the mask on a manual clear. Not covered: `multiple` (no live mask)
* or the format-hint/placeholder derivation.
*/
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
Expand Down
Loading
Loading