@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rupe/v-datepicker",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.2",
4
+ "version": "1.0.0-beta.1",
5
5
  "description": "A Vue 3 datepicker component",
6
6
  "packageManager": "pnpm@10.13.1",
7
7
  "author": "Anja Rupnik",
@@ -1,23 +1,43 @@
1
- <script setup lang="ts">
2
- import { computed } from "vue";
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
- const props = defineProps<CalendarMonthYearOverlayProps>();
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(() => rootContext.months.value);
20
- const years = computed(() => rootContext.years.value);
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
- date: DateValue;
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: "button",
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 (rootContext.isDateDisabled(props.date) || rootContext.isDateUnavailable?.(props.date))
19
- return;
59
+ if (!isDateSelectable(props.date)) return;
20
60
 
21
61
  rootContext.onDateChange(props.date);
22
- rootContext.monthYearOverlayState.value = false;
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 v-bind="props" @click="handleClick">
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 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
-
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
- 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(() => {
@@ -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: 1875 });
425
- const endDate = props.placeholder.value.set({ year: 2100 });
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