@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1373 -1023
- package/package.json +2 -1
- package/src/components/Calendar/Calendar.test.ts +54 -44
- package/src/components/Calendar/Calendar.vue +31 -13
- package/src/components/Calendar/_CalendarDayView.test.ts +48 -89
- package/src/components/Calendar/_CalendarDayView.vue +19 -6
- package/src/components/Calendar/_CalendarMonthView.test.ts +24 -40
- package/src/components/Calendar/_CalendarMonthView.vue +17 -8
- package/src/components/Calendar/_CalendarYearView.test.ts +56 -0
- package/src/components/Calendar/_CalendarYearView.vue +8 -2
- package/src/components/Calendar/calendar.types.ts +1 -0
- package/src/components/DatePicker/DatePicker.vue +9 -6
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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]
|
|
52
|
-
expect(disabledButtons[1]
|
|
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 =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
31
|
-
if (
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
29
|
-
Object.
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|