@uxf/datepicker 11.111.2 → 11.113.0
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/hooks/use-date-picker-navigation.d.ts +4 -3
- package/hooks/use-date-picker-navigation.js +30 -34
- package/hooks/use-date-picker-navigation.test.js +40 -4
- package/hooks/use-date-picker.js +40 -26
- package/hooks/use-date-picker.test.js +2 -2
- package/hooks/use-date-range-picker.js +103 -76
- package/hooks/use-day.js +12 -11
- package/hooks/use-decade.js +9 -8
- package/jest.dst.config.js +22 -0
- package/jest.dst.global-setup.js +14 -0
- package/package.json +7 -2
- package/utils/can-select-range.d.ts +6 -5
- package/utils/can-select-range.js +7 -19
- package/utils/can-select-range.test.js +30 -27
- package/utils/date-string.d.ts +34 -0
- package/utils/date-string.js +64 -0
- package/utils/date-string.test.d.ts +1 -0
- package/utils/date-string.test.js +63 -0
- package/utils/dayjs-utils.d.ts +0 -1
- package/utils/dayjs-utils.js +1 -5
- package/utils/dst-robustness.test.d.ts +11 -0
- package/utils/dst-robustness.test.js +328 -0
- package/utils/get-current-year-month-and-date.js +1 -2
- package/utils/get-date-invervals.js +2 -5
- package/utils/get-date-month-and-year.js +6 -5
- package/utils/get-days.js +12 -9
- package/utils/get-each.d.ts +2 -1
- package/utils/get-each.js +37 -9
- package/utils/get-each.test.js +34 -19
- package/utils/get-initial-months.js +3 -6
- package/utils/get-months.js +7 -7
- package/utils/get-next-active-month.js +6 -10
- package/utils/get-weekday-labels.js +4 -2
- package/utils/get-years.js +5 -4
- package/utils/is-date-blocked.d.ts +7 -6
- package/utils/is-date-blocked.js +6 -7
- package/utils/is-date-blocked.test.js +108 -11
- package/utils/is-date-hovered.d.ts +6 -5
- package/utils/is-date-hovered.js +17 -24
- package/utils/is-date-hovered.test.js +42 -25
- package/utils/is-date-inside-range.d.ts +2 -1
- package/utils/is-date-inside-range.js +1 -2
- package/utils/is-date-selected.test.js +16 -8
- package/utils/is-end-date.d.ts +2 -1
- package/utils/is-end-date.js +1 -2
- package/utils/is-end-date.test.js +11 -6
- package/utils/is-first-or-last-selected-date.d.ts +2 -1
- package/utils/is-first-or-last-selected-date.js +1 -3
- package/utils/is-first-or-last-selected-date.test.js +9 -6
- package/utils/is-in-unavailable-dates.d.ts +2 -1
- package/utils/is-in-unavailable-dates.js +1 -2
- package/utils/is-in-unavailable-dates.test.js +10 -6
- package/utils/is-start-date.d.ts +2 -1
- package/utils/is-start-date.js +1 -2
- package/utils/is-start-date.test.js +12 -6
package/hooks/use-day.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useDay = useDay;
|
|
4
4
|
const empty_array_1 = require("@uxf/core/constants/empty-array");
|
|
5
5
|
const react_1 = require("react");
|
|
6
|
-
const
|
|
6
|
+
const date_string_1 = require("../utils/date-string");
|
|
7
7
|
const get_date_invervals_1 = require("../utils/get-date-invervals");
|
|
8
8
|
function useDay({ date, datesConfig, dayRef, focusedDate, isDateBlocked, isDateFocused, isDateHovered, isDateInsideRange, isDateSelected, onDateFocus, onDateHover, onDateSelect, preventScroll, unavailableDates, }) {
|
|
9
9
|
var _a;
|
|
@@ -12,7 +12,7 @@ function useDay({ date, datesConfig, dayRef, focusedDate, isDateBlocked, isDateF
|
|
|
12
12
|
const disabledDates = [...unavailableDates, ...disabledDatesFromConfig].filter((day, index, self) => index === self.findIndex((d) => d.getTime() === day.getTime()));
|
|
13
13
|
const onClick = (0, react_1.useCallback)(() => onDateSelect(date), [date, onDateSelect]);
|
|
14
14
|
const onMouseEnter = (0, react_1.useCallback)(() => onDateHover(date), [date, onDateHover]);
|
|
15
|
-
const isToday = (0,
|
|
15
|
+
const isToday = (0, date_string_1.todayDateString)() === (0, date_string_1.toDateString)(date);
|
|
16
16
|
(0, react_1.useEffect)(() => {
|
|
17
17
|
if (dayRef && dayRef.current && isDateFocused(date)) {
|
|
18
18
|
dayRef.current.focus({
|
|
@@ -25,28 +25,29 @@ function useDay({ date, datesConfig, dayRef, focusedDate, isDateBlocked, isDateF
|
|
|
25
25
|
e.preventDefault();
|
|
26
26
|
switch (e.key) {
|
|
27
27
|
case "ArrowRight":
|
|
28
|
-
onDateFocus((0,
|
|
28
|
+
onDateFocus((0, date_string_1.fromDateString)((0, date_string_1.addDays)((0, date_string_1.toDateString)(date), 1)));
|
|
29
29
|
break;
|
|
30
30
|
case "ArrowLeft":
|
|
31
|
-
onDateFocus((0,
|
|
31
|
+
onDateFocus((0, date_string_1.fromDateString)((0, date_string_1.addDays)((0, date_string_1.toDateString)(date), -1)));
|
|
32
32
|
break;
|
|
33
33
|
case "ArrowUp":
|
|
34
|
-
onDateFocus((0,
|
|
34
|
+
onDateFocus((0, date_string_1.fromDateString)((0, date_string_1.addDays)((0, date_string_1.toDateString)(date), -7)));
|
|
35
35
|
break;
|
|
36
36
|
case "ArrowDown":
|
|
37
|
-
onDateFocus((0,
|
|
37
|
+
onDateFocus((0, date_string_1.fromDateString)((0, date_string_1.addDays)((0, date_string_1.toDateString)(date), 7)));
|
|
38
38
|
break;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}, [date, onDateFocus]);
|
|
42
42
|
const dateIntervals = (0, get_date_invervals_1.getDateIntervals)(disabledDates);
|
|
43
|
+
const dateStr = (0, date_string_1.toDateString)(date);
|
|
43
44
|
const _isFirstUnavailableDate = () => {
|
|
44
|
-
return dateIntervals.some((interval) => (0,
|
|
45
|
+
return dateIntervals.some((interval) => (0, date_string_1.toDateString)(interval.start) === dateStr);
|
|
45
46
|
};
|
|
46
47
|
const _isLastUnavailableDate = () => {
|
|
47
|
-
return dateIntervals.some((interval) => (0,
|
|
48
|
+
return dateIntervals.some((interval) => (0, date_string_1.toDateString)(interval.end) === dateStr);
|
|
48
49
|
};
|
|
49
|
-
const applicableDatesConfigs = datesConfig === null || datesConfig === void 0 ? void 0 : datesConfig.filter((config) => config.dates.some((configDay) => (0,
|
|
50
|
+
const applicableDatesConfigs = datesConfig === null || datesConfig === void 0 ? void 0 : datesConfig.filter((config) => config.dates.some((configDay) => (0, date_string_1.toDateString)(configDay) === dateStr));
|
|
50
51
|
const flags = applicableDatesConfigs === null || applicableDatesConfigs === void 0 ? void 0 : applicableDatesConfigs.reduce((acc, curr) => {
|
|
51
52
|
if (!curr.flag) {
|
|
52
53
|
return acc;
|
|
@@ -54,8 +55,8 @@ function useDay({ date, datesConfig, dayRef, focusedDate, isDateBlocked, isDateF
|
|
|
54
55
|
const intervals = (0, get_date_invervals_1.getDateIntervals)(curr.dates);
|
|
55
56
|
acc.push({
|
|
56
57
|
flag: curr.flag,
|
|
57
|
-
isFirst: intervals.some((interval) => (0,
|
|
58
|
-
isLast: intervals.some((interval) => (0,
|
|
58
|
+
isFirst: intervals.some((interval) => (0, date_string_1.toDateString)(interval.start) === dateStr),
|
|
59
|
+
isLast: intervals.some((interval) => (0, date_string_1.toDateString)(interval.end) === dateStr),
|
|
59
60
|
});
|
|
60
61
|
return acc;
|
|
61
62
|
}, []);
|
package/hooks/use-decade.js
CHANGED
|
@@ -11,12 +11,13 @@ exports.yearLabelFormatFn = yearLabelFormatFn;
|
|
|
11
11
|
const decadeLabelFormatFn = (start, end) => `${(0, dayjs_utils_1.dayjsEnWithTz)(start).format("YYYY")} \u2013 ${(0, dayjs_utils_1.dayjsEnWithTz)(end).format("YYYY")}`;
|
|
12
12
|
exports.decadeLabelFormatFn = decadeLabelFormatFn;
|
|
13
13
|
function useDecade({ yearLabelFormat = exports.yearLabelFormatFn, decadeLabelFormat = exports.decadeLabelFormatFn, year, }) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
14
|
+
const { years, decadeLabel } = (0, react_1.useMemo)(() => {
|
|
15
|
+
const startYear = new Date((0, create_date_string_1.createDateString)(year - 4, 0));
|
|
16
|
+
const endYear = new Date((0, create_date_string_1.createDateString)(year + 4, 0));
|
|
17
|
+
return {
|
|
18
|
+
years: (0, get_years_1.getYears)({ startYear, endYear, yearLabelFormat }),
|
|
19
|
+
decadeLabel: decadeLabelFormat(startYear, endYear),
|
|
20
|
+
};
|
|
21
|
+
}, [year, yearLabelFormat, decadeLabelFormat]);
|
|
22
|
+
return { years, decadeLabel };
|
|
22
23
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest config for DST robustness tests.
|
|
3
|
+
* Uses America/New_York timezone so that new Date("2026-10-25") (UTC midnight)
|
|
4
|
+
* resolves to Oct 24 20:00 local time — a different calendar day than new Date(2026, 9, 25).
|
|
5
|
+
* This exposes timezone bugs where local and UTC calendar days diverge.
|
|
6
|
+
*
|
|
7
|
+
* Tests are expected to FAIL until the DateString refactor (Tasks 2–10) is complete.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
11
|
+
module.exports = {
|
|
12
|
+
preset: "ts-jest",
|
|
13
|
+
// "node" overrides the project-wide jsdom default (set in root jest.config.js via ts-jest preset).
|
|
14
|
+
// DST tests must run in node environment — jsdom swallows timezone behaviour.
|
|
15
|
+
testEnvironment: "node",
|
|
16
|
+
globalSetup: "<rootDir>/jest.dst.global-setup.js",
|
|
17
|
+
rootDir: ".",
|
|
18
|
+
testMatch: ["<rootDir>/utils/dst-robustness.test.ts"],
|
|
19
|
+
transform: {
|
|
20
|
+
"\\.[t]sx?$": ["ts-jest", { diagnostics: false }],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom globalSetup for DST robustness tests.
|
|
3
|
+
*
|
|
4
|
+
* Belt-and-suspenders reinforcement: the npm scripts (test:dst, test:tz:ny, test:tz:prague)
|
|
5
|
+
* already set TZ=... as an env prefix before Node starts, which is the reliable way to
|
|
6
|
+
* set the timezone. This file re-asserts the TZ for America/New_York as a fallback in case
|
|
7
|
+
* the config is ever invoked without the env prefix (e.g. direct `jest --config=...` call).
|
|
8
|
+
*
|
|
9
|
+
* Note: the monorepo root global-setup.js sets TZ=UTC, but it only runs when Jest is
|
|
10
|
+
* invoked via the root jest.config.js. This custom globalSetup overrides that for DST tests.
|
|
11
|
+
*/
|
|
12
|
+
module.exports = async () => {
|
|
13
|
+
process.env.TZ = process.env.TZ || "America/New_York";
|
|
14
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uxf/datepicker",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.113.0",
|
|
4
4
|
"description": "A set of React hooks to create an awesome datepicker.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc -P tsconfig.json",
|
|
8
|
-
"typecheck": "tsc --noEmit --skipLibCheck"
|
|
8
|
+
"typecheck": "tsc --noEmit --skipLibCheck",
|
|
9
|
+
"test:tz:prague": "TZ=Europe/Prague jest --config=jest.dst.config.js --no-coverage",
|
|
10
|
+
"test:tz:ny": "TZ=America/New_York jest --config=jest.dst.config.js --no-coverage",
|
|
11
|
+
"test:tz:tokyo": "TZ=Asia/Tokyo jest --config=jest.dst.config.js --no-coverage",
|
|
12
|
+
"test:dst": "TZ=America/New_York jest --config=jest.dst.config.js --no-coverage",
|
|
13
|
+
"test:tz": "npm run test:tz:prague && npm run test:tz:ny && npm run test:tz:tokyo"
|
|
9
14
|
},
|
|
10
15
|
"publishConfig": {
|
|
11
16
|
"access": "public"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { DateString } from "@uxf/core/date";
|
|
1
2
|
export interface CanSelectRangeProps {
|
|
2
|
-
startDate:
|
|
3
|
-
endDate:
|
|
4
|
-
isDateBlocked: ((date:
|
|
3
|
+
startDate: DateString;
|
|
4
|
+
endDate: DateString | null;
|
|
5
|
+
isDateBlocked: ((date: DateString) => boolean) | null;
|
|
5
6
|
minBookingDays: number;
|
|
6
7
|
exactMinBookingDays?: boolean;
|
|
7
|
-
minBookingDate?:
|
|
8
|
-
maxBookingDate?:
|
|
8
|
+
minBookingDate?: DateString;
|
|
9
|
+
maxBookingDate?: DateString;
|
|
9
10
|
}
|
|
10
11
|
export declare function canSelectRange({ startDate, endDate, isDateBlocked, minBookingDays, exactMinBookingDays, minBookingDate, maxBookingDate, }: CanSelectRangeProps): boolean;
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.canSelectRange = canSelectRange;
|
|
4
|
-
const
|
|
4
|
+
const date_string_1 = require("./date-string");
|
|
5
5
|
const get_each_1 = require("./get-each");
|
|
6
6
|
// eslint-disable-next-line complexity
|
|
7
7
|
function canSelectRange({ startDate, endDate, isDateBlocked, minBookingDays, exactMinBookingDays, minBookingDate, maxBookingDate, }) {
|
|
8
|
-
const isStartDateAfterOrEqualMinDate = minBookingDate
|
|
9
|
-
? !(0, dayjs_utils_1.dayjsEnRaw)(startDate).isBefore((0, dayjs_utils_1.dayjsEnRaw)(minBookingDate).subtract(1, "day"))
|
|
10
|
-
: true;
|
|
8
|
+
const isStartDateAfterOrEqualMinDate = minBookingDate ? startDate >= (0, date_string_1.addDays)(minBookingDate, -1) : true;
|
|
11
9
|
const isStartDateBeforeOrEqualMaxDate = maxBookingDate
|
|
12
|
-
?
|
|
13
|
-
.add(minBookingDays - 1, "day")
|
|
14
|
-
.isAfter(maxBookingDate)
|
|
15
|
-
: true;
|
|
16
|
-
const isEndDatBeforeOrEqualMaxDate = maxBookingDate
|
|
17
|
-
? !(0, dayjs_utils_1.dayjsEnRaw)(endDate)
|
|
18
|
-
.add(minBookingDays - 1, "day")
|
|
19
|
-
.isAfter(maxBookingDate)
|
|
10
|
+
? (0, date_string_1.addDays)(startDate, minBookingDays - 1) <= maxBookingDate
|
|
20
11
|
: true;
|
|
12
|
+
const isEndDatBeforeOrEqualMaxDate = maxBookingDate && endDate ? (0, date_string_1.addDays)(endDate, minBookingDays - 1) <= maxBookingDate : true;
|
|
21
13
|
if (!isStartDateAfterOrEqualMinDate || !isStartDateBeforeOrEqualMaxDate || !isEndDatBeforeOrEqualMaxDate) {
|
|
22
14
|
return false;
|
|
23
15
|
}
|
|
@@ -30,15 +22,11 @@ function canSelectRange({ startDate, endDate, isDateBlocked, minBookingDays, exa
|
|
|
30
22
|
if (!isDateBlocked) {
|
|
31
23
|
return true;
|
|
32
24
|
}
|
|
33
|
-
return !(0, get_each_1.getEach)(startDate, (0,
|
|
34
|
-
.add(minBookingDays - 1, "day")
|
|
35
|
-
.toDate(), "day").some(isDateBlocked);
|
|
25
|
+
return !(0, get_each_1.getEach)(startDate, (0, date_string_1.addDays)(startDate, minBookingDays - 1), "day").some(isDateBlocked);
|
|
36
26
|
}
|
|
37
27
|
else if (endDate && !exactMinBookingDays) {
|
|
38
|
-
const minBookingDaysDate = (0,
|
|
39
|
-
|
|
40
|
-
.toDate();
|
|
41
|
-
if ((0, dayjs_utils_1.dayjsEnWithTz)(endDate).isBefore(minBookingDaysDate)) {
|
|
28
|
+
const minBookingDaysDate = (0, date_string_1.addDays)(startDate, minBookingDays - 1);
|
|
29
|
+
if (endDate < minBookingDaysDate) {
|
|
42
30
|
return false;
|
|
43
31
|
}
|
|
44
32
|
if (!isDateBlocked) {
|
|
@@ -4,7 +4,7 @@ const can_select_range_1 = require("./can-select-range");
|
|
|
4
4
|
test("canSelectRange", () => {
|
|
5
5
|
// start day only
|
|
6
6
|
expect((0, can_select_range_1.canSelectRange)({
|
|
7
|
-
startDate:
|
|
7
|
+
startDate: "2021-08-08",
|
|
8
8
|
endDate: null,
|
|
9
9
|
minBookingDays: 1,
|
|
10
10
|
minBookingDate: undefined,
|
|
@@ -14,8 +14,8 @@ test("canSelectRange", () => {
|
|
|
14
14
|
})).toBe(true);
|
|
15
15
|
// start and end date
|
|
16
16
|
expect((0, can_select_range_1.canSelectRange)({
|
|
17
|
-
startDate:
|
|
18
|
-
endDate:
|
|
17
|
+
startDate: "2021-08-08",
|
|
18
|
+
endDate: "2021-08-08",
|
|
19
19
|
minBookingDays: 1,
|
|
20
20
|
minBookingDate: undefined,
|
|
21
21
|
maxBookingDate: undefined,
|
|
@@ -24,18 +24,8 @@ test("canSelectRange", () => {
|
|
|
24
24
|
})).toBe(true);
|
|
25
25
|
// minBookingDays longer than range
|
|
26
26
|
expect((0, can_select_range_1.canSelectRange)({
|
|
27
|
-
startDate:
|
|
28
|
-
endDate:
|
|
29
|
-
minBookingDays: 2,
|
|
30
|
-
minBookingDate: undefined,
|
|
31
|
-
maxBookingDate: undefined,
|
|
32
|
-
exactMinBookingDays: false,
|
|
33
|
-
isDateBlocked: () => false,
|
|
34
|
-
})).toBe(false);
|
|
35
|
-
// minBookingDays longer than range
|
|
36
|
-
expect((0, can_select_range_1.canSelectRange)({
|
|
37
|
-
startDate: new Date("2021-08-08"),
|
|
38
|
-
endDate: new Date("2021-08-08"),
|
|
27
|
+
startDate: "2021-08-08",
|
|
28
|
+
endDate: "2021-08-08",
|
|
39
29
|
minBookingDays: 2,
|
|
40
30
|
minBookingDate: undefined,
|
|
41
31
|
maxBookingDate: undefined,
|
|
@@ -44,8 +34,8 @@ test("canSelectRange", () => {
|
|
|
44
34
|
})).toBe(false);
|
|
45
35
|
// blocked date
|
|
46
36
|
expect((0, can_select_range_1.canSelectRange)({
|
|
47
|
-
startDate:
|
|
48
|
-
endDate:
|
|
37
|
+
startDate: "2021-08-08",
|
|
38
|
+
endDate: "2021-08-08",
|
|
49
39
|
minBookingDays: 1,
|
|
50
40
|
minBookingDate: undefined,
|
|
51
41
|
maxBookingDate: undefined,
|
|
@@ -54,37 +44,50 @@ test("canSelectRange", () => {
|
|
|
54
44
|
})).toBe(false);
|
|
55
45
|
// before minBookingDate
|
|
56
46
|
expect((0, can_select_range_1.canSelectRange)({
|
|
57
|
-
startDate:
|
|
58
|
-
endDate:
|
|
47
|
+
startDate: "2021-08-05",
|
|
48
|
+
endDate: "2021-08-12",
|
|
59
49
|
minBookingDays: 1,
|
|
60
|
-
minBookingDate:
|
|
50
|
+
minBookingDate: "2021-08-10",
|
|
61
51
|
maxBookingDate: undefined,
|
|
62
52
|
exactMinBookingDays: false,
|
|
63
53
|
isDateBlocked: () => false,
|
|
64
54
|
})).toBe(false);
|
|
65
55
|
// after maxBookingDate
|
|
66
56
|
expect((0, can_select_range_1.canSelectRange)({
|
|
67
|
-
startDate:
|
|
68
|
-
endDate:
|
|
57
|
+
startDate: "2021-08-01",
|
|
58
|
+
endDate: "2021-08-08",
|
|
69
59
|
minBookingDays: 1,
|
|
70
60
|
minBookingDate: undefined,
|
|
71
|
-
maxBookingDate:
|
|
61
|
+
maxBookingDate: "2021-08-06",
|
|
72
62
|
exactMinBookingDays: false,
|
|
73
63
|
isDateBlocked: () => false,
|
|
74
64
|
})).toBe(false);
|
|
75
65
|
// exactMinBookingDays
|
|
76
66
|
expect((0, can_select_range_1.canSelectRange)({
|
|
77
|
-
startDate:
|
|
78
|
-
endDate:
|
|
67
|
+
startDate: "2021-08-01",
|
|
68
|
+
endDate: "2021-08-08",
|
|
79
69
|
minBookingDays: 8,
|
|
80
70
|
minBookingDate: undefined,
|
|
81
71
|
maxBookingDate: undefined,
|
|
82
72
|
exactMinBookingDays: true,
|
|
83
73
|
isDateBlocked: () => false,
|
|
84
74
|
})).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
test("canSelectRange DST fall-back (Oct 25 2026)", () => {
|
|
77
|
+
expect((0, can_select_range_1.canSelectRange)({
|
|
78
|
+
startDate: "2026-10-25",
|
|
79
|
+
endDate: "2026-10-26",
|
|
80
|
+
minBookingDays: 2,
|
|
81
|
+
minBookingDate: undefined,
|
|
82
|
+
maxBookingDate: undefined,
|
|
83
|
+
exactMinBookingDays: false,
|
|
84
|
+
isDateBlocked: () => false,
|
|
85
|
+
})).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
test("canSelectRange DST spring-forward (Mar 29 2026)", () => {
|
|
85
88
|
expect((0, can_select_range_1.canSelectRange)({
|
|
86
|
-
startDate:
|
|
87
|
-
endDate:
|
|
89
|
+
startDate: "2026-03-29",
|
|
90
|
+
endDate: "2026-03-30",
|
|
88
91
|
minBookingDays: 2,
|
|
89
92
|
minBookingDate: undefined,
|
|
90
93
|
maxBookingDate: undefined,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DateString } from "@uxf/core/date";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Date to a zero-padded DateString.
|
|
4
|
+
*
|
|
5
|
+
* Handles both creation styles correctly:
|
|
6
|
+
* - new Date("2026-10-25") → ISO string → UTC midnight → use UTC methods
|
|
7
|
+
* - new Date(2026, 9, 25) → local constructor → use local methods
|
|
8
|
+
*
|
|
9
|
+
* The heuristic: if all UTC time components are zero the date was parsed from
|
|
10
|
+
* an ISO date string (UTC midnight convention); otherwise it came from a local
|
|
11
|
+
* constructor and local methods give the correct calendar day.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toDateString(date: Date): DateString;
|
|
14
|
+
/**
|
|
15
|
+
* Convert a DateString back to a Date at UTC midnight.
|
|
16
|
+
*/
|
|
17
|
+
export declare function fromDateString(s: DateString): Date;
|
|
18
|
+
/**
|
|
19
|
+
* Today as a DateString in UTC — matches the `new Date("Y-M-D")` convention
|
|
20
|
+
* used throughout the package. Use this instead of `toDateString(new Date())`
|
|
21
|
+
* whenever you need a stable "today" string.
|
|
22
|
+
*/
|
|
23
|
+
export declare function todayDateString(): DateString;
|
|
24
|
+
/**
|
|
25
|
+
* Add n calendar months to a DateString, snapping to the 1st of the resulting month.
|
|
26
|
+
* Month arithmetic is purely numeric — no DST exposure.
|
|
27
|
+
*/
|
|
28
|
+
export declare function addMonths(date: DateString, n: number): DateString;
|
|
29
|
+
/**
|
|
30
|
+
* Add n calendar days to a DateString.
|
|
31
|
+
* Uses UTC midnight arithmetic — UTC has no DST, so adding DAY_MS is always correct.
|
|
32
|
+
* Pass negative n to subtract days.
|
|
33
|
+
*/
|
|
34
|
+
export declare function addDays(date: DateString, n: number): DateString;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toDateString = toDateString;
|
|
4
|
+
exports.fromDateString = fromDateString;
|
|
5
|
+
exports.todayDateString = todayDateString;
|
|
6
|
+
exports.addMonths = addMonths;
|
|
7
|
+
exports.addDays = addDays;
|
|
8
|
+
const create_date_string_1 = require("./create-date-string");
|
|
9
|
+
const DAY_MS = 86400000;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a Date to a zero-padded DateString.
|
|
12
|
+
*
|
|
13
|
+
* Handles both creation styles correctly:
|
|
14
|
+
* - new Date("2026-10-25") → ISO string → UTC midnight → use UTC methods
|
|
15
|
+
* - new Date(2026, 9, 25) → local constructor → use local methods
|
|
16
|
+
*
|
|
17
|
+
* The heuristic: if all UTC time components are zero the date was parsed from
|
|
18
|
+
* an ISO date string (UTC midnight convention); otherwise it came from a local
|
|
19
|
+
* constructor and local methods give the correct calendar day.
|
|
20
|
+
*/
|
|
21
|
+
function toDateString(date) {
|
|
22
|
+
if (date.getUTCHours() === 0 &&
|
|
23
|
+
date.getUTCMinutes() === 0 &&
|
|
24
|
+
date.getUTCSeconds() === 0 &&
|
|
25
|
+
date.getUTCMilliseconds() === 0) {
|
|
26
|
+
return (0, create_date_string_1.createDateString)(date.getUTCFullYear(), date.getUTCMonth(), // 0-indexed; createDateString adds 1
|
|
27
|
+
date.getUTCDate());
|
|
28
|
+
}
|
|
29
|
+
return (0, create_date_string_1.createDateString)(date.getFullYear(), date.getMonth(), // 0-indexed; createDateString adds 1
|
|
30
|
+
date.getDate());
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert a DateString back to a Date at UTC midnight.
|
|
34
|
+
*/
|
|
35
|
+
function fromDateString(s) {
|
|
36
|
+
return new Date(s);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Today as a DateString in UTC — matches the `new Date("Y-M-D")` convention
|
|
40
|
+
* used throughout the package. Use this instead of `toDateString(new Date())`
|
|
41
|
+
* whenever you need a stable "today" string.
|
|
42
|
+
*/
|
|
43
|
+
function todayDateString() {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
return (0, create_date_string_1.createDateString)(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Add n calendar months to a DateString, snapping to the 1st of the resulting month.
|
|
49
|
+
* Month arithmetic is purely numeric — no DST exposure.
|
|
50
|
+
*/
|
|
51
|
+
function addMonths(date, n) {
|
|
52
|
+
const y = parseInt(date.slice(0, 4), 10);
|
|
53
|
+
const m = parseInt(date.slice(5, 7), 10) - 1; // 0-indexed
|
|
54
|
+
const total = y * 12 + m + n;
|
|
55
|
+
return (0, create_date_string_1.createDateString)(Math.floor(total / 12), total % 12);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add n calendar days to a DateString.
|
|
59
|
+
* Uses UTC midnight arithmetic — UTC has no DST, so adding DAY_MS is always correct.
|
|
60
|
+
* Pass negative n to subtract days.
|
|
61
|
+
*/
|
|
62
|
+
function addDays(date, n) {
|
|
63
|
+
return toDateString(new Date(new Date(date).getTime() + n * DAY_MS));
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const date_string_1 = require("./date-string");
|
|
4
|
+
test("toDateString reads UTC date components from ISO-string Date", () => {
|
|
5
|
+
expect((0, date_string_1.toDateString)(new Date("2026-10-25T00:00:00Z"))).toBe("2026-10-25");
|
|
6
|
+
expect((0, date_string_1.toDateString)(new Date("2026-03-29T00:00:00Z"))).toBe("2026-03-29");
|
|
7
|
+
expect((0, date_string_1.toDateString)(new Date("2021-01-01T00:00:00Z"))).toBe("2021-01-01");
|
|
8
|
+
});
|
|
9
|
+
test("toDateString reads local date components from local-constructor Date", () => {
|
|
10
|
+
// new Date(y, m, d) creates local midnight — must give same DateString as ISO string
|
|
11
|
+
expect((0, date_string_1.toDateString)(new Date(2026, 9, 25))).toBe("2026-10-25");
|
|
12
|
+
expect((0, date_string_1.toDateString)(new Date(2026, 2, 29))).toBe("2026-03-29");
|
|
13
|
+
expect((0, date_string_1.toDateString)(new Date(2025, 11, 31))).toBe("2025-12-31");
|
|
14
|
+
expect((0, date_string_1.toDateString)(new Date(2026, 0, 1))).toBe("2026-01-01");
|
|
15
|
+
});
|
|
16
|
+
test("toDateString: ISO string and local constructor give the same DateString for the same calendar day", () => {
|
|
17
|
+
expect((0, date_string_1.toDateString)(new Date(2026, 9, 25))).toBe((0, date_string_1.toDateString)(new Date("2026-10-25")));
|
|
18
|
+
expect((0, date_string_1.toDateString)(new Date(2025, 11, 31))).toBe((0, date_string_1.toDateString)(new Date("2025-12-31")));
|
|
19
|
+
expect((0, date_string_1.toDateString)(new Date(2026, 0, 1))).toBe((0, date_string_1.toDateString)(new Date("2026-01-01")));
|
|
20
|
+
});
|
|
21
|
+
test("toDateString zero-pads month and day", () => {
|
|
22
|
+
expect((0, date_string_1.toDateString)(new Date(Date.UTC(2021, 0, 5)))).toBe("2021-01-05");
|
|
23
|
+
expect((0, date_string_1.toDateString)(new Date(Date.UTC(2021, 8, 3)))).toBe("2021-09-03");
|
|
24
|
+
});
|
|
25
|
+
test("fromDateString returns UTC midnight Date", () => {
|
|
26
|
+
const d = (0, date_string_1.fromDateString)("2026-10-25");
|
|
27
|
+
expect(d.getUTCFullYear()).toBe(2026);
|
|
28
|
+
expect(d.getUTCMonth()).toBe(9);
|
|
29
|
+
expect(d.getUTCDate()).toBe(25);
|
|
30
|
+
expect(d.getUTCHours()).toBe(0);
|
|
31
|
+
expect(d.getUTCMinutes()).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
test("toDateString and fromDateString are inverses", () => {
|
|
34
|
+
const s = "2026-10-25";
|
|
35
|
+
expect((0, date_string_1.toDateString)((0, date_string_1.fromDateString)(s))).toBe(s);
|
|
36
|
+
});
|
|
37
|
+
test("addDays adds calendar days across DST fall-back (Oct 25 2026)", () => {
|
|
38
|
+
expect((0, date_string_1.addDays)("2026-10-24", 1)).toBe("2026-10-25");
|
|
39
|
+
expect((0, date_string_1.addDays)("2026-10-25", 1)).toBe("2026-10-26");
|
|
40
|
+
});
|
|
41
|
+
test("addDays adds calendar days across DST spring-forward (Mar 29 2026)", () => {
|
|
42
|
+
expect((0, date_string_1.addDays)("2026-03-28", 1)).toBe("2026-03-29");
|
|
43
|
+
expect((0, date_string_1.addDays)("2026-03-29", 1)).toBe("2026-03-30");
|
|
44
|
+
});
|
|
45
|
+
test("addDays with negative n subtracts days", () => {
|
|
46
|
+
expect((0, date_string_1.addDays)("2026-10-25", -1)).toBe("2026-10-24");
|
|
47
|
+
expect((0, date_string_1.addDays)("2026-10-01", -1)).toBe("2026-09-30");
|
|
48
|
+
});
|
|
49
|
+
test("addDays with n=0 returns same date", () => {
|
|
50
|
+
expect((0, date_string_1.addDays)("2026-10-25", 0)).toBe("2026-10-25");
|
|
51
|
+
});
|
|
52
|
+
test("addMonths advances by positive months", () => {
|
|
53
|
+
expect((0, date_string_1.addMonths)("2026-04-01", 1)).toBe("2026-05-01");
|
|
54
|
+
expect((0, date_string_1.addMonths)("2026-11-01", 2)).toBe("2027-01-01");
|
|
55
|
+
expect((0, date_string_1.addMonths)("2026-04-15", 1)).toBe("2026-05-01");
|
|
56
|
+
});
|
|
57
|
+
test("addMonths goes back with negative months", () => {
|
|
58
|
+
expect((0, date_string_1.addMonths)("2026-03-01", -1)).toBe("2026-02-01");
|
|
59
|
+
expect((0, date_string_1.addMonths)("2026-01-01", -1)).toBe("2025-12-01");
|
|
60
|
+
});
|
|
61
|
+
test("addMonths with n=0 returns first of same month", () => {
|
|
62
|
+
expect((0, date_string_1.addMonths)("2026-04-15", 0)).toBe("2026-04-01");
|
|
63
|
+
});
|
package/utils/dayjs-utils.d.ts
CHANGED
package/utils/dayjs-utils.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.dayjsEnWithTz =
|
|
39
|
+
exports.dayjsEnWithTz = void 0;
|
|
40
40
|
const dayjs_1 = __importStar(require("dayjs"));
|
|
41
41
|
require("dayjs/locale/en");
|
|
42
42
|
const isBetween_1 = __importDefault(require("dayjs/plugin/isBetween"));
|
|
@@ -45,10 +45,6 @@ const utc_1 = __importDefault(require("dayjs/plugin/utc"));
|
|
|
45
45
|
(0, dayjs_1.extend)(isBetween_1.default);
|
|
46
46
|
(0, dayjs_1.extend)(timezone_1.default);
|
|
47
47
|
(0, dayjs_1.extend)(utc_1.default);
|
|
48
|
-
const dayjsEnRaw = (value) => {
|
|
49
|
-
return (0, dayjs_1.default)(value).locale("en");
|
|
50
|
-
};
|
|
51
|
-
exports.dayjsEnRaw = dayjsEnRaw;
|
|
52
48
|
const dayjsEnWithTz = (value, tz = "UTC") => {
|
|
53
49
|
return (0, dayjs_1.default)(value).tz(tz).locale("en");
|
|
54
50
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DST robustness tests. Utility functions now operate on DateString, so they are
|
|
3
|
+
* inherently timezone-proof. These tests verify string-based comparison correctness
|
|
4
|
+
* across DST boundaries and large UTC offsets.
|
|
5
|
+
*
|
|
6
|
+
* Run with:
|
|
7
|
+
* npm run test:tz:prague -w @uxf/datepicker
|
|
8
|
+
* npm run test:tz:ny -w @uxf/datepicker
|
|
9
|
+
* npm run test:tz:tokyo -w @uxf/datepicker
|
|
10
|
+
*/
|
|
11
|
+
export {};
|