Skip to content

Commit dc699f6

Browse files
committed
fix(date-picker): add null safety check for createCalendar, guard RangePlugin use
- add null safety for DOM nodes in createCalendar - guard RangePlugin before use in createCalendar
1 parent a5b4daf commit dc699f6

File tree

2 files changed

+103
-18
lines changed

2 files changed

+103
-18
lines changed

src/DatePicker/createCalendar.js

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
11
import flatpickr from "flatpickr";
22

3+
/**
4+
* Minimal flatpickr instance shape used by updateClasses and updateMonthNode.
5+
* Matches flatpickr's Instance where some elements may be optional.
6+
* @typedef {{
7+
* calendarContainer: HTMLElement;
8+
* days: HTMLElement;
9+
* daysContainer?: HTMLElement;
10+
* weekdayContainer: HTMLElement;
11+
* selectedDates: unknown[];
12+
* l10n: { months: { longhand: string[] }; weekdays?: { shorthand?: string[] } };
13+
* currentMonth: number;
14+
* monthNav: HTMLElement;
15+
* monthsDropdownContainer: HTMLElement;
16+
* }} FlatpickrInstance
17+
*/
18+
19+
/**
20+
* Locale override with optional en.weekdays.shorthand for custom formatting.
21+
* @typedef {{ en?: { weekdays: { shorthand: string[] } } }} L10nOverrides
22+
*/
23+
24+
/** @type {L10nOverrides | undefined} */
325
let l10n;
426

27+
/**
28+
* @param {FlatpickrInstance} instance
29+
*/
530
function updateClasses(instance) {
631
const {
732
calendarContainer,
@@ -14,14 +39,16 @@ function updateClasses(instance) {
1439
calendarContainer.classList.add("bx--date-picker__calendar");
1540
calendarContainer
1641
.querySelector(".flatpickr-month")
17-
.classList.add("bx--date-picker__month");
42+
?.classList.add("bx--date-picker__month");
1843

1944
weekdayContainer.classList.add("bx--date-picker__weekdays");
2045
for (const node of weekdayContainer.querySelectorAll(".flatpickr-weekday")) {
2146
node.classList.add("bx--date-picker__weekday");
2247
}
2348

24-
daysContainer.classList.add("bx--date-picker__days");
49+
if (daysContainer) {
50+
daysContainer.classList.add("bx--date-picker__days");
51+
}
2552
for (const node of days.querySelectorAll(".flatpickr-day")) {
2653
node.classList.add("bx--date-picker__day");
2754
if (node.classList.contains("today") && selectedDates.length > 0) {
@@ -32,6 +59,9 @@ function updateClasses(instance) {
3259
}
3360
}
3461

62+
/**
63+
* @param {FlatpickrInstance} instance
64+
*/
3565
function updateMonthNode(instance) {
3666
const monthText = instance.l10n.months.longhand[instance.currentMonth];
3767
const staticMonthNode = instance.monthNav.querySelector(".cur-month");
@@ -43,39 +73,59 @@ function updateMonthNode(instance) {
4373
const span = document.createElement("span");
4474
span.setAttribute("class", "cur-month");
4575
span.textContent = monthText;
46-
monthSelectNode.parentNode.replaceChild(span, monthSelectNode);
76+
monthSelectNode.parentNode?.replaceChild(span, monthSelectNode);
4777
}
4878
}
4979

80+
/**
81+
* @typedef {{
82+
* options: { locale?: string; mode?: string };
83+
* base: HTMLElement;
84+
* input: HTMLInputElement;
85+
* dispatch: (event: string) => void;
86+
* }} CreateCalendarArgs
87+
*/
88+
89+
/**
90+
* @param {CreateCalendarArgs} args
91+
* @returns {Promise<FlatpickrInstance>}
92+
*/
5093
async function createCalendar({ options, base, input, dispatch }) {
94+
/** @type {string | L10nOverrides["en"]} */
5195
let locale = options.locale;
5296

5397
if (options.locale === "en" && l10n && l10n.en) {
54-
for (let index = 0; index < l10n.en.weekdays.shorthand.length; index++) {
55-
const _ = l10n.en.weekdays.shorthand[index];
56-
const shorthand = _.slice(0, 2);
57-
l10n.en.weekdays.shorthand[index] =
58-
shorthand === "Th" ? "Th" : shorthand.charAt(0);
98+
const shorthand = l10n.en.weekdays.shorthand;
99+
if (shorthand) {
100+
for (let index = 0; index < shorthand.length; index++) {
101+
const _ = shorthand[index];
102+
const s = _.slice(0, 2);
103+
shorthand[index] = s === "Th" ? "Th" : s.charAt(0);
104+
}
59105
}
60-
61106
locale = l10n.en;
62107
}
63108

64-
let rangePlugin;
109+
/** @type {((new (config: { position: string; input: HTMLInputElement }) => unknown) | undefined)} */
110+
let RangePlugin;
65111

66112
if (options.mode === "range") {
67113
const importee = await import("flatpickr/dist/esm/plugins/rangePlugin");
68-
rangePlugin = importee.default;
114+
RangePlugin = importee.default;
69115
}
70116

71-
return new flatpickr(base, {
117+
const plugins = [
118+
options.mode === "range" && RangePlugin
119+
? new RangePlugin({ position: "left", input })
120+
: false,
121+
].filter(Boolean);
122+
123+
const config = {
72124
allowInput: true,
73125
disableMobile: true,
74126
clickOpens: true,
75127
locale,
76-
plugins: [
77-
options.mode === "range" && new rangePlugin({ position: "left", input }),
78-
].filter(Boolean),
128+
plugins,
79129
nextArrow:
80130
'<svg width="16px" height="16px" viewBox="0 0 16 16"><polygon points="11,8 6,13 5.3,12.3 9.6,8 5.3,3.7 6,3 "/><rect width="16" height="16" style="fill: none" /></svg>',
81131
prevArrow:
@@ -86,16 +136,25 @@ async function createCalendar({ options, base, input, dispatch }) {
86136
onClose: () => {
87137
dispatch("close");
88138
},
89-
onMonthChange: (_s, _d, instance) => {
139+
onMonthChange: (
140+
/** @type {any} */ _s,
141+
/** @type {any} */ _d,
142+
/** @type {FlatpickrInstance} */ instance,
143+
) => {
90144
updateMonthNode(instance);
91145
},
92-
onOpen: (_s, _d, instance) => {
146+
onOpen: (
147+
/** @type {any} */ _s,
148+
/** @type {any} */ _d,
149+
/** @type {FlatpickrInstance} */ instance,
150+
) => {
93151
dispatch("open");
94152
updateClasses(instance);
95153
updateMonthNode(instance);
96154
},
97155
...options,
98-
});
156+
};
157+
return new /** @type {any} */ (flatpickr)(base, config);
99158
}
100159

101160
export { createCalendar };
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
vi.mock("flatpickr", (importee) => ({
2+
...importee,
3+
default: vi.fn(function FlatpickrMock() {
4+
return {};
5+
}),
6+
}));
7+
8+
describe("createCalendar", () => {
9+
it("resolves without throw when mode is not range (RangePlugin guard)", async () => {
10+
const { createCalendar } = await import(
11+
"../../src/DatePicker/createCalendar.js"
12+
);
13+
const base = document.createElement("div");
14+
const input = document.createElement("input");
15+
const dispatch = vi.fn();
16+
17+
await expect(
18+
createCalendar({
19+
options: { mode: "single" },
20+
base,
21+
input,
22+
dispatch,
23+
}),
24+
).resolves.toBeDefined();
25+
});
26+
});

0 commit comments

Comments
 (0)