From 28a267fc9488087c6fb828a451ffe5ccf9342d1c Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 24 Aug 2026 19:59:52 +0200 Subject: [PATCH 1/2] FSHSP-118 chore(ui-datepicker): trim overly verbose review-fix comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0960a11 and d76845a piled multi-paragraph docblocks onto small pieces of logic (typingSlots alone had ~25 lines across two paragraphs) — too long to actually get read. Same content, compressed to what the next reader needs, in ui-datepicker.ts, mask-engine.ts, and their spec files. No behavior change. --- .../ui-kit/forms/src/lib/mask-engine.spec.ts | 34 ++--- projects/ui-kit/forms/src/lib/mask-engine.ts | 21 +-- .../src/lib/ui-datepicker.spec.ts | 17 +-- .../ui-datepicker/src/lib/ui-datepicker.ts | 124 +++++------------- 4 files changed, 54 insertions(+), 142 deletions(-) diff --git a/projects/ui-kit/forms/src/lib/mask-engine.spec.ts b/projects/ui-kit/forms/src/lib/mask-engine.spec.ts index cde7882..32be6ad 100644 --- a/projects/ui-kit/forms/src/lib/mask-engine.spec.ts +++ b/projects/ui-kit/forms/src/lib/mask-engine.spec.ts @@ -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 }); @@ -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 @@ -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 }, @@ -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 }, diff --git a/projects/ui-kit/forms/src/lib/mask-engine.ts b/projects/ui-kit/forms/src/lib/mask-engine.ts index 0eb732d..1c86c14 100644 --- a/projects/ui-kit/forms/src/lib/mask-engine.ts +++ b/projects/ui-kit/forms/src/lib/mask-engine.ts @@ -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 })); @@ -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; } diff --git a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.spec.ts b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.spec.ts index eb79f70..56478ec 100644 --- a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.spec.ts +++ b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.spec.ts @@ -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 - * `` — 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 `` — 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'; 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 dbedeb4..11eca4f 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 @@ -123,25 +123,13 @@ export interface DatepickerMonthPanel { let nextPanelUid = 0; -/** - * Joins the two dates of a TYPED `range` value (FSHSP-118: `"jj/mm/aaaa - jj/mm/aaaa"`) — a - * plain hyphen, practically typable on a standard keyboard. Used to compose the placeholder, as - * the literal separator of the live auto-"/" mask (`typingSlots`), and to split typed text back - * apart in `parseTypedMulti`. - * - * Deliberately NOT used for `displayValue` (see `RANGE_DISPLAY_SEPARATOR`): a committed range - * had always rendered with an en dash, for every `range` consumer, typing or not: reusing this - * hyphen there too was a code-review-caught regression — it silently changed that display text - * for every existing grid-only range consumer that never opted into typed entry at all. - */ +/** Typed `range` separator (FSHSP-118: `"jj/mm/aaaa - jj/mm/aaaa"`) — placeholder, live mask, + * and `parseTypedMulti` splitting. A plain hyphen, distinct from the DISPLAYED en dash below — + * reusing this one for display used to silently change the look of every non-typing consumer. */ const RANGE_SEPARATOR = ' - '; -/** Joins the two dates of a DISPLAYED (committed) `range` value — unchanged from before typed - * entry existed. Typing a plain hyphen (`RANGE_SEPARATOR`) still round-trips to this on commit; - * parsing and display are deliberately decoupled so the pre-existing look survives untouched. */ +/** Displayed (committed) `range` separator — unchanged since before typed entry existed. */ const RANGE_DISPLAY_SEPARATOR = ' – '; -/** Joins the dates of a typed/displayed `multiple` value (`"jj/mm/aaaa, jj/mm/aaaa, ..."`) — one - * separator for both: unlike `range`, `multiple`'s display never had an en-dash-style - * convention to preserve. */ +/** Typed/displayed `multiple` separator (`"jj/mm/aaaa, jj/mm/aaaa, ..."`) — one separator for both. */ const MULTIPLE_SEPARATOR = ', '; /** @@ -399,15 +387,8 @@ export class UiDatepicker extends BaseFormField { private readonly resolvedDisabledDates = computed(() => (this.disabledDates() ?? []).map(normalizeDateInput).filter((d): d is Date => d !== null), ); - /** - * @ignore The trigger is not typeable: manual input off, read-only, or `timeOnly` — free-form - * typing in time-only mode isn't implemented (no dedicated parser/formatter for a bare time - * string), so it's disabled outright here rather than silently mis-parsing. `range`/`multiple` - * ARE typeable (FSHSP-118: "jj/mm/aaaa - jj/mm/aaaa", "jj/mm/aaaa, jj/mm/aaaa, ...") — see - * `parseTypedMulti`. `range` gets the same live auto-"/" mask as `single` (see `typingSlots`); - * `multiple`'s unbounded date count doesn't fit a fixed mask template, so it stays plain text - * parsed on blur/Enter only. - */ + /** @ignore Not typeable: manual input off, read-only, or `timeOnly` (no bare-time parser). + * `range`/`multiple` ARE typeable (FSHSP-118) — see `parseTypedMulti`/`typingSlots`. */ protected readonly triggerReadonly = computed( () => this.readonly() || !this.allowInput() || this.timeOnly(), ); @@ -489,31 +470,17 @@ export class UiDatepicker extends BaseFormField { this.dateFieldOrder().filter((f) => (this.view() === 'month' ? f !== 'day' : true)), ); /** - * @ignore Dynamic mask (day/month/year widths in locale order, plus hour/minute — and AM/PM — - * widths when `showTime`; `range` repeats the same widths a second time, joined by - * `RANGE_SEPARATOR`) driving the auto-"/" (resp. ":", " - ") formatting of the typeable trigger. - * `null` disables it: `triggerReadonly` (covers `timeOnly` — see there), `view === 'year'` - * (free-form numeric field, out of scope), a custom `parseDate` (a non-numeric format would - * make the auto-slash wrong), `hasValue()` (FSHSP-118), or `multiple` (unbounded number of - * dates — a fixed mask template can't model it; typed through plain text on blur/Enter only, - * via `parseTypedMulti`, same as `range` before this mask covered it too). + * @ignore Dynamic mask (day/month/year widths, `+` time if `showTime`, `+` a second date for + * `range`) driving the auto-"/"/":"/" - " formatting of the typeable trigger. `null` disables + * it: `triggerReadonly`, `view === 'year'` (free-form field), a custom `parseDate` (non-numeric + * format), `hasValue()`, or `multiple` (unbounded date count, no fixed template fits — plain + * text on blur/Enter instead, see `parseTypedMulti`). * - * That `hasValue()` gate: re-deriving the mask from a flat digit stream on every keystroke only - * ever behaves well for *constructing* a date (or, in `range`, a pair of dates) from nothing — - * sequential forward typing, or backspacing from the end. It has no notion of "this segment was - * already valid, only touch it" (day/month are bounds-checked against arbitrary residual digits - * after any edit; year, the one unbounded segment, is the sole exception, which is why editing - * it works and looks like an inconsistency until you know why). Once a value already exists, - * editing it in place is common (fixing a typo, changing the year to file the same form again) - * and hits exactly that gap. Disabling the mask there routes typing to the plain passthrough - * branch below instead — no live auto-formatting, but no corruption either — and defers to - * `commitTyped`'s parser (already tolerant of arbitrary separators, see `defaultParse`/ - * `parseTypedMulti`) on blur/Enter. The mask re-arms on its own once the field is cleared and - * `hasValue()` goes back to `false` — see `onTriggerInput`, which commits the clear the instant - * the raw text reads empty (not just on blur/Enter): a transient "text is empty" check here - * instead would only hold for the one keystroke that empties the field — `hasValue()` alone, - * unrefreshed, flips back on with the very next character typed, since nothing ever committed it - * to `false` for real. + * `hasValue()`: re-deriving the mask from the raw digit stream only works for *constructing* a + * value from nothing, not editing one in place (day/month re-validate arbitrary residual digits + * after any edit — see `onTriggerInput`). So it's off once a value exists — plain text instead, + * parsed on blur/Enter — and re-arms the moment the field reads empty (`onTriggerInput` commits + * the clear right there, not just on blur, so `hasValue()` actually flips before the next key). */ private readonly typingSlots = computed(() => { const mode = this.selectionMode(); @@ -1003,20 +970,11 @@ export class UiDatepicker extends BaseFormField { if (this.panelOpen()) this.previewTyped(); } - /** - * @ignore Whether `text` already carries a time portion too, when one is required - * (`showTime`, single-date `view === 'date'`) — code-review fix, FSHSP-118. `defaultParse`'s - * own `requireComplete` only checks day/month/year are present, never the trailing hour/minute - * groups, so `previewTyped` (below) used to treat the date-only prefix as "complete" the - * moment day/month/year were typed. That flipped `hasValue()` true — and so, per `typingSlots`, - * turned the live mask off — before a single time digit had been typed: the very next keystroke - * (the first hour digit) then landed with no mask active to insert the "HH:MM" separators, - * concatenating straight onto the year (e.g. `"08/07/20261030"`) and corrupting the eventual - * parse. Gating the PREVIEW specifically (not `commitTyped`'s own completeness check — a - * date-only value typed then immediately blurred is still a legitimate final commit, time - * defaulting to the steppers, exactly as before) on the time portion also being present keeps - * `hasValue()` — and the mask — off until the whole thing, date and time, is actually done. - */ + /** @ignore Whether `text` already has its time portion too, when `showTime` requires one. + * `previewTyped` needs this: without it, `hasValue()` (and the mask) flipped off the moment + * day/month/year were typed, before a single time digit — the next digit then glued onto the + * year with no mask to insert "HH:MM" (FSHSP-118). `commitTyped` doesn't use this: a date typed + * and blurred without a time is still a legitimate commit (time defaults to the steppers). */ private hasCompleteTimeIfNeeded(text: string): boolean { if (!this.showTime() || this.view() !== 'date') return true; const groups = text.match(/\d+/g) ?? []; @@ -1103,18 +1061,11 @@ export class UiDatepicker extends BaseFormField { return this.parseTypedMulti(text, requireComplete); } - /** - * @ignore `range`/`multiple` typed entry (FSHSP-118): splits on `RANGE_SEPARATOR`/ - * `MULTIPLE_SEPARATOR` (via `splitTypedSegments` — see there for why a plain `String.split` - * isn't safe) and parses each part with the very same single-date logic as `single` mode — - * `parseTyped`, so a custom `parseDate` applies per part too, symmetric with how a custom - * `dateFormat` already applies per date via `formatDate`. `range` requires exactly two parts, - * reordered chronologically (mirrors the grid's own reordering in `selectDay`); `multiple` - * accepts any number, duplicates collapsed (mirrors the grid's click-to-toggle). Any - * unparseable or disabled part fails the whole thing — never a partial commit. Always - * `startOfDay` (no `showTime` support here: a "jj/mm/aaaa hh:mm - jj/mm/aaaa hh:mm" format is a - * further chantier of its own, out of scope for now). - */ + /** @ignore `range`/`multiple` typed entry (FSHSP-118): splits via `splitTypedSegments`, parses + * each part with `parseTyped` (a custom `parseDate` applies per part, like `dateFormat` does + * per date on display). `range` needs exactly 2 parts, reordered chronologically; `multiple` + * takes any count, deduped. One bad/disabled part fails the whole thing. Always `startOfDay` — + * no `showTime` support here. */ private parseTypedMulti(text: string, requireComplete: boolean): Date[] | null { const mode = this.selectionMode(); const sep = mode === 'range' ? RANGE_SEPARATOR : MULTIPLE_SEPARATOR; @@ -1128,23 +1079,10 @@ export class UiDatepicker extends BaseFormField { return dates.filter((d, i) => dates.findIndex((o) => isSameDay(o, d)) === i); } - /** - * @ignore Splits typed `range`/`multiple` text into per-date segments (code-review fix, - * FSHSP-118). A plain `text.split(sep)` breaks the moment a single date's own formatted text - * contains the separator character — a custom `dateFormat` producing `"Jul 8, 2026"` already - * contains the `", "` `multiple` splits on; an ISO/dash `dateFormat` like `"2026-07-08"` - * already contains the `-` `range` splits on. - * - * Instead of splitting blindly, this scans for each occurrence of `sep` and only commits to a - * boundary once the text *up to* it already parses as a complete date via `parseTyped` — the - * same single-date parser used everywhere else, so a custom `parseDate` decides completeness - * exactly as it does for `single` mode. An in-progress prefix (`"Jul 8"`, still missing its - * year) fails that check and is skipped in favor of the next `sep` occurrence, so a date's own - * internal separator is never mistaken for the boundary between two dates. Falls back to - * treating the remainder as one final segment once no more `sep` occurrences parse or exist — - * this is also what naturally reports "still incomplete" (e.g. only the first date typed so - * far in `range`) up to the part-count check in `parseTypedMulti`. - */ + /** @ignore Splits typed `range`/`multiple` text into per-date segments (FSHSP-118). A plain + * `text.split(sep)` breaks when a date's own formatted text contains `sep` (e.g. a `", "` + * `dateFormat` in `multiple`, or an ISO dash in `range`) — instead, a `sep` occurrence is only + * accepted as a boundary once the text before it already parses as a complete date. */ private splitTypedSegments(text: string, sep: string): string[] { const segments: string[] = []; let rest = text.trim(); From 4a724f6c0ec63ad754a079520cc2670ccece49c7 Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 24 Aug 2026 20:00:40 +0200 Subject: [PATCH 2/2] FSHSP-118 docs(changelog): fix stale range/multiple read-only note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Changed entry for allowInput's default (still true) said range/ multiple stay read-only — true when written, but the Added entry right above it (and the code: triggerReadonly no longer tests selectionMode) already documents that both are covered now. Spotted in passing while working on a separate ticket. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cac64d..b68b0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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