diff --git a/CHANGELOG.md b/CHANGELOG.md index b68b0fb..9da9db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr - **`ui-datepicker` : taper l'heure juste après l'année (`showTime`) pouvait corrompre l'année affichée** (FSHSP-118). Le segment année, volontairement sans borne (`1-12`/`1-31` s'appliquent au jour/mois, pas à elle), ne déclenchait jamais l'insertion de son propre séparateur (l'espace avant l'heure) une fois ses 4 chiffres tapés — un suivi de position réservé aux segments bornés. L'heure tapée ensuite s'accolait donc directement à l'année (ex. `08/07/2026` + `10` tapé → `08/07/202610`, relu comme une année à 6 chiffres). Le suivi de position du moteur de masque partagé (`mask-engine.ts`, utilisé aussi par `ui-input-mask`) couvre maintenant tout segment, borné ou non ; la prévisualisation en direct attend en plus que l'heure soit complète avant de la commiter. - **`ui-datepicker` : la saisie tapée en `range`/`multiple` pouvait couper une date en plein milieu si son `dateFormat` custom contenait le caractère du séparateur** (FSHSP-118). Le découpage retenait la première occurrence littérale de `" - "` (`range`) ou `", "` (`multiple`) dans le texte tapé, sans vérifier qu'elle délimitait bien deux dates plutôt que d'appartenir à l'une d'elles (ex. un `dateFormat` ISO contenant un tiret). Chaque occurrence candidate doit désormais faire parser valablement le texte qui la précède comme une date avant d'être retenue comme frontière. - **`ui-datepicker` : le séparateur affiché d'une plage (`range`) était le même que celui attendu en saisie**, rendant les deux indiscernables à l'écran. L'affichage utilise maintenant un tiret cadratin dédié (« 08/07/2026 – 18/07/2026 »), découplé du séparateur de saisie/parsing (`" - "`, inchangé). -- **`ui-datepicker` : ouvrir le panneau via l'icône calendrier en `range`/`multiple` ne redonnait plus le focus à la grille** (FSHSP-118). Sortir la lecture-seule du déclencheur de `triggerReadonly` (pour permettre la saisie tapée sur ces modes) avait supprimé avec elle le renvoi systématique du focus vers la grille à l'ouverture, propre à ces deux modes. Restauré explicitement, indépendamment de `triggerReadonly`. +- **`ui-datepicker` : ouvrir le panneau en `range`/`multiple` — icône ou champ — renvoyait toujours le focus vers la grille, y compris pour taper** (FSHSP-118). Un premier correctif (renvoi vers la grille dès que `selectionMode() !== 'single'`) visait à restaurer le comportement de l'icône, mais s'appliquait aussi au clic sur le **champ** lui-même — qui ouvre le panneau via le même `open()` — volant le focus au clavier au moment même de cliquer pour taper, dès que `range` a reçu son propre masque en direct. `open()` distingue maintenant explicitement les deux : un clic sur l'icône renvoie vers la grille (« je veux la grille »), un clic sur le champ garde le focus dedans pour continuer à taper — dans tous les modes, pas seulement `single`. ## [0.6.1] - 2026-08-22 diff --git a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts index 11eca4f..dbd4e3b 100644 --- a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts +++ b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts @@ -843,7 +843,11 @@ export class UiDatepicker extends BaseFormField { // --- Panel open/close ------------------------------------------------ - open(): void { + /** + * @param viaIcon Whether this open was specifically triggered by the calendar icon (vs. + * clicking the field itself, or a keyboard shortcut) — see the rove-into-grid comment below. + */ + open(viaIcon = false): void { if (this.inline() || this.isDisabled() || this.readonly() || this.panelOpen()) return; const base = this.firstSelectedFrom(this.internalValue() ?? null) ?? startOfDay(new Date()); this.viewDate.set(firstOfMonth(base)); @@ -854,14 +858,13 @@ export class UiDatepicker extends BaseFormField { this.overlayOrigin.set(this.resolveOverlayOrigin()); this.panelOpen.set(true); this.opened.emit(); - // Keep focus in the input when it's typeable in single mode (so typing can continue - // uninterrupted); otherwise rove into the (active) grid. `range`/`multiple` always rove, - // regardless of `triggerReadonly()`: typing there is a plain-text complement (no live mask, - // see `typingSlots`), never the primary interaction — the grid is, exactly as before - // `allowInput` covered these modes, and opening via the calendar icon signals "I want the - // grid" (code review finding: this used to be implicit in `triggerReadonly` hardcoding - // non-single modes read-only; restored explicitly now that it no longer does). - if (this.showCalendar() && (this.triggerReadonly() || this.selectionMode() !== 'single')) { + // Keep focus in the input whenever it's typeable; only rove into the grid when it isn't + // (`triggerReadonly`) or when opened via the icon specifically (`viaIcon` — "I want the + // grid"). Used to key off `selectionMode() !== 'single'` instead, which broke once `range` + // got its own live mask: the field's own click ALSO calls `open()` (see the template), so + // roving on every non-`single` mode stole focus back out of the field the moment you clicked + // it to type (FSHSP-118 follow-up). + if (this.showCalendar() && (this.triggerReadonly() || viaIcon)) { if (this.currentView() === 'date') this.queueDayFocus(); else if (this.currentView() === 'month') this.queueMonthFocus(); else this.queueYearFocus(); @@ -877,16 +880,17 @@ export class UiDatepicker extends BaseFormField { } /** @ignore */ - protected toggle(): void { - this.panelOpen() ? this.close() : this.open(); + protected toggle(viaIcon = false): void { + this.panelOpen() ? this.close() : this.open(viaIcon); } - /** @ignore Right action: clear when clearable + set, otherwise toggle the panel. - * `stopPropagation` keeps the click from bubbling to the field (which opens it). */ + /** @ignore Right action: clear when clearable + set, otherwise toggle the panel (via the icon — + * see `open`'s `viaIcon`). `stopPropagation` keeps the click from bubbling to the field (which + * would otherwise also call `open()`, without `viaIcon`, right behind this one). */ protected onIconClick(event: MouseEvent): void { event.stopPropagation(); if (this.showClearButton()) this.clear(); - else this.toggle(); + else this.toggle(true); } /** @ignore */