| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié --------- Co-authored-by: admin malio <malio@yuno.malio.fr> Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-authored-by: matthieu <matthieu@yuno.malio.fr> Reviewed-on: #80 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #80.
This commit is contained in:
@@ -73,6 +73,18 @@ describe('MalioDate', () => {
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('opens on calendar icon click', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="calendar-icon"]').trigger('click')
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('opens on calendar icon click in editable mode', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
await wrapper.get('[data-test="calendar-icon"]').trigger('click')
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('opens on the current month when there is no value', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
@@ -226,6 +238,23 @@ describe('MalioDate', () => {
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('disabled : label grisé', () => {
|
||||
const wrapper = mountDate({disabled: true, label: 'Date'})
|
||||
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||
})
|
||||
|
||||
it('disabled : pas de croix d\'effacement même avec une valeur', () => {
|
||||
const wrapper = mountDate({disabled: true, modelValue: '2026-05-19'})
|
||||
expect(wrapper.find('[data-test="clear"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('disabled + rempli : icône calendrier grisée (pas noire)', () => {
|
||||
const wrapper = mountDate({disabled: true, modelValue: '2026-05-19'})
|
||||
const icon = wrapper.get('[data-test="calendar-icon"]')
|
||||
expect(icon.classes()).toContain('text-m-muted')
|
||||
expect(icon.classes()).not.toContain('text-black')
|
||||
})
|
||||
|
||||
it('does not open when readonly', async () => {
|
||||
const wrapper = mountDate({readonly: true, modelValue: '2026-05-19'})
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
@@ -308,7 +337,7 @@ describe('MalioDate', () => {
|
||||
it('efface l\'erreur de saisie quand modelValue change de l\'extérieur', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
await wrapper.setProps({modelValue: '2026-05-19'})
|
||||
@@ -338,10 +367,10 @@ describe('MalioDate', () => {
|
||||
it('garde le texte et affiche « Date invalide » sur saisie invalide au blur', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect((input.element as HTMLInputElement).value).toBe('32/13/2026')
|
||||
expect((input.element as HTMLInputElement).value).toBe('31/02/2026')
|
||||
expect(input.attributes('aria-invalid')).toBe('true')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
@@ -366,7 +395,7 @@ describe('MalioDate', () => {
|
||||
it('efface l\'erreur de saisie quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
await input.trigger('focus')
|
||||
@@ -391,10 +420,34 @@ describe('MalioDate', () => {
|
||||
it('utilise le message invalidMessage personnalisé', async () => {
|
||||
const wrapper = mountDate({editable: true, invalidMessage: 'Format incorrect'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999')
|
||||
// 31/02/2026 : champs valides (jour ≤ 31, mois ≤ 12) mais le 31 février n'existe pas.
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Format incorrect')
|
||||
})
|
||||
|
||||
it('empêche la frappe d\'une date absurde (99/99/9999 borné par le masque)', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999')
|
||||
await input.trigger('blur')
|
||||
// Le bornage refuse « 9 » dès le 1er chiffre du jour/mois : la saisie absurde
|
||||
// ne s'inscrit jamais et aucune date réelle n'est émise.
|
||||
expect((input.element as HTMLInputElement).value).not.toContain('99')
|
||||
const emitted = wrapper.emitted('update:modelValue') ?? []
|
||||
expect(emitted.every(([value]) => value === null)).toBe(true)
|
||||
})
|
||||
|
||||
it('empêche un jour > 31 ou un mois > 12 (exemple métier 33/19, 2e chiffre borné)', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
// 33 (jour) : le 2e « 3 » est refusé → seul « 3 » subsiste.
|
||||
await input.setValue('33')
|
||||
expect((input.element as HTMLInputElement).value).toBe('3')
|
||||
// 19 en mois : après un jour valide, le 2e chiffre du mois (« 9 ») est refusé.
|
||||
await input.setValue('15/19')
|
||||
expect((input.element as HTMLInputElement).value).not.toContain('19')
|
||||
})
|
||||
})
|
||||
|
||||
describe('gabarit de saisie (editable)', () => {
|
||||
@@ -438,9 +491,9 @@ describe('MalioDate', () => {
|
||||
it('vide le champ au clic sur la croix même après une saisie invalide (modelValue déjà null)', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect((input.element as HTMLInputElement).value).toBe('32/13/2026')
|
||||
expect((input.element as HTMLInputElement).value).toBe('31/02/2026')
|
||||
await wrapper.get('[data-test="clear"]').trigger('click')
|
||||
expect((input.element as HTMLInputElement).value).toBe('')
|
||||
})
|
||||
@@ -468,7 +521,7 @@ describe('MalioDate', () => {
|
||||
it('émet valid=false sur saisie malformée sans émettre modelValue', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
@@ -507,7 +560,7 @@ describe('MalioDate', () => {
|
||||
it('repasse valid=true quand modelValue change de l\'extérieur après une saisie invalide', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
await wrapper.setProps({modelValue: '2026-05-19'})
|
||||
@@ -519,9 +572,9 @@ describe('MalioDate', () => {
|
||||
it('émet le texte brut trimmé sur saisie malformée, sans émettre modelValue', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026'])
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['31/02/2026'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -560,9 +613,9 @@ describe('MalioDate', () => {
|
||||
it('émet rawValue vide quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.setValue('31/02/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026'])
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['31/02/2026'])
|
||||
await input.trigger('focus')
|
||||
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||
|
||||
@@ -145,14 +145,27 @@ describe('MalioDateTime', () => {
|
||||
it('garde le texte et affiche « Date invalide » sur saisie invalide au blur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect((input.element as HTMLInputElement).value).toBe('32/13/2026 14:30')
|
||||
expect((input.element as HTMLInputElement).value).toBe('31/02/2026 14:30')
|
||||
expect(input.attributes('aria-invalid')).toBe('true')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('empêche la frappe d\'un datetime absurde (99/99/9999 99:99 borné par le masque)', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999 99:99')
|
||||
await input.trigger('blur')
|
||||
// Le masque borne le 1er chiffre de chaque champ (jour 0-3, mois 0-1,
|
||||
// heure 0-2, minute 0-5) : « 9 » est rejeté partout, rien ne s'inscrit
|
||||
// et aucun datetime réel n'est émis.
|
||||
expect((input.element as HTMLInputElement).value).not.toContain('99')
|
||||
const emitted = wrapper.emitted('update:modelValue') ?? []
|
||||
expect(emitted.every(([value]) => value === null)).toBe(true)
|
||||
})
|
||||
|
||||
it('passe en erreur si le datetime saisi est hors min/max', async () => {
|
||||
const wrapper = mountDateTime({editable: true, min: '2026-05-10T00:00:00', max: '2026-05-20T00:00:00'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
@@ -184,7 +197,8 @@ describe('MalioDateTime', () => {
|
||||
it('utilise le message invalidMessage personnalisé', async () => {
|
||||
const wrapper = mountDateTime({editable: true, invalidMessage: 'Format incorrect'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999 10:00')
|
||||
// 31/02 : champs valides mais date inexistante (le masque la laisse passer, la validation la rejette).
|
||||
await input.setValue('31/02/2026 10:00')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Format incorrect')
|
||||
})
|
||||
@@ -192,7 +206,7 @@ describe('MalioDateTime', () => {
|
||||
it('efface l\'erreur de saisie quand modelValue change de l\'extérieur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
await wrapper.setProps({modelValue: '2026-05-20T14:30:00'})
|
||||
@@ -246,7 +260,7 @@ describe('MalioDateTime', () => {
|
||||
it('émet valid=false sur saisie malformée sans émettre modelValue', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
@@ -276,7 +290,7 @@ describe('MalioDateTime', () => {
|
||||
it('repasse valid=true quand modelValue change de l\'extérieur après une saisie invalide', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
await wrapper.setProps({modelValue: '2026-05-20T14:30:00'})
|
||||
@@ -288,9 +302,9 @@ describe('MalioDateTime', () => {
|
||||
it('émet le texte brut trimmé sur saisie malformée, sans émettre modelValue', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026 14:30'])
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['31/02/2026 14:30'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -329,9 +343,9 @@ describe('MalioDateTime', () => {
|
||||
it('émet rawValue vide quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026 14:30'])
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['31/02/2026 14:30'])
|
||||
await input.trigger('focus')
|
||||
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {describe, it, expect} from 'vitest'
|
||||
import {buildBoundedMask} from './maskTemplate'
|
||||
|
||||
describe('buildBoundedMask', () => {
|
||||
it('dérive le masque structurel du gabarit (séparateurs conservés)', () => {
|
||||
expect(buildBoundedMask('JJ/MM/AAAA').mask).toBe('##/##/####')
|
||||
expect(buildBoundedMask('JJ/MM/AAAA HH:MM').mask).toBe('##/##/#### ##:##')
|
||||
})
|
||||
})
|
||||
|
||||
describe('preProcess — bornage de la saisie (1er ET 2e chiffre)', () => {
|
||||
const pre = (template: string, value: string) => buildBoundedMask(template).preProcess!(value)
|
||||
|
||||
it('jour : refuse > 31 et 00, accepte 01-31', () => {
|
||||
expect(pre('JJ/MM/AAAA', '32')).toBe('3') // 32 impossible → 2e chiffre refusé
|
||||
expect(pre('JJ/MM/AAAA', '33')).toBe('3') // exemple métier : 33 refusé
|
||||
expect(pre('JJ/MM/AAAA', '31')).toBe('31')
|
||||
expect(pre('JJ/MM/AAAA', '00')).toBe('0') // 00 impossible
|
||||
expect(pre('JJ/MM/AAAA', '09')).toBe('09')
|
||||
})
|
||||
|
||||
it('mois : refuse > 12 et 00 (après un jour valide) — cas 33/19', () => {
|
||||
expect(pre('JJ/MM/AAAA', '0119')).toBe('011') // 19 (mois) refusé
|
||||
expect(pre('JJ/MM/AAAA', '0113')).toBe('011')
|
||||
expect(pre('JJ/MM/AAAA', '0112')).toBe('0112')
|
||||
expect(pre('JJ/MM/AAAA', '0100')).toBe('010')
|
||||
})
|
||||
|
||||
it('laisse l’année libre', () => {
|
||||
expect(pre('JJ/MM/AAAA', '01012026')).toBe('01012026')
|
||||
})
|
||||
|
||||
it('heure 00-23 et minute 00-59 (datetime), sans confondre minute et mois', () => {
|
||||
const t = 'JJ/MM/AAAA HH:MM'
|
||||
expect(pre(t, '010120262300')).toBe('010120262300') // 23:00 ok
|
||||
expect(pre(t, '010120262460')).toBe('010120262') // heure 24 refusée
|
||||
expect(pre(t, '010120261259')).toBe('010120261259') // minute 59 ok (≠ mois)
|
||||
expect(pre(t, '010120261260')).toBe('0101202612') // minute 60 refusée
|
||||
})
|
||||
|
||||
it('stoppe à la première saisie invalide (99/99/9999 → rien)', () => {
|
||||
expect(pre('JJ/MM/AAAA', '99/99/9999')).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import type {MaskInputOptions} from 'maska'
|
||||
|
||||
// Un champ numérique du gabarit : sa longueur et la plage de valeurs autorisée.
|
||||
interface Field {
|
||||
length: number
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
// Découpe un gabarit (ex. `JJ/MM/AAAA HH:MM`) en champs numériques bornés.
|
||||
// Le `M` désigne le mois avant les heures, et les minutes après — d'où le suivi
|
||||
// de `seenHour`, pour ne pas borner les minutes comme un mois (0-59 vs 1-12).
|
||||
function parseFields(template: string): Field[] {
|
||||
const fields: Field[] = []
|
||||
let seenHour = false
|
||||
let i = 0
|
||||
|
||||
while (i < template.length) {
|
||||
const ch = template[i]!
|
||||
if (!/[A-Za-z]/.test(ch)) {
|
||||
i++ // séparateur (/, espace, :)
|
||||
continue
|
||||
}
|
||||
|
||||
let j = i
|
||||
while (j < template.length && template[j] === ch) j++
|
||||
const length = j - i
|
||||
const letter = ch.toUpperCase()
|
||||
if (letter === 'H') seenHour = true
|
||||
|
||||
if (letter === 'J') fields.push({length, min: 1, max: 31})
|
||||
else if (letter === 'M') fields.push(seenHour ? {length, min: 0, max: 59} : {length, min: 1, max: 12})
|
||||
else if (letter === 'H') fields.push({length, min: 0, max: 23})
|
||||
else fields.push({length, min: 0, max: 10 ** length - 1}) // année (ou autre) : libre
|
||||
|
||||
i = j
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// Un chiffre est accepté tant qu'il existe encore une complétion valide du champ :
|
||||
// on borne la valeur partielle [min possible (padding 0), max possible (padding 9)]
|
||||
// et on vérifie qu'elle croise la plage autorisée [field.min, field.max].
|
||||
function canComplete(partial: string, field: Field): boolean {
|
||||
const low = Number(partial.padEnd(field.length, '0'))
|
||||
const high = Number(partial.padEnd(field.length, '9'))
|
||||
return high >= field.min && low <= field.max
|
||||
}
|
||||
|
||||
// Ne conserve que les chiffres qui gardent chaque champ complétable, et s'arrête
|
||||
// au premier chiffre invalide (rien de ce qui suit n'est réinterprété). maska
|
||||
// réinsère ensuite les séparateurs via le masque structurel.
|
||||
function clampDigits(rawDigits: string, fields: Field[]): string {
|
||||
let result = ''
|
||||
let di = 0
|
||||
|
||||
for (const field of fields) {
|
||||
let fieldDigits = ''
|
||||
while (fieldDigits.length < field.length) {
|
||||
if (di >= rawDigits.length) return result + fieldDigits // plus de saisie
|
||||
const candidate = fieldDigits + rawDigits[di]
|
||||
if (!canComplete(candidate, field)) return result + fieldDigits // 1er chiffre invalide → stop
|
||||
fieldDigits = candidate
|
||||
di++
|
||||
}
|
||||
result += fieldDigits
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les options maska d'un champ date/heure à partir d'un gabarit
|
||||
* d'affichage (ex. `JJ/MM/AAAA`, `JJ/MM/AAAA HH:MM`).
|
||||
*
|
||||
* - `mask` : masque structurel (chiffres + séparateurs), pour le formatage/eager.
|
||||
* - `preProcess` : borne la saisie AVANT masquage, sur le 1er **et** le 2e chiffre
|
||||
* de chaque champ (jour 1-31, mois 1-12, heure 0-23, minute 0-59), si bien
|
||||
* qu'une valeur impossible (99/99/9999, 33, 19 en mois…) ne peut pas être tapée.
|
||||
* Les impossibilités calendaires fines (31/02, 29/02 non bissextile) et les
|
||||
* bornes `min`/`max` restent du ressort de la validation, en filet.
|
||||
*/
|
||||
export function buildBoundedMask(template: string): Pick<MaskInputOptions, 'mask' | 'preProcess'> {
|
||||
const mask = template.replace(/[A-Za-z]/g, '#')
|
||||
const fields = parseFields(template)
|
||||
return {
|
||||
mask,
|
||||
preProcess: (value: string) => clampDigits(value.replace(/\D/g, ''), fields),
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,8 @@
|
||||
icon="mdi:calendar-blank"
|
||||
:width="24"
|
||||
:height="24"
|
||||
:class="iconStateClass"
|
||||
:class="[iconStateClass, (disabled || readonly) ? 'cursor-not-allowed' : 'cursor-pointer']"
|
||||
@click="onFieldClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -128,6 +129,7 @@ import CalendarHeader from './CalendarHeader.vue'
|
||||
import MonthPicker from './MonthPicker.vue'
|
||||
import {useCalendarPopover} from '../composables/useCalendarPopover'
|
||||
import {useCalendarView} from '../composables/useCalendarView'
|
||||
import {buildBoundedMask} from '../composables/maskTemplate'
|
||||
import {useKbdFocusRing} from '../../shared/useKbdFocusRing'
|
||||
|
||||
defineOptions({name: 'MalioCalendarField', inheritAttrs: false})
|
||||
@@ -190,12 +192,15 @@ const generatedId = useId()
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
|
||||
const draft = ref(props.displayValue)
|
||||
// Le masque maska est dérivé du gabarit (lettres → slot `#`, séparateurs conservés).
|
||||
// eager : pose les séparateurs (/, espace, :) dès qu'un groupe est complet.
|
||||
const maskaOptions = computed<MaskInputOptions>(() => ({
|
||||
mask: props.editable ? props.placeholderTemplate.replace(/[A-Za-z]/g, '#') : undefined,
|
||||
eager: props.editable,
|
||||
}))
|
||||
// Le masque maska est dérivé du gabarit : masque structurel pour le formatage,
|
||||
// + preProcess qui borne la saisie (1er ET 2e chiffre : jour 1-31, mois 1-12,
|
||||
// heure 0-23, minute 0-59) afin qu'une valeur impossible (99/99/9999, 33, mois 19…)
|
||||
// ne puisse pas être tapée. eager : pose les séparateurs dès qu'un groupe est complet.
|
||||
const maskaOptions = computed<MaskInputOptions>(() => {
|
||||
if (!props.editable) return {eager: false}
|
||||
const {mask, preProcess} = buildBoundedMask(props.placeholderTemplate)
|
||||
return {mask, preProcess, eager: true}
|
||||
})
|
||||
const inputReadonly = computed(() => !props.editable || props.readonly || props.disabled)
|
||||
|
||||
// Gabarit fantôme : la partie saisie (noire) + le reste du gabarit (gris), affiché
|
||||
@@ -353,11 +358,13 @@ const mergedLabelClass = computed(() =>
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: isReadonly.value
|
||||
? isFilled.value ? 'text-black' : 'text-m-muted'
|
||||
: isOpen.value
|
||||
? 'text-m-primary'
|
||||
: 'peer-placeholder-shown:text-m-muted text-black',
|
||||
: props.disabled
|
||||
? 'text-m-muted'
|
||||
: isReadonly.value
|
||||
? isFilled.value ? 'text-black' : 'text-m-muted'
|
||||
: isOpen.value
|
||||
? 'text-m-primary'
|
||||
: 'peer-placeholder-shown:text-m-muted text-black',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
@@ -365,6 +372,7 @@ const mergedLabelClass = computed(() =>
|
||||
const iconStateClass = computed(() => {
|
||||
if (hasError.value) return 'text-m-danger'
|
||||
if (hasSuccess.value) return 'text-m-success'
|
||||
if (props.disabled) return 'text-m-muted'
|
||||
if (isReadonly.value) return isFilled.value ? 'text-black' : 'text-m-muted'
|
||||
if (isOpen.value) return 'text-m-primary'
|
||||
if (isFilled.value) return 'text-black'
|
||||
|
||||
Reference in New Issue
Block a user