mtrl 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +33 -0
- package/index.ts +0 -2
- package/package.json +3 -1
- package/src/components/navigation/index.ts +4 -1
- package/src/components/navigation/types.ts +33 -0
- package/src/components/snackbar/index.ts +7 -1
- package/src/components/snackbar/types.ts +25 -0
- package/src/components/switch/index.ts +5 -1
- package/src/components/switch/types.ts +13 -0
- package/src/components/textfield/index.ts +7 -1
- package/src/components/textfield/types.ts +36 -0
- package/test/components/badge.test.ts +545 -0
- package/test/components/bottom-app-bar.test.ts +303 -0
- package/test/components/button.test.ts +233 -0
- package/test/components/card.test.ts +560 -0
- package/test/components/carousel.test.ts +951 -0
- package/test/components/checkbox.test.ts +462 -0
- package/test/components/chip.test.ts +692 -0
- package/test/components/datepicker.test.ts +1124 -0
- package/test/components/dialog.test.ts +990 -0
- package/test/components/divider.test.ts +412 -0
- package/test/components/extended-fab.test.ts +672 -0
- package/test/components/fab.test.ts +561 -0
- package/test/components/list.test.ts +365 -0
- package/test/components/menu.test.ts +718 -0
- package/test/components/navigation.test.ts +186 -0
- package/test/components/progress.test.ts +567 -0
- package/test/components/radios.test.ts +699 -0
- package/test/components/search.test.ts +1135 -0
- package/test/components/segmented-button.test.ts +732 -0
- package/test/components/sheet.test.ts +641 -0
- package/test/components/slider.test.ts +1220 -0
- package/test/components/snackbar.test.ts +461 -0
- package/test/components/switch.test.ts +452 -0
- package/test/components/tabs.test.ts +1369 -0
- package/test/components/textfield.test.ts +400 -0
- package/test/components/timepicker.test.ts +592 -0
- package/test/components/tooltip.test.ts +630 -0
- package/test/components/top-app-bar.test.ts +566 -0
- package/test/core/dom.attributes.test.ts +148 -0
- package/test/core/dom.classes.test.ts +152 -0
- package/test/core/dom.events.test.ts +243 -0
- package/test/core/emitter.test.ts +141 -0
- package/test/core/ripple.test.ts +99 -0
- package/test/core/state.store.test.ts +189 -0
- package/test/core/utils.normalize.test.ts +61 -0
- package/test/core/utils.object.test.ts +120 -0
- package/test/setup.ts +451 -0
- package/tsconfig.json +2 -2
- package/src/components/snackbar/constants.ts +0 -26
- package/test/components/button.test.js +0 -170
- package/test/components/checkbox.test.js +0 -238
- package/test/components/list.test.js +0 -105
- package/test/components/menu.test.js +0 -385
- package/test/components/navigation.test.js +0 -227
- package/test/components/snackbar.test.js +0 -234
- package/test/components/switch.test.js +0 -186
- package/test/components/textfield.test.js +0 -314
- package/test/core/emitter.test.js +0 -141
- package/test/core/ripple.test.js +0 -66
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
// test/components/datepicker.test.ts
|
|
2
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
import {
|
|
5
|
+
type DatePickerComponent,
|
|
6
|
+
type DatePickerConfig,
|
|
7
|
+
type DatePickerVariant,
|
|
8
|
+
type DatePickerView,
|
|
9
|
+
type DatePickerSelectionMode,
|
|
10
|
+
DAY_NAMES,
|
|
11
|
+
MONTH_NAMES,
|
|
12
|
+
DEFAULT_DATE_FORMAT
|
|
13
|
+
} from '../../src/components/datepicker/types';
|
|
14
|
+
|
|
15
|
+
// Setup jsdom environment
|
|
16
|
+
let dom: JSDOM;
|
|
17
|
+
let window: Window;
|
|
18
|
+
let document: Document;
|
|
19
|
+
let originalGlobalDocument: any;
|
|
20
|
+
let originalGlobalWindow: any;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
// Create a new JSDOM instance
|
|
24
|
+
dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
25
|
+
url: 'http://localhost/',
|
|
26
|
+
pretendToBeVisual: true
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Get window and document from jsdom
|
|
30
|
+
window = dom.window;
|
|
31
|
+
document = window.document;
|
|
32
|
+
|
|
33
|
+
// Store original globals
|
|
34
|
+
originalGlobalDocument = global.document;
|
|
35
|
+
originalGlobalWindow = global.window;
|
|
36
|
+
|
|
37
|
+
// Set globals to use jsdom
|
|
38
|
+
global.document = document;
|
|
39
|
+
global.window = window;
|
|
40
|
+
global.Element = window.Element;
|
|
41
|
+
global.HTMLElement = window.HTMLElement;
|
|
42
|
+
global.HTMLButtonElement = window.HTMLButtonElement;
|
|
43
|
+
global.Event = window.Event;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
// Restore original globals
|
|
48
|
+
global.document = originalGlobalDocument;
|
|
49
|
+
global.window = originalGlobalWindow;
|
|
50
|
+
|
|
51
|
+
// Clean up jsdom
|
|
52
|
+
window.close();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Mock datepicker implementation
|
|
56
|
+
const createMockDatePicker = (config: DatePickerConfig = {}): DatePickerComponent => {
|
|
57
|
+
// Create the main elements
|
|
58
|
+
const element = document.createElement('div');
|
|
59
|
+
element.className = 'mtrl-datepicker';
|
|
60
|
+
|
|
61
|
+
const input = document.createElement('input');
|
|
62
|
+
input.type = 'text';
|
|
63
|
+
input.className = 'mtrl-datepicker__input';
|
|
64
|
+
|
|
65
|
+
// Set default configuration
|
|
66
|
+
const settings = {
|
|
67
|
+
variant: config.variant || 'docked',
|
|
68
|
+
disabled: config.disabled || false,
|
|
69
|
+
initialView: config.initialView || 'day',
|
|
70
|
+
selectionMode: config.selectionMode || 'single',
|
|
71
|
+
dateFormat: config.dateFormat || DEFAULT_DATE_FORMAT,
|
|
72
|
+
componentName: config.componentName || 'datepicker',
|
|
73
|
+
prefix: config.prefix || 'mtrl',
|
|
74
|
+
closeOnSelect: config.closeOnSelect === undefined
|
|
75
|
+
? (config.variant === 'modal' || config.variant === 'modal-input')
|
|
76
|
+
: config.closeOnSelect
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Apply variant class
|
|
80
|
+
if (settings.variant) {
|
|
81
|
+
element.classList.add(`mtrl-datepicker--${settings.variant}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply disabled state
|
|
85
|
+
if (settings.disabled) {
|
|
86
|
+
element.classList.add('mtrl-datepicker--disabled');
|
|
87
|
+
input.disabled = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Apply label if provided
|
|
91
|
+
if (config.label) {
|
|
92
|
+
const label = document.createElement('label');
|
|
93
|
+
label.className = 'mtrl-datepicker__label';
|
|
94
|
+
label.textContent = config.label;
|
|
95
|
+
element.appendChild(label);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply placeholder if provided
|
|
99
|
+
if (config.placeholder) {
|
|
100
|
+
input.placeholder = config.placeholder;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Apply additional classes
|
|
104
|
+
if (config.class) {
|
|
105
|
+
const classes = config.class.split(' ');
|
|
106
|
+
classes.forEach(className => element.classList.add(className));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
element.appendChild(input);
|
|
110
|
+
|
|
111
|
+
// Create calendar container
|
|
112
|
+
const calendarContainer = document.createElement('div');
|
|
113
|
+
calendarContainer.className = 'mtrl-datepicker__calendar';
|
|
114
|
+
calendarContainer.style.display = 'none'; // Hidden by default
|
|
115
|
+
|
|
116
|
+
// Create calendar header
|
|
117
|
+
const calendarHeader = document.createElement('div');
|
|
118
|
+
calendarHeader.className = 'mtrl-datepicker__header';
|
|
119
|
+
|
|
120
|
+
const prevButton = document.createElement('button');
|
|
121
|
+
prevButton.className = 'mtrl-datepicker__prev';
|
|
122
|
+
prevButton.setAttribute('aria-label', 'Previous month');
|
|
123
|
+
|
|
124
|
+
const nextButton = document.createElement('button');
|
|
125
|
+
nextButton.className = 'mtrl-datepicker__next';
|
|
126
|
+
nextButton.setAttribute('aria-label', 'Next month');
|
|
127
|
+
|
|
128
|
+
const titleContainer = document.createElement('div');
|
|
129
|
+
titleContainer.className = 'mtrl-datepicker__title';
|
|
130
|
+
|
|
131
|
+
const monthYearDisplay = document.createElement('button');
|
|
132
|
+
monthYearDisplay.className = 'mtrl-datepicker__month-year';
|
|
133
|
+
|
|
134
|
+
titleContainer.appendChild(monthYearDisplay);
|
|
135
|
+
calendarHeader.appendChild(prevButton);
|
|
136
|
+
calendarHeader.appendChild(titleContainer);
|
|
137
|
+
calendarHeader.appendChild(nextButton);
|
|
138
|
+
|
|
139
|
+
calendarContainer.appendChild(calendarHeader);
|
|
140
|
+
|
|
141
|
+
// Create calendar body
|
|
142
|
+
const calendarBody = document.createElement('div');
|
|
143
|
+
calendarBody.className = 'mtrl-datepicker__body';
|
|
144
|
+
|
|
145
|
+
// Create days-of-week header
|
|
146
|
+
const daysHeader = document.createElement('div');
|
|
147
|
+
daysHeader.className = 'mtrl-datepicker__days-header';
|
|
148
|
+
|
|
149
|
+
DAY_NAMES.forEach(day => {
|
|
150
|
+
const dayEl = document.createElement('span');
|
|
151
|
+
dayEl.className = 'mtrl-datepicker__day-name';
|
|
152
|
+
dayEl.textContent = day;
|
|
153
|
+
daysHeader.appendChild(dayEl);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
calendarBody.appendChild(daysHeader);
|
|
157
|
+
|
|
158
|
+
// Create days grid
|
|
159
|
+
const daysGrid = document.createElement('div');
|
|
160
|
+
daysGrid.className = 'mtrl-datepicker__days-grid';
|
|
161
|
+
calendarBody.appendChild(daysGrid);
|
|
162
|
+
|
|
163
|
+
// Create months grid (initially hidden)
|
|
164
|
+
const monthsGrid = document.createElement('div');
|
|
165
|
+
monthsGrid.className = 'mtrl-datepicker__months-grid';
|
|
166
|
+
monthsGrid.style.display = 'none';
|
|
167
|
+
|
|
168
|
+
MONTH_NAMES.forEach((month, index) => {
|
|
169
|
+
const monthEl = document.createElement('button');
|
|
170
|
+
monthEl.className = 'mtrl-datepicker__month';
|
|
171
|
+
monthEl.textContent = month;
|
|
172
|
+
monthEl.setAttribute('data-month', index.toString());
|
|
173
|
+
monthsGrid.appendChild(monthEl);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
calendarBody.appendChild(monthsGrid);
|
|
177
|
+
|
|
178
|
+
// Create years grid (initially hidden)
|
|
179
|
+
const yearsGrid = document.createElement('div');
|
|
180
|
+
yearsGrid.className = 'mtrl-datepicker__years-grid';
|
|
181
|
+
yearsGrid.style.display = 'none';
|
|
182
|
+
|
|
183
|
+
const currentYear = new Date().getFullYear();
|
|
184
|
+
for (let i = currentYear - 50; i <= currentYear + 50; i++) {
|
|
185
|
+
const yearEl = document.createElement('button');
|
|
186
|
+
yearEl.className = 'mtrl-datepicker__year';
|
|
187
|
+
yearEl.textContent = i.toString();
|
|
188
|
+
yearEl.setAttribute('data-year', i.toString());
|
|
189
|
+
yearsGrid.appendChild(yearEl);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
calendarBody.appendChild(yearsGrid);
|
|
193
|
+
calendarContainer.appendChild(calendarBody);
|
|
194
|
+
|
|
195
|
+
// Create footer with action buttons
|
|
196
|
+
const footer = document.createElement('div');
|
|
197
|
+
footer.className = 'mtrl-datepicker__footer';
|
|
198
|
+
|
|
199
|
+
const cancelButton = document.createElement('button');
|
|
200
|
+
cancelButton.className = 'mtrl-datepicker__cancel';
|
|
201
|
+
cancelButton.textContent = 'Cancel';
|
|
202
|
+
|
|
203
|
+
const okButton = document.createElement('button');
|
|
204
|
+
okButton.className = 'mtrl-datepicker__ok';
|
|
205
|
+
okButton.textContent = 'OK';
|
|
206
|
+
|
|
207
|
+
const clearButton = document.createElement('button');
|
|
208
|
+
clearButton.className = 'mtrl-datepicker__clear';
|
|
209
|
+
clearButton.textContent = 'Clear';
|
|
210
|
+
|
|
211
|
+
footer.appendChild(cancelButton);
|
|
212
|
+
footer.appendChild(clearButton);
|
|
213
|
+
footer.appendChild(okButton);
|
|
214
|
+
|
|
215
|
+
calendarContainer.appendChild(footer);
|
|
216
|
+
element.appendChild(calendarContainer);
|
|
217
|
+
|
|
218
|
+
// Track current state
|
|
219
|
+
let currentView: DatePickerView = settings.initialView as DatePickerView;
|
|
220
|
+
let currentDate = new Date();
|
|
221
|
+
let isOpen = false;
|
|
222
|
+
let selectedDates: Date[] = [];
|
|
223
|
+
let minDate: Date | null = null;
|
|
224
|
+
let maxDate: Date | null = null;
|
|
225
|
+
|
|
226
|
+
// Parse initial dates
|
|
227
|
+
if (config.minDate) {
|
|
228
|
+
minDate = typeof config.minDate === 'string'
|
|
229
|
+
? new Date(config.minDate)
|
|
230
|
+
: config.minDate;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (config.maxDate) {
|
|
234
|
+
maxDate = typeof config.maxDate === 'string'
|
|
235
|
+
? new Date(config.maxDate)
|
|
236
|
+
: config.maxDate;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Parse initial value
|
|
240
|
+
if (config.value) {
|
|
241
|
+
if (Array.isArray(config.value)) {
|
|
242
|
+
// Handle range selection
|
|
243
|
+
const startDate = typeof config.value[0] === 'string'
|
|
244
|
+
? new Date(config.value[0])
|
|
245
|
+
: config.value[0];
|
|
246
|
+
|
|
247
|
+
const endDate = typeof config.value[1] === 'string'
|
|
248
|
+
? new Date(config.value[1])
|
|
249
|
+
: config.value[1];
|
|
250
|
+
|
|
251
|
+
selectedDates = [startDate, endDate];
|
|
252
|
+
currentDate = new Date(startDate);
|
|
253
|
+
} else {
|
|
254
|
+
// Handle single date selection
|
|
255
|
+
const date = typeof config.value === 'string'
|
|
256
|
+
? new Date(config.value)
|
|
257
|
+
: config.value;
|
|
258
|
+
|
|
259
|
+
selectedDates = [date];
|
|
260
|
+
currentDate = new Date(date);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Update input value
|
|
264
|
+
updateInputValue();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Update calendar display based on current view and date
|
|
268
|
+
const updateCalendarDisplay = () => {
|
|
269
|
+
// Update month-year title
|
|
270
|
+
const month = currentDate.getMonth();
|
|
271
|
+
const year = currentDate.getFullYear();
|
|
272
|
+
monthYearDisplay.textContent = `${MONTH_NAMES[month]} ${year}`;
|
|
273
|
+
|
|
274
|
+
// Show/hide appropriate grid based on current view
|
|
275
|
+
daysGrid.style.display = currentView === 'day' ? 'grid' : 'none';
|
|
276
|
+
monthsGrid.style.display = currentView === 'month' ? 'grid' : 'none';
|
|
277
|
+
yearsGrid.style.display = currentView === 'year' ? 'grid' : 'none';
|
|
278
|
+
|
|
279
|
+
if (currentView === 'day') {
|
|
280
|
+
renderDaysGrid();
|
|
281
|
+
} else if (currentView === 'month') {
|
|
282
|
+
highlightCurrentMonth();
|
|
283
|
+
} else if (currentView === 'year') {
|
|
284
|
+
highlightCurrentYear();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Render the days grid for the current month
|
|
289
|
+
const renderDaysGrid = () => {
|
|
290
|
+
// Clear existing days
|
|
291
|
+
daysGrid.innerHTML = '';
|
|
292
|
+
|
|
293
|
+
const year = currentDate.getFullYear();
|
|
294
|
+
const month = currentDate.getMonth();
|
|
295
|
+
|
|
296
|
+
// Get first day of month and number of days
|
|
297
|
+
const firstDayOfMonth = new Date(year, month, 1);
|
|
298
|
+
const lastDayOfMonth = new Date(year, month + 1, 0);
|
|
299
|
+
const daysInMonth = lastDayOfMonth.getDate();
|
|
300
|
+
|
|
301
|
+
// Get day of week for first day (0-6, Sunday-Saturday)
|
|
302
|
+
const firstDayWeekday = firstDayOfMonth.getDay();
|
|
303
|
+
|
|
304
|
+
// Get days from previous month to display
|
|
305
|
+
const daysFromPrevMonth = firstDayWeekday;
|
|
306
|
+
const prevMonthLastDay = new Date(year, month, 0).getDate();
|
|
307
|
+
|
|
308
|
+
// Today's date for highlighting
|
|
309
|
+
const today = new Date();
|
|
310
|
+
const isCurrentMonth = today.getMonth() === month && today.getFullYear() === year;
|
|
311
|
+
const todayDate = today.getDate();
|
|
312
|
+
|
|
313
|
+
// Add days from previous month
|
|
314
|
+
for (let i = daysFromPrevMonth - 1; i >= 0; i--) {
|
|
315
|
+
const dayNum = prevMonthLastDay - i;
|
|
316
|
+
const dayEl = document.createElement('button');
|
|
317
|
+
dayEl.className = 'mtrl-datepicker__day mtrl-datepicker__day--outside-month';
|
|
318
|
+
dayEl.textContent = dayNum.toString();
|
|
319
|
+
dayEl.setAttribute('data-date', `${year}-${month === 0 ? 12 : month}-${dayNum}`);
|
|
320
|
+
daysGrid.appendChild(dayEl);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add days for current month
|
|
324
|
+
for (let i = 1; i <= daysInMonth; i++) {
|
|
325
|
+
const dayEl = document.createElement('button');
|
|
326
|
+
dayEl.className = 'mtrl-datepicker__day';
|
|
327
|
+
|
|
328
|
+
// Check if this day is today
|
|
329
|
+
if (isCurrentMonth && i === todayDate) {
|
|
330
|
+
dayEl.classList.add('mtrl-datepicker__day--today');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check if this day is selected
|
|
334
|
+
const dayDate = new Date(year, month, i);
|
|
335
|
+
if (selectedDates.length > 0) {
|
|
336
|
+
if (settings.selectionMode === 'single' && isSameDay(selectedDates[0], dayDate)) {
|
|
337
|
+
dayEl.classList.add('mtrl-datepicker__day--selected');
|
|
338
|
+
} else if (settings.selectionMode === 'range' && selectedDates.length === 2) {
|
|
339
|
+
// Check for range selection (start, end, and in-between days)
|
|
340
|
+
if (isSameDay(selectedDates[0], dayDate)) {
|
|
341
|
+
dayEl.classList.add('mtrl-datepicker__day--range-start');
|
|
342
|
+
} else if (isSameDay(selectedDates[1], dayDate)) {
|
|
343
|
+
dayEl.classList.add('mtrl-datepicker__day--range-end');
|
|
344
|
+
} else if (dayDate > selectedDates[0] && dayDate < selectedDates[1]) {
|
|
345
|
+
dayEl.classList.add('mtrl-datepicker__day--range-middle');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if this day is within min/max limits
|
|
351
|
+
if (isDateDisabled(dayDate)) {
|
|
352
|
+
dayEl.classList.add('mtrl-datepicker__day--disabled');
|
|
353
|
+
dayEl.disabled = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
dayEl.textContent = i.toString();
|
|
357
|
+
dayEl.setAttribute('data-date', `${year}-${month + 1}-${i}`);
|
|
358
|
+
daysGrid.appendChild(dayEl);
|
|
359
|
+
|
|
360
|
+
// Add click handler
|
|
361
|
+
dayEl.addEventListener('click', () => handleDayClick(dayDate));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Add days from next month to fill the grid
|
|
365
|
+
const totalCells = 42; // 6 rows * 7 days
|
|
366
|
+
const remainingCells = totalCells - (daysFromPrevMonth + daysInMonth);
|
|
367
|
+
|
|
368
|
+
for (let i = 1; i <= remainingCells; i++) {
|
|
369
|
+
const dayEl = document.createElement('button');
|
|
370
|
+
dayEl.className = 'mtrl-datepicker__day mtrl-datepicker__day--outside-month';
|
|
371
|
+
dayEl.textContent = i.toString();
|
|
372
|
+
dayEl.setAttribute('data-date', `${year}-${month + 2}-${i}`);
|
|
373
|
+
daysGrid.appendChild(dayEl);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Highlight the current month in the months grid
|
|
378
|
+
const highlightCurrentMonth = () => {
|
|
379
|
+
const currentMonth = currentDate.getMonth();
|
|
380
|
+
|
|
381
|
+
const monthButtons = monthsGrid.querySelectorAll('.mtrl-datepicker__month');
|
|
382
|
+
monthButtons.forEach((button: Element) => {
|
|
383
|
+
button.classList.remove('mtrl-datepicker__month--selected');
|
|
384
|
+
|
|
385
|
+
const monthIndex = parseInt(button.getAttribute('data-month') || '0');
|
|
386
|
+
if (monthIndex === currentMonth) {
|
|
387
|
+
button.classList.add('mtrl-datepicker__month--selected');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Add click handler
|
|
391
|
+
button.addEventListener('click', () => {
|
|
392
|
+
currentDate.setMonth(monthIndex);
|
|
393
|
+
currentView = 'day';
|
|
394
|
+
updateCalendarDisplay();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Highlight the current year in the years grid
|
|
400
|
+
const highlightCurrentYear = () => {
|
|
401
|
+
const currentYearValue = currentDate.getFullYear();
|
|
402
|
+
|
|
403
|
+
const yearButtons = yearsGrid.querySelectorAll('.mtrl-datepicker__year');
|
|
404
|
+
yearButtons.forEach((button: Element) => {
|
|
405
|
+
button.classList.remove('mtrl-datepicker__year--selected');
|
|
406
|
+
|
|
407
|
+
const yearValue = parseInt(button.getAttribute('data-year') || '0');
|
|
408
|
+
if (yearValue === currentYearValue) {
|
|
409
|
+
button.classList.add('mtrl-datepicker__year--selected');
|
|
410
|
+
// Remove scrollIntoView as it's not implemented in our JSDOM environment
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Add click handler
|
|
414
|
+
button.addEventListener('click', () => {
|
|
415
|
+
currentDate.setFullYear(yearValue);
|
|
416
|
+
currentView = 'month';
|
|
417
|
+
updateCalendarDisplay();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Handle day selection
|
|
423
|
+
const handleDayClick = (date: Date) => {
|
|
424
|
+
if (isDateDisabled(date)) return;
|
|
425
|
+
|
|
426
|
+
if (settings.selectionMode === 'single') {
|
|
427
|
+
selectedDates = [new Date(date)];
|
|
428
|
+
updateInputValue();
|
|
429
|
+
|
|
430
|
+
if (settings.closeOnSelect) {
|
|
431
|
+
datepicker.close();
|
|
432
|
+
}
|
|
433
|
+
} else if (settings.selectionMode === 'range') {
|
|
434
|
+
if (selectedDates.length === 0 || selectedDates.length === 2) {
|
|
435
|
+
// Start new range selection
|
|
436
|
+
selectedDates = [new Date(date)];
|
|
437
|
+
} else {
|
|
438
|
+
// Complete range selection
|
|
439
|
+
if (date < selectedDates[0]) {
|
|
440
|
+
// Swap dates if second selection is before first
|
|
441
|
+
selectedDates = [new Date(date), selectedDates[0]];
|
|
442
|
+
} else {
|
|
443
|
+
selectedDates.push(new Date(date));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
updateInputValue();
|
|
447
|
+
|
|
448
|
+
if (settings.closeOnSelect) {
|
|
449
|
+
datepicker.close();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
renderDaysGrid();
|
|
455
|
+
emit('change', datepicker.getValue());
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Check if two dates are the same day
|
|
459
|
+
const isSameDay = (date1: Date, date2: Date) => {
|
|
460
|
+
return date1.getDate() === date2.getDate() &&
|
|
461
|
+
date1.getMonth() === date2.getMonth() &&
|
|
462
|
+
date1.getFullYear() === date2.getFullYear();
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Check if a date is disabled
|
|
466
|
+
const isDateDisabled = (date: Date) => {
|
|
467
|
+
if (minDate && date < minDate) return true;
|
|
468
|
+
if (maxDate && date > maxDate) return true;
|
|
469
|
+
|
|
470
|
+
// Check special dates
|
|
471
|
+
if (config.specialDates) {
|
|
472
|
+
for (const specialDate of config.specialDates) {
|
|
473
|
+
const checkDate = typeof specialDate.date === 'string'
|
|
474
|
+
? new Date(specialDate.date)
|
|
475
|
+
: specialDate.date;
|
|
476
|
+
|
|
477
|
+
if (isSameDay(checkDate, date) && specialDate.disabled) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return false;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Format date to string based on format
|
|
487
|
+
const formatDate = (date: Date) => {
|
|
488
|
+
const month = date.getMonth() + 1;
|
|
489
|
+
const day = date.getDate();
|
|
490
|
+
const year = date.getFullYear();
|
|
491
|
+
|
|
492
|
+
let formatted = settings.dateFormat;
|
|
493
|
+
formatted = formatted.replace('MM', month.toString().padStart(2, '0'));
|
|
494
|
+
formatted = formatted.replace('DD', day.toString().padStart(2, '0'));
|
|
495
|
+
formatted = formatted.replace('YYYY', year.toString());
|
|
496
|
+
|
|
497
|
+
return formatted;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Update input value based on selected dates
|
|
501
|
+
const updateInputValue = () => {
|
|
502
|
+
if (selectedDates.length === 0) {
|
|
503
|
+
input.value = '';
|
|
504
|
+
} else if (selectedDates.length === 1 || settings.selectionMode === 'single') {
|
|
505
|
+
input.value = formatDate(selectedDates[0]);
|
|
506
|
+
} else if (selectedDates.length === 2 && settings.selectionMode === 'range') {
|
|
507
|
+
input.value = `${formatDate(selectedDates[0])} - ${formatDate(selectedDates[1])}`;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Event handlers
|
|
512
|
+
const eventHandlers: Record<string, Function[]> = {};
|
|
513
|
+
|
|
514
|
+
const emit = (event: string, data?: any) => {
|
|
515
|
+
if (eventHandlers[event]) {
|
|
516
|
+
eventHandlers[event].forEach(handler => handler(data));
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Add calendar navigation event handlers
|
|
521
|
+
prevButton.addEventListener('click', () => {
|
|
522
|
+
if (currentView === 'day') {
|
|
523
|
+
currentDate.setMonth(currentDate.getMonth() - 1);
|
|
524
|
+
} else if (currentView === 'year') {
|
|
525
|
+
currentDate.setFullYear(currentDate.getFullYear() - 10);
|
|
526
|
+
}
|
|
527
|
+
updateCalendarDisplay();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
nextButton.addEventListener('click', () => {
|
|
531
|
+
if (currentView === 'day') {
|
|
532
|
+
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
533
|
+
} else if (currentView === 'year') {
|
|
534
|
+
currentDate.setFullYear(currentDate.getFullYear() + 10);
|
|
535
|
+
}
|
|
536
|
+
updateCalendarDisplay();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Month/year display click handler
|
|
540
|
+
monthYearDisplay.addEventListener('click', () => {
|
|
541
|
+
if (currentView === 'day') {
|
|
542
|
+
currentView = 'month';
|
|
543
|
+
} else if (currentView === 'month') {
|
|
544
|
+
currentView = 'year';
|
|
545
|
+
} else {
|
|
546
|
+
currentView = 'day';
|
|
547
|
+
}
|
|
548
|
+
updateCalendarDisplay();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Input click handler to open datepicker
|
|
552
|
+
input.addEventListener('click', () => {
|
|
553
|
+
if (!settings.disabled) {
|
|
554
|
+
datepicker.open();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Footer button handlers
|
|
559
|
+
cancelButton.addEventListener('click', () => {
|
|
560
|
+
datepicker.close();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
okButton.addEventListener('click', () => {
|
|
564
|
+
datepicker.close();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
clearButton.addEventListener('click', () => {
|
|
568
|
+
datepicker.clear();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Create the calendar API
|
|
572
|
+
const calendarAPI = {
|
|
573
|
+
goToDate: (date: Date) => {
|
|
574
|
+
currentDate = new Date(date);
|
|
575
|
+
updateCalendarDisplay();
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
nextMonth: () => {
|
|
579
|
+
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
580
|
+
updateCalendarDisplay();
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
prevMonth: () => {
|
|
584
|
+
currentDate.setMonth(currentDate.getMonth() - 1);
|
|
585
|
+
updateCalendarDisplay();
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
nextYear: () => {
|
|
589
|
+
currentDate.setFullYear(currentDate.getFullYear() + 1);
|
|
590
|
+
updateCalendarDisplay();
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
prevYear: () => {
|
|
594
|
+
currentDate.setFullYear(currentDate.getFullYear() - 1);
|
|
595
|
+
updateCalendarDisplay();
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
showDayView: () => {
|
|
599
|
+
currentView = 'day';
|
|
600
|
+
updateCalendarDisplay();
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
showMonthView: () => {
|
|
604
|
+
currentView = 'month';
|
|
605
|
+
updateCalendarDisplay();
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
showYearView: () => {
|
|
609
|
+
currentView = 'year';
|
|
610
|
+
updateCalendarDisplay();
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
getCurrentView: () => currentView
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Create the datepicker component
|
|
617
|
+
const datepicker: DatePickerComponent = {
|
|
618
|
+
element,
|
|
619
|
+
input,
|
|
620
|
+
calendar: calendarAPI,
|
|
621
|
+
|
|
622
|
+
disabled: {
|
|
623
|
+
enable: () => {
|
|
624
|
+
element.classList.remove('mtrl-datepicker--disabled');
|
|
625
|
+
input.disabled = false;
|
|
626
|
+
settings.disabled = false;
|
|
627
|
+
},
|
|
628
|
+
disable: () => {
|
|
629
|
+
element.classList.add('mtrl-datepicker--disabled');
|
|
630
|
+
input.disabled = true;
|
|
631
|
+
settings.disabled = true;
|
|
632
|
+
},
|
|
633
|
+
isDisabled: () => settings.disabled
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
lifecycle: {
|
|
637
|
+
destroy: () => {
|
|
638
|
+
datepicker.destroy();
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
getClass: (name: string) => {
|
|
643
|
+
const prefix = settings.prefix;
|
|
644
|
+
return name ? `${prefix}-${name}` : `${prefix}-datepicker`;
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
open: () => {
|
|
648
|
+
if (!settings.disabled && !isOpen) {
|
|
649
|
+
calendarContainer.style.display = 'block';
|
|
650
|
+
isOpen = true;
|
|
651
|
+
updateCalendarDisplay();
|
|
652
|
+
emit('open');
|
|
653
|
+
}
|
|
654
|
+
return datepicker;
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
close: () => {
|
|
658
|
+
if (isOpen) {
|
|
659
|
+
calendarContainer.style.display = 'none';
|
|
660
|
+
isOpen = false;
|
|
661
|
+
emit('close');
|
|
662
|
+
}
|
|
663
|
+
return datepicker;
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
getValue: () => {
|
|
667
|
+
if (selectedDates.length === 0) {
|
|
668
|
+
return null;
|
|
669
|
+
} else if (settings.selectionMode === 'single' || selectedDates.length === 1) {
|
|
670
|
+
return selectedDates[0];
|
|
671
|
+
} else {
|
|
672
|
+
return [selectedDates[0], selectedDates[1]] as [Date, Date];
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
setValue: (value) => {
|
|
677
|
+
if (value === null || value === undefined) {
|
|
678
|
+
selectedDates = [];
|
|
679
|
+
} else if (Array.isArray(value)) {
|
|
680
|
+
// Handle range selection
|
|
681
|
+
selectedDates = value.map(d => typeof d === 'string' ? new Date(d) : new Date(d));
|
|
682
|
+
if (selectedDates.length > 0) {
|
|
683
|
+
currentDate = new Date(selectedDates[0]);
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
// Handle single date selection
|
|
687
|
+
const date = typeof value === 'string' ? new Date(value) : new Date(value);
|
|
688
|
+
selectedDates = [date];
|
|
689
|
+
currentDate = new Date(date);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
updateInputValue();
|
|
693
|
+
updateCalendarDisplay();
|
|
694
|
+
emit('change', datepicker.getValue());
|
|
695
|
+
|
|
696
|
+
return datepicker;
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
getFormattedValue: () => {
|
|
700
|
+
if (selectedDates.length === 0) {
|
|
701
|
+
return '';
|
|
702
|
+
} else if (settings.selectionMode === 'single' || selectedDates.length === 1) {
|
|
703
|
+
return formatDate(selectedDates[0]);
|
|
704
|
+
} else {
|
|
705
|
+
return `${formatDate(selectedDates[0])} - ${formatDate(selectedDates[1])}`;
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
clear: () => {
|
|
710
|
+
selectedDates = [];
|
|
711
|
+
updateInputValue();
|
|
712
|
+
renderDaysGrid();
|
|
713
|
+
emit('change', null);
|
|
714
|
+
|
|
715
|
+
return datepicker;
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
enable: () => {
|
|
719
|
+
datepicker.disabled.enable();
|
|
720
|
+
return datepicker;
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
disable: () => {
|
|
724
|
+
datepicker.disabled.disable();
|
|
725
|
+
return datepicker;
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
setMinDate: (date) => {
|
|
729
|
+
minDate = typeof date === 'string' ? new Date(date) : new Date(date);
|
|
730
|
+
updateCalendarDisplay();
|
|
731
|
+
return datepicker;
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
setMaxDate: (date) => {
|
|
735
|
+
maxDate = typeof date === 'string' ? new Date(date) : new Date(date);
|
|
736
|
+
updateCalendarDisplay();
|
|
737
|
+
return datepicker;
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
destroy: () => {
|
|
741
|
+
// Remove event listeners
|
|
742
|
+
input.removeEventListener('click', datepicker.open);
|
|
743
|
+
|
|
744
|
+
// Remove the element from the DOM if it has a parent
|
|
745
|
+
if (element.parentNode) {
|
|
746
|
+
element.parentNode.removeChild(element);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Clear event handlers
|
|
750
|
+
for (const event in eventHandlers) {
|
|
751
|
+
eventHandlers[event] = [];
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
on: (event: string, handler: Function) => {
|
|
756
|
+
if (!eventHandlers[event]) {
|
|
757
|
+
eventHandlers[event] = [];
|
|
758
|
+
}
|
|
759
|
+
eventHandlers[event].push(handler);
|
|
760
|
+
return datepicker;
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
off: (event: string, handler: Function) => {
|
|
764
|
+
if (eventHandlers[event]) {
|
|
765
|
+
eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
|
|
766
|
+
}
|
|
767
|
+
return datepicker;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// Initialize calendar display
|
|
772
|
+
updateCalendarDisplay();
|
|
773
|
+
|
|
774
|
+
return datepicker;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
describe('DatePicker Component', () => {
|
|
778
|
+
test('should create a datepicker element', () => {
|
|
779
|
+
const datepicker = createMockDatePicker();
|
|
780
|
+
expect(datepicker.element).toBeDefined();
|
|
781
|
+
expect(datepicker.element.tagName).toBe('DIV');
|
|
782
|
+
expect(datepicker.element.className).toContain('mtrl-datepicker');
|
|
783
|
+
|
|
784
|
+
expect(datepicker.input).toBeDefined();
|
|
785
|
+
expect(datepicker.input.tagName).toBe('INPUT');
|
|
786
|
+
expect(datepicker.input.className).toContain('mtrl-datepicker__input');
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test('should support different variants', () => {
|
|
790
|
+
const variants: DatePickerVariant[] = ['docked', 'modal', 'modal-input'];
|
|
791
|
+
|
|
792
|
+
variants.forEach(variant => {
|
|
793
|
+
const datepicker = createMockDatePicker({ variant });
|
|
794
|
+
expect(datepicker.element.className).toContain(`mtrl-datepicker--${variant}`);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test('should set label text', () => {
|
|
799
|
+
const datepicker = createMockDatePicker({ label: 'Select Date' });
|
|
800
|
+
|
|
801
|
+
const label = datepicker.element.querySelector('.mtrl-datepicker__label');
|
|
802
|
+
expect(label).toBeDefined();
|
|
803
|
+
expect(label?.textContent).toBe('Select Date');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test('should set placeholder text', () => {
|
|
807
|
+
const datepicker = createMockDatePicker({ placeholder: 'MM/DD/YYYY' });
|
|
808
|
+
|
|
809
|
+
expect(datepicker.input.placeholder).toBe('MM/DD/YYYY');
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test('should apply disabled state', () => {
|
|
813
|
+
const datepicker = createMockDatePicker({ disabled: true });
|
|
814
|
+
|
|
815
|
+
expect(datepicker.element.className).toContain('mtrl-datepicker--disabled');
|
|
816
|
+
expect(datepicker.input.disabled).toBe(true);
|
|
817
|
+
expect(datepicker.disabled.isDisabled()).toBe(true);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('should open and close the calendar', () => {
|
|
821
|
+
const datepicker = createMockDatePicker();
|
|
822
|
+
|
|
823
|
+
const calendar = datepicker.element.querySelector('.mtrl-datepicker__calendar');
|
|
824
|
+
expect(calendar).toBeDefined();
|
|
825
|
+
expect(calendar?.style.display).toBe('none');
|
|
826
|
+
|
|
827
|
+
datepicker.open();
|
|
828
|
+
expect(calendar?.style.display).toBe('block');
|
|
829
|
+
|
|
830
|
+
datepicker.close();
|
|
831
|
+
expect(calendar?.style.display).toBe('none');
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('should navigate between months', () => {
|
|
835
|
+
const datepicker = createMockDatePicker();
|
|
836
|
+
|
|
837
|
+
// Set a specific date for testing
|
|
838
|
+
const testDate = new Date(2023, 0, 1); // January 1, 2023
|
|
839
|
+
datepicker.calendar.goToDate(testDate);
|
|
840
|
+
|
|
841
|
+
// Get the month-year display
|
|
842
|
+
const monthYearDisplay = datepicker.element.querySelector('.mtrl-datepicker__month-year');
|
|
843
|
+
expect(monthYearDisplay?.textContent).toContain('January 2023');
|
|
844
|
+
|
|
845
|
+
// Navigate to next month
|
|
846
|
+
datepicker.calendar.nextMonth();
|
|
847
|
+
expect(monthYearDisplay?.textContent).toContain('February 2023');
|
|
848
|
+
|
|
849
|
+
// Navigate to previous month
|
|
850
|
+
datepicker.calendar.prevMonth();
|
|
851
|
+
expect(monthYearDisplay?.textContent).toContain('January 2023');
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test('should navigate between years', () => {
|
|
855
|
+
const datepicker = createMockDatePicker();
|
|
856
|
+
|
|
857
|
+
// Set a specific date for testing
|
|
858
|
+
const testDate = new Date(2023, 0, 1); // January 1, 2023
|
|
859
|
+
datepicker.calendar.goToDate(testDate);
|
|
860
|
+
|
|
861
|
+
// Get the month-year display
|
|
862
|
+
const monthYearDisplay = datepicker.element.querySelector('.mtrl-datepicker__month-year');
|
|
863
|
+
expect(monthYearDisplay?.textContent).toContain('January 2023');
|
|
864
|
+
|
|
865
|
+
// Navigate to next year
|
|
866
|
+
datepicker.calendar.nextYear();
|
|
867
|
+
expect(monthYearDisplay?.textContent).toContain('January 2024');
|
|
868
|
+
|
|
869
|
+
// Navigate to previous year
|
|
870
|
+
datepicker.calendar.prevYear();
|
|
871
|
+
expect(monthYearDisplay?.textContent).toContain('January 2023');
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test('should switch between calendar views', () => {
|
|
875
|
+
const datepicker = createMockDatePicker();
|
|
876
|
+
|
|
877
|
+
// Default view should be day view
|
|
878
|
+
expect(datepicker.calendar.getCurrentView()).toBe('day');
|
|
879
|
+
|
|
880
|
+
const daysGrid = datepicker.element.querySelector('.mtrl-datepicker__days-grid');
|
|
881
|
+
const monthsGrid = datepicker.element.querySelector('.mtrl-datepicker__months-grid');
|
|
882
|
+
const yearsGrid = datepicker.element.querySelector('.mtrl-datepicker__years-grid');
|
|
883
|
+
|
|
884
|
+
// Open the datepicker to make sure display is updated
|
|
885
|
+
datepicker.open();
|
|
886
|
+
|
|
887
|
+
expect(daysGrid?.style.display).not.toBe('none');
|
|
888
|
+
expect(monthsGrid?.style.display).toBe('none');
|
|
889
|
+
expect(yearsGrid?.style.display).toBe('none');
|
|
890
|
+
|
|
891
|
+
// Switch to month view
|
|
892
|
+
datepicker.calendar.showMonthView();
|
|
893
|
+
expect(datepicker.calendar.getCurrentView()).toBe('month');
|
|
894
|
+
expect(daysGrid?.style.display).toBe('none');
|
|
895
|
+
expect(monthsGrid?.style.display).not.toBe('none');
|
|
896
|
+
expect(yearsGrid?.style.display).toBe('none');
|
|
897
|
+
|
|
898
|
+
// Switch to year view
|
|
899
|
+
datepicker.calendar.showYearView();
|
|
900
|
+
expect(datepicker.calendar.getCurrentView()).toBe('year');
|
|
901
|
+
expect(daysGrid?.style.display).toBe('none');
|
|
902
|
+
expect(monthsGrid?.style.display).toBe('none');
|
|
903
|
+
expect(yearsGrid?.style.display).not.toBe('none');
|
|
904
|
+
|
|
905
|
+
// Switch back to day view
|
|
906
|
+
datepicker.calendar.showDayView();
|
|
907
|
+
expect(datepicker.calendar.getCurrentView()).toBe('day');
|
|
908
|
+
expect(daysGrid?.style.display).not.toBe('none');
|
|
909
|
+
expect(monthsGrid?.style.display).toBe('none');
|
|
910
|
+
expect(yearsGrid?.style.display).toBe('none');
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test('should set and get date value', () => {
|
|
914
|
+
const datepicker = createMockDatePicker();
|
|
915
|
+
|
|
916
|
+
// Initial value should be null
|
|
917
|
+
expect(datepicker.getValue()).toBeNull();
|
|
918
|
+
|
|
919
|
+
// Set a date
|
|
920
|
+
const testDate = new Date(2023, 0, 15); // January 15, 2023
|
|
921
|
+
datepicker.setValue(testDate);
|
|
922
|
+
|
|
923
|
+
// Get the selected date
|
|
924
|
+
const selectedDate = datepicker.getValue() as Date;
|
|
925
|
+
expect(selectedDate).toBeInstanceOf(Date);
|
|
926
|
+
expect(selectedDate.getFullYear()).toBe(2023);
|
|
927
|
+
expect(selectedDate.getMonth()).toBe(0);
|
|
928
|
+
expect(selectedDate.getDate()).toBe(15);
|
|
929
|
+
|
|
930
|
+
// Check input value
|
|
931
|
+
expect(datepicker.input.value).toBe('01/15/2023');
|
|
932
|
+
expect(datepicker.getFormattedValue()).toBe('01/15/2023');
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
test('should set and get date range', () => {
|
|
936
|
+
const datepicker = createMockDatePicker({ selectionMode: 'range' });
|
|
937
|
+
|
|
938
|
+
// Initial value should be null
|
|
939
|
+
expect(datepicker.getValue()).toBeNull();
|
|
940
|
+
|
|
941
|
+
// Set a date range
|
|
942
|
+
const startDate = new Date(2023, 0, 10); // January 10, 2023
|
|
943
|
+
const endDate = new Date(2023, 0, 20); // January 20, 2023
|
|
944
|
+
datepicker.setValue([startDate, endDate]);
|
|
945
|
+
|
|
946
|
+
// Get the selected date range
|
|
947
|
+
const selectedRange = datepicker.getValue() as [Date, Date];
|
|
948
|
+
expect(Array.isArray(selectedRange)).toBe(true);
|
|
949
|
+
expect(selectedRange.length).toBe(2);
|
|
950
|
+
|
|
951
|
+
expect(selectedRange[0].getFullYear()).toBe(2023);
|
|
952
|
+
expect(selectedRange[0].getMonth()).toBe(0);
|
|
953
|
+
expect(selectedRange[0].getDate()).toBe(10);
|
|
954
|
+
|
|
955
|
+
expect(selectedRange[1].getFullYear()).toBe(2023);
|
|
956
|
+
expect(selectedRange[1].getMonth()).toBe(0);
|
|
957
|
+
expect(selectedRange[1].getDate()).toBe(20);
|
|
958
|
+
|
|
959
|
+
// Check input value
|
|
960
|
+
expect(datepicker.input.value).toBe('01/10/2023 - 01/20/2023');
|
|
961
|
+
expect(datepicker.getFormattedValue()).toBe('01/10/2023 - 01/20/2023');
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
test('should clear the selected date', () => {
|
|
965
|
+
const datepicker = createMockDatePicker();
|
|
966
|
+
|
|
967
|
+
// Set a date
|
|
968
|
+
const testDate = new Date(2023, 0, 15);
|
|
969
|
+
datepicker.setValue(testDate);
|
|
970
|
+
|
|
971
|
+
// Check that the date is set
|
|
972
|
+
expect(datepicker.getValue()).not.toBeNull();
|
|
973
|
+
expect(datepicker.input.value).not.toBe('');
|
|
974
|
+
|
|
975
|
+
// Clear the date
|
|
976
|
+
datepicker.clear();
|
|
977
|
+
|
|
978
|
+
// Check that the date is cleared
|
|
979
|
+
expect(datepicker.getValue()).toBeNull();
|
|
980
|
+
expect(datepicker.input.value).toBe('');
|
|
981
|
+
expect(datepicker.getFormattedValue()).toBe('');
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test('should respect min and max date constraints', () => {
|
|
985
|
+
const minDate = new Date(2023, 0, 10); // January 10, 2023
|
|
986
|
+
const maxDate = new Date(2023, 0, 20); // January 20, 2023
|
|
987
|
+
|
|
988
|
+
const datepicker = createMockDatePicker({
|
|
989
|
+
minDate,
|
|
990
|
+
maxDate
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// Try to set a date before min date
|
|
994
|
+
const tooEarlyDate = new Date(2023, 0, 5);
|
|
995
|
+
datepicker.setValue(tooEarlyDate);
|
|
996
|
+
|
|
997
|
+
// The datepicker should accept the date (validation is only on UI interaction)
|
|
998
|
+
const selectedDate1 = datepicker.getValue() as Date;
|
|
999
|
+
expect(selectedDate1.getDate()).toBe(5);
|
|
1000
|
+
|
|
1001
|
+
// Try to set a date after max date
|
|
1002
|
+
const tooLateDate = new Date(2023, 0, 25);
|
|
1003
|
+
datepicker.setValue(tooLateDate);
|
|
1004
|
+
|
|
1005
|
+
// The datepicker should accept the date (validation is only on UI interaction)
|
|
1006
|
+
const selectedDate2 = datepicker.getValue() as Date;
|
|
1007
|
+
expect(selectedDate2.getDate()).toBe(25);
|
|
1008
|
+
|
|
1009
|
+
// Set constraints after selection
|
|
1010
|
+
datepicker.setValue(new Date(2023, 0, 15)); // Set to valid date
|
|
1011
|
+
datepicker.setMinDate(new Date(2023, 0, 10));
|
|
1012
|
+
datepicker.setMaxDate(new Date(2023, 0, 20));
|
|
1013
|
+
|
|
1014
|
+
// The current date should remain valid
|
|
1015
|
+
const selectedDate3 = datepicker.getValue() as Date;
|
|
1016
|
+
expect(selectedDate3.getDate()).toBe(15);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test('should enable and disable the datepicker', () => {
|
|
1020
|
+
const datepicker = createMockDatePicker();
|
|
1021
|
+
|
|
1022
|
+
expect(datepicker.disabled.isDisabled()).toBe(false);
|
|
1023
|
+
|
|
1024
|
+
datepicker.disable();
|
|
1025
|
+
|
|
1026
|
+
expect(datepicker.disabled.isDisabled()).toBe(true);
|
|
1027
|
+
expect(datepicker.element.className).toContain('mtrl-datepicker--disabled');
|
|
1028
|
+
expect(datepicker.input.disabled).toBe(true);
|
|
1029
|
+
|
|
1030
|
+
datepicker.enable();
|
|
1031
|
+
|
|
1032
|
+
expect(datepicker.disabled.isDisabled()).toBe(false);
|
|
1033
|
+
expect(datepicker.element.className).not.toContain('mtrl-datepicker--disabled');
|
|
1034
|
+
expect(datepicker.input.disabled).toBe(false);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test('should respect custom date format', () => {
|
|
1038
|
+
const datepicker = createMockDatePicker({
|
|
1039
|
+
dateFormat: 'YYYY-MM-DD'
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// Set a date
|
|
1043
|
+
const testDate = new Date(2023, 0, 15); // January 15, 2023
|
|
1044
|
+
datepicker.setValue(testDate);
|
|
1045
|
+
|
|
1046
|
+
// Check the formatted output
|
|
1047
|
+
expect(datepicker.input.value).toBe('2023-01-15');
|
|
1048
|
+
expect(datepicker.getFormattedValue()).toBe('2023-01-15');
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test('should emit change events', () => {
|
|
1052
|
+
const datepicker = createMockDatePicker();
|
|
1053
|
+
let eventFired = false;
|
|
1054
|
+
let eventValue: any = null;
|
|
1055
|
+
|
|
1056
|
+
datepicker.on('change', (value) => {
|
|
1057
|
+
eventFired = true;
|
|
1058
|
+
eventValue = value;
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// Set a date
|
|
1062
|
+
const testDate = new Date(2023, 0, 15);
|
|
1063
|
+
datepicker.setValue(testDate);
|
|
1064
|
+
|
|
1065
|
+
expect(eventFired).toBe(true);
|
|
1066
|
+
expect(eventValue).toBeInstanceOf(Date);
|
|
1067
|
+
expect(eventValue.getFullYear()).toBe(2023);
|
|
1068
|
+
expect(eventValue.getMonth()).toBe(0);
|
|
1069
|
+
expect(eventValue.getDate()).toBe(15);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test('should emit open and close events', () => {
|
|
1073
|
+
const datepicker = createMockDatePicker();
|
|
1074
|
+
let openEventFired = false;
|
|
1075
|
+
let closeEventFired = false;
|
|
1076
|
+
|
|
1077
|
+
datepicker.on('open', () => {
|
|
1078
|
+
openEventFired = true;
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
datepicker.on('close', () => {
|
|
1082
|
+
closeEventFired = true;
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
datepicker.open();
|
|
1086
|
+
expect(openEventFired).toBe(true);
|
|
1087
|
+
|
|
1088
|
+
datepicker.close();
|
|
1089
|
+
expect(closeEventFired).toBe(true);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test('should remove event listeners', () => {
|
|
1093
|
+
const datepicker = createMockDatePicker();
|
|
1094
|
+
let count = 0;
|
|
1095
|
+
|
|
1096
|
+
const handler = () => {
|
|
1097
|
+
count++;
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
datepicker.on('change', handler);
|
|
1101
|
+
|
|
1102
|
+
// First change
|
|
1103
|
+
datepicker.setValue(new Date());
|
|
1104
|
+
expect(count).toBe(1);
|
|
1105
|
+
|
|
1106
|
+
// Remove listener
|
|
1107
|
+
datepicker.off('change', handler);
|
|
1108
|
+
|
|
1109
|
+
// Second change
|
|
1110
|
+
datepicker.setValue(new Date());
|
|
1111
|
+
expect(count).toBe(1); // Count should not increase
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test('should be properly destroyed', () => {
|
|
1115
|
+
const datepicker = createMockDatePicker();
|
|
1116
|
+
document.body.appendChild(datepicker.element);
|
|
1117
|
+
|
|
1118
|
+
expect(document.body.contains(datepicker.element)).toBe(true);
|
|
1119
|
+
|
|
1120
|
+
datepicker.destroy();
|
|
1121
|
+
|
|
1122
|
+
expect(document.body.contains(datepicker.element)).toBe(false);
|
|
1123
|
+
});
|
|
1124
|
+
});
|