@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/README.md CHANGED
@@ -62,10 +62,53 @@ import { DatePicker } from '@rupe/v-datepicker'
62
62
  <DatePicker.Calendar v-slot="{ weekDays, grid }">
63
63
  <DatePicker.Header class="flex items-center justify-between mb-4">
64
64
  <DatePicker.Prev class="p-1 hover:bg-gray-100 rounded">⬅️</DatePicker.Prev>
65
+
65
66
  <DatePicker.Heading class="font-bold flex gap-1">
66
- <DatePicker.MonthHeading />
67
- <DatePicker.YearHeading />
67
+ <!-- Month Heading & Overlay -->
68
+ <DatePicker.MonthHeading class="hover:bg-gray-100 rounded px-1 cursor-pointer" />
69
+ <DatePicker.MonthYearOverlay type="month" class="bg-white rounded-lg shadow-lg p-2" :items-per-row="3">
70
+ <template #default="{ months }">
71
+ <DatePicker.Grid class="w-full">
72
+ <DatePicker.GridBody>
73
+ <DatePicker.GridRow v-for="(row, index) in months" :key="index" class="flex gap-1 mb-1">
74
+ <DatePicker.Cell v-for="month in row" :key="month.monthName" :date="month" class="flex-1">
75
+ <DatePicker.OverlayItem
76
+ :date="month"
77
+ type="month"
78
+ class="w-full p-2 text-center rounded hover:bg-blue-50 data-[selected]:bg-blue-600 data-[selected]:text-white"
79
+ >
80
+ {{ month.monthName }}
81
+ </DatePicker.OverlayItem>
82
+ </DatePicker.Cell>
83
+ </DatePicker.GridRow>
84
+ </DatePicker.GridBody>
85
+ </DatePicker.Grid>
86
+ </template>
87
+ </DatePicker.MonthYearOverlay>
88
+
89
+ <!-- Year Heading & Overlay -->
90
+ <DatePicker.YearHeading class="hover:bg-gray-100 rounded px-1 cursor-pointer" />
91
+ <DatePicker.MonthYearOverlay type="year" class="bg-white rounded-lg shadow-lg p-2 h-64 overflow-y-auto" :items-per-row="3">
92
+ <template #default="{ years }">
93
+ <DatePicker.Grid class="w-full">
94
+ <DatePicker.GridBody>
95
+ <DatePicker.GridRow v-for="(row, index) in years" :key="index" class="flex gap-1 mb-1">
96
+ <DatePicker.Cell v-for="year in row" :key="year.year" :date="year" class="flex-1">
97
+ <DatePicker.OverlayItem
98
+ :date="year"
99
+ type="year"
100
+ class="w-full p-2 text-center rounded hover:bg-blue-50 data-[selected]:bg-blue-600 data-[selected]:text-white"
101
+ >
102
+ {{ year.year }}
103
+ </DatePicker.OverlayItem>
104
+ </DatePicker.Cell>
105
+ </DatePicker.GridRow>
106
+ </DatePicker.GridBody>
107
+ </DatePicker.Grid>
108
+ </template>
109
+ </DatePicker.MonthYearOverlay>
68
110
  </DatePicker.Heading>
111
+
69
112
  <DatePicker.Next class="p-1 hover:bg-gray-100 rounded">➡️</DatePicker.Next>
70
113
  </DatePicker.Header>
71
114
 
@@ -97,7 +140,9 @@ import { DatePicker } from '@rupe/v-datepicker'
97
140
 
98
141
  ## Playground
99
142
 
100
- The repository includes a playground project built with **Nuxt** and **Tailwind CSS** to test the library locally.
143
+ You can check out the package in action on [StackBlitz](https://stackblitz.com/edit/nuxt-starter-tjmzbch9?file=package.json).
144
+
145
+ The repository also includes a playground project built with **Nuxt** and **Tailwind CSS** to test the library locally.
101
146
 
102
147
  ### Running the Playground
103
148
 
@@ -99,6 +99,10 @@ export interface CalendarRootProps extends PrimitiveProps {
99
99
  multiple?: boolean;
100
100
  /** Whether or not to disable days outside the current view. */
101
101
  disableDaysOutsideCurrentView?: boolean;
102
+ /** The minimum year displayed in the year overlay */
103
+ minYear?: number;
104
+ /** The maximum year displayed in the year overlay */
105
+ maxYear?: number;
102
106
  }
103
107
  export type CalendarRootEmits = {
104
108
  /** Event handler called whenever the model value changes */
@@ -130,6 +134,8 @@ declare const _default: __VLS_WithTemplateSlots< DefineComponent<CalendarRootPro
130
134
  isDateDisabled: Matcher;
131
135
  isDateUnavailable: Matcher;
132
136
  disableDaysOutsideCurrentView: boolean;
137
+ minYear: number;
138
+ maxYear: number;
133
139
  }, {}, {}, {}, string, ComponentProvideOptions, false, {
134
140
  primitiveElement: CreateComponentPublicInstanceWithMixins<Readonly< ExtractPropTypes<{
135
141
  asChild: {
@@ -18,6 +18,8 @@ export type UseCalendarProps = {
18
18
  calendarLabel: Ref<string | undefined>;
19
19
  nextPage: Ref<((placeholder: DateValue) => DateValue) | undefined>;
20
20
  prevPage: Ref<((placeholder: DateValue) => DateValue) | undefined>;
21
+ minYear: Ref<number>;
22
+ maxYear: Ref<number>;
21
23
  };
22
24
  export type UseCalendarStateProps = {
23
25
  isDateDisabled: Matcher;
@@ -36,8 +36,10 @@ type DatePickerRootContext = {
36
36
  dir: Ref<Direction>;
37
37
  step: Ref<DateStep | undefined>;
38
38
  closeOnSelect: Ref<boolean>;
39
+ minYear: Ref<number>;
40
+ maxYear: Ref<number>;
39
41
  };
40
- export type DatePickerRootProps = DateFieldRootProps & PopoverRootProps & Pick<CalendarRootProps, "isDateDisabled" | "pagedNavigation" | "weekStartsOn" | "weekdayFormat" | "fixedWeeks" | "numberOfMonths" | "preventDeselect"> & {
42
+ export type DatePickerRootProps = DateFieldRootProps & PopoverRootProps & Pick<CalendarRootProps, "isDateDisabled" | "pagedNavigation" | "weekStartsOn" | "weekdayFormat" | "fixedWeeks" | "numberOfMonths" | "preventDeselect" | "minYear" | "maxYear"> & {
41
43
  /** Whether or not to close the popover on date select */
42
44
  closeOnSelect?: boolean;
43
45
  };
@@ -72,6 +74,8 @@ declare const _default: __VLS_WithTemplateSlots< DefineComponent<DatePickerRootP
72
74
  readonly: boolean;
73
75
  isDateDisabled: Matcher;
74
76
  isDateUnavailable: Matcher;
77
+ minYear: number;
78
+ maxYear: number;
75
79
  closeOnSelect: boolean;
76
80
  }, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>, {
77
81
  default?(_: {}): any;
package/dist/index.cjs CHANGED
@@ -3201,6 +3201,9 @@ function handlePrevPage(date2, prevPageFunc) {
3201
3201
  }
3202
3202
  function useCalendar(props) {
3203
3203
  const formatter = useDateFormatter(props.locale.value);
3204
+ vue.watch(props.locale, (newLocale) => {
3205
+ formatter.setLocale(newLocale);
3206
+ });
3204
3207
  const headingFormatOptions = vue.computed(() => {
3205
3208
  const options = {
3206
3209
  calendar: props.placeholder.value.calendar.identifier
@@ -3367,15 +3370,14 @@ function useCalendar(props) {
3367
3370
  }
3368
3371
  );
3369
3372
  const headingValue = vue.computed(() => {
3370
- if (!grid.value.length) return "";
3371
- if (props.locale.value !== formatter.getLocale())
3372
- formatter.setLocale(props.locale.value);
3373
+ if (!grid.value.length || !grid.value[0]) return "";
3373
3374
  if (grid.value.length === 1) {
3374
3375
  const month = grid.value[0].value;
3375
3376
  return `${formatter.fullMonthAndYear(toDate(month), headingFormatOptions.value)}`;
3376
3377
  }
3377
3378
  const startMonth = toDate(grid.value[0].value);
3378
- const endMonth = toDate(grid.value[grid.value.length - 1].value);
3379
+ const lastMonth = grid.value[grid.value.length - 1];
3380
+ const endMonth = toDate(lastMonth ? lastMonth.value : grid.value[0].value);
3379
3381
  const startMonthName = formatter.fullMonth(
3380
3382
  startMonth,
3381
3383
  headingFormatOptions.value
@@ -3395,26 +3397,18 @@ function useCalendar(props) {
3395
3397
  const content = startMonthYear === endMonthYear ? `${startMonthName} - ${endMonthName} ${endMonthYear}` : `${startMonthName} ${startMonthYear} - ${endMonthName} ${endMonthYear}`;
3396
3398
  return content;
3397
3399
  });
3398
- const currentMonth = vue.computed(() => {
3399
- if (!grid.value.length) return "";
3400
- if (props.locale.value !== formatter.getLocale())
3401
- formatter.setLocale(props.locale.value);
3402
- const date2 = grid.value[0]?.value;
3403
- if (date2) {
3404
- return `${formatter.fullMonth(toDate(date2), headingFormatOptions.value)}`;
3405
- }
3406
- return "";
3407
- });
3408
- const currentYear = vue.computed(() => {
3400
+ function formatGridDate(fn) {
3409
3401
  if (!grid.value.length) return "";
3410
- if (props.locale.value !== formatter.getLocale())
3411
- formatter.setLocale(props.locale.value);
3412
3402
  const date2 = grid.value[0]?.value;
3413
- if (date2) {
3414
- return `${formatter.fullYear(toDate(date2), headingFormatOptions.value)}`;
3415
- }
3416
- return "";
3417
- });
3403
+ if (!date2) return "";
3404
+ return fn(toDate(date2));
3405
+ }
3406
+ const currentMonth = vue.computed(
3407
+ () => formatGridDate((d) => formatter.fullMonth(d, headingFormatOptions.value))
3408
+ );
3409
+ const currentYear = vue.computed(
3410
+ () => formatGridDate((d) => formatter.fullYear(d, headingFormatOptions.value))
3411
+ );
3418
3412
  const months = vue.computed(() => {
3419
3413
  const startDate = props.placeholder.value.set({ month: 1 });
3420
3414
  const monthsArray = createMonths({
@@ -3431,8 +3425,8 @@ function useCalendar(props) {
3431
3425
  });
3432
3426
  });
3433
3427
  const years = vue.computed(() => {
3434
- const startDate = props.placeholder.value.set({ year: 1875 });
3435
- const endDate = props.placeholder.value.set({ year: 2100 });
3428
+ const startDate = props.placeholder.value.set({ year: props.minYear.value });
3429
+ const endDate = props.placeholder.value.set({ year: props.maxYear.value });
3436
3430
  return createYearRange({ start: startDate, end: endDate });
3437
3431
  });
3438
3432
  const fullCalendarLabel = vue.computed(
@@ -3491,6 +3485,8 @@ const _sfc_main$E = /* @__PURE__ */ vue.defineComponent({
3491
3485
  modelValue: {},
3492
3486
  multiple: { type: Boolean, default: false },
3493
3487
  disableDaysOutsideCurrentView: { type: Boolean, default: false },
3488
+ minYear: { default: 1900 },
3489
+ maxYear: { default: 2100 },
3494
3490
  asChild: { type: Boolean },
3495
3491
  as: { default: "div" }
3496
3492
  },
@@ -3519,7 +3515,9 @@ const _sfc_main$E = /* @__PURE__ */ vue.defineComponent({
3519
3515
  prevPage: propsPrevPage,
3520
3516
  dir: propDir,
3521
3517
  locale: propLocale,
3522
- disableDaysOutsideCurrentView
3518
+ disableDaysOutsideCurrentView,
3519
+ minYear,
3520
+ maxYear
3523
3521
  } = vue.toRefs(props);
3524
3522
  const { primitiveElement, currentElement: parentElement } = usePrimitiveElement();
3525
3523
  const locale = useLocale(propLocale);
@@ -3573,7 +3571,9 @@ const _sfc_main$E = /* @__PURE__ */ vue.defineComponent({
3573
3571
  isDateUnavailable: propsIsDateUnavailable.value,
3574
3572
  calendarLabel,
3575
3573
  nextPage: propsNextPage,
3576
- prevPage: propsPrevPage
3574
+ prevPage: propsPrevPage,
3575
+ minYear,
3576
+ maxYear
3577
3577
  });
3578
3578
  const { isInvalid, isDateSelected } = useCalendarState({
3579
3579
  date: modelValue,
@@ -4211,8 +4211,7 @@ const _sfc_main$q = /* @__PURE__ */ vue.defineComponent({
4211
4211
  left: "0",
4212
4212
  zIndex: "1000"
4213
4213
  },
4214
- role: "dialog",
4215
- tabindex: "0"
4214
+ role: "dialog"
4216
4215
  }), {
4217
4216
  default: vue.withCtx(() => [
4218
4217
  vue.renderSlot(_ctx.$slots, "default", {
@@ -4265,6 +4264,8 @@ const _sfc_main$p = /* @__PURE__ */ vue.defineComponent({
4265
4264
  fixedWeeks: { type: Boolean, default: false },
4266
4265
  numberOfMonths: { default: 1 },
4267
4266
  preventDeselect: { type: Boolean, default: false },
4267
+ minYear: { default: 1900 },
4268
+ maxYear: { default: 2100 },
4268
4269
  closeOnSelect: { type: Boolean, default: false }
4269
4270
  },
4270
4271
  emits: ["update:modelValue", "update:placeholder", "update:open"],
@@ -4295,7 +4296,9 @@ const _sfc_main$p = /* @__PURE__ */ vue.defineComponent({
4295
4296
  defaultValue,
4296
4297
  dir: propDir,
4297
4298
  step,
4298
- closeOnSelect
4299
+ closeOnSelect,
4300
+ minYear,
4301
+ maxYear
4299
4302
  } = vue.toRefs(props);
4300
4303
  const dir = useDirection(propDir);
4301
4304
  const modelValue = core.useVModel(props, "modelValue", emits, {
@@ -4366,7 +4369,9 @@ const _sfc_main$p = /* @__PURE__ */ vue.defineComponent({
4366
4369
  onPlaceholderChange(date2) {
4367
4370
  placeholder.value = date2.copy();
4368
4371
  },
4369
- closeOnSelect
4372
+ closeOnSelect,
4373
+ minYear,
4374
+ maxYear
4370
4375
  });
4371
4376
  return (_ctx, _cache) => {
4372
4377
  return vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$S), {
@@ -4401,7 +4406,9 @@ const _sfc_main$o = /* @__PURE__ */ vue.defineComponent({
4401
4406
  numberOfMonths: vue.unref(rootContext).numberOfMonths.value,
4402
4407
  readonly: vue.unref(rootContext).readonly.value,
4403
4408
  preventDeselect: vue.unref(rootContext).preventDeselect.value,
4404
- dir: vue.unref(rootContext).dir.value
4409
+ dir: vue.unref(rootContext).dir.value,
4410
+ minYear: vue.unref(rootContext).minYear.value,
4411
+ maxYear: vue.unref(rootContext).maxYear.value
4405
4412
  }, {
4406
4413
  "model-value": vue.unref(rootContext).modelValue.value,
4407
4414
  placeholder: vue.unref(rootContext).placeholder.value,
@@ -5192,74 +5199,106 @@ const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
5192
5199
  const props = __props;
5193
5200
  const rootContext = injectCalendarRootContext();
5194
5201
  const years = vue.computed(() => rootContext.years.value);
5195
- const maxValue = vue.computed(
5196
- () => props.type === "month" ? 12 : years.value.length
5197
- );
5198
5202
  const overlayContext = injectCalendarMonthYearOverlayContext();
5199
5203
  const dataValue = vue.computed(() => `${props.type}-${props.date[props.type]}`);
5204
+ const { primitiveElement, currentElement } = usePrimitiveElement();
5200
5205
  const isFocusedDate = vue.computed(() => {
5201
5206
  if (props.type === "month")
5202
5207
  return rootContext.currentMonth.value === props.date.monthName;
5203
5208
  return rootContext.currentYear.value === props.date.year.toString();
5204
5209
  });
5210
+ const ariaLabel = vue.computed(() => {
5211
+ if (props.type === "month") return props.date.monthName;
5212
+ return props.date.year.toString();
5213
+ });
5214
+ vue.onMounted(() => {
5215
+ if (isFocusedDate.value) {
5216
+ vue.nextTick(() => {
5217
+ currentElement.value?.focus();
5218
+ currentElement.value?.scrollIntoView({ block: "nearest" });
5219
+ });
5220
+ }
5221
+ });
5222
+ function isDateSelectable(date2) {
5223
+ return !rootContext.isDateDisabled(date2) && !rootContext.isDateUnavailable?.(date2);
5224
+ }
5205
5225
  function closeOverlay() {
5206
5226
  rootContext.monthYearOverlayState.value = false;
5207
5227
  }
5208
5228
  function handleClick() {
5209
- if (rootContext.isDateDisabled(props.date) || rootContext.isDateUnavailable?.(props.date))
5210
- return;
5229
+ if (!isDateSelectable(props.date)) return;
5211
5230
  rootContext.onDateChange(props.date);
5212
5231
  closeOverlay();
5213
5232
  }
5214
5233
  const kbd = useKbd();
5215
- function handleArrowKey(e) {
5234
+ function handleKeydown(e) {
5216
5235
  if (props.disabled) return;
5217
5236
  e.preventDefault();
5218
5237
  e.stopPropagation();
5219
5238
  const parentElement = rootContext.parentElement.value;
5220
- const indexIncrementation = overlayContext.itemsPerRow.value;
5239
+ const itemsPerRow = overlayContext.itemsPerRow.value;
5221
5240
  const sign = rootContext.dir.value === "rtl" ? -1 : 1;
5241
+ const currentValue = props.date[props.type];
5242
+ function focusItem(value) {
5243
+ const candidate = parentElement.querySelector(
5244
+ `[data-value='${props.type}-${value}']`
5245
+ );
5246
+ candidate?.focus();
5247
+ candidate?.scrollIntoView({ block: "nearest" });
5248
+ }
5249
+ function shiftFocus(add) {
5250
+ let nextValue = currentValue + add;
5251
+ if (props.type === "month") {
5252
+ nextValue = (nextValue - 1 + 12) % 12 + 1;
5253
+ } else {
5254
+ const minYear = years.value[0].year;
5255
+ const maxYear = years.value[years.value.length - 1].year;
5256
+ nextValue = Math.max(minYear, Math.min(nextValue, maxYear));
5257
+ }
5258
+ focusItem(nextValue);
5259
+ }
5222
5260
  switch (e.code) {
5223
5261
  case kbd.ARROW_RIGHT:
5224
- shiftFocus(props.date, sign);
5262
+ shiftFocus(sign);
5225
5263
  break;
5226
5264
  case kbd.ARROW_LEFT:
5227
- shiftFocus(props.date, -sign);
5265
+ shiftFocus(-sign);
5228
5266
  break;
5229
5267
  case kbd.ARROW_UP:
5230
- shiftFocus(props.date, -indexIncrementation);
5268
+ shiftFocus(-itemsPerRow);
5231
5269
  break;
5232
5270
  case kbd.ARROW_DOWN:
5233
- shiftFocus(props.date, indexIncrementation);
5271
+ shiftFocus(itemsPerRow);
5272
+ break;
5273
+ case kbd.HOME: {
5274
+ const firstValue = props.type === "month" ? 1 : years.value[0].year;
5275
+ focusItem(firstValue);
5234
5276
  break;
5235
- case kbd.ENTER:
5236
- case kbd.SPACE_CODE:
5237
- rootContext.onDateChange(props.date);
5238
- closeOverlay();
5239
- }
5240
- function shiftFocus(date2, add) {
5241
- let nextDate = date2[props.type] + add;
5242
- if (nextDate <= 0) {
5243
- nextDate = maxValue.value + nextDate;
5244
5277
  }
5245
- if (nextDate > years.value[maxValue.value - 1].year) {
5246
- nextDate = nextDate - maxValue.value;
5278
+ case kbd.END: {
5279
+ const lastValue = props.type === "month" ? 12 : years.value[years.value.length - 1].year;
5280
+ focusItem(lastValue);
5281
+ break;
5247
5282
  }
5248
- const candidateDay = parentElement.querySelector(
5249
- `[data-value='${props.type}-${nextDate}']`
5250
- );
5251
- candidateDay?.focus();
5283
+ case kbd.ENTER:
5284
+ case kbd.SPACE_CODE:
5285
+ if (isDateSelectable(props.date)) {
5286
+ rootContext.onDateChange(props.date);
5287
+ closeOverlay();
5288
+ }
5252
5289
  }
5253
5290
  }
5254
5291
  return (_ctx, _cache) => {
5255
5292
  return vue.openBlock(), vue.createBlock(vue.unref(Primitive), vue.mergeProps(props, {
5256
5293
  onClick: handleClick,
5257
- ref: "primitiveElement",
5294
+ ref_key: "primitiveElement",
5295
+ ref: primitiveElement,
5258
5296
  role: "button",
5297
+ "aria-label": ariaLabel.value,
5259
5298
  "aria-disabled": __props.disabled,
5260
5299
  "data-value": dataValue.value,
5261
5300
  onKeydown: [
5262
- vue.withKeys(handleArrowKey, ["up", "down", "left", "right", "space", "enter"]),
5301
+ vue.withKeys(handleKeydown, ["up", "down", "left", "right", "space", "enter", "home", "end"]),
5263
5302
  _cache[0] || (_cache[0] = vue.withKeys(vue.withModifiers(() => {
5264
5303
  }, ["prevent"]), ["enter"]))
5265
5304
  ],
@@ -5269,7 +5308,7 @@ const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
5269
5308
  vue.renderSlot(_ctx.$slots, "default")
5270
5309
  ]),
5271
5310
  _: 3
5272
- }, 16, ["aria-disabled", "data-value", "tabindex"]);
5311
+ }, 16, ["aria-label", "aria-disabled", "data-value", "tabindex"]);
5273
5312
  };
5274
5313
  }
5275
5314
  });