@rupe/v-datepicker 1.0.0-alpha.2 → 1.0.0-beta.1
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/README.md +3 -1
- package/dist/components/Calendar/CalendarMonthYearOverlay.vue.d.ts +16 -17
- package/dist/components/Calendar/CalendarOverlayItem.vue.d.ts +82 -4
- package/dist/components/Calendar/CalendarRoot.vue.d.ts +6 -0
- package/dist/components/Calendar/useCalendar.d.ts +2 -0
- package/dist/components/DatePicker/DatePickerMonthYearOverlay.vue.d.ts +2 -2
- package/dist/components/DatePicker/DatePickerRoot.vue.d.ts +5 -1
- package/dist/index.cjs +166 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.mjs +166 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Calendar/CalendarMonthYearOverlay.vue +32 -9
- package/src/components/Calendar/CalendarOverlayItem.vue +125 -7
- package/src/components/Calendar/CalendarRoot.vue +10 -0
- package/src/components/Calendar/useCalendar.ts +23 -36
- package/src/components/DatePicker/DatePickerCalendar.vue +2 -0
- package/src/components/DatePicker/DatePickerRoot.vue +10 -0
package/package.json
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import {
|
|
3
|
-
import { Primitive, type PrimitiveProps } from "../Primitive";
|
|
4
|
-
import { injectCalendarRootContext } from "./CalendarRoot.vue";
|
|
5
|
-
import DismissableLayer from "../DismissableLayer/DismissableLayer.vue";
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createContext } from "../../shared";
|
|
6
3
|
|
|
7
4
|
export interface CalendarMonthYearOverlayProps extends PrimitiveProps {
|
|
8
5
|
type: "month" | "year";
|
|
6
|
+
itemsPerRow?: number;
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
type CalendarMonthYearOverlayContext = {
|
|
10
|
+
itemsPerRow: ComputedRef<number>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const [
|
|
14
|
+
injectCalendarMonthYearOverlayContext,
|
|
15
|
+
provideCalendarMonthYearOverlayContext,
|
|
16
|
+
] = createContext<CalendarMonthYearOverlayContext>("CalendarMonthYearOverlay");
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, type ComputedRef } from "vue";
|
|
21
|
+
import { Primitive, type PrimitiveProps } from "../Primitive";
|
|
22
|
+
import { injectCalendarRootContext } from "./CalendarRoot.vue";
|
|
23
|
+
import DismissableLayer from "../DismissableLayer/DismissableLayer.vue";
|
|
24
|
+
import { chunk } from "../../shared";
|
|
12
25
|
|
|
13
26
|
defineOptions({
|
|
14
27
|
inheritAttrs: false,
|
|
15
28
|
});
|
|
16
29
|
|
|
30
|
+
const props = withDefaults(defineProps<CalendarMonthYearOverlayProps>(), {
|
|
31
|
+
itemsPerRow: 4,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const itemsPerRow = computed(() => props.itemsPerRow);
|
|
17
35
|
const rootContext = injectCalendarRootContext();
|
|
18
36
|
|
|
19
|
-
const months = computed(() =>
|
|
20
|
-
|
|
37
|
+
const months = computed(() =>
|
|
38
|
+
chunk(rootContext.months.value, props.itemsPerRow),
|
|
39
|
+
);
|
|
40
|
+
const years = computed(() => chunk(rootContext.years.value, props.itemsPerRow));
|
|
21
41
|
|
|
22
42
|
const isOpen = computed(
|
|
23
43
|
() => rootContext.monthYearOverlayState.value === props.type,
|
|
@@ -26,6 +46,10 @@ const isOpen = computed(
|
|
|
26
46
|
function onEscapeKeyDown() {
|
|
27
47
|
rootContext.monthYearOverlayState.value = false;
|
|
28
48
|
}
|
|
49
|
+
|
|
50
|
+
provideCalendarMonthYearOverlayContext({
|
|
51
|
+
itemsPerRow,
|
|
52
|
+
});
|
|
29
53
|
</script>
|
|
30
54
|
|
|
31
55
|
<template>
|
|
@@ -41,7 +65,6 @@ function onEscapeKeyDown() {
|
|
|
41
65
|
zIndex: '1000',
|
|
42
66
|
}"
|
|
43
67
|
role="dialog"
|
|
44
|
-
tabindex="0"
|
|
45
68
|
>
|
|
46
69
|
<slot
|
|
47
70
|
:months="months"
|
|
@@ -1,30 +1,148 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, nextTick } from "vue";
|
|
2
3
|
import type { DateValue } from "@internationalized/date";
|
|
3
|
-
import { Primitive } from "../Primitive";
|
|
4
|
+
import { Primitive, usePrimitiveElement } from "../Primitive";
|
|
4
5
|
import type { PrimitiveProps } from "../Primitive";
|
|
5
6
|
import { injectCalendarRootContext } from "./CalendarRoot.vue";
|
|
7
|
+
import { useKbd } from "../../shared";
|
|
8
|
+
import { injectCalendarMonthYearOverlayContext } from "./CalendarMonthYearOverlay.vue";
|
|
6
9
|
|
|
7
10
|
export interface CalendarOverlayItemProps extends PrimitiveProps {
|
|
8
|
-
|
|
11
|
+
// TODO: extract DateValue & { monthName: string }
|
|
12
|
+
date: DateValue & { monthName: string };
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
type: "month" | "year";
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
const props = withDefaults(defineProps<CalendarOverlayItemProps>(), {
|
|
12
|
-
as: "
|
|
18
|
+
as: "div",
|
|
13
19
|
});
|
|
14
20
|
|
|
15
21
|
const rootContext = injectCalendarRootContext();
|
|
22
|
+
const years = computed(() => rootContext.years.value);
|
|
23
|
+
const overlayContext = injectCalendarMonthYearOverlayContext();
|
|
24
|
+
const dataValue = computed(() => `${props.type}-${props.date[props.type]}`);
|
|
25
|
+
|
|
26
|
+
const { primitiveElement, currentElement } = usePrimitiveElement();
|
|
27
|
+
|
|
28
|
+
const isFocusedDate = computed(() => {
|
|
29
|
+
if (props.type === "month")
|
|
30
|
+
return rootContext.currentMonth.value === props.date.monthName;
|
|
31
|
+
return rootContext.currentYear.value === props.date.year.toString();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const ariaLabel = computed(() => {
|
|
35
|
+
if (props.type === "month") return props.date.monthName;
|
|
36
|
+
return props.date.year.toString();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onMounted(() => {
|
|
40
|
+
if (isFocusedDate.value) {
|
|
41
|
+
nextTick(() => {
|
|
42
|
+
currentElement.value?.focus();
|
|
43
|
+
currentElement.value?.scrollIntoView({ block: "nearest" });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function isDateSelectable(date: DateValue) {
|
|
49
|
+
return (
|
|
50
|
+
!rootContext.isDateDisabled(date) && !rootContext.isDateUnavailable?.(date)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function closeOverlay() {
|
|
55
|
+
rootContext.monthYearOverlayState.value = false;
|
|
56
|
+
}
|
|
16
57
|
|
|
17
58
|
function handleClick() {
|
|
18
|
-
if (
|
|
19
|
-
return;
|
|
59
|
+
if (!isDateSelectable(props.date)) return;
|
|
20
60
|
|
|
21
61
|
rootContext.onDateChange(props.date);
|
|
22
|
-
|
|
62
|
+
closeOverlay();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const kbd = useKbd();
|
|
66
|
+
|
|
67
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
68
|
+
if (props.disabled) return;
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
const parentElement = rootContext.parentElement.value!;
|
|
72
|
+
|
|
73
|
+
const itemsPerRow = overlayContext.itemsPerRow.value;
|
|
74
|
+
const sign = rootContext.dir.value === "rtl" ? -1 : 1;
|
|
75
|
+
const currentValue = props.date[props.type];
|
|
76
|
+
|
|
77
|
+
function focusItem(value: number) {
|
|
78
|
+
const candidate = parentElement.querySelector<HTMLElement>(
|
|
79
|
+
`[data-value='${props.type}-${value}']`,
|
|
80
|
+
);
|
|
81
|
+
candidate?.focus();
|
|
82
|
+
candidate?.scrollIntoView({ block: "nearest" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function shiftFocus(add: number) {
|
|
86
|
+
let nextValue = currentValue + add;
|
|
87
|
+
|
|
88
|
+
if (props.type === "month") {
|
|
89
|
+
nextValue = ((nextValue - 1 + 12) % 12) + 1;
|
|
90
|
+
} else {
|
|
91
|
+
const minYear = years.value[0]!.year;
|
|
92
|
+
const maxYear = years.value[years.value.length - 1]!.year;
|
|
93
|
+
nextValue = Math.max(minYear, Math.min(nextValue, maxYear));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
focusItem(nextValue);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (e.code) {
|
|
100
|
+
case kbd.ARROW_RIGHT:
|
|
101
|
+
shiftFocus(sign);
|
|
102
|
+
break;
|
|
103
|
+
case kbd.ARROW_LEFT:
|
|
104
|
+
shiftFocus(-sign);
|
|
105
|
+
break;
|
|
106
|
+
case kbd.ARROW_UP:
|
|
107
|
+
shiftFocus(-itemsPerRow);
|
|
108
|
+
break;
|
|
109
|
+
case kbd.ARROW_DOWN:
|
|
110
|
+
shiftFocus(itemsPerRow);
|
|
111
|
+
break;
|
|
112
|
+
case kbd.HOME: {
|
|
113
|
+
const firstValue = props.type === "month" ? 1 : years.value[0]!.year;
|
|
114
|
+
focusItem(firstValue);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case kbd.END: {
|
|
118
|
+
const lastValue =
|
|
119
|
+
props.type === "month" ? 12 : years.value[years.value.length - 1]!.year;
|
|
120
|
+
focusItem(lastValue);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case kbd.ENTER:
|
|
124
|
+
case kbd.SPACE_CODE:
|
|
125
|
+
if (isDateSelectable(props.date)) {
|
|
126
|
+
rootContext.onDateChange(props.date);
|
|
127
|
+
closeOverlay();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
23
130
|
}
|
|
24
131
|
</script>
|
|
25
132
|
|
|
26
133
|
<template>
|
|
27
|
-
<Primitive
|
|
134
|
+
<Primitive
|
|
135
|
+
v-bind="props"
|
|
136
|
+
@click="handleClick"
|
|
137
|
+
ref="primitiveElement"
|
|
138
|
+
role="button"
|
|
139
|
+
:aria-label="ariaLabel"
|
|
140
|
+
:aria-disabled="disabled"
|
|
141
|
+
:data-value="dataValue"
|
|
142
|
+
@keydown.up.down.left.right.space.enter.home.end="handleKeydown"
|
|
143
|
+
@keydown.enter.prevent
|
|
144
|
+
:tabindex="isFocusedDate ? 0 : -1"
|
|
145
|
+
>
|
|
28
146
|
<slot />
|
|
29
147
|
</Primitive>
|
|
30
148
|
</template>
|
|
@@ -109,6 +109,10 @@ export interface CalendarRootProps extends PrimitiveProps {
|
|
|
109
109
|
multiple?: boolean;
|
|
110
110
|
/** Whether or not to disable days outside the current view. */
|
|
111
111
|
disableDaysOutsideCurrentView?: boolean;
|
|
112
|
+
/** The minimum year displayed in the year overlay */
|
|
113
|
+
minYear?: number;
|
|
114
|
+
/** The maximum year displayed in the year overlay */
|
|
115
|
+
maxYear?: number;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
export type CalendarRootEmits = {
|
|
@@ -144,6 +148,8 @@ const props = withDefaults(defineProps<CalendarRootProps>(), {
|
|
|
144
148
|
isDateDisabled: undefined,
|
|
145
149
|
isDateUnavailable: undefined,
|
|
146
150
|
disableDaysOutsideCurrentView: false,
|
|
151
|
+
minYear: 1900,
|
|
152
|
+
maxYear: 2100,
|
|
147
153
|
});
|
|
148
154
|
const emits = defineEmits<CalendarRootEmits>();
|
|
149
155
|
defineSlots<{
|
|
@@ -187,6 +193,8 @@ const {
|
|
|
187
193
|
dir: propDir,
|
|
188
194
|
locale: propLocale,
|
|
189
195
|
disableDaysOutsideCurrentView,
|
|
196
|
+
minYear,
|
|
197
|
+
maxYear,
|
|
190
198
|
} = toRefs(props);
|
|
191
199
|
|
|
192
200
|
const { primitiveElement, currentElement: parentElement } =
|
|
@@ -249,6 +257,8 @@ const {
|
|
|
249
257
|
calendarLabel,
|
|
250
258
|
nextPage: propsNextPage,
|
|
251
259
|
prevPage: propsPrevPage,
|
|
260
|
+
minYear,
|
|
261
|
+
maxYear,
|
|
252
262
|
});
|
|
253
263
|
|
|
254
264
|
const { isInvalid, isDateSelected } = useCalendarState({
|
|
@@ -34,6 +34,8 @@ export type UseCalendarProps = {
|
|
|
34
34
|
calendarLabel: Ref<string | undefined>;
|
|
35
35
|
nextPage: Ref<((placeholder: DateValue) => DateValue) | undefined>;
|
|
36
36
|
prevPage: Ref<((placeholder: DateValue) => DateValue) | undefined>;
|
|
37
|
+
minYear: Ref<number>;
|
|
38
|
+
maxYear: Ref<number>;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
export type UseCalendarStateProps = {
|
|
@@ -110,6 +112,10 @@ function handlePrevPage(
|
|
|
110
112
|
export function useCalendar(props: UseCalendarProps) {
|
|
111
113
|
const formatter = useDateFormatter(props.locale.value);
|
|
112
114
|
|
|
115
|
+
watch(props.locale, (newLocale) => {
|
|
116
|
+
formatter.setLocale(newLocale);
|
|
117
|
+
});
|
|
118
|
+
|
|
113
119
|
const headingFormatOptions = computed(() => {
|
|
114
120
|
const options: DateFormatterOptions = {
|
|
115
121
|
calendar: props.placeholder.value.calendar.identifier,
|
|
@@ -329,10 +335,7 @@ export function useCalendar(props: UseCalendarProps) {
|
|
|
329
335
|
);
|
|
330
336
|
|
|
331
337
|
const headingValue = computed(() => {
|
|
332
|
-
if (!grid.value.length) return "";
|
|
333
|
-
|
|
334
|
-
if (props.locale.value !== formatter.getLocale())
|
|
335
|
-
formatter.setLocale(props.locale.value);
|
|
338
|
+
if (!grid.value.length || !grid.value[0]) return "";
|
|
336
339
|
|
|
337
340
|
if (grid.value.length === 1) {
|
|
338
341
|
const month = grid.value[0].value;
|
|
@@ -340,7 +343,8 @@ export function useCalendar(props: UseCalendarProps) {
|
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
const startMonth = toDate(grid.value[0].value);
|
|
343
|
-
const
|
|
346
|
+
const lastMonth = grid.value[grid.value.length - 1];
|
|
347
|
+
const endMonth = toDate(lastMonth ? lastMonth.value : grid.value[0].value);
|
|
344
348
|
|
|
345
349
|
const startMonthName = formatter.fullMonth(
|
|
346
350
|
startMonth,
|
|
@@ -367,36 +371,22 @@ export function useCalendar(props: UseCalendarProps) {
|
|
|
367
371
|
return content;
|
|
368
372
|
});
|
|
369
373
|
|
|
370
|
-
|
|
371
|
-
const currentMonth = computed(() => {
|
|
374
|
+
function formatGridDate(fn: (date: Date) => string): string {
|
|
372
375
|
if (!grid.value.length) return "";
|
|
373
|
-
|
|
374
|
-
if (props.locale.value !== formatter.getLocale())
|
|
375
|
-
formatter.setLocale(props.locale.value);
|
|
376
376
|
|
|
377
377
|
const date = grid.value[0]?.value;
|
|
378
|
+
if (!date) return "";
|
|
378
379
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return "";
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const currentYear = computed(() => {
|
|
387
|
-
if (!grid.value.length) return "";
|
|
388
|
-
|
|
389
|
-
if (props.locale.value !== formatter.getLocale())
|
|
390
|
-
formatter.setLocale(props.locale.value);
|
|
391
|
-
|
|
392
|
-
const date = grid.value[0]?.value;
|
|
380
|
+
return fn(toDate(date));
|
|
381
|
+
}
|
|
393
382
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
383
|
+
const currentMonth = computed(() =>
|
|
384
|
+
formatGridDate((d) => formatter.fullMonth(d, headingFormatOptions.value)),
|
|
385
|
+
);
|
|
397
386
|
|
|
398
|
-
|
|
399
|
-
|
|
387
|
+
const currentYear = computed(() =>
|
|
388
|
+
formatGridDate((d) => formatter.fullYear(d, headingFormatOptions.value)),
|
|
389
|
+
);
|
|
400
390
|
|
|
401
391
|
// TODO: look if there is a better way to create months with locale month name
|
|
402
392
|
const months = computed(() => {
|
|
@@ -410,8 +400,6 @@ export function useCalendar(props: UseCalendarProps) {
|
|
|
410
400
|
numberOfMonths: 12,
|
|
411
401
|
});
|
|
412
402
|
|
|
413
|
-
|
|
414
|
-
|
|
415
403
|
return monthsArray.map((month) => {
|
|
416
404
|
const date = month.value.copy() as DateValue & { monthName: string };
|
|
417
405
|
date.monthName = `${formatter.fullMonth(toDate(date), headingFormatOptions.value)}`;
|
|
@@ -419,12 +407,11 @@ export function useCalendar(props: UseCalendarProps) {
|
|
|
419
407
|
});
|
|
420
408
|
});
|
|
421
409
|
|
|
422
|
-
// TODO: add min and max for years and maybe we should switch to date object here as well for consistency
|
|
423
410
|
const years = computed(() => {
|
|
424
|
-
const startDate = props.placeholder.value.set({ year:
|
|
425
|
-
const endDate = props.placeholder.value.set({ year:
|
|
426
|
-
|
|
427
|
-
return createYearRange({ start: startDate, end: endDate })
|
|
411
|
+
const startDate = props.placeholder.value.set({ year: props.minYear.value });
|
|
412
|
+
const endDate = props.placeholder.value.set({ year: props.maxYear.value });
|
|
413
|
+
|
|
414
|
+
return createYearRange({ start: startDate, end: endDate });
|
|
428
415
|
});
|
|
429
416
|
|
|
430
417
|
const fullCalendarLabel = computed(
|
|
@@ -27,6 +27,8 @@ const rootContext = injectDatePickerRootContext();
|
|
|
27
27
|
readonly: rootContext.readonly.value,
|
|
28
28
|
preventDeselect: rootContext.preventDeselect.value,
|
|
29
29
|
dir: rootContext.dir.value,
|
|
30
|
+
minYear: rootContext.minYear.value,
|
|
31
|
+
maxYear: rootContext.maxYear.value,
|
|
30
32
|
}"
|
|
31
33
|
:model-value="rootContext.modelValue.value"
|
|
32
34
|
:placeholder="rootContext.placeholder.value"
|
|
@@ -44,6 +44,8 @@ type DatePickerRootContext = {
|
|
|
44
44
|
dir: Ref<Direction>;
|
|
45
45
|
step: Ref<DateStep | undefined>;
|
|
46
46
|
closeOnSelect: Ref<boolean>;
|
|
47
|
+
minYear: Ref<number>;
|
|
48
|
+
maxYear: Ref<number>;
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
export type DatePickerRootProps = DateFieldRootProps &
|
|
@@ -57,6 +59,8 @@ export type DatePickerRootProps = DateFieldRootProps &
|
|
|
57
59
|
| "fixedWeeks"
|
|
58
60
|
| "numberOfMonths"
|
|
59
61
|
| "preventDeselect"
|
|
62
|
+
| "minYear"
|
|
63
|
+
| "maxYear"
|
|
60
64
|
> & {
|
|
61
65
|
/** Whether or not to close the popover on date select */
|
|
62
66
|
closeOnSelect?: boolean;
|
|
@@ -96,6 +100,8 @@ const props = withDefaults(defineProps<DatePickerRootProps>(), {
|
|
|
96
100
|
isDateDisabled: undefined,
|
|
97
101
|
isDateUnavailable: undefined,
|
|
98
102
|
closeOnSelect: false,
|
|
103
|
+
minYear: 1900,
|
|
104
|
+
maxYear: 2100,
|
|
99
105
|
});
|
|
100
106
|
const emits = defineEmits<DatePickerRootEmits & PopoverRootEmits>();
|
|
101
107
|
const {
|
|
@@ -123,6 +129,8 @@ const {
|
|
|
123
129
|
dir: propDir,
|
|
124
130
|
step,
|
|
125
131
|
closeOnSelect,
|
|
132
|
+
minYear,
|
|
133
|
+
maxYear,
|
|
126
134
|
} = toRefs(props);
|
|
127
135
|
|
|
128
136
|
const dir = useDirection(propDir);
|
|
@@ -206,6 +214,8 @@ provideDatePickerRootContext({
|
|
|
206
214
|
placeholder.value = date.copy();
|
|
207
215
|
},
|
|
208
216
|
closeOnSelect,
|
|
217
|
+
minYear,
|
|
218
|
+
maxYear,
|
|
209
219
|
});
|
|
210
220
|
</script>
|
|
211
221
|
|