@rupe/v-datepicker 1.0.0-beta.0 → 1.0.0-beta.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rupe/v-datepicker",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.0",
4
+ "version": "1.0.0-beta.3",
5
5
  "description": "A Vue 3 datepicker component",
6
6
  "packageManager": "pnpm@10.13.1",
7
7
  "author": "Anja Rupnik",
@@ -65,7 +65,6 @@ provideCalendarMonthYearOverlayContext({
65
65
  zIndex: '1000',
66
66
  }"
67
67
  role="dialog"
68
- tabindex="0"
69
68
  >
70
69
  <slot
71
70
  :months="months"
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { computed } from "vue";
2
+ import { computed, onMounted, nextTick } from "vue";
3
3
  import type { DateValue } from "@internationalized/date";
4
- import { Primitive } from "../Primitive";
4
+ import { Primitive, usePrimitiveElement } from "../Primitive";
5
5
  import type { PrimitiveProps } from "../Primitive";
6
6
  import { injectCalendarRootContext } from "./CalendarRoot.vue";
7
7
  import { useKbd } from "../../shared";
@@ -20,29 +20,55 @@ const props = withDefaults(defineProps<CalendarOverlayItemProps>(), {
20
20
 
21
21
  const rootContext = injectCalendarRootContext();
22
22
  const years = computed(() => rootContext.years.value);
23
- const maxValue = computed(() =>
24
- props.type === "month" ? 12 : years.value.length,
25
- );
26
23
  const overlayContext = injectCalendarMonthYearOverlayContext();
27
24
  const dataValue = computed(() => `${props.type}-${props.date[props.type]}`);
28
25
 
26
+ const { primitiveElement, currentElement } = usePrimitiveElement();
27
+
29
28
  const isFocusedDate = computed(() => {
30
- if (props.type === "month")
29
+ const selected = rootContext.modelValue.value;
30
+ const selectedDate = Array.isArray(selected)
31
+ ? selected[selected.length - 1]
32
+ : selected;
33
+
34
+ if (selectedDate) {
35
+ if (props.type === "month") return selectedDate.month === props.date.month;
36
+ return selectedDate.year === props.date.year;
37
+ }
38
+
39
+ if (props.type === "month") {
31
40
  return rootContext.currentMonth.value === props.date.monthName;
41
+ }
42
+
32
43
  return rootContext.currentYear.value === props.date.year.toString();
33
44
  });
34
45
 
46
+ const ariaLabel = computed(() => {
47
+ if (props.type === "month") return props.date.monthName;
48
+ return props.date.year.toString();
49
+ });
50
+
51
+ onMounted(() => {
52
+ if (isFocusedDate.value) {
53
+ nextTick(() => {
54
+ currentElement.value?.focus();
55
+ currentElement.value?.scrollIntoView({ block: "nearest" });
56
+ });
57
+ }
58
+ });
59
+
60
+ function isDateSelectable(date: DateValue) {
61
+ return (
62
+ !rootContext.isDateDisabled(date) && !rootContext.isDateUnavailable?.(date)
63
+ );
64
+ }
65
+
35
66
  function closeOverlay() {
36
67
  rootContext.monthYearOverlayState.value = false;
37
68
  }
38
69
 
39
70
  function handleClick() {
40
- // TODO: double check if disabled and unavailable works properly for a month
41
- if (
42
- rootContext.isDateDisabled(props.date) ||
43
- rootContext.isDateUnavailable?.(props.date)
44
- )
45
- return;
71
+ if (!isDateSelectable(props.date)) return;
46
72
 
47
73
  rootContext.onDateChange(props.date);
48
74
  closeOverlay();
@@ -50,52 +76,68 @@ function handleClick() {
50
76
 
51
77
  const kbd = useKbd();
52
78
 
53
- function handleArrowKey(e: KeyboardEvent) {
79
+ function handleKeydown(e: KeyboardEvent) {
54
80
  if (props.disabled) return;
55
81
  e.preventDefault();
56
82
  e.stopPropagation();
57
83
  const parentElement = rootContext.parentElement.value!;
58
84
 
59
- const indexIncrementation = overlayContext.itemsPerRow.value;
85
+ const itemsPerRow = overlayContext.itemsPerRow.value;
60
86
  const sign = rootContext.dir.value === "rtl" ? -1 : 1;
87
+ const currentValue = props.date[props.type];
88
+
89
+ function focusItem(value: number) {
90
+ const candidate = parentElement.querySelector<HTMLElement>(
91
+ `[data-value='${props.type}-${value}']`,
92
+ );
93
+ candidate?.focus();
94
+ candidate?.scrollIntoView({ block: "nearest" });
95
+ }
96
+
97
+ function shiftFocus(add: number) {
98
+ let nextValue = currentValue + add;
99
+
100
+ if (props.type === "month") {
101
+ nextValue = ((nextValue - 1 + 12) % 12) + 1;
102
+ } else {
103
+ const minYear = years.value[0]!.year;
104
+ const maxYear = years.value[years.value.length - 1]!.year;
105
+ nextValue = Math.max(minYear, Math.min(nextValue, maxYear));
106
+ }
107
+
108
+ focusItem(nextValue);
109
+ }
61
110
 
62
- // TODO: check how we can reuse code with CalendarCellTrigger
63
111
  switch (e.code) {
64
112
  case kbd.ARROW_RIGHT:
65
- shiftFocus(props.date, sign);
113
+ shiftFocus(sign);
66
114
  break;
67
115
  case kbd.ARROW_LEFT:
68
- shiftFocus(props.date, -sign);
116
+ shiftFocus(-sign);
69
117
  break;
70
118
  case kbd.ARROW_UP:
71
- shiftFocus(props.date, -indexIncrementation);
119
+ shiftFocus(-itemsPerRow);
72
120
  break;
73
121
  case kbd.ARROW_DOWN:
74
- shiftFocus(props.date, indexIncrementation);
122
+ shiftFocus(itemsPerRow);
123
+ break;
124
+ case kbd.HOME: {
125
+ const firstValue = props.type === "month" ? 1 : years.value[0]!.year;
126
+ focusItem(firstValue);
75
127
  break;
76
- case kbd.ENTER:
77
- case kbd.SPACE_CODE:
78
- rootContext.onDateChange(props.date);
79
- closeOverlay();
80
- }
81
-
82
- function shiftFocus(date: DateValue, add: number) {
83
- let nextDate = date[props.type] + add;
84
-
85
- // TODO: fix for years
86
- if (nextDate <= 0) {
87
- nextDate = maxValue.value + nextDate;
88
128
  }
89
-
90
- if (nextDate > years.value[maxValue.value - 1].year) {
91
- nextDate = nextDate - maxValue.value;
129
+ case kbd.END: {
130
+ const lastValue =
131
+ props.type === "month" ? 12 : years.value[years.value.length - 1]!.year;
132
+ focusItem(lastValue);
133
+ break;
92
134
  }
93
-
94
- const candidateDay = parentElement.querySelector<HTMLElement>(
95
- `[data-value='${props.type}-${nextDate}']`,
96
- );
97
-
98
- candidateDay?.focus();
135
+ case kbd.ENTER:
136
+ case kbd.SPACE_CODE:
137
+ if (isDateSelectable(props.date)) {
138
+ rootContext.onDateChange(props.date);
139
+ closeOverlay();
140
+ }
99
141
  }
100
142
  }
101
143
  </script>
@@ -106,9 +148,10 @@ function handleArrowKey(e: KeyboardEvent) {
106
148
  @click="handleClick"
107
149
  ref="primitiveElement"
108
150
  role="button"
151
+ :aria-label="ariaLabel"
109
152
  :aria-disabled="disabled"
110
153
  :data-value="dataValue"
111
- @keydown.up.down.left.right.space.enter="handleArrowKey"
154
+ @keydown.up.down.left.right.space.enter.home.end="handleKeydown"
112
155
  @keydown.enter.prevent
113
156
  :tabindex="isFocusedDate ? 0 : -1"
114
157
  >
@@ -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 endMonth = toDate(grid.value[grid.value.length - 1].value);
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
- // TODO: rewrite (a lot of duplication)
371
- const currentMonth = computed(() => {
374
+ function formatGridDate(fn: (date: Date) => string): string {
372
375
  if (!grid.value.length) return "";
373
376
 
374
- if (props.locale.value !== formatter.getLocale())
375
- formatter.setLocale(props.locale.value);
376
-
377
377
  const date = grid.value[0]?.value;
378
+ if (!date) return "";
378
379
 
379
- if (date) {
380
- return `${formatter.fullMonth(toDate(date), headingFormatOptions.value)}`;
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
- if (date) {
395
- return `${formatter.fullYear(toDate(date), headingFormatOptions.value)}`;
396
- }
383
+ const currentMonth = computed(() =>
384
+ formatGridDate((d) => formatter.fullMonth(d, headingFormatOptions.value)),
385
+ );
397
386
 
398
- return "";
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(() => {
@@ -417,10 +407,9 @@ export function useCalendar(props: UseCalendarProps) {
417
407
  });
418
408
  });
419
409
 
420
- // TODO: add min and max for years and maybe we should switch to date object here as well for consistency
421
410
  const years = computed(() => {
422
- const startDate = props.placeholder.value.set({ year: 1875 });
423
- const endDate = props.placeholder.value.set({ year: 2100 });
411
+ const startDate = props.placeholder.value.set({ year: props.minYear.value });
412
+ const endDate = props.placeholder.value.set({ year: props.maxYear.value });
424
413
 
425
414
  return createYearRange({ start: startDate, end: endDate });
426
415
  });
@@ -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