vueless 1.5.1-beta.2 → 1.5.1-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 +1 -1
- package/ui.form-calendar/UCalendar.vue +2 -0
- package/ui.form-calendar/UCalendarDayView.vue +18 -0
- package/ui.form-calendar/storybook/stories.ts +26 -0
- package/ui.form-calendar/tests/UCalendar.test.ts +13 -0
- package/ui.form-calendar/tests/UCalendarDayView.test.ts +33 -0
- package/ui.form-calendar/types.ts +6 -0
- package/ui.form-date-picker/UDatePicker.vue +2 -0
- package/ui.form-date-picker/storybook/stories.ts +29 -0
- package/ui.form-date-picker/tests/UDatePicker.test.ts +13 -0
- package/ui.form-date-picker/types.ts +5 -0
- package/ui.form-date-picker-range/UDatePickerRange.vue +63 -8
- package/ui.form-date-picker-range/UDatePickerRangePeriodMenu.vue +26 -29
- package/ui.form-date-picker-range/config.ts +17 -2
- package/ui.form-date-picker-range/storybook/stories.ts +77 -0
- package/ui.form-date-picker-range/tests/UDatePickerRange.test.ts +203 -1
- package/ui.form-date-picker-range/types.ts +14 -1
package/package.json
CHANGED
|
@@ -54,6 +54,7 @@ const props = withDefaults(defineProps<Props<TModelValue>>(), {
|
|
|
54
54
|
modelValue: undefined,
|
|
55
55
|
minDate: undefined,
|
|
56
56
|
maxDate: undefined,
|
|
57
|
+
dayClasses: undefined,
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
const emit = defineEmits([
|
|
@@ -918,6 +919,7 @@ const {
|
|
|
918
919
|
:date-format="actualDateFormat"
|
|
919
920
|
:locale="locale"
|
|
920
921
|
:config="config"
|
|
922
|
+
:day-classes="dayClasses"
|
|
921
923
|
:is-arrow-key-dirty="isArrowKeyDirty"
|
|
922
924
|
:data-test="getDataTest('day-view')"
|
|
923
925
|
@input="onInputDate"
|
|
@@ -232,6 +232,10 @@ function onMouseleaveDayView() {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
function getDayCustomClasses(day: Date) {
|
|
236
|
+
return props.dayClasses?.(day);
|
|
237
|
+
}
|
|
238
|
+
|
|
235
239
|
/**
|
|
236
240
|
* Get element / nested component attributes for each config token ✨
|
|
237
241
|
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
|
|
@@ -279,6 +283,7 @@ defineExpose({
|
|
|
279
283
|
size="md"
|
|
280
284
|
square
|
|
281
285
|
v-bind="selectedDayAttrs"
|
|
286
|
+
:class="getDayCustomClasses(day)"
|
|
282
287
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
283
288
|
:label="formatDate(day, 'j', locale)"
|
|
284
289
|
@mousedown.prevent.capture
|
|
@@ -294,6 +299,7 @@ defineExpose({
|
|
|
294
299
|
size="md"
|
|
295
300
|
square
|
|
296
301
|
v-bind="selectedDayAttrs"
|
|
302
|
+
:class="getDayCustomClasses(day)"
|
|
297
303
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
298
304
|
:label="formatDate(day, 'j', locale)"
|
|
299
305
|
@mousedown.prevent.capture
|
|
@@ -311,6 +317,7 @@ defineExpose({
|
|
|
311
317
|
size="md"
|
|
312
318
|
square
|
|
313
319
|
v-bind="firstDayInRangeAttrs"
|
|
320
|
+
:class="getDayCustomClasses(day)"
|
|
314
321
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
315
322
|
:label="formatDate(day, 'j', locale)"
|
|
316
323
|
@mousedown.prevent.capture
|
|
@@ -328,6 +335,7 @@ defineExpose({
|
|
|
328
335
|
size="md"
|
|
329
336
|
square
|
|
330
337
|
v-bind="lastDayInRangeAttrs"
|
|
338
|
+
:class="getDayCustomClasses(day)"
|
|
331
339
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
332
340
|
:label="formatDate(day, 'j', locale)"
|
|
333
341
|
@mousedown.prevent.capture
|
|
@@ -343,6 +351,7 @@ defineExpose({
|
|
|
343
351
|
size="md"
|
|
344
352
|
square
|
|
345
353
|
v-bind="anotherMonthFirstDayInRangeAttrs"
|
|
354
|
+
:class="getDayCustomClasses(day)"
|
|
346
355
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
347
356
|
:label="formatDate(day, 'j', locale)"
|
|
348
357
|
@mousedown.prevent.capture
|
|
@@ -358,6 +367,7 @@ defineExpose({
|
|
|
358
367
|
size="md"
|
|
359
368
|
square
|
|
360
369
|
v-bind="anotherMonthLastDayInRangeAttrs"
|
|
370
|
+
:class="getDayCustomClasses(day)"
|
|
361
371
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
362
372
|
:label="formatDate(day, 'j', locale)"
|
|
363
373
|
@mousedown.prevent.capture
|
|
@@ -373,6 +383,7 @@ defineExpose({
|
|
|
373
383
|
size="md"
|
|
374
384
|
square
|
|
375
385
|
v-bind="lastDayInRangeAttrs"
|
|
386
|
+
:class="getDayCustomClasses(day)"
|
|
376
387
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
377
388
|
:label="formatDate(day, 'j', locale)"
|
|
378
389
|
@mousedown.prevent.capture
|
|
@@ -391,6 +402,7 @@ defineExpose({
|
|
|
391
402
|
size="md"
|
|
392
403
|
square
|
|
393
404
|
v-bind="anotherMonthDayInRangeAttrs"
|
|
405
|
+
:class="getDayCustomClasses(day)"
|
|
394
406
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
395
407
|
:label="formatDate(day, 'j', locale)"
|
|
396
408
|
@mousedown.prevent.capture
|
|
@@ -409,6 +421,7 @@ defineExpose({
|
|
|
409
421
|
size="md"
|
|
410
422
|
square
|
|
411
423
|
v-bind="currentDayInRangeAttrs"
|
|
424
|
+
:class="getDayCustomClasses(day)"
|
|
412
425
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
413
426
|
:label="formatDate(day, 'j', locale)"
|
|
414
427
|
@mousedown.prevent.capture
|
|
@@ -424,6 +437,7 @@ defineExpose({
|
|
|
424
437
|
size="md"
|
|
425
438
|
square
|
|
426
439
|
v-bind="dayInRangeAttrs"
|
|
440
|
+
:class="getDayCustomClasses(day)"
|
|
427
441
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
428
442
|
:label="formatDate(day, 'j', locale)"
|
|
429
443
|
@mousedown.prevent.capture
|
|
@@ -439,6 +453,7 @@ defineExpose({
|
|
|
439
453
|
size="md"
|
|
440
454
|
square
|
|
441
455
|
v-bind="currentDayAttrs"
|
|
456
|
+
:class="getDayCustomClasses(day)"
|
|
442
457
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
443
458
|
:label="formatDate(day, 'j', locale)"
|
|
444
459
|
@mousedown.prevent.capture
|
|
@@ -454,6 +469,7 @@ defineExpose({
|
|
|
454
469
|
size="md"
|
|
455
470
|
square
|
|
456
471
|
v-bind="activeDayAttrs"
|
|
472
|
+
:class="getDayCustomClasses(day)"
|
|
457
473
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
458
474
|
:label="formatDate(day, 'j', locale)"
|
|
459
475
|
@mousedown.prevent.capture
|
|
@@ -469,6 +485,7 @@ defineExpose({
|
|
|
469
485
|
size="md"
|
|
470
486
|
square
|
|
471
487
|
v-bind="anotherMonthDayAttrs"
|
|
488
|
+
:class="getDayCustomClasses(day)"
|
|
472
489
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
473
490
|
:label="formatDate(day, 'j', locale)"
|
|
474
491
|
@mousedown.prevent.capture
|
|
@@ -484,6 +501,7 @@ defineExpose({
|
|
|
484
501
|
size="md"
|
|
485
502
|
square
|
|
486
503
|
v-bind="dayAttrs"
|
|
504
|
+
:class="getDayCustomClasses(day)"
|
|
487
505
|
:disabled="dateIsOutOfRange(day, minDate, maxDate, locale, dateFormat)"
|
|
488
506
|
:label="formatDate(day, 'j', locale)"
|
|
489
507
|
@mousedown.prevent.capture
|
|
@@ -147,3 +147,29 @@ MinMax.parameters = {
|
|
|
147
147
|
},
|
|
148
148
|
},
|
|
149
149
|
};
|
|
150
|
+
|
|
151
|
+
export const DayClasses = DefaultTemplate.bind({});
|
|
152
|
+
DayClasses.args = {
|
|
153
|
+
dayClasses: (day: Date) => {
|
|
154
|
+
return [5, 12, 19].includes(day.getDate()) ? "bg-success" : "";
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
DayClasses.parameters = {
|
|
158
|
+
docs: {
|
|
159
|
+
description: {
|
|
160
|
+
story:
|
|
161
|
+
// eslint-disable-next-line vue/max-len
|
|
162
|
+
"Use the `dayClasses` prop to apply extra CSS classes to individual day cells, e.g. to highlight dates by status.",
|
|
163
|
+
},
|
|
164
|
+
source: {
|
|
165
|
+
code: `
|
|
166
|
+
<UCalendar
|
|
167
|
+
v-model="args.modelValue"
|
|
168
|
+
:day-classes="(day) => ([5, 12, 19].includes(day.getDate()) ? 'bg-success' : '')"
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
<UText color="neutral" class="mt-4">{{ args.modelValue }}</UText>
|
|
172
|
+
`,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
@@ -295,6 +295,19 @@ describe("UCalendar.vue", () => {
|
|
|
295
295
|
expect(updatedValue.to).toMatch(dateFormatRegex);
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
+
it("Day Classes – passes dayClasses to UCalendarDayView", () => {
|
|
299
|
+
const dayClasses = (day: Date) => (day.getDate() === 15 ? "bg-mint-highlight" : "");
|
|
300
|
+
|
|
301
|
+
const component = mount(UCalendar, {
|
|
302
|
+
props: {
|
|
303
|
+
modelValue: null,
|
|
304
|
+
dayClasses,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(component.findComponent(DayView).props("dayClasses")).toBe(dayClasses);
|
|
309
|
+
});
|
|
310
|
+
|
|
298
311
|
it("Tabindex – sets tabindex attribute on wrapper", () => {
|
|
299
312
|
const tabindex = 5;
|
|
300
313
|
|
|
@@ -386,6 +386,39 @@ describe("UCalendarDayView.vue", () => {
|
|
|
386
386
|
});
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
+
describe("Day Classes Prop", () => {
|
|
390
|
+
it("applies function-based dayClasses only to the matching day", () => {
|
|
391
|
+
const component = mount(UCalendarDayView, {
|
|
392
|
+
props: {
|
|
393
|
+
...defaultProps,
|
|
394
|
+
selectedDate: null,
|
|
395
|
+
dayClasses: (day: Date) => (day.getDate() === 10 ? "bg-mint-highlight" : ""),
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const dayButtons = component.findAll('[vl-key="day"]');
|
|
400
|
+
const targetDay = dayButtons.find((button) => button.text() === "10");
|
|
401
|
+
const otherDay = dayButtons.find((button) => button.text() === "12");
|
|
402
|
+
|
|
403
|
+
expect(targetDay?.classes()).toContain("bg-mint-highlight");
|
|
404
|
+
expect(otherDay?.classes()).not.toContain("bg-mint-highlight");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("renders regular day cells without error when dayClasses prop is not provided", () => {
|
|
408
|
+
const component = mount(UCalendarDayView, {
|
|
409
|
+
props: {
|
|
410
|
+
...defaultProps,
|
|
411
|
+
selectedDate: null,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const regularDay = component.find('[vl-key="day"]');
|
|
416
|
+
|
|
417
|
+
expect(regularDay.exists()).toBe(true);
|
|
418
|
+
expect(regularDay.classes()).not.toContain("bg-mint-highlight");
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
389
422
|
describe("Exposed Properties", () => {
|
|
390
423
|
it("exposes days computed property", () => {
|
|
391
424
|
const component = mount(UCalendarDayView, {
|
|
@@ -72,6 +72,11 @@ export interface Props<TModelValue extends DateValue> {
|
|
|
72
72
|
*/
|
|
73
73
|
maxDate?: Date | string;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Extra CSS classes to apply to each day cell, on top of the built-in state classes.
|
|
77
|
+
*/
|
|
78
|
+
dayClasses?: (day: Date) => string;
|
|
79
|
+
|
|
75
80
|
/**
|
|
76
81
|
* Component config object.
|
|
77
82
|
*/
|
|
@@ -93,5 +98,6 @@ export interface UCalendarViewProps {
|
|
|
93
98
|
isArrowKeyDirty: boolean;
|
|
94
99
|
maxDate?: Date | string;
|
|
95
100
|
minDate?: Date | string;
|
|
101
|
+
dayClasses?: (day: Date) => string;
|
|
96
102
|
config: Config;
|
|
97
103
|
}
|
|
@@ -34,6 +34,7 @@ const props = withDefaults(defineProps<Props<TModelValue>>(), {
|
|
|
34
34
|
modelValue: undefined,
|
|
35
35
|
minDate: undefined,
|
|
36
36
|
maxDate: undefined,
|
|
37
|
+
dayClasses: undefined,
|
|
37
38
|
placeholder: "",
|
|
38
39
|
label: "",
|
|
39
40
|
});
|
|
@@ -364,6 +365,7 @@ watchEffect(() => {
|
|
|
364
365
|
:user-date-time-format="userDateTimeFormat"
|
|
365
366
|
:max-date="maxDate"
|
|
366
367
|
:min-date="minDate"
|
|
368
|
+
:day-classes="dayClasses"
|
|
367
369
|
v-bind="datepickerCalendarAttrs"
|
|
368
370
|
:data-test="getDataTest('calendar')"
|
|
369
371
|
@keydown.esc="deactivate"
|
|
@@ -454,3 +454,32 @@ Slots.parameters = {
|
|
|
454
454
|
},
|
|
455
455
|
},
|
|
456
456
|
};
|
|
457
|
+
|
|
458
|
+
export const DayClasses = DefaultTemplate.bind({});
|
|
459
|
+
DayClasses.args = {
|
|
460
|
+
modelValue: dateValue,
|
|
461
|
+
dayClasses: (day: Date) => {
|
|
462
|
+
return [5, 12, 19].includes(day.getDate()) ? "bg-success" : "";
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
DayClasses.parameters = {
|
|
466
|
+
docs: {
|
|
467
|
+
description: {
|
|
468
|
+
story:
|
|
469
|
+
// eslint-disable-next-line vue/max-len
|
|
470
|
+
"Use the `dayClasses` prop to apply extra CSS classes to individual day cells, e.g. to highlight dates by status.",
|
|
471
|
+
},
|
|
472
|
+
source: {
|
|
473
|
+
code: `
|
|
474
|
+
<UDatePicker
|
|
475
|
+
v-model="args.modelValue"
|
|
476
|
+
label="Select a date"
|
|
477
|
+
:day-classes="(day) => ([5, 12, 19].includes(day.getDate()) ? 'bg-success' : '')"
|
|
478
|
+
class="max-w-96"
|
|
479
|
+
/>
|
|
480
|
+
|
|
481
|
+
<UText color="neutral" class="mt-4">{{ args.modelValue }}</UText>
|
|
482
|
+
`,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
};
|
|
@@ -284,6 +284,19 @@ describe("UDatePicker.vue", () => {
|
|
|
284
284
|
expect(component.findComponent({ name: "UCalendar" }).props("maxDate")).toBe(maxDate);
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
+
it("Day Classes – passes dayClasses to UCalendar", () => {
|
|
288
|
+
const dayClasses = (day: Date) => (day.getDate() === 15 ? "bg-mint-highlight" : "");
|
|
289
|
+
|
|
290
|
+
const component = mount(UDatePicker, {
|
|
291
|
+
props: {
|
|
292
|
+
modelValue: new Date(),
|
|
293
|
+
dayClasses,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(component.findComponent({ name: "UCalendar" }).props("dayClasses")).toBe(dayClasses);
|
|
298
|
+
});
|
|
299
|
+
|
|
287
300
|
it("Id – sets id attribute on input", () => {
|
|
288
301
|
const idValue = "date-picker-id";
|
|
289
302
|
|
|
@@ -103,6 +103,11 @@ export interface Props<TModelValue> {
|
|
|
103
103
|
*/
|
|
104
104
|
rightIcon?: string;
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Extra CSS classes to apply to each day cell, on top of the built-in state classes.
|
|
108
|
+
*/
|
|
109
|
+
dayClasses?: (day: Date) => string;
|
|
110
|
+
|
|
106
111
|
/**
|
|
107
112
|
* Unique element id.
|
|
108
113
|
*/
|
|
@@ -73,9 +73,12 @@ defineOptions({ inheritAttrs: false });
|
|
|
73
73
|
const props = withDefaults(defineProps<Props<TModelValue>>(), {
|
|
74
74
|
...getDefaults<Props<TModelValue>, Config>(defaultConfig, COMPONENT_NAME),
|
|
75
75
|
customRangeButton: () => ({ range: { from: null, to: null } }),
|
|
76
|
+
periods: () =>
|
|
77
|
+
getDefaults<Props<TModelValue>, Config>(defaultConfig, COMPONENT_NAME).periods || [],
|
|
76
78
|
modelValue: undefined,
|
|
77
79
|
minDate: undefined,
|
|
78
80
|
maxDate: undefined,
|
|
81
|
+
dayClasses: undefined,
|
|
79
82
|
placeholder: "",
|
|
80
83
|
label: "",
|
|
81
84
|
});
|
|
@@ -129,7 +132,21 @@ const activeDate: Ref<string | Date | null> = ref(
|
|
|
129
132
|
? parseDate<SortedLocale>(props.modelValue.from, props.dateFormat, locale.value)
|
|
130
133
|
: new Date(),
|
|
131
134
|
);
|
|
132
|
-
|
|
135
|
+
let isInvalidPeriodsWarned = false;
|
|
136
|
+
const allowedPeriods = computed(() => getAllowedPeriods());
|
|
137
|
+
const isPeriodSwitchShown = computed(() => {
|
|
138
|
+
const hasCustomRange = Boolean(
|
|
139
|
+
props.customRangeButton.range.from && props.customRangeButton.range.to,
|
|
140
|
+
);
|
|
141
|
+
const periodButtonCount = allowedPeriods.value.filter((type) => type !== Period.Custom).length;
|
|
142
|
+
|
|
143
|
+
return periodButtonCount + Number(hasCustomRange) > 1;
|
|
144
|
+
});
|
|
145
|
+
const rawPeriod = ref(Period.OwnRange);
|
|
146
|
+
const period = computed({
|
|
147
|
+
get: () => getAllowedPeriod(rawPeriod.value),
|
|
148
|
+
set: (value: Period) => (rawPeriod.value = value),
|
|
149
|
+
});
|
|
133
150
|
const rangeStart = ref("");
|
|
134
151
|
const rangeEnd = ref("");
|
|
135
152
|
const inputRangeFromError = ref("");
|
|
@@ -319,6 +336,29 @@ function deactivate() {
|
|
|
319
336
|
isShownMenu.value = false;
|
|
320
337
|
}
|
|
321
338
|
|
|
339
|
+
function getAllowedPeriods(): readonly Period[] {
|
|
340
|
+
const allPeriods = Object.values(Period);
|
|
341
|
+
const validPeriods = allPeriods.filter((type) => props.periods.includes(type));
|
|
342
|
+
|
|
343
|
+
if (validPeriods.length) return validPeriods;
|
|
344
|
+
|
|
345
|
+
if (import.meta.env.DEV && !isInvalidPeriodsWarned) {
|
|
346
|
+
// eslint-disable-next-line no-console
|
|
347
|
+
console.warn(`[${COMPONENT_NAME}] No valid "periods" provided, falling back to all periods.`);
|
|
348
|
+
isInvalidPeriodsWarned = true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return allPeriods;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getAllowedPeriod(detectedPeriod: Period): Period {
|
|
355
|
+
if (detectedPeriod === Period.Custom || allowedPeriods.value.includes(detectedPeriod)) {
|
|
356
|
+
return detectedPeriod;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return allowedPeriods.value.includes(Period.OwnRange) ? Period.OwnRange : allowedPeriods.value[0];
|
|
360
|
+
}
|
|
361
|
+
|
|
322
362
|
setDefaultPeriodForButton();
|
|
323
363
|
|
|
324
364
|
function setDefaultPeriodForButton() {
|
|
@@ -404,16 +444,12 @@ function shiftRangeNext(to: Date, from: Date, daysDifference: number) {
|
|
|
404
444
|
return;
|
|
405
445
|
}
|
|
406
446
|
|
|
407
|
-
let nextDate = periodDateList.value.find(
|
|
408
|
-
(item) => localValue.value.to && item.endRange > localValue.value.to,
|
|
409
|
-
);
|
|
447
|
+
let nextDate = periodDateList.value.find((item) => item.startRange > from);
|
|
410
448
|
|
|
411
449
|
if (!nextDate) {
|
|
412
450
|
onClickShiftDatesList(ShiftAction.Next);
|
|
413
451
|
|
|
414
|
-
nextDate = periodDateList.value.find(
|
|
415
|
-
(item) => localValue.value.to && item.endRange > localValue.value.to,
|
|
416
|
-
);
|
|
452
|
+
nextDate = periodDateList.value.find((item) => item.startRange > from);
|
|
417
453
|
}
|
|
418
454
|
|
|
419
455
|
if (nextDate && isDatePeriodOutOfRange(nextDate)) return;
|
|
@@ -468,8 +504,19 @@ function shiftRangePrev(to: Date, from: Date, daysDifference: number) {
|
|
|
468
504
|
}
|
|
469
505
|
}
|
|
470
506
|
|
|
507
|
+
function getPeriodDateList(date: Date): DatePeriodRange[] {
|
|
508
|
+
if (isPeriod.value.week) return getWeekDateList(date, locale.value.months.shorthand);
|
|
509
|
+
if (isPeriod.value.month) return getMonthsDateList(date, locale.value.months.longhand);
|
|
510
|
+
if (isPeriod.value.quarter) return getQuartersDateList(date, locale.value.quarter);
|
|
511
|
+
if (isPeriod.value.year) return getYearDateList(date);
|
|
512
|
+
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
|
|
471
516
|
function onClickShiftRange(action: ShiftActions) {
|
|
472
|
-
|
|
517
|
+
const isLeavingCustom = isPeriod.value.custom;
|
|
518
|
+
|
|
519
|
+
if (isLeavingCustom) {
|
|
473
520
|
period.value = Period.OwnRange;
|
|
474
521
|
}
|
|
475
522
|
|
|
@@ -487,6 +534,10 @@ function onClickShiftRange(action: ShiftActions) {
|
|
|
487
534
|
if (!from) return;
|
|
488
535
|
to = to || addDays(from, 1);
|
|
489
536
|
|
|
537
|
+
if (isLeavingCustom && !isPeriod.value.ownRange) {
|
|
538
|
+
periodDateList.value = getPeriodDateList(from);
|
|
539
|
+
}
|
|
540
|
+
|
|
490
541
|
const daysDifference = Math.ceil(Math.abs(getDatesDifference(from, to)) / millisecondsPerDay);
|
|
491
542
|
|
|
492
543
|
action === ShiftAction.Next
|
|
@@ -568,6 +619,7 @@ const mutatedProps = computed(() => ({
|
|
|
568
619
|
quarter: isPeriod.value.quarter,
|
|
569
620
|
year: isPeriod.value.year,
|
|
570
621
|
customRangeButtonDescription: Boolean(props.customRangeButton.description),
|
|
622
|
+
periodSwitch: isPeriodSwitchShown.value,
|
|
571
623
|
}));
|
|
572
624
|
|
|
573
625
|
const {
|
|
@@ -730,6 +782,8 @@ watchEffect(() => {
|
|
|
730
782
|
v-model:period="period"
|
|
731
783
|
:config="config"
|
|
732
784
|
:is-period="isPeriod"
|
|
785
|
+
:allowed-periods="allowedPeriods"
|
|
786
|
+
:is-period-switch-shown="isPeriodSwitchShown"
|
|
733
787
|
:custom-range-button="customRangeButton"
|
|
734
788
|
:locale="locale"
|
|
735
789
|
:date-format="dateFormat"
|
|
@@ -781,6 +835,7 @@ watchEffect(() => {
|
|
|
781
835
|
:min-date="minDate"
|
|
782
836
|
:max-date="maxDate"
|
|
783
837
|
:date-format="dateFormat"
|
|
838
|
+
:day-classes="dayClasses"
|
|
784
839
|
v-bind="datepickerCalendarAttrs as KeyAttrsWithConfig<UCalendarConfig>"
|
|
785
840
|
range
|
|
786
841
|
:data-test="getDataTest('calendar')"
|
|
@@ -36,12 +36,20 @@ const isDatePeriodOutOfRange = inject<IsDatePeriodOutOfRange | null>(
|
|
|
36
36
|
null,
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
const periods = computed(() =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const periods = computed(() =>
|
|
40
|
+
[
|
|
41
|
+
{ name: Period.Week, title: props.locale.week },
|
|
42
|
+
{ name: Period.Month, title: props.locale.month },
|
|
43
|
+
{ name: Period.Quarter, title: props.locale.quarter },
|
|
44
|
+
{ name: Period.Year, title: props.locale.year },
|
|
45
|
+
].filter(({ name }) => props.allowedPeriods.includes(name)),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const isOwnRangeAllowed = computed(() => props.allowedPeriods.includes(Period.OwnRange));
|
|
49
|
+
|
|
50
|
+
const hasCustomRange = computed(() =>
|
|
51
|
+
Boolean(props.customRangeButton.range.from && props.customRangeButton.range.to),
|
|
52
|
+
);
|
|
45
53
|
|
|
46
54
|
const rangeSwitchTitle = computed(() => {
|
|
47
55
|
if (props.isPeriod.month || props.isPeriod.quarter) {
|
|
@@ -148,7 +156,11 @@ function getDatePeriodState(date: DatePeriodRange) {
|
|
|
148
156
|
</script>
|
|
149
157
|
|
|
150
158
|
<template>
|
|
151
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
v-if="isPeriodSwitchShown && periods.length"
|
|
161
|
+
v-bind="attrs.periodRowAttrs.value"
|
|
162
|
+
:data-test="getDataTest()"
|
|
163
|
+
>
|
|
152
164
|
<template v-for="periodButton in periods" :key="periodButton.name">
|
|
153
165
|
<UButton
|
|
154
166
|
v-if="periodButton.name !== period"
|
|
@@ -176,27 +188,12 @@ function getDatePeriodState(date: DatePeriodRange) {
|
|
|
176
188
|
</template>
|
|
177
189
|
</div>
|
|
178
190
|
|
|
179
|
-
<div
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
size="xs"
|
|
184
|
-
color="grayscale"
|
|
185
|
-
variant="soft"
|
|
186
|
-
:data-test="getDataTest('custom-range')"
|
|
187
|
-
v-bind="attrs.customRangeButtonAttrs.value"
|
|
188
|
-
@click="onClickCustomRangeButton"
|
|
189
|
-
>
|
|
190
|
-
{{ customRangeButton.label }}
|
|
191
|
-
<span
|
|
192
|
-
v-if="customRangeButton.description"
|
|
193
|
-
v-bind="attrs.customRangeDescriptionAttrs.value"
|
|
194
|
-
v-text="customRangeButton.description"
|
|
195
|
-
/>
|
|
196
|
-
</UButton>
|
|
197
|
-
|
|
191
|
+
<div
|
|
192
|
+
v-if="isPeriodSwitchShown && (hasCustomRange || isOwnRangeAllowed)"
|
|
193
|
+
v-bind="attrs.periodRowAttrs.value"
|
|
194
|
+
>
|
|
198
195
|
<UButton
|
|
199
|
-
v-if="
|
|
196
|
+
v-if="hasCustomRange"
|
|
200
197
|
square
|
|
201
198
|
size="xs"
|
|
202
199
|
color="grayscale"
|
|
@@ -214,7 +211,7 @@ function getDatePeriodState(date: DatePeriodRange) {
|
|
|
214
211
|
</UButton>
|
|
215
212
|
|
|
216
213
|
<UButton
|
|
217
|
-
v-if="Period.OwnRange !== period"
|
|
214
|
+
v-if="isOwnRangeAllowed && Period.OwnRange !== period"
|
|
218
215
|
square
|
|
219
216
|
size="xs"
|
|
220
217
|
color="grayscale"
|
|
@@ -227,7 +224,7 @@ function getDatePeriodState(date: DatePeriodRange) {
|
|
|
227
224
|
/>
|
|
228
225
|
|
|
229
226
|
<UButton
|
|
230
|
-
v-else
|
|
227
|
+
v-else-if="isOwnRangeAllowed"
|
|
231
228
|
square
|
|
232
229
|
size="xs"
|
|
233
230
|
color="grayscale"
|
|
@@ -60,7 +60,14 @@ export default /*tw*/ {
|
|
|
60
60
|
periodRow: "mb-1 flex min-w-64 gap-1",
|
|
61
61
|
periodButton: "{UButton} h-[3.125rem] w-full",
|
|
62
62
|
periodButtonActive: "{>periodButton} bg-grayscale-accented/15!",
|
|
63
|
-
rangeSwitchWrapper:
|
|
63
|
+
rangeSwitchWrapper: {
|
|
64
|
+
base: "flex items-center justify-between py-2",
|
|
65
|
+
variants: {
|
|
66
|
+
periodSwitch: {
|
|
67
|
+
false: "pt-0",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
64
71
|
rangeSwitchButton: "{UButton}",
|
|
65
72
|
rangeSwitchTitle: "font-medium text-medium",
|
|
66
73
|
periodDateList: {
|
|
@@ -87,7 +94,14 @@ export default /*tw*/ {
|
|
|
87
94
|
},
|
|
88
95
|
},
|
|
89
96
|
customRangeDescription: "",
|
|
90
|
-
rangeInputWrapper:
|
|
97
|
+
rangeInputWrapper: {
|
|
98
|
+
base: "flex mt-4 group/range-input-wrapper",
|
|
99
|
+
variants: {
|
|
100
|
+
periodSwitch: {
|
|
101
|
+
false: "mt-2",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
91
105
|
rangeInput: {
|
|
92
106
|
base: "{UInput}",
|
|
93
107
|
inputLabel: {
|
|
@@ -211,6 +225,7 @@ export default /*tw*/ {
|
|
|
211
225
|
dateFormat: undefined,
|
|
212
226
|
maxDate: undefined,
|
|
213
227
|
minDate: undefined,
|
|
228
|
+
periods: ["week", "month", "quarter", "year", "ownRange"],
|
|
214
229
|
/* icons */
|
|
215
230
|
calendarIcon: "calendar_month-fill",
|
|
216
231
|
nextIcon: "keyboard_arrow_right",
|
|
@@ -225,6 +225,54 @@ OpenDirection.parameters = {
|
|
|
225
225
|
},
|
|
226
226
|
};
|
|
227
227
|
|
|
228
|
+
export const Periods: StoryFn<DefaultUDatePickerRangeArgs> = (args) => ({
|
|
229
|
+
components: { UDatePickerRange, UCol, UText },
|
|
230
|
+
setup: () => ({ args }),
|
|
231
|
+
template: `
|
|
232
|
+
<UCol>
|
|
233
|
+
<UDatePickerRange
|
|
234
|
+
v-bind="args"
|
|
235
|
+
v-model="args.modelValue"
|
|
236
|
+
label="All periods (default)"
|
|
237
|
+
class="w-full max-w-96"
|
|
238
|
+
/>
|
|
239
|
+
<UDatePickerRange
|
|
240
|
+
v-bind="args"
|
|
241
|
+
v-model="args.modelValue"
|
|
242
|
+
:periods="['month', 'quarter', 'year']"
|
|
243
|
+
label="Month, quarter, year"
|
|
244
|
+
class="w-full max-w-96"
|
|
245
|
+
/>
|
|
246
|
+
<UDatePickerRange
|
|
247
|
+
v-bind="args"
|
|
248
|
+
v-model="args.modelValue"
|
|
249
|
+
:periods="['week', 'month']"
|
|
250
|
+
label="Week, month"
|
|
251
|
+
class="w-full max-w-96"
|
|
252
|
+
/>
|
|
253
|
+
<UDatePickerRange
|
|
254
|
+
v-bind="args"
|
|
255
|
+
v-model="args.modelValue"
|
|
256
|
+
:periods="['ownRange']"
|
|
257
|
+
label="Own range only"
|
|
258
|
+
class="w-full max-w-96"
|
|
259
|
+
/>
|
|
260
|
+
</UCol>
|
|
261
|
+
|
|
262
|
+
<UText color="neutral" class="mt-4">{{ args.modelValue }}</UText>
|
|
263
|
+
`,
|
|
264
|
+
});
|
|
265
|
+
Periods.parameters = {
|
|
266
|
+
docs: {
|
|
267
|
+
description: {
|
|
268
|
+
story: "Use the `periods` prop to limit which period types are available for selection.",
|
|
269
|
+
},
|
|
270
|
+
story: {
|
|
271
|
+
height: "800px",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
228
276
|
export const DateFormat = DefaultTemplate.bind({});
|
|
229
277
|
DateFormat.args = { variant: "input", dateFormat: "Y-m-d" };
|
|
230
278
|
DateFormat.parameters = {
|
|
@@ -259,6 +307,35 @@ MinMax.parameters = {
|
|
|
259
307
|
},
|
|
260
308
|
};
|
|
261
309
|
|
|
310
|
+
export const DayClasses = DefaultTemplate.bind({});
|
|
311
|
+
DayClasses.args = {
|
|
312
|
+
modelValue: { from: fromDate, to: toDate },
|
|
313
|
+
dayClasses: (day: Date) => {
|
|
314
|
+
return [5, 12, 19].includes(day.getDate()) ? "bg-success" : "";
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
DayClasses.parameters = {
|
|
318
|
+
docs: {
|
|
319
|
+
description: {
|
|
320
|
+
story:
|
|
321
|
+
// eslint-disable-next-line vue/max-len
|
|
322
|
+
"Use the `dayClasses` prop to apply extra CSS classes to individual day cells, e.g. to highlight dates by status.",
|
|
323
|
+
},
|
|
324
|
+
source: {
|
|
325
|
+
code: `
|
|
326
|
+
<UDatePickerRange
|
|
327
|
+
v-model="args.modelValue"
|
|
328
|
+
label="Select date range"
|
|
329
|
+
:day-classes="(day) => ([5, 12, 19].includes(day.getDate()) ? 'bg-success' : '')"
|
|
330
|
+
class="w-full max-w-96"
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
<UText color="neutral" class="mt-4">{{ args.modelValue }}</UText>
|
|
334
|
+
`,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
262
339
|
export const CustomRangeButton = DefaultTemplate.bind({});
|
|
263
340
|
CustomRangeButton.args = {
|
|
264
341
|
customRangeButton: {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { mount } from "@vue/test-utils";
|
|
2
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import UDatePickerRange from "../UDatePickerRange.vue";
|
|
5
5
|
import UInput from "../../ui.form-input/UInput.vue";
|
|
6
6
|
import ULabel from "../../ui.form-label/ULabel.vue";
|
|
7
7
|
import UButton from "../../ui.button/UButton.vue";
|
|
8
8
|
import UDatePickerRangePeriodMenu from "../UDatePickerRangePeriodMenu.vue";
|
|
9
|
+
import { Period } from "../constants";
|
|
9
10
|
|
|
10
11
|
import type { RangeDate } from "../../ui.form-calendar/types";
|
|
11
12
|
|
|
@@ -298,6 +299,24 @@ describe("UDatePickerRange.vue", () => {
|
|
|
298
299
|
expect(component.props("maxDate")).toBe(maxDate);
|
|
299
300
|
});
|
|
300
301
|
|
|
302
|
+
it("Day Classes – passes dayClasses to UCalendar", async () => {
|
|
303
|
+
const dayClasses = (day: Date) => (day.getDate() === 15 ? "bg-mint-highlight" : "");
|
|
304
|
+
|
|
305
|
+
const component = mount(UDatePickerRange, {
|
|
306
|
+
props: {
|
|
307
|
+
variant: "input",
|
|
308
|
+
dayClasses,
|
|
309
|
+
modelValue: { from: null, to: null },
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const input = component.findComponent(UInput).get("input");
|
|
314
|
+
|
|
315
|
+
await input.trigger("focus");
|
|
316
|
+
|
|
317
|
+
expect(component.findComponent({ name: "UCalendar" }).props("dayClasses")).toBe(dayClasses);
|
|
318
|
+
});
|
|
319
|
+
|
|
301
320
|
it("Custom Range Button – applies custom range button configuration", async () => {
|
|
302
321
|
const customRangeButton = {
|
|
303
322
|
range: { from: new Date("2023-01-01"), to: new Date("2023-01-31") },
|
|
@@ -440,6 +459,189 @@ describe("UDatePickerRange.vue", () => {
|
|
|
440
459
|
});
|
|
441
460
|
});
|
|
442
461
|
|
|
462
|
+
describe("Periods", () => {
|
|
463
|
+
const dataTest = "date-picker-range";
|
|
464
|
+
const menuPrefix = `${dataTest}-period-menu`;
|
|
465
|
+
const periodButtonNames: string[] = Object.values(Period).filter(
|
|
466
|
+
(type) => type !== Period.OwnRange && type !== Period.Custom,
|
|
467
|
+
);
|
|
468
|
+
const fullWeek: RangeDate = {
|
|
469
|
+
from: new Date(2023, 11, 4),
|
|
470
|
+
to: new Date(2023, 11, 10, 23, 59, 59),
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
async function mountOpened(props: Record<string, unknown>) {
|
|
474
|
+
const component = mount(UDatePickerRange, {
|
|
475
|
+
props: {
|
|
476
|
+
variant: "input",
|
|
477
|
+
dataTest,
|
|
478
|
+
modelValue: { from: null, to: null },
|
|
479
|
+
...props,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await component.findComponent(UInput).get("input").trigger("focus");
|
|
484
|
+
|
|
485
|
+
return component;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function findPeriodButton(component: ReturnType<typeof mount>, name: string) {
|
|
489
|
+
return component.find(`[data-test="${menuPrefix}-period-${name}"]`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function findOwnRangeButton(component: ReturnType<typeof mount>) {
|
|
493
|
+
return component.find(`[data-test="${menuPrefix}-own-range"]`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getRenderedPeriodNames(component: ReturnType<typeof mount>) {
|
|
497
|
+
return component
|
|
498
|
+
.findAll(`[data-test^="${menuPrefix}-period-"]`)
|
|
499
|
+
.map((button) => button.attributes("data-test")!.replace(`${menuPrefix}-period-`, ""))
|
|
500
|
+
.filter((name) => periodButtonNames.includes(name));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function isActive(button: ReturnType<ReturnType<typeof mount>["find"]>) {
|
|
504
|
+
return button.attributes("vl-key") === "periodButtonActive";
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
it("Periods – renders all period buttons and own range button by default", async () => {
|
|
508
|
+
const component = await mountOpened({});
|
|
509
|
+
|
|
510
|
+
expect(getRenderedPeriodNames(component)).toEqual(periodButtonNames);
|
|
511
|
+
expect(findOwnRangeButton(component).exists()).toBe(true);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("Periods – renders only allowed period buttons without own range button", async () => {
|
|
515
|
+
const component = await mountOpened({ periods: ["month", "year"] });
|
|
516
|
+
|
|
517
|
+
expect(getRenderedPeriodNames(component)).toEqual(["month", "year"]);
|
|
518
|
+
expect(findOwnRangeButton(component).exists()).toBe(false);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it.each(["ownRange", "month"])(
|
|
522
|
+
"Periods – hides period switch when %s is the only period",
|
|
523
|
+
async (period) => {
|
|
524
|
+
const component = await mountOpened({ periods: [period] });
|
|
525
|
+
|
|
526
|
+
expect(getRenderedPeriodNames(component)).toEqual([]);
|
|
527
|
+
expect(findOwnRangeButton(component).exists()).toBe(false);
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
it("Periods – reduces range inputs top margin when period switch is hidden", async () => {
|
|
532
|
+
const component = await mountOpened({ periods: ["ownRange"] });
|
|
533
|
+
const rangeInputWrapper = component.get("[vl-key='rangeInputWrapper']");
|
|
534
|
+
|
|
535
|
+
expect(rangeInputWrapper.classes()).toContain("mt-2");
|
|
536
|
+
expect(rangeInputWrapper.classes()).not.toContain("mt-4");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("Periods – removes range switch top padding when period switch is hidden", async () => {
|
|
540
|
+
const component = await mountOpened({ periods: ["month"] });
|
|
541
|
+
|
|
542
|
+
expect(component.get("[vl-key='rangeSwitchWrapper']").classes()).toContain("pt-0");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("Periods – keeps range inputs top margin when period switch is shown", async () => {
|
|
546
|
+
const component = await mountOpened({});
|
|
547
|
+
|
|
548
|
+
expect(component.get("[vl-key='rangeInputWrapper']").classes()).toContain("mt-4");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("Periods – shows own range button when custom range is also available", async () => {
|
|
552
|
+
const component = await mountOpened({
|
|
553
|
+
periods: ["ownRange"],
|
|
554
|
+
customRangeButton: {
|
|
555
|
+
range: { from: new Date(2023, 5, 1), to: new Date(2023, 5, 15) },
|
|
556
|
+
label: "First half of June",
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(findOwnRangeButton(component).exists()).toBe(true);
|
|
561
|
+
expect(component.find(`[data-test="${menuPrefix}-custom-range"]`).exists()).toBe(true);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("Periods – renders period buttons in canonical order", async () => {
|
|
565
|
+
const component = await mountOpened({
|
|
566
|
+
periods: ["year", "ownRange", "week", "quarter"],
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
expect(getRenderedPeriodNames(component)).toEqual(["week", "quarter", "year"]);
|
|
570
|
+
expect(findOwnRangeButton(component).exists()).toBe(true);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("Periods – falls back to own range when model matches a disallowed period", async () => {
|
|
574
|
+
const component = await mountOpened({
|
|
575
|
+
periods: ["month", "ownRange"],
|
|
576
|
+
modelValue: fullWeek,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
expect(findPeriodButton(component, "week").exists()).toBe(false);
|
|
580
|
+
expect(isActive(findOwnRangeButton(component))).toBe(true);
|
|
581
|
+
expect(isActive(findPeriodButton(component, "month"))).toBe(false);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("Periods – falls back to the first allowed period when own range is not allowed", async () => {
|
|
585
|
+
const component = await mountOpened({
|
|
586
|
+
periods: ["year", "month"],
|
|
587
|
+
modelValue: fullWeek,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
expect(isActive(findPeriodButton(component, "month"))).toBe(true);
|
|
591
|
+
expect(isActive(findPeriodButton(component, "year"))).toBe(false);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("Periods – treats an empty array as all periods and warns", async () => {
|
|
595
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
596
|
+
|
|
597
|
+
const component = await mountOpened({ periods: [] });
|
|
598
|
+
|
|
599
|
+
expect(getRenderedPeriodNames(component)).toEqual(periodButtonNames);
|
|
600
|
+
expect(findOwnRangeButton(component).exists()).toBe(true);
|
|
601
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No valid "periods"'));
|
|
602
|
+
|
|
603
|
+
warnSpy.mockRestore();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it.each([
|
|
607
|
+
["next", new Date(2023, 6, 1), new Date(2023, 6, 31)],
|
|
608
|
+
["prev", new Date(2023, 4, 1), new Date(2023, 4, 31)],
|
|
609
|
+
])(
|
|
610
|
+
"Periods – shifts custom range by the resolved period (%s)",
|
|
611
|
+
async (direction, expectedFrom, expectedTo) => {
|
|
612
|
+
const component = mount(UDatePickerRange, {
|
|
613
|
+
props: {
|
|
614
|
+
variant: "button",
|
|
615
|
+
dataTest,
|
|
616
|
+
periods: ["month", "year"],
|
|
617
|
+
modelValue: { from: null, to: null },
|
|
618
|
+
customRangeButton: {
|
|
619
|
+
range: { from: new Date(2023, 5, 1), to: new Date(2023, 5, 15) },
|
|
620
|
+
label: "First half of June",
|
|
621
|
+
},
|
|
622
|
+
"onUpdate:modelValue": (value: RangeDate) => {
|
|
623
|
+
component.setProps({ modelValue: value });
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
await component.get("[vl-key='rangeButtonSelect']").trigger("click");
|
|
629
|
+
await findPeriodButton(component, "year").trigger("click");
|
|
630
|
+
await component.get(`[data-test="${menuPrefix}-custom-range"]`).trigger("click");
|
|
631
|
+
|
|
632
|
+
await component.get(`[data-test="${dataTest}-button-${direction}"]`).trigger("click");
|
|
633
|
+
|
|
634
|
+
const { from, to } = component.props("modelValue") as {
|
|
635
|
+
from: Date;
|
|
636
|
+
to: Date;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
expect(from.toDateString()).toBe(expectedFrom.toDateString());
|
|
640
|
+
expect(to.toDateString()).toBe(expectedTo.toDateString());
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
});
|
|
644
|
+
|
|
443
645
|
describe("Menu", () => {
|
|
444
646
|
it("Menu – opens when input is focused", async () => {
|
|
445
647
|
const component = mount(UDatePickerRange, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import defaultConfig from "./config";
|
|
2
|
-
import { ShiftAction } from "./constants";
|
|
2
|
+
import { ShiftAction, Period } from "./constants";
|
|
3
3
|
|
|
4
4
|
import type { Ref } from "vue";
|
|
5
5
|
import type { UnknownObject, ComponentConfig, GetDataTest } from "../types";
|
|
@@ -9,6 +9,7 @@ export type Locale = typeof defaultConfig.i18n;
|
|
|
9
9
|
export type Config = typeof defaultConfig;
|
|
10
10
|
export type IsDatePeriodOutOfRange = (datePeriod: DatePeriodRange) => boolean;
|
|
11
11
|
export type ShiftActions = `${ShiftAction}`;
|
|
12
|
+
export type PeriodType = `${Period}`;
|
|
12
13
|
|
|
13
14
|
export interface SortedLocale extends Omit<Locale, "weekdays" | "months"> {
|
|
14
15
|
months: {
|
|
@@ -53,6 +54,8 @@ export interface UDatePickerRangePeriodMenuProps {
|
|
|
53
54
|
locale: SortedLocale;
|
|
54
55
|
dateFormat: string | undefined;
|
|
55
56
|
isPeriod: IsPeriod;
|
|
57
|
+
allowedPeriods: readonly Period[];
|
|
58
|
+
isPeriodSwitchShown: boolean;
|
|
56
59
|
maxDate: string | Date | undefined;
|
|
57
60
|
minDate: string | Date | undefined;
|
|
58
61
|
customRangeButton: CustomRangeButton;
|
|
@@ -122,6 +125,11 @@ export interface Props<TModelValue> {
|
|
|
122
125
|
*/
|
|
123
126
|
customRangeButton?: CustomRangeButton;
|
|
124
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Period types available for selection.
|
|
130
|
+
*/
|
|
131
|
+
periods?: PeriodType[];
|
|
132
|
+
|
|
125
133
|
/**
|
|
126
134
|
* Datepicker open direction on x-axis.
|
|
127
135
|
* @extendOnly
|
|
@@ -179,6 +187,11 @@ export interface Props<TModelValue> {
|
|
|
179
187
|
*/
|
|
180
188
|
rightIcon?: string;
|
|
181
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Extra CSS classes to apply to each day cell, on top of the built-in state classes.
|
|
192
|
+
*/
|
|
193
|
+
dayClasses?: (day: Date) => string;
|
|
194
|
+
|
|
182
195
|
/**
|
|
183
196
|
* Unique element id.
|
|
184
197
|
*/
|