| 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: #77 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #77.
This commit is contained in:
@@ -17,6 +17,7 @@ type DateProps = {
|
||||
success?: string
|
||||
min?: string
|
||||
max?: string
|
||||
markedDates?: Record<string, 'success' | 'danger'>
|
||||
clearable?: boolean
|
||||
editable?: boolean
|
||||
invalidMessage?: string
|
||||
@@ -109,6 +110,48 @@ describe('MalioDate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('month-change', () => {
|
||||
it('émet month-change à l\'ouverture avec le mois courant', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 4, year: 2026}])
|
||||
})
|
||||
|
||||
it('émet month-change sur le mois de la valeur à l\'ouverture', async () => {
|
||||
const wrapper = mountDate({modelValue: '2025-12-25'})
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 11, year: 2025}])
|
||||
})
|
||||
|
||||
it('émet month-change à chaque navigation de mois', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-next"]').trigger('click')
|
||||
expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 5, year: 2026}])
|
||||
await wrapper.get('[data-test="header-prev"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-prev"]').trigger('click')
|
||||
expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 3, year: 2026}])
|
||||
})
|
||||
|
||||
it('ne ré-émet pas month-change après fermeture', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
const countOpen = wrapper.emitted('month-change')?.length ?? 0
|
||||
document.body.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}))
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted('month-change')?.length ?? 0).toBe(countOpen)
|
||||
})
|
||||
})
|
||||
|
||||
describe('markedDates', () => {
|
||||
it('transmet markedDates à la grille (fond tokenisé)', async () => {
|
||||
const wrapper = mountDate({markedDates: {'2026-05-20': 'success'}})
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
const pill = wrapper.get('[data-iso="2026-05-20"]').get('span.rounded-full')
|
||||
expect(pill.classes()).toContain('bg-m-success/15')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sélection', () => {
|
||||
it('emits the ISO date and closes on day click', async () => {
|
||||
const wrapper = mountDate()
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
v-bind="$attrs"
|
||||
@clear="onClear"
|
||||
@commit="onCommit"
|
||||
@month-change="(payload) => emit('month-change', payload)"
|
||||
>
|
||||
<template #default="{ currentMonth, currentYear, close }">
|
||||
<MonthGrid
|
||||
:month="currentMonth"
|
||||
:year="currentYear"
|
||||
:selected-date="modelValue ?? null"
|
||||
:marked-dates="markedDates"
|
||||
:min="min"
|
||||
:max="max"
|
||||
@select="(iso) => onSelect(iso, close)"
|
||||
@@ -57,6 +59,9 @@ const props = withDefaults(
|
||||
success?: string
|
||||
min?: string
|
||||
max?: string
|
||||
// Statut générique par jour, ISO yyyy-mm-dd → variante de fond. Aucune
|
||||
// logique métier dans le layer : le consommateur fournit la liste.
|
||||
markedDates?: Record<string, 'success' | 'danger'>
|
||||
clearable?: boolean
|
||||
editable?: boolean
|
||||
invalidMessage?: string
|
||||
@@ -78,6 +83,7 @@ const props = withDefaults(
|
||||
success: '',
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
markedDates: undefined,
|
||||
clearable: true,
|
||||
editable: false,
|
||||
invalidMessage: 'Date invalide',
|
||||
@@ -94,6 +100,8 @@ const emit = defineEmits<{
|
||||
// tel que tapé sur saisie non parsable/hors plage, '' sinon. Ne JAMAIS transiter
|
||||
// par modelValue, qui doit rester ISO|null pour l'affichage et le round-trip.
|
||||
(e: 'update:rawValue', value: string): void
|
||||
// Mois affiché dans le popover (month 0-11) : à l'ouverture et à chaque nav.
|
||||
(e: 'month-change', value: {month: number, year: number}): void
|
||||
}>()
|
||||
|
||||
const displayValue = computed(() => formatIsoToDisplay(props.modelValue ?? null))
|
||||
|
||||
@@ -180,6 +180,9 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{
|
||||
(e: 'clear' | 'close'): void
|
||||
(e: 'commit', value: string): void
|
||||
// Mois affiché (month 0-11) : émis à l'ouverture du popover et à chaque
|
||||
// navigation, pour qu'un consommateur (ex. SIRH) charge les données du mois.
|
||||
(e: 'month-change', value: {month: number, year: number}): void
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
@@ -229,6 +232,12 @@ watch(isOpen, (value) => {
|
||||
if (!value) emit('close')
|
||||
})
|
||||
|
||||
// Émet le mois affiché tant que le popover est ouvert : une fois à l'ouverture
|
||||
// (isOpen → true, après syncToIso), puis à chaque changement de mois/année.
|
||||
watch([isOpen, currentMonth, currentYear], () => {
|
||||
if (isOpen.value) emit('month-change', {month: currentMonth.value, year: currentYear.value})
|
||||
})
|
||||
|
||||
const onFieldClick = () => {
|
||||
if (props.disabled || props.readonly) return
|
||||
if (props.editable) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
||||
import {mount} from '@vue/test-utils'
|
||||
import type {DefineComponent} from 'vue'
|
||||
import MonthGrid from './MonthGrid.vue'
|
||||
|
||||
type MonthGridProps = {
|
||||
month: number
|
||||
year: number
|
||||
selectedDate?: string | null
|
||||
markedDates?: Record<string, 'success' | 'danger'>
|
||||
min?: string
|
||||
max?: string
|
||||
}
|
||||
|
||||
const Grid = MonthGrid as DefineComponent<MonthGridProps>
|
||||
const mountGrid = (props: MonthGridProps) => mount(Grid, {props, attachTo: document.body})
|
||||
|
||||
// Récupère la pastille (span rond) qui porte les classes de `cellClass` pour un jour donné.
|
||||
const pill = (wrapper: ReturnType<typeof mountGrid>, iso: string) =>
|
||||
wrapper.get(`[data-iso="${iso}"]`).get('span.rounded-full')
|
||||
|
||||
describe('MalioDateMonthGrid — markedDates', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('applique un fond success sur un jour marqué', () => {
|
||||
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-20': 'success'}})
|
||||
expect(pill(wrapper, '2026-05-20').classes()).toContain('bg-m-success/15')
|
||||
})
|
||||
|
||||
it('applique un fond danger sur un jour marqué', () => {
|
||||
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-21': 'danger'}})
|
||||
expect(pill(wrapper, '2026-05-21').classes()).toContain('bg-m-danger/15')
|
||||
})
|
||||
|
||||
it('ne marque pas les jours absents de markedDates', () => {
|
||||
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-20': 'success'}})
|
||||
const classes = pill(wrapper, '2026-05-22').classes()
|
||||
expect(classes).not.toContain('bg-m-success/15')
|
||||
expect(classes).not.toContain('bg-m-danger/15')
|
||||
})
|
||||
|
||||
it('précédence : la sélection (primary) prime sur la variante marquée', () => {
|
||||
const wrapper = mountGrid({
|
||||
month: 4,
|
||||
year: 2026,
|
||||
selectedDate: '2026-05-22',
|
||||
markedDates: {'2026-05-22': 'success'},
|
||||
})
|
||||
const classes = pill(wrapper, '2026-05-22').classes()
|
||||
expect(classes).toContain('bg-m-primary')
|
||||
expect(classes).toContain('text-white')
|
||||
expect(classes).not.toContain('bg-m-success/15')
|
||||
})
|
||||
|
||||
it('today marqué : garde sa bordure ET reçoit le fond marqué', () => {
|
||||
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-19': 'success'}})
|
||||
const classes = pill(wrapper, '2026-05-19').classes()
|
||||
expect(classes).toContain('border-m-primary')
|
||||
expect(classes).toContain('bg-m-success/15')
|
||||
})
|
||||
|
||||
it('today non marqué : bordure sans fond marqué', () => {
|
||||
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-20': 'success'}})
|
||||
const classes = pill(wrapper, '2026-05-19').classes()
|
||||
expect(classes).toContain('border-m-primary')
|
||||
expect(classes).not.toContain('bg-m-success/15')
|
||||
})
|
||||
})
|
||||
@@ -84,6 +84,14 @@ import {dayRangeRole, resolveRangeBounds, type DayRangeRole} from '../composable
|
||||
|
||||
defineOptions({name: 'MalioDateMonthGrid'})
|
||||
|
||||
// Statut générique par jour : aucune sémantique métier dans le layer, juste un
|
||||
// fond tokenisé. `success` et `danger` suffisent pour l'instant (MUI-45).
|
||||
type MarkedVariant = 'success' | 'danger'
|
||||
const markedBg: Record<MarkedVariant, string> = {
|
||||
success: 'bg-m-success/15',
|
||||
danger: 'bg-m-danger/15',
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
month: number
|
||||
@@ -94,6 +102,7 @@ const props = withDefaults(
|
||||
previewDate?: string | null
|
||||
interactiveWeekNumber?: boolean
|
||||
markedWeekStart?: string | null
|
||||
markedDates?: Record<string, MarkedVariant>
|
||||
min?: string
|
||||
max?: string
|
||||
}>(),
|
||||
@@ -104,6 +113,7 @@ const props = withDefaults(
|
||||
previewDate: undefined,
|
||||
interactiveWeekNumber: false,
|
||||
markedWeekStart: null,
|
||||
markedDates: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
},
|
||||
@@ -165,6 +175,10 @@ const cellClass = (cell: DayCell) => {
|
||||
if (role === 'start' || role === 'end' || role === 'single') return 'bg-m-primary text-white'
|
||||
if (role === 'in-range') return 'text-black'
|
||||
const parts = ['hover:bg-m-primary/10']
|
||||
// Précédence : sélection/range (primary, return ci-dessus) > variante marquée > défaut.
|
||||
// `today` n'est pas exclusif : il garde sa bordure ET peut recevoir le fond marqué.
|
||||
const marked = props.markedDates?.[cell.isoDate]
|
||||
if (marked) parts.push(markedBg[marked])
|
||||
if (cell.isToday) parts.push('border border-m-primary text-m-primary')
|
||||
else if (cell.isCurrentMonth) parts.push('text-black')
|
||||
else parts.push('opacity-[60%]')
|
||||
|
||||
Reference in New Issue
Block a user