@nexxtmove/ui 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nuxt.js CHANGED
@@ -1,20 +1,23 @@
1
- import { defineNuxtModule as a, addComponent as c, addTemplate as s } from "@nuxt/kit";
2
- const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button/Button.vue", x = "components/Calendar/Calendar.vue", p = "components/Chip/Chip.vue", u = "components/DatePicker/DatePicker.vue", d = "components/Header/Header.vue", r = "components/Icon/Icon.vue", l = "components/InfoBlock/InfoBlock.vue", v = "components/ProgressBar/ProgressBar.vue", S = "components/SocialIcons/SocialIcons.vue", N = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", f = "components/SocialMediaTemplate/SocialMediaTemplate.vue", C = "components/SocialMediaType/SocialMediaType.vue", M = "components/StepperHeader/StepperHeader.vue", T = {
3
- NexxtAnimatedNumber: i,
1
+ import { defineNuxtModule as c, addComponent as s, addTemplate as i } from "@nuxt/kit";
2
+ const a = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button/Button.vue", x = "components/Calendar/Calendar.vue", p = "components/Chip/Chip.vue", l = "components/DatePicker/DatePicker.vue", u = "components/Header/Header.vue", d = "components/Icon/Icon.vue", r = "components/InfoBlock/InfoBlock.vue", v = "components/ProgressBar/ProgressBar.vue", N = "components/SocialIcons/SocialIcons.vue", S = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", T = "components/SocialMediaTemplate/SocialMediaTemplate.vue", f = "components/SocialMediaType/SocialMediaType.vue", C = "components/StepperHeader/StepperHeader.vue", h = "components/TimelineEvent/TimelineEvent.vue", M = "components/TimelinePhaseblock/TimelinePhaseblock.vue", P = "components/Tooltip/Tooltip.vue", b = {
3
+ NexxtAnimatedNumber: a,
4
4
  NexxtButton: m,
5
5
  NexxtCalendar: x,
6
6
  NexxtChip: p,
7
- NexxtDatePicker: u,
8
- NexxtHeader: d,
9
- NexxtIcon: r,
10
- NexxtInfoBlock: l,
7
+ NexxtDatePicker: l,
8
+ NexxtHeader: u,
9
+ NexxtIcon: d,
10
+ NexxtInfoBlock: r,
11
11
  NexxtProgressBar: v,
12
- NexxtSocialIcons: S,
13
- NexxtSocialMediaCustomTemplate: N,
14
- NexxtSocialMediaTemplate: f,
15
- NexxtSocialMediaType: C,
16
- NexxtStepperHeader: M
17
- }, I = a({
12
+ NexxtSocialIcons: N,
13
+ NexxtSocialMediaCustomTemplate: S,
14
+ NexxtSocialMediaTemplate: T,
15
+ NexxtSocialMediaType: f,
16
+ NexxtStepperHeader: C,
17
+ NexxtTimelineEvent: h,
18
+ NexxtTimelinePhaseblock: M,
19
+ NexxtTooltip: P
20
+ }, B = c({
18
21
  meta: {
19
22
  name: "@nexxtmove/ui",
20
23
  configKey: "nexxtmoveUi",
@@ -26,13 +29,13 @@ const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button
26
29
  addCSS: !0
27
30
  },
28
31
  setup(e, o) {
29
- for (const [t, n] of Object.entries(T))
30
- c({
32
+ for (const [t, n] of Object.entries(b))
33
+ s({
31
34
  name: t,
32
35
  export: "default",
33
36
  filePath: `@nexxtmove/ui/${n}`
34
37
  });
35
- e.addCSS && (s({
38
+ e.addCSS && (i({
36
39
  filename: "nexxtmove-ui.css",
37
40
  getContents: () => `
38
41
  @import "tailwindcss";
@@ -45,5 +48,5 @@ const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button
45
48
  }
46
49
  });
47
50
  export {
48
- I as default
51
+ B as default
49
52
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "0.1.23",
4
+ "version": "0.1.25",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "date-fns": "^4.1.0",
24
+ "date-fns-tz": "^3.2.0",
24
25
  "reka-ui": "^2.9.2"
25
26
  },
26
27
  "peerDependencies": {
@@ -1,12 +1,17 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, it, expect, vi } from 'vitest'
2
2
  import { mount } from '@vue/test-utils'
3
3
  import { nextTick } from 'vue'
4
4
  import Calendar from './Calendar.vue'
5
5
 
6
+ // Mock today's date for consistent testing
7
+ const MOCK_TODAY = new Date(Date.UTC(2026, 2, 20)) // March 20, 2026
8
+ vi.useFakeTimers()
9
+ vi.setSystemTime(MOCK_TODAY)
10
+
6
11
  // Fixed reference date used across tests
7
12
  const YEAR = 2026
8
13
  const MONTH = 2 // March (0-indexed)
9
- const TODAY = new Date(YEAR, MONTH, 20)
14
+ const TODAY = new Date(Date.UTC(YEAR, MONTH, 20))
10
15
 
11
16
  const mountCalendar = (props = {}) => mount(Calendar, { props })
12
17
 
@@ -43,38 +48,42 @@ describe('Calendar', () => {
43
48
  // ── Month navigation ───────────────────────────────────────────────────────
44
49
 
45
50
  it('navigates to next month', async () => {
46
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 1) })
51
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 1)) })
47
52
 
48
- const labelBefore = wrapper.find('[class*="cornflower-blue-600"]').text()
53
+ const labelBefore = wrapper
54
+ .find('[class*="flex"][class*="items-center"][class*="gap-1"]')
55
+ .text()
49
56
  const nextBtn = wrapper
50
57
  .findAll('button[aria-label]')
51
58
  .find((b) => b.attributes('aria-label') === 'Volgende maand')
52
59
  await nextBtn!.trigger('click')
53
60
  await nextTick()
54
61
 
55
- const labelAfter = wrapper.find('[class*="cornflower-blue-600"]').text()
62
+ const labelAfter = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]').text()
56
63
  expect(labelAfter).not.toBe(labelBefore)
57
64
  })
58
65
 
59
66
  it('navigates to previous month', async () => {
60
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 1) })
67
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 1)) })
61
68
 
62
- const labelBefore = wrapper.find('[class*="cornflower-blue-600"]').text()
69
+ const labelBefore = wrapper
70
+ .find('[class*="flex"][class*="items-center"][class*="gap-1"]')
71
+ .text()
63
72
  const prevBtn = wrapper
64
73
  .findAll('button[aria-label]')
65
74
  .find((b) => b.attributes('aria-label') === 'Vorige maand')
66
75
  await prevBtn!.trigger('click')
67
76
  await nextTick()
68
77
 
69
- const labelAfter = wrapper.find('[class*="cornflower-blue-600"]').text()
78
+ const labelAfter = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]').text()
70
79
  expect(labelAfter).not.toBe(labelBefore)
71
80
  })
72
81
 
73
82
  // ── Marked dates ───────────────────────────────────────────────────────────
74
83
 
75
84
  it('renders a dot for marked dates', () => {
76
- const markedDates = [new Date(YEAR, MONTH, 10)]
77
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 1), markedDates })
85
+ const markedDates = [new Date(Date.UTC(YEAR, MONTH, 10))]
86
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 1)), markedDates })
78
87
 
79
88
  expect(wrapper.find('[class*="rounded-full"][class*="bg-cornflower"]').exists()).toBe(true)
80
89
  })
@@ -82,8 +91,8 @@ describe('Calendar', () => {
82
91
  // ── Disabled dates ─────────────────────────────────────────────────────────
83
92
 
84
93
  it('disables specified dates', () => {
85
- const disabledDates = [new Date(YEAR, MONTH, 15)]
86
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 1), disabledDates })
94
+ const disabledDates = [new Date(Date.UTC(YEAR, MONTH, 15))]
95
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 1)), disabledDates })
87
96
 
88
97
  const disabled = wrapper
89
98
  .findAll('button[disabled]')
@@ -92,13 +101,13 @@ describe('Calendar', () => {
92
101
  })
93
102
 
94
103
  it('does not emit when clicking a disabled date', async () => {
95
- const disabledDates = [new Date(YEAR, MONTH, 15)]
96
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 1), disabledDates })
104
+ const disabledDates = [new Date(Date.UTC(YEAR, MONTH, 15))]
105
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 1)), disabledDates })
97
106
 
98
- const disabled = wrapper
107
+ const disabledBtn = wrapper
99
108
  .findAll('button[disabled]')
100
109
  .find((b) => /^\d{1,2}$/.test(b.text().trim()))
101
- await disabled?.trigger('click')
110
+ await disabledBtn?.trigger('click')
102
111
  await nextTick()
103
112
 
104
113
  expect(wrapper.emitted('update:modelValue')).toBeFalsy()
@@ -107,8 +116,8 @@ describe('Calendar', () => {
107
116
  // ── minDate / maxDate ──────────────────────────────────────────────────────
108
117
 
109
118
  it('disables dates before minDate', () => {
110
- const minDate = new Date(YEAR, MONTH, 10)
111
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 15), minDate })
119
+ const minDate = new Date(Date.UTC(YEAR, MONTH, 10))
120
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 15)), minDate })
112
121
 
113
122
  const disabled = wrapper
114
123
  .findAll('button[disabled]')
@@ -117,8 +126,8 @@ describe('Calendar', () => {
117
126
  })
118
127
 
119
128
  it('disables dates after maxDate', () => {
120
- const maxDate = new Date(YEAR, MONTH, 20)
121
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 15), maxDate })
129
+ const maxDate = new Date(Date.UTC(YEAR, MONTH, 20))
130
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 15)), maxDate })
122
131
 
123
132
  const disabled = wrapper
124
133
  .findAll('button[disabled]')
@@ -127,43 +136,42 @@ describe('Calendar', () => {
127
136
  })
128
137
 
129
138
  it('disables prev nav when already at minDate month', () => {
130
- const minDate = new Date(YEAR, MONTH, 1)
131
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 15), minDate })
139
+ const minDate = new Date(Date.UTC(YEAR, MONTH, 1))
140
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 15)), minDate })
132
141
 
133
- const prevBtn = wrapper.findAll('[class*="rounded-full"][type="button"]')[0]!
142
+ const prevBtn = wrapper.find('button[aria-label="Vorige maand"]')
134
143
  expect(prevBtn.attributes('disabled')).toBeDefined()
135
144
  })
136
145
 
137
146
  it('disables next nav when already at maxDate month', () => {
138
- const maxDate = new Date(YEAR, MONTH, 28)
139
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 15), maxDate })
147
+ const maxDate = new Date(Date.UTC(YEAR, MONTH, 28))
148
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 15)), maxDate })
140
149
 
141
- const navButtons = wrapper.findAll('[class*="rounded-full"][type="button"]')
142
- const nextBtn = navButtons[navButtons.length - 1]!
150
+ const nextBtn = wrapper.find('button[aria-label="Volgende maand"]')
143
151
  expect(nextBtn.attributes('disabled')).toBeDefined()
144
152
  })
145
153
 
146
154
  it('does not navigate prev when at minDate month', async () => {
147
- const minDate = new Date(YEAR, MONTH, 1)
148
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 15), minDate })
155
+ const minDate = new Date(Date.UTC(YEAR, MONTH, 1))
156
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 15)), minDate })
149
157
 
150
158
  const prevBtn = wrapper.find('button[aria-label="Vorige maand"]')
151
159
  await prevBtn.trigger('click')
152
160
  await nextTick()
153
161
 
154
- const label = wrapper.find('[class*="cornflower-blue-600"]').text()
162
+ const label = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]').text()
155
163
  expect(label).toContain('Mrt')
156
164
  })
157
165
 
158
166
  it('does not navigate next when at maxDate month', async () => {
159
- const maxDate = new Date(YEAR, MONTH, 28)
160
- const wrapper = mountCalendar({ modelValue: new Date(YEAR, MONTH, 15), maxDate })
167
+ const maxDate = new Date(Date.UTC(YEAR, MONTH, 28))
168
+ const wrapper = mountCalendar({ modelValue: new Date(Date.UTC(YEAR, MONTH, 15)), maxDate })
161
169
 
162
170
  const nextBtn = wrapper.find('button[aria-label="Volgende maand"]')
163
171
  await nextBtn.trigger('click')
164
172
  await nextTick()
165
173
 
166
- const label = wrapper.find('[class*="cornflower-blue-600"]').text()
174
+ const label = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]').text()
167
175
  expect(label).toContain('Mrt')
168
176
  })
169
177
 
@@ -172,31 +180,33 @@ describe('Calendar', () => {
172
180
  it('month name is not a button without monthYearPicker prop', () => {
173
181
  const wrapper = mountCalendar({ modelValue: TODAY })
174
182
 
175
- const header = wrapper.find('[class*="cornflower-blue-600"]')
183
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
176
184
  expect(header.find('button').exists()).toBe(false)
177
185
  })
178
186
 
179
187
  it('month name is a button with monthYearPicker prop', () => {
180
188
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
181
189
 
182
- const header = wrapper.find('[class*="cornflower-blue-600"]')
190
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
183
191
  expect(header.find('button').exists()).toBe(true)
184
192
  })
185
193
 
186
194
  it('opens month picker when month name is clicked', async () => {
187
195
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
188
196
 
189
- const header = wrapper.find('[class*="cornflower-blue-600"]')
190
- await header.find('button').trigger('click')
197
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
198
+ const monthBtn = header.findAll('button').find((b) => b.text().includes('Mrt'))
199
+ await monthBtn!.trigger('click')
191
200
  await nextTick()
192
201
 
202
+ // Month view uses grid-cols-3
193
203
  expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(true)
194
204
  })
195
205
 
196
206
  it('opens year picker when year is clicked', async () => {
197
207
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
198
208
 
199
- const header = wrapper.find('[class*="cornflower-blue-600"]')
209
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
200
210
  const buttons = header.findAll('button')
201
211
  await buttons[1]!.trigger('click')
202
212
  await nextTick()
@@ -207,7 +217,7 @@ describe('Calendar', () => {
207
217
  it('returns to days view after selecting a month', async () => {
208
218
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
209
219
 
210
- const header = wrapper.find('[class*="cornflower-blue-600"]')
220
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
211
221
  await header.find('button').trigger('click')
212
222
  await nextTick()
213
223
 
@@ -222,7 +232,7 @@ describe('Calendar', () => {
222
232
  it('returns to months view after selecting a year', async () => {
223
233
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
224
234
 
225
- const header = wrapper.find('[class*="cornflower-blue-600"]')
235
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
226
236
  await header.findAll('button')[1]!.trigger('click')
227
237
  await nextTick()
228
238
 
@@ -237,12 +247,12 @@ describe('Calendar', () => {
237
247
  it('selects correct date from month picker', async () => {
238
248
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
239
249
 
240
- const header = wrapper.find('[class*="cornflower-blue-600"]')
250
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
241
251
  await header.find('button').trigger('click')
242
252
  await nextTick()
243
253
 
244
254
  const monthButtons = wrapper.findAll('[class*="grid-cols-3"] button')
245
- await monthButtons[0].trigger('click')
255
+ await monthButtons[0]?.trigger('click')
246
256
  await nextTick()
247
257
 
248
258
  const labelAfter = wrapper.find('[class*="cornflower-blue-600"]').text()
@@ -252,9 +262,9 @@ describe('Calendar', () => {
252
262
  it('selects correct year from year drum', async () => {
253
263
  const wrapper = mountCalendar({ modelValue: TODAY, monthYearPicker: true })
254
264
 
255
- const header = wrapper.find('[class*="cornflower-blue-600"]')
265
+ const header = wrapper.find('[class*="flex"][class*="items-center"][class*="gap-1"]')
256
266
  const headerButtons = header.findAll('button')
257
- await headerButtons[1].trigger('click')
267
+ await headerButtons[1]?.trigger('click')
258
268
  await nextTick()
259
269
 
260
270
  const yearButtons = wrapper.findAll('[class*="snap-center"]')
@@ -15,6 +15,7 @@ import {
15
15
  startOfDay,
16
16
  endOfDay,
17
17
  } from 'date-fns'
18
+ import { fromZonedTime, toZonedTime } from 'date-fns-tz'
18
19
  import { nl } from 'date-fns/locale'
19
20
  import CalendarHeader from './_CalendarHeader.vue'
20
21
  import CalendarDayView from './_CalendarDayView.vue'
@@ -42,11 +43,14 @@ const {
42
43
  minDate,
43
44
  maxDate,
44
45
  monthYearPicker = false,
46
+ timezone = 'UTC',
45
47
  } = defineProps<NexxtCalendarProps>()
46
48
 
47
49
  // ── State ─────────────────────────────────────────────────────────────────────
48
50
 
49
- const viewDate = ref<Date>(model.value ? new Date(model.value) : new Date())
51
+ const viewDate = ref<Date>(
52
+ model.value ? toZonedTime(new Date(model.value), timezone) : toZonedTime(new Date(), timezone),
53
+ )
50
54
  const pickerView = ref<PickerView>('days')
51
55
  const dayViewRef = ref<InstanceType<typeof CalendarDayView> | null>(null)
52
56
  const dayTransition = ref<DayTransition>('fade')
@@ -54,7 +58,7 @@ const dayTransition = ref<DayTransition>('fade')
54
58
  // ── Watchers ──────────────────────────────────────────────────────────────────
55
59
 
56
60
  watch(model, (val) => {
57
- if (val) viewDate.value = new Date(val)
61
+ if (val) viewDate.value = toZonedTime(new Date(val), timezone)
58
62
  })
59
63
 
60
64
  // ── Logic ─────────────────────────────────────────────────────────────────────
@@ -66,29 +70,38 @@ const calendarDays = computed(() => {
66
70
  })
67
71
 
68
72
  const isDisabled = (day: Date): boolean => {
69
- if (disabledDates.some((d) => isSameDay(d, day))) return true
70
- if (minDate && isBefore(day, startOfDay(minDate))) return true
71
- if (maxDate && isAfter(day, endOfDay(maxDate))) return true
73
+ const zonedDay = toZonedTime(day, timezone)
74
+ if (disabledDates.some((d) => isSameDay(toZonedTime(d, timezone), zonedDay))) return true
75
+ if (minDate && isBefore(zonedDay, startOfDay(toZonedTime(minDate, timezone)))) return true
76
+ if (maxDate && isAfter(zonedDay, endOfDay(toZonedTime(maxDate, timezone)))) return true
72
77
  return false
73
78
  }
74
79
 
75
80
  // ── Views ─────────────────────────────────────────────────────────────────────
76
81
 
77
82
  const handleSelectDay = (day: Date) => {
78
- if (isDisabled(day)) return
79
- if (!isSameMonth(day, viewDate.value)) viewDate.value = startOfMonth(day)
80
- model.value = day
81
- emit('select', day)
83
+ const zonedDay = toZonedTime(day, timezone)
84
+ if (isDisabled(zonedDay)) return
85
+ if (!isSameMonth(zonedDay, viewDate.value)) viewDate.value = startOfMonth(zonedDay)
86
+ model.value = fromZonedTime(zonedDay, timezone)
87
+ emit('select', model.value)
82
88
  }
83
89
 
84
90
  const handleSelectMonth = (monthIndex: number) => {
85
91
  dayTransition.value = 'fade'
86
- viewDate.value = new Date(viewDate.value.getFullYear(), monthIndex, 1)
92
+ // Create a new date in the target timezone at the middle of the month
93
+ const newDate = toZonedTime(
94
+ new Date(Date.UTC(viewDate.value.getUTCFullYear(), monthIndex, 1)),
95
+ timezone,
96
+ )
97
+ viewDate.value = newDate
87
98
  pickerView.value = 'days'
88
99
  }
89
100
 
90
101
  const handleSelectYear = (year: number) => {
91
- viewDate.value = new Date(year, viewDate.value.getMonth(), 1)
102
+ // Use target timezone to create a date at the start of the year/current month
103
+ const newDate = toZonedTime(new Date(Date.UTC(year, viewDate.value.getUTCMonth(), 1)), timezone)
104
+ viewDate.value = newDate
92
105
  pickerView.value = 'months'
93
106
  }
94
107
 
@@ -97,13 +110,15 @@ const handleSelectYear = (year: number) => {
97
110
  const canGoPrev = computed(() => {
98
111
  if (pickerView.value !== 'days') return false
99
112
  if (!minDate) return true
100
- return !isBefore(endOfMonth(subMonths(viewDate.value, 1)), startOfDay(minDate))
113
+ const prevMonth = subMonths(viewDate.value, 1)
114
+ return !isBefore(endOfMonth(prevMonth), startOfDay(toZonedTime(minDate, timezone)))
101
115
  })
102
116
 
103
117
  const canGoNext = computed(() => {
104
118
  if (pickerView.value !== 'days') return false
105
119
  if (!maxDate) return true
106
- return !isAfter(startOfMonth(addMonths(viewDate.value, 1)), endOfDay(maxDate))
120
+ const nextMonth = addMonths(viewDate.value, 1)
121
+ return !isAfter(startOfMonth(nextMonth), endOfDay(toZonedTime(maxDate, timezone)))
107
122
  })
108
123
 
109
124
  const handlePrev = () => {
@@ -192,6 +207,7 @@ const currentDayTransition = computed(() =>
192
207
  :marked-dates="markedDates"
193
208
  :locale="locale"
194
209
  :is-disabled="isDisabled"
210
+ :timezone="timezone"
195
211
  @select="handleSelectDay"
196
212
  />
197
213
 
@@ -204,6 +220,7 @@ const currentDayTransition = computed(() =>
204
220
  :min-date="minDate"
205
221
  :max-date="maxDate"
206
222
  :locale="locale"
223
+ :timezone="timezone"
207
224
  @select="handleSelectMonth"
208
225
  />
209
226
 
@@ -213,6 +230,7 @@ const currentDayTransition = computed(() =>
213
230
  :view-year="viewDate.getFullYear()"
214
231
  :min-date="minDate"
215
232
  :max-date="maxDate"
233
+ :timezone="timezone"
216
234
  @select="handleSelectYear"
217
235
  />
218
236
  </Transition>
@@ -9,33 +9,32 @@ const start = startOfWeek(startOfMonth(VIEW_DATE), { weekStartsOn: 1 })
9
9
  const end = endOfWeek(endOfMonth(VIEW_DATE), { weekStartsOn: 1 })
10
10
  const DAYS = eachDayOfInterval({ start, end })
11
11
 
12
+ const defaults = {
13
+ viewDate: VIEW_DATE,
14
+ calendarDays: DAYS,
15
+ modelValue: null,
16
+ markedDates: [],
17
+ locale: nl,
18
+ isDisabled: () => false,
19
+ timezone: 'UTC',
20
+ }
21
+
22
+ const mountDayView = (props: Record<string, unknown> = {}) =>
23
+ mount(CalendarDayView, {
24
+ props: { ...defaults, ...props },
25
+ })
26
+
12
27
  describe('CalendarDayView', () => {
13
28
  it('renders the correct number of days', () => {
14
- const wrapper = mount(CalendarDayView, {
15
- props: {
16
- viewDate: VIEW_DATE,
17
- calendarDays: DAYS,
18
- modelValue: null,
19
- markedDates: [],
20
- locale: nl,
21
- isDisabled: () => false,
22
- },
23
- })
29
+ const wrapper = mountDayView()
24
30
  const dayButtons = wrapper.findAll('button')
25
31
  expect(dayButtons.length).toBe(DAYS.length)
26
32
  })
27
33
 
28
34
  it('highlights the selected day', () => {
29
35
  const selectedDate = new Date(2026, 2, 15)
30
- const wrapper = mount(CalendarDayView, {
31
- props: {
32
- viewDate: VIEW_DATE,
33
- calendarDays: DAYS,
34
- modelValue: selectedDate,
35
- markedDates: [],
36
- locale: nl,
37
- isDisabled: () => false,
38
- },
36
+ const wrapper = mountDayView({
37
+ modelValue: selectedDate,
39
38
  })
40
39
  const selectedCell = wrapper.find('[role="gridcell"][aria-selected="true"]')
41
40
  expect(selectedCell.exists()).toBe(true)
@@ -44,15 +43,8 @@ describe('CalendarDayView', () => {
44
43
 
45
44
  it('marks dates correctly', () => {
46
45
  const markedDates = [new Date(2026, 2, 10)]
47
- const wrapper = mount(CalendarDayView, {
48
- props: {
49
- viewDate: VIEW_DATE,
50
- calendarDays: DAYS,
51
- modelValue: null,
52
- markedDates,
53
- locale: nl,
54
- isDisabled: () => false,
55
- },
46
+ const wrapper = mountDayView({
47
+ markedDates,
56
48
  })
57
49
  // In our component, isMarked(day) is used to show a small dot.
58
50
  // The dot is a span with bg-cornflower-blue-500 class.
@@ -60,15 +52,8 @@ describe('CalendarDayView', () => {
60
52
  })
61
53
 
62
54
  it('disables dates based on isDisabled prop', () => {
63
- const wrapper = mount(CalendarDayView, {
64
- props: {
65
- viewDate: VIEW_DATE,
66
- calendarDays: DAYS,
67
- modelValue: null,
68
- markedDates: [],
69
- locale: nl,
70
- isDisabled: (day: Date) => day.getDate() === 15,
71
- },
55
+ const wrapper = mountDayView({
56
+ isDisabled: (day: Date) => day.getDate() === 15,
72
57
  })
73
58
  const disabledButton = wrapper.find('button[disabled]')
74
59
  expect(disabledButton.exists()).toBe(true)
@@ -76,70 +61,44 @@ describe('CalendarDayView', () => {
76
61
  })
77
62
 
78
63
  it('emits select event when a day is clicked', async () => {
79
- const wrapper = mount(CalendarDayView, {
80
- props: {
81
- viewDate: VIEW_DATE,
82
- calendarDays: DAYS,
83
- modelValue: null,
84
- markedDates: [],
85
- locale: nl,
86
- isDisabled: () => false,
87
- },
88
- })
89
- await wrapper.findAll('button')[10].trigger('click')
90
- expect(wrapper.emitted('select')).toBeTruthy()
91
- expect(wrapper.emitted('select')![0][0]).toBeInstanceOf(Date)
64
+ const wrapper = mountDayView()
65
+ const button = wrapper.findAll('button')[10]
66
+ if (button) {
67
+ await button.trigger('click')
68
+ expect(wrapper.emitted('select')).toBeTruthy()
69
+ expect(wrapper.emitted('select')?.[0]?.[0]).toBeInstanceOf(Date)
70
+ }
92
71
  })
93
72
 
94
73
  it('handles keyboard navigation (ArrowRight)', async () => {
95
- const wrapper = mount(CalendarDayView, {
96
- props: {
97
- viewDate: VIEW_DATE,
98
- calendarDays: DAYS,
99
- modelValue: null,
100
- markedDates: [],
101
- locale: nl,
102
- isDisabled: () => false,
103
- },
74
+ const wrapper = mountDayView({
104
75
  attachTo: document.body,
105
76
  })
106
- const buttons = wrapper.findAll('button')
107
- await buttons[0].trigger('keydown', { key: 'ArrowRight' })
77
+ const button = wrapper.findAll('button')[0]
78
+ if (button) {
79
+ await button.trigger('keydown', { key: 'ArrowRight' })
80
+ }
108
81
  // In vitest/jsdom, focus might not move as easily, so we check if the method logic runs
109
82
  // For unit tests, we can also check if emit('select') is called on Enter/Space
110
83
  })
111
84
 
112
85
  it('emits select when Enter or Space is pressed', async () => {
113
- const wrapper = mount(CalendarDayView, {
114
- props: {
115
- viewDate: VIEW_DATE,
116
- calendarDays: DAYS,
117
- modelValue: null,
118
- markedDates: [],
119
- locale: nl,
120
- isDisabled: () => false,
121
- },
122
- })
123
- const buttons = wrapper.findAll('button')
124
- await buttons[0].trigger('keydown', { key: 'Enter' })
125
- expect(wrapper.emitted('select')).toBeTruthy()
126
- await buttons[0].trigger('keydown', { key: ' ' })
127
- expect(wrapper.emitted('select')![1]).toBeTruthy()
86
+ const wrapper = mountDayView()
87
+ const button = wrapper.findAll('button')[0]
88
+ if (button) {
89
+ await button.trigger('keydown', { key: 'Enter' })
90
+ expect(wrapper.emitted('select')).toBeTruthy()
91
+ await button.trigger('keydown', { key: ' ' })
92
+ expect(wrapper.emitted('select')?.[1]).toBeTruthy()
93
+ }
128
94
  })
129
95
 
130
96
  it('does not emit select for other keys', async () => {
131
- const wrapper = mount(CalendarDayView, {
132
- props: {
133
- viewDate: VIEW_DATE,
134
- calendarDays: DAYS,
135
- modelValue: null,
136
- markedDates: [],
137
- locale: nl,
138
- isDisabled: () => false,
139
- },
140
- })
141
- const buttons = wrapper.findAll('button')
142
- await buttons[0].trigger('keydown', { key: 'Tab' })
143
- expect(wrapper.emitted('select')).toBeFalsy()
97
+ const wrapper = mountDayView()
98
+ const button = wrapper.findAll('button')[0]
99
+ if (button) {
100
+ await button.trigger('keydown', { key: 'Tab' })
101
+ expect(wrapper.emitted('select')).toBeFalsy()
102
+ }
144
103
  })
145
104
  })