@nexxtmove/ui 0.1.23 → 0.1.24

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.
@@ -6,63 +6,47 @@ import { nl } from 'date-fns/locale'
6
6
  const VIEW_YEAR = 2026
7
7
  const CURRENT_MONTH = 2 // March
8
8
 
9
+ const mountMonthView = (props: Record<string, unknown>) =>
10
+ mount(CalendarMonthView, {
11
+ props: {
12
+ viewYear: VIEW_YEAR,
13
+ modelValue: null,
14
+ currentMonth: CURRENT_MONTH,
15
+ locale: nl,
16
+ timezone: 'UTC',
17
+ ...(props as Record<string, unknown>),
18
+ },
19
+ })
20
+
9
21
  describe('CalendarMonthView', () => {
10
22
  it('renders all 12 months', () => {
11
- const wrapper = mount(CalendarMonthView, {
12
- props: {
13
- viewYear: VIEW_YEAR,
14
- modelValue: null,
15
- currentMonth: CURRENT_MONTH,
16
- locale: nl,
17
- },
18
- })
23
+ const wrapper = mountMonthView({})
19
24
  const buttons = wrapper.findAll('button')
20
25
  expect(buttons.length).toBe(12)
21
26
  })
22
27
 
23
28
  it('highlights the selected month', () => {
24
- const selectedDate = new Date(VIEW_YEAR, 5, 15) // June
25
- const wrapper = mount(CalendarMonthView, {
26
- props: {
27
- viewYear: VIEW_YEAR,
28
- modelValue: selectedDate,
29
- currentMonth: CURRENT_MONTH,
30
- locale: nl,
31
- },
32
- })
29
+ const selectedDate = new Date(Date.UTC(VIEW_YEAR, 5, 15)) // June
30
+ const wrapper = mountMonthView({ modelValue: selectedDate })
33
31
  const selectedButton = wrapper.find('button[aria-selected="true"]')
34
32
  expect(selectedButton.text()).toBe('Jun')
35
33
  })
36
34
 
37
35
  it('disables months based on minDate and maxDate', () => {
38
- const minDate = new Date(VIEW_YEAR, 2, 1) // March 1st
39
- const wrapper = mount(CalendarMonthView, {
40
- props: {
41
- viewYear: VIEW_YEAR,
42
- modelValue: null,
43
- currentMonth: CURRENT_MONTH,
44
- locale: nl,
45
- minDate,
46
- },
47
- })
36
+ const minDate = new Date(Date.UTC(VIEW_YEAR, 2, 1)) // March 1st
37
+ const wrapper = mountMonthView({ minDate })
48
38
  // January (0) and February (1) should be disabled
49
39
  const disabledButtons = wrapper.findAll('button[disabled]')
50
40
  expect(disabledButtons.length).toBe(2)
51
- expect(disabledButtons[0].text()).toBe('Jan')
52
- expect(disabledButtons[1].text()).toBe('Feb')
41
+ expect(disabledButtons[0]?.text()).toBe('Jan')
42
+ expect(disabledButtons[1]?.text()).toBe('Feb')
53
43
  })
54
44
 
55
45
  it('emits select event when a month is clicked', async () => {
56
- const wrapper = mount(CalendarMonthView, {
57
- props: {
58
- viewYear: VIEW_YEAR,
59
- modelValue: null,
60
- currentMonth: CURRENT_MONTH,
61
- locale: nl,
62
- },
63
- })
64
- await wrapper.findAll('button')[5].trigger('click')
65
- expect(wrapper.emitted('select')).toBeTruthy()
66
- expect(wrapper.emitted('select')![0][0]).toBe(5)
46
+ const wrapper = mountMonthView({})
47
+ await wrapper.findAll('button')[5]?.trigger('click')
48
+ const emitted = wrapper.emitted('select')
49
+ expect(emitted).toBeTruthy()
50
+ expect(emitted?.[0]?.[0]).toBe(5)
67
51
  })
68
52
  })
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed } from 'vue'
3
3
  import { format, startOfMonth, endOfMonth, startOfDay, endOfDay } from 'date-fns'
4
+ import { toZonedTime } from 'date-fns-tz'
4
5
  import type { Locale } from 'date-fns'
5
6
 
6
7
  interface CalendarMonthViewProps {
@@ -10,6 +11,7 @@ interface CalendarMonthViewProps {
10
11
  minDate?: Date
11
12
  maxDate?: Date
12
13
  locale: Locale
14
+ timezone: string
13
15
  }
14
16
 
15
17
  const props = defineProps<CalendarMonthViewProps>()
@@ -19,7 +21,7 @@ const emit = defineEmits<{
19
21
 
20
22
  const monthItems = computed(() =>
21
23
  Array.from({ length: 12 }, (_, i) => {
22
- const d = new Date(props.viewYear, i, 1)
24
+ const d = toZonedTime(new Date(Date.UTC(props.viewYear, i, 1)), props.timezone)
23
25
  const raw = format(d, 'LLL', { locale: props.locale }).replace('.', '').trim()
24
26
  return { index: i, label: raw.charAt(0).toUpperCase() + raw.slice(1) }
25
27
  }),
@@ -27,17 +29,24 @@ const monthItems = computed(() =>
27
29
 
28
30
  const isMonthDisabled = (i: number): boolean => {
29
31
  const y = props.viewYear
30
- if (props.minDate && endOfMonth(new Date(y, i)) < startOfDay(props.minDate)) return true
31
- if (props.maxDate && startOfMonth(new Date(y, i)) > endOfDay(props.maxDate)) return true
32
+ const monthDate = toZonedTime(new Date(Date.UTC(y, i, 1)), props.timezone)
33
+ if (
34
+ props.minDate &&
35
+ endOfMonth(monthDate) < startOfDay(toZonedTime(props.minDate, props.timezone))
36
+ )
37
+ return true
38
+ if (
39
+ props.maxDate &&
40
+ startOfMonth(monthDate) > endOfDay(toZonedTime(props.maxDate, props.timezone))
41
+ )
42
+ return true
32
43
  return false
33
44
  }
34
45
 
35
46
  const isMonthSelected = (i: number): boolean => {
36
- return (
37
- !!props.modelValue &&
38
- props.modelValue.getFullYear() === props.viewYear &&
39
- props.modelValue.getMonth() === i
40
- )
47
+ if (!props.modelValue) return false
48
+ const zonedModel = toZonedTime(props.modelValue, props.timezone)
49
+ return zonedModel.getFullYear() === props.viewYear && zonedModel.getMonth() === i
41
50
  }
42
51
 
43
52
  const isMonthCurrent = (i: number): boolean => {
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import CalendarYearView from './_CalendarYearView.vue'
4
+
5
+ describe('CalendarYearView', () => {
6
+ it('renders a range of years', () => {
7
+ const wrapper = mount(CalendarYearView, {
8
+ props: {
9
+ viewYear: 2024,
10
+ timezone: 'UTC',
11
+ },
12
+ })
13
+ const items = wrapper.findAll('button')
14
+ expect(items.length).toBeGreaterThan(100) // Default range is viewYear - 100 to + 20
15
+ })
16
+
17
+ it('respects minDate and maxDate', () => {
18
+ const wrapper = mount(CalendarYearView, {
19
+ props: {
20
+ viewYear: 2024,
21
+ minDate: new Date(Date.UTC(2020, 0, 1)),
22
+ maxDate: new Date(Date.UTC(2025, 0, 1)),
23
+ timezone: 'UTC',
24
+ },
25
+ })
26
+ const items = wrapper.findAll('button')
27
+ // 2020 to 2025 inclusive is 6 years
28
+ expect(items.length).toBe(6)
29
+ expect(items[0]?.text()).toBe('2020')
30
+ expect(items[5]?.text()).toBe('2025')
31
+ })
32
+
33
+ it('emits select when a year is clicked', async () => {
34
+ const wrapper = mount(CalendarYearView, {
35
+ props: {
36
+ viewYear: 2024,
37
+ timezone: 'UTC',
38
+ },
39
+ })
40
+ const year2024 = wrapper.findAll('button').find((i) => i.text() === '2024')
41
+ await year2024?.trigger('click')
42
+ expect(wrapper.emitted('select')).toBeTruthy()
43
+ expect(wrapper.emitted('select')?.[0]?.[0]).toBe(2024)
44
+ })
45
+
46
+ it('highlights the current year', async () => {
47
+ const wrapper = mount(CalendarYearView, {
48
+ props: {
49
+ viewYear: 2024,
50
+ timezone: 'UTC',
51
+ },
52
+ })
53
+ const activeYear = wrapper.find('button.text-cornflower-blue-500')
54
+ expect(activeYear.text()).toBe('2024')
55
+ })
56
+ })
@@ -1,10 +1,12 @@
1
1
  <script lang="ts" setup>
2
2
  import { ref, computed, nextTick, onMounted } from 'vue'
3
+ import { toZonedTime } from 'date-fns-tz'
3
4
 
4
5
  interface CalendarYearViewProps {
5
6
  viewYear: number
6
7
  minDate?: Date
7
8
  maxDate?: Date
9
+ timezone: string
8
10
  }
9
11
 
10
12
  const props = defineProps<CalendarYearViewProps>()
@@ -16,8 +18,12 @@ const YEAR_ITEM_H = 36 // px — matches h-9
16
18
  const yearScrollerRef = ref<HTMLElement | null>(null)
17
19
 
18
20
  const yearRange = computed(() => {
19
- const min = props.minDate ? props.minDate.getFullYear() : props.viewYear - 100
20
- const max = props.maxDate ? props.maxDate.getFullYear() : props.viewYear + 20
21
+ const min = props.minDate
22
+ ? toZonedTime(props.minDate, props.timezone).getFullYear()
23
+ : props.viewYear - 100
24
+ const max = props.maxDate
25
+ ? toZonedTime(props.maxDate, props.timezone).getFullYear()
26
+ : props.viewYear + 20
21
27
  return Array.from({ length: max - min + 1 }, (_, i) => min + i)
22
28
  })
23
29
 
@@ -7,4 +7,5 @@ export interface NexxtCalendarProps {
7
7
  minDate?: Date
8
8
  maxDate?: Date
9
9
  monthYearPicker?: boolean
10
+ timezone?: string
10
11
  }
@@ -24,12 +24,15 @@ const props = withDefaults(defineProps<NexxtDatePickerProps>(), {
24
24
 
25
25
  const PICKER_ONLY_KEYS = new Set(['placeholder', 'anchor', 'autoClose'])
26
26
 
27
- const calendarProps = computed(
28
- (): NexxtCalendarProps =>
29
- Object.fromEntries(
30
- Object.entries(props).filter(([key]) => !PICKER_ONLY_KEYS.has(key)),
31
- ) as NexxtCalendarProps,
32
- )
27
+ const calendarProps = computed((): NexxtCalendarProps => {
28
+ const commonProps = Object.fromEntries(
29
+ Object.entries(props).filter(([key]) => !PICKER_ONLY_KEYS.has(key)),
30
+ )
31
+ return {
32
+ timezone: 'UTC',
33
+ ...commonProps,
34
+ } as NexxtCalendarProps
35
+ })
33
36
 
34
37
  // ── State ─────────────────────────────────────────────────────────────────────
35
38