@@ -22,7 +22,7 @@
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3a0d0b..6bc13e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,12 +41,23 @@ Liste des évolutions de la librairie Malio layer UI
* Token Tailwind partagé `w-m-btn-action` (150px) exposé via `tailwind.config.ts` + CSS var `--m-btn-action-width` dans `malio.css` — utilisable côté consommateur pour les boutons d'action (``), themable en redéfinissant la CSS var
* [#MUI-41] Prop `required` cohérente + astérisque rouge dans le label sur la famille formulaire (Select, SelectCheckbox, InputUpload, InputRichText gagnent la prop)
* [#MUI-41] InputEmail : sanitisation à la saisie (suppression des espaces, option `lowercase`)
+* [#MUI-42] MalioDate : saisie clavier `JJ/MM/AAAA` optionnelle (prop `editable`, masque maska, validation au blur, message `invalidMessage`)
+* InputEmail : bouton `+` d'ajout optionnel (prop `addable`, event `add`), calqué sur InputPhone ; l'icône email passe à gauche quand le bouton est actif
+* InputAmount : affichage groupé des milliers à la française (`1 234 567,89`) en temps réel ; `modelValue` reste propre (`'1234567.89'`) ; `maxLength` borne la longueur du modèle
+* [#MUI-42] Anneau de focus clavier standardisé (`outline` 2px `m-primary`, offset 2px) affiché **uniquement** à la navigation clavier (jamais au clic souris), sur l'ensemble des champs et contrôles : inputs (Text, Email, Password, Phone, Amount, Number + boutons ±, Upload, TextArea, Autocomplete), Select, SelectCheckbox, famille Date (Date, DateRange, DateTime, DateWeek), Button, ButtonIcon. Mécanique : composable `useKbdFocusRing` (détection de modalité clavier/souris) + utilitaires CSS `.m-focus-ring` (éléments à `:focus-visible` natif) et `.m-focus-ring-kbd` (champs texte, où `:focus-visible` se déclenche aussi à la souris)
+* [#MUI-42] Anneau « combo » : quand un dropdown / calendrier est ouvert (Autocomplete, Select, SelectCheckbox, Date), l'anneau entoure le champ **et** la liste / le calendrier d'un seul tenant, adapté au sens d'ouverture (utilitaires `.m-combo-ring-top` / `.m-combo-ring-bottom`)
+* [#MUI-42] Navigation clavier WAI-ARIA APG sur les listes déroulantes : Select et SelectCheckbox gagnent la navigation (flèches, Home/End, Entrée/Espace, Échap, Tab — absente jusque-là), avec scroll automatique de l'option active et `aria-activedescendant` ; InputAutocomplete complété (scroll auto, ArrowUp ouvre sur la dernière option, Home/End, Tab ferme)
+* [#MUI-42] SelectCheckbox : la ligne « Tout sélectionner » est intégrée à la navigation clavier ; le clic sur toute la ligne d'option (et plus seulement le label) coche/décoche
+* [#MUI-42] InputUpload : prop `clearable` (croix `mdi:close` focusable qui vide le champ + event `clear`) et ouverture du sélecteur de fichier au clavier (Entrée / Espace)
+* [#MUI-42] Famille Date : ouverture du calendrier au clavier (Entrée / Espace), fermeture par Échap
### Changed
+* DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés.
* MalioButton : dimensions par défaut `w-[180px]` / `h-[38px]` (étaient `w-[200px]` / `h-[40px]`).
* DataTable : tailles par défaut revues — texte header `16px` (était `20px`), texte body `14px` (était `18px`), sélecteur de lignes et boutons de pagination (Prev / numéros / Next) alignés à `30px` de haut, padding de `12px` entre le bas du tableau et la barre de pagination, texte header et body passés en noir (`text-black`, étaient `text-m-primary`).
* Select : nouvelle prop `fieldClass` pour surcharger les classes du field (notamment la hauteur `h-[40px]` jusqu'ici codée en dur) ; utilisée par le DataTable pour passer le sélecteur de perPage à `30px`.
* [#MUI-35] Refonte du composant drawer : slots `#header`/`#footer`, prop `side` (droite/gauche), `dismissable`, `closeOnEscape`, classes d'override, focus-trap, scroll-lock et fermeture au clavier. **Breaking** : la prop `title` est remplacée par le slot `#header`.
+* [#MUI-42] Button / ButtonIcon : l'anneau de focus passe du halo `ring-2 ring-m-primary/50` à l'anneau standard `.m-focus-ring` (outline plein, offset 2px), pour l'homogénéité avec les autres composants.
### Fixed
* DataTable : pagination réalignée verticalement après l'introduction du `min-h-[1rem]` du Select — la barre pagination passe en `items-center`, et le MalioSelect du sélecteur de perPage est encapsulé dans un wrapper `h-12` qui borne sa taille flex à la hauteur du field (le slot vide déborde invisiblement en dessous). Span « Lignes : » et boutons Prev/Page/Next sont désormais centrés exactement sur le field (y=24)
@@ -61,3 +72,6 @@ Liste des évolutions de la librairie Malio layer UI
* InputAutocomplete : suppression de 4 sources de saut visuel au focus / ouverture (extra translate label, padding `grow-height:focus`, `focus:pl-[11px]`, `!border-b-0` remplacé par `!border-b-transparent`)
* Select / SelectCheckbox : mêmes correctifs anti-saut (suppression du padding `grow-height:focus` et remplacement de `!border-b-0` / `!border-t-0` par leurs variantes `transparent`)
* MalioButton : largeur par défaut alignée sur `w-[200px]` (au lieu de `w-[240px]`) pour correspondre au sizing des formulaires de l'app
+* [#MUI-42] RadioButton : ajout d'un focus visible au clavier (`outline` 2px `m-primary`, offset 2px) — l'input en `appearance-none` n'avait aucun indicateur de focus, seul l'`outline: auto 1px` du navigateur restait, quasi invisible. La navigation native (Tab entre groupes, flèches dans le groupe) reste inchangée
+* [#MUI-42] Checkbox : ajout d'un focus visible au clavier sur la case (`outline` 2px `m-primary`, offset 2px) — l'input réel est masqué (`clip-path`), aucun indicateur n'apparaissait à la tabulation
+* [#MUI-42] Select : le focus reste sur le bouton après sélection (un `blur()` renvoyait le focus au `body`, cassant la tabulation clavier — un Tab repartait du haut de page)
diff --git a/COMPONENTS.md b/COMPONENTS.md
index fcc7332..805579d 100644
--- a/COMPONENTS.md
+++ b/COMPONENTS.md
@@ -4,6 +4,8 @@ Tous les composants sont auto-importés avec le préfixe `Malio`. Utiliser `v-mo
> **Champ obligatoire :** sur les composants de formulaire, la prop `required` ajoute un astérisque rouge dans le label. C'est un repère visuel ; la sémantique « obligatoire » est portée par l'attribut natif `required` ou `aria-required`.
+> **Focus clavier :** tous les champs et contrôles affichent un anneau de focus (`outline` 2px `m-primary`, offset 2px) **uniquement** à la navigation clavier (Tab), jamais au clic souris. Sur les composants à dropdown/calendrier ouverts, l'anneau entoure le champ et la liste d'un seul tenant. Voir la note « Clavier » de chaque composant pour la navigation détaillée.
+
---
## MalioInputText
@@ -94,19 +96,25 @@ Champ email (`type="email"` + `inputmode="email"`) avec icône `mdi:email-outlin
| `iconPosition` | `'left' \| 'right'` | `'right'` | Position de l'icône |
| `iconSize` | `string \| number` | `24` | Taille icône |
| `iconColor` | `string` | `'text-m-muted'` | Classe couleur icône |
+| `addable` | `boolean` | `false` | Affiche un bouton `+` à droite qui émet l'event `add` (l'icône email passe à gauche) |
+| `addIconName` | `string` | `'mdi:plus'` | Icône Iconify du bouton d'ajout |
+| `addButtonLabel` | `string` | `'Ajouter une adresse email'` | aria-label du bouton d'ajout |
| `inputClass` | `string` | `''` | Classes CSS input |
| `labelClass` | `string` | `''` | Classes CSS label |
| `groupClass` | `string` | `''` | Classes CSS conteneur |
> **Sanitisation à la saisie :** tous les espaces sont supprimés automatiquement au fil de la frappe (sans masque). Avec `lowercase=true`, la valeur est également convertie en minuscules à la frappe. La validation du format (ex. présence d'un `@`) reste à la charge du parent via la prop `error` ou la couche de validation.
-**Events :** `update:modelValue(value: string)`
+**Events :**
+- `update:modelValue(value: string)`
+- `add()` — émis au clic du bouton `+` (uniquement si `addable`, non `disabled`, non `readonly`)
```vue
+
```
---
@@ -194,7 +202,7 @@ Champ de saisie assistée (typeahead / combobox) : l'utilisateur tape pour filtr
- `select(option: Option \| null)` — émis avec l'objet `Option` complet (utile pour récupérer aussi le `label`)
- `create(value: string)` — émis quand `allowCreate=true` et que l'utilisateur valide une valeur libre
-**Clavier :** `↓` / `↑` navigation, `Entrée` sélection (ou création), `Échap` ferme le dropdown.
+**Clavier (WAI-ARIA APG) :** `↓` ouvre / option suivante, `↑` précédente (ou ouvre sur la dernière option si fermé), `Début`/`Fin`, scroll automatique de l'option active, `Entrée` sélection (ou création), `Échap` annule, `Tab` ferme. Anneau de focus clavier (combo champ + liste à l'ouverture).
```vue
@@ -236,6 +244,8 @@ async function onSearchClients(query: string) {
Champ montant avec icône devise (euro par défaut).
+L'affichage est groupé à la française (`1 234 567,89` : espace pour les milliers, virgule décimale), mis à jour en temps réel pendant la saisie. La valeur émise (`modelValue`) reste une **chaîne numérique propre** (point décimal, sans espaces, ex. `'1234567.89'`). `maxLength` borne la longueur de cette chaîne propre (pas de l'affichage).
+
| Prop | Type | Défaut | Description |
|------|------|--------|-------------|
| `modelValue` | `string \| null` | `undefined` | Valeur (v-model) |
@@ -251,6 +261,8 @@ Champ montant avec icône devise (euro par défaut).
```vue
+
+
```
---
@@ -352,16 +364,20 @@ Champ d'upload de fichier.
| `label` | `string` | `''` | Label |
| `accept` | `string` | `''` | Types de fichiers acceptés |
| `displayIcon` | `boolean` | `true` | Afficher l'icône |
+| `clearable` | `boolean` | `false` | Affiche une croix (`mdi:close`) focusable qui vide le champ quand un fichier est sélectionné |
| `disabled` | `boolean` | `false` | Désactivé |
| `readonly` | `boolean` | `false` | Champ en lecture seule (bordure noire, pas de focus bleu/grossissement, label/icône gris→noir selon rempli). |
| `required` | `boolean` | `false` | Champ requis (astérisque rouge dans le label) |
| `error` | `string` | `''` | Message d'erreur |
| `reserveMessageSpace` | `boolean` | `true` | Réserve l'espace de la ligne message sous le champ (min-h) même sans message. `false` = la ligne ne prend de place que s'il y a un hint/error/success. |
-**Events :** `update:modelValue(value: string)`, `file-selected(file: File)`
+**Events :** `update:modelValue(value: string)`, `file-selected(file: File)`, `clear()`
+
+**Clavier :** `Entrée` / `Espace` ouvrent le sélecteur de fichier. La croix `clearable` est focusable (anneau clavier, `Entrée`/`Espace`).
```vue
+
```
---
@@ -394,6 +410,8 @@ Liste déroulante.
**Events :** `update:modelValue(value: string | number | null)`
**Slots :** `icon` (icône dropdown custom)
+**Clavier (WAI-ARIA APG) :** `↓`/`↑`/`Entrée`/`Espace` ouvrent ; liste ouverte → `↑↓` naviguent (scroll auto de l'option active), `Début`/`Fin`, `Entrée`/`Espace` sélectionnent, `Échap`/`Tab` ferment. Le focus reste sur le bouton après sélection. Anneau de focus clavier (combo bouton + liste à l'ouverture, adapté au sens haut/bas).
+
```vue
@@ -422,6 +440,8 @@ Liste déroulante multi-sélection avec checkboxes.
**Events :** `update:modelValue(value: (string | number)[])`
+**Clavier (WAI-ARIA APG) :** `↓`/`↑`/`Entrée`/`Espace` ouvrent ; liste ouverte → `↑↓` naviguent (scroll auto), `Début`/`Fin`, `Entrée`/`Espace` cochent/décochent l'option active (la liste **reste ouverte**), `Échap`/`Tab` ferment. La ligne « Tout sélectionner » est navigable au clavier. Le clic sur toute la ligne (pas que le label) coche/décoche. Anneau de focus clavier (combo bouton + liste à l'ouverture).
+
```vue
@@ -445,6 +465,8 @@ Case à cocher.
**Events :** `update:modelValue(value: boolean)`
+**Clavier :** `Espace` coche/décoche. Focus clavier visible sur la case (`outline` 2px `m-primary`).
+
```vue
@@ -468,6 +490,8 @@ Bouton radio (à utiliser en groupe avec le même `name`).
**Events :** `update:modelValue(value: string | number | boolean | null)`
+**Clavier :** comportement natif d'un groupe radio (options partageant le même `name`) — `Tab` / `Maj+Tab` entre/sort du groupe (1 seul arrêt par groupe), `↑↓←→` déplacent la sélection entre les options d'un même groupe. Focus clavier visible (`outline` 2px `m-primary`).
+
```vue
@@ -481,6 +505,8 @@ Sélecteur de date unique avec popover (grille de calendrier + vue mois/année).
La valeur est une chaîne ISO `"YYYY-MM-DD"`. Cliquer un jour émet la date et ferme le popover.
+Avec `editable`, l'utilisateur peut aussi taper la date au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`).
+
| Prop | Type | Défaut | Description |
|------|------|--------|-------------|
| `modelValue` | `string \| null` | `undefined` | Date ISO `"YYYY-MM-DD"` (v-model) |
@@ -497,15 +523,20 @@ La valeur est une chaîne ISO `"YYYY-MM-DD"`. Cliquer un jour émet la date et f
| `min` | `string` | `undefined` | Date min `"YYYY-MM-DD"` (jours antérieurs désactivés) |
| `max` | `string` | `undefined` | Date max `"YYYY-MM-DD"` (jours postérieurs désactivés) |
| `clearable` | `boolean` | `true` | Affiche la croix d'effacement |
+| `editable` | `boolean` | `false` | Autorise la saisie clavier `JJ/MM/AAAA` (masque maska, validation au blur) en plus du calendrier |
+| `invalidMessage` | `string` | `'Date invalide'` | Message affiché quand la saisie clavier est invalide ou hors `min`/`max` |
| `reserveMessageSpace` | `boolean` | `true` | Réserve l'espace de la ligne message sous le champ (min-h) même sans message. `false` = la ligne ne prend de place que s'il y a un hint/error/success. |
| `inputClass` / `labelClass` / `groupClass` | `string` | `''` | Override des classes |
**Events :** `update:modelValue(value: string | null)`
+**Clavier :** `Entrée` / `Espace` ouvrent le calendrier, `Échap` ferme. Anneau de focus clavier (combo champ + calendrier à l'ouverture). La croix d'effacement est focusable. _(Comportement partagé par DateRange, DateTime, DateWeek via le shell CalendarField.)_
+
```vue
+
```
---
diff --git a/app/assets/css/malio.css b/app/assets/css/malio.css
index 2d5f716..0ab6772 100644
--- a/app/assets/css/malio.css
+++ b/app/assets/css/malio.css
@@ -2,6 +2,41 @@
@tailwind components;
@tailwind utilities;
+@layer components {
+ /* Anneau de focus clavier standard (navigation au Tab), invisible à la souris.
+ Deux déclencheurs, même rendu :
+ - .m-focus-ring → s'appuie sur :focus-visible natif. Pour les éléments
+ où :focus-visible se limite déjà au clavier (boutons,
+ onglets, tuiles, checkbox/radio…).
+ - .m-focus-ring-kbd → classe ajoutée en JS (via useKbdFocusRing) uniquement
+ quand le focus vient du clavier. Pour les champs texte,
+ où :focus-visible natif se déclenche aussi à la souris.
+ Le `:focus` sur .m-focus-ring-kbd élève la spécificité pour passer devant le
+ `outline-none` des inputs. */
+ .m-focus-ring:focus-visible,
+ .m-focus-ring-kbd:focus {
+ outline: 2px solid rgb(var(--m-primary) / 1);
+ outline-offset: 2px;
+ }
+
+ /* Anneau de focus clavier pour un combobox ouvert (input + liste) : l'anneau
+ entoure le bloc entier d'un seul tenant. L'input porte le contour haut+côtés,
+ la liste le contour côtés+bas ; la jonction (bas de l'input / haut de la liste)
+ reste sans contour pour un raccord sans couture. */
+ .m-combo-ring-top {
+ box-shadow:
+ -2px 0 0 0 rgb(var(--m-primary) / 1),
+ 2px 0 0 0 rgb(var(--m-primary) / 1),
+ 0 -2px 0 0 rgb(var(--m-primary) / 1);
+ }
+ .m-combo-ring-bottom {
+ box-shadow:
+ -2px 0 0 0 rgb(var(--m-primary) / 1),
+ 2px 0 0 0 rgb(var(--m-primary) / 1),
+ 0 2px 0 0 rgb(var(--m-primary) / 1);
+ }
+}
+
@layer base {
:root {
/* ── Globales ── */
diff --git a/app/components/malio/button/Button.vue b/app/components/malio/button/Button.vue
index 11037c9..81f5e9f 100644
--- a/app/components/malio/button/Button.vue
+++ b/app/components/malio/button/Button.vue
@@ -84,7 +84,7 @@ const variantClasses = computed(() => {
const mergedButtonClass = computed(() =>
twMerge(
- 'inline-flex w-[180px] h-[38px] items-center justify-center gap-1 p-[10px] rounded-md text-base font-bold leading-[150%] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-m-primary/50',
+ 'inline-flex w-[180px] h-[38px] items-center justify-center gap-1 p-[10px] rounded-md text-base font-bold leading-[150%] transition-colors duration-150 m-focus-ring',
variantClasses.value,
props.buttonClass,
),
diff --git a/app/components/malio/button/ButtonIcon.vue b/app/components/malio/button/ButtonIcon.vue
index 9534324..645d93e 100644
--- a/app/components/malio/button/ButtonIcon.vue
+++ b/app/components/malio/button/ButtonIcon.vue
@@ -52,7 +52,7 @@ const isFilled = computed(() => props.variant === 'filled')
const mergedButtonClass = computed(() =>
twMerge(
- 'inline-flex items-center justify-center rounded-md p-1 transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-m-primary/50',
+ 'inline-flex items-center justify-center rounded-md p-1 transition-colors duration-150 m-focus-ring',
isFilled.value
? props.disabled
? 'bg-m-disabled text-white cursor-not-allowed'
diff --git a/app/components/malio/checkbox/Checkbox.vue b/app/components/malio/checkbox/Checkbox.vue
index 73ba5eb..bd950a7 100644
--- a/app/components/malio/checkbox/Checkbox.vue
+++ b/app/components/malio/checkbox/Checkbox.vue
@@ -180,6 +180,11 @@ const onChange = (event: Event) => {
border-color: rgb(0, 0, 0);
}
+.inp-cbx:focus-visible + .cbx span:first-child {
+ outline: 2px solid rgb(var(--m-primary) / 1);
+ outline-offset: 2px;
+}
+
.cbx span:first-child svg {
position: absolute;
top: 2px;
diff --git a/app/components/malio/datatable/DataTable.vue b/app/components/malio/datatable/DataTable.vue
index 00817ee..f2575d1 100644
--- a/app/components/malio/datatable/DataTable.vue
+++ b/app/components/malio/datatable/DataTable.vue
@@ -81,7 +81,7 @@