@tailng-ui/primitives 0.14.0 → 0.15.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.
Files changed (52) hide show
  1. package/package.json +3 -2
  2. package/src/index.d.ts +1 -0
  3. package/src/index.d.ts.map +1 -1
  4. package/src/index.js +1 -0
  5. package/src/index.js.map +1 -1
  6. package/src/lib/form/datepicker/__tests__/tng-datepicker.test-helpers.d.ts +25 -0
  7. package/src/lib/form/datepicker/__tests__/tng-datepicker.test-helpers.d.ts.map +1 -0
  8. package/src/lib/form/datepicker/__tests__/tng-datepicker.test-helpers.js +105 -0
  9. package/src/lib/form/datepicker/__tests__/tng-datepicker.test-helpers.js.map +1 -0
  10. package/src/lib/form/datepicker/datepicker.adapters.d.ts +5 -0
  11. package/src/lib/form/datepicker/datepicker.adapters.d.ts.map +1 -0
  12. package/src/lib/form/datepicker/datepicker.adapters.js +194 -0
  13. package/src/lib/form/datepicker/datepicker.adapters.js.map +1 -0
  14. package/src/lib/form/datepicker/datepicker.state.d.ts +10 -0
  15. package/src/lib/form/datepicker/datepicker.state.d.ts.map +1 -0
  16. package/src/lib/form/datepicker/datepicker.state.js +97 -0
  17. package/src/lib/form/datepicker/datepicker.state.js.map +1 -0
  18. package/src/lib/form/datepicker/datepicker.types.d.ts +269 -0
  19. package/src/lib/form/datepicker/datepicker.types.d.ts.map +1 -0
  20. package/src/lib/form/datepicker/datepicker.types.js +1 -0
  21. package/src/lib/form/datepicker/datepicker.types.js.map +1 -0
  22. package/src/lib/form/datepicker/datepicker.utils.d.ts +75 -0
  23. package/src/lib/form/datepicker/datepicker.utils.d.ts.map +1 -0
  24. package/src/lib/form/datepicker/datepicker.utils.js +238 -0
  25. package/src/lib/form/datepicker/datepicker.utils.js.map +1 -0
  26. package/src/lib/form/datepicker/index.d.ts +3 -0
  27. package/src/lib/form/datepicker/index.d.ts.map +1 -0
  28. package/src/lib/form/datepicker/index.js +3 -0
  29. package/src/lib/form/datepicker/index.js.map +1 -0
  30. package/src/lib/form/datepicker/tng-datepicker.d.ts +5 -0
  31. package/src/lib/form/datepicker/tng-datepicker.d.ts.map +1 -0
  32. package/src/lib/form/datepicker/tng-datepicker.js +1948 -0
  33. package/src/lib/form/datepicker/tng-datepicker.js.map +1 -0
  34. package/src/lib/form/datepicker/tng-datepicker.overlay.d.ts +60 -0
  35. package/src/lib/form/datepicker/tng-datepicker.overlay.d.ts.map +1 -0
  36. package/src/lib/form/datepicker/tng-datepicker.overlay.js +275 -0
  37. package/src/lib/form/datepicker/tng-datepicker.overlay.js.map +1 -0
  38. package/src/lib/layout/grid/__tests__/tng-grid.test-harness.d.ts +32 -0
  39. package/src/lib/layout/grid/__tests__/tng-grid.test-harness.d.ts.map +1 -0
  40. package/src/lib/layout/grid/__tests__/tng-grid.test-harness.js +312 -0
  41. package/src/lib/layout/grid/__tests__/tng-grid.test-harness.js.map +1 -0
  42. package/src/lib/layout/grid/tng-grid.d.ts +144 -2
  43. package/src/lib/layout/grid/tng-grid.d.ts.map +1 -1
  44. package/src/lib/layout/grid/tng-grid.js +669 -2
  45. package/src/lib/layout/grid/tng-grid.js.map +1 -1
  46. package/src/lib/layout/tree/__tests__/tng-tree.test-harness.d.ts.map +1 -1
  47. package/src/lib/layout/tree/__tests__/tng-tree.test-harness.js +0 -1
  48. package/src/lib/layout/tree/__tests__/tng-tree.test-harness.js.map +1 -1
  49. package/src/lib/navigation/tabs/tng-tabs.d.ts +13 -6
  50. package/src/lib/navigation/tabs/tng-tabs.d.ts.map +1 -1
  51. package/src/lib/navigation/tabs/tng-tabs.js +77 -35
  52. package/src/lib/navigation/tabs/tng-tabs.js.map +1 -1
@@ -0,0 +1,1948 @@
1
+ import { createActiveDescendantController, createOverlayFocusHandoffController, createOverlayRuntime, createRovingFocusController, createTngIdFactory, createTypeaheadController, resolveFocusableElements, resolveGridNavigationKeyAction, resolveNavigableGridCell, } from '@tailng-ui/cdk';
2
+ import { coerceWeekStartsOn, defaultDatepickerDateAdapter, normalizeDateInput } from './datepicker.adapters';
3
+ import { buildMonthGrid, buildMonthOptions, buildYearOptions, clampDateToMonth, compareMonthIdentity, datesEqual, findFirstEnabledDateInMonth, hasSelectableDateInMonth, hasSelectableDateInYear, isDateValueInMonth, moveDateSkippingDisabled, normalizeRangeOrder, resolveInitialFocusedSection, resolveLocaleWeekStartsOn, resolveViewForEscape, toDateKey, } from './datepicker.utils';
4
+ import { clearSelectionForMode, normalizeSelectionInput, rangeIncludesDate, selectionValuesEqual, valueIncludesDate, } from './datepicker.state';
5
+ const createDatepickerId = createTngIdFactory('tng-datepicker');
6
+ const createDatepickerFocusableId = createTngIdFactory('tng-datepicker-focusable');
7
+ const datepickerRegistry = new Set();
8
+ const datepickerFocusHandoff = createOverlayFocusHandoffController();
9
+ const emptyWeekdayLabels = Object.freeze([]);
10
+ const emptyCells = Object.freeze([]);
11
+ const emptyMonths = Object.freeze([]);
12
+ const emptyYears = Object.freeze([]);
13
+ function freezeAttributes(attributes) {
14
+ const normalized = {};
15
+ for (const [key, value] of Object.entries(attributes)) {
16
+ if (value === null || value === undefined || value === '') {
17
+ continue;
18
+ }
19
+ normalized[key] = value;
20
+ }
21
+ return Object.freeze(normalized);
22
+ }
23
+ function mapOverlayDismissReason(reason) {
24
+ if (reason === 'escape-key') {
25
+ return 'escape';
26
+ }
27
+ if (reason === 'outside-pointer' || reason === 'focus-outside') {
28
+ return 'outside';
29
+ }
30
+ return 'programmatic';
31
+ }
32
+ function hasDisallowedModifiers(event) {
33
+ return event.altKey === true || event.ctrlKey === true || event.metaKey === true;
34
+ }
35
+ function normalizeSelectionMode(value) {
36
+ return value === 'multiple' || value === 'range' ? value : 'single';
37
+ }
38
+ function normalizeView(value) {
39
+ return value === 'month' || value === 'year' ? value : 'day';
40
+ }
41
+ function normalizePosition(value) {
42
+ return value === 'center' || value === 'end' ? value : 'start';
43
+ }
44
+ function normalizeOverlayMode(value) {
45
+ return value === 'push' || value === 'side' ? value : 'overlay';
46
+ }
47
+ function normalizeDirection(value) {
48
+ return value === 'rtl' ? 'rtl' : 'ltr';
49
+ }
50
+ function resolveDefaultLocale(ownerDocument) {
51
+ const documentLocale = ownerDocument?.documentElement?.lang?.trim();
52
+ if (documentLocale !== undefined && documentLocale !== '') {
53
+ return documentLocale;
54
+ }
55
+ const navigatorLocale = ownerDocument?.defaultView?.navigator.languages[0] ??
56
+ ownerDocument?.defaultView?.navigator.language;
57
+ if (typeof navigatorLocale === 'string' && navigatorLocale.trim() !== '') {
58
+ return navigatorLocale;
59
+ }
60
+ const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;
61
+ if (intlLocale.trim() !== '') {
62
+ return intlLocale;
63
+ }
64
+ return 'en-US';
65
+ }
66
+ function ensureYearPageSize(value) {
67
+ if (!Number.isFinite(value) || value === undefined) {
68
+ return 24;
69
+ }
70
+ return Math.max(12, Math.trunc(value));
71
+ }
72
+ function isRangeSelectionValue(value) {
73
+ return (typeof value === 'object' &&
74
+ value !== null &&
75
+ !Array.isArray(value) &&
76
+ Object.prototype.hasOwnProperty.call(value, 'start') &&
77
+ Object.prototype.hasOwnProperty.call(value, 'end'));
78
+ }
79
+ function readDocument(input) {
80
+ return input;
81
+ }
82
+ class DatepickerController {
83
+ instanceId;
84
+ gridId;
85
+ overlayId;
86
+ monthLabelId;
87
+ listenerSet = new Set();
88
+ typeahead = createTypeaheadController({
89
+ items: [],
90
+ matchStrategy: 'start',
91
+ });
92
+ dayRovingFocus;
93
+ monthRovingFocus;
94
+ yearRovingFocus;
95
+ dayActiveDescendant;
96
+ config;
97
+ state;
98
+ destroyed = false;
99
+ version = 0;
100
+ rangeAnchorDate = null;
101
+ focusVisibleDate = null;
102
+ hoverDate = null;
103
+ yearPageStart = 0;
104
+ cachedOutputsVersion = -1;
105
+ cachedOutputs = null;
106
+ cachedWeekdayLabels = emptyWeekdayLabels;
107
+ cachedCells = emptyCells;
108
+ cachedMonthOptions = emptyMonths;
109
+ cachedYearOptions = emptyYears;
110
+ cachedGridVersion = -1;
111
+ cachedMonthVersion = -1;
112
+ cachedYearVersion = -1;
113
+ triggerElement = null;
114
+ overlayElement = null;
115
+ restoreFocusTargetId = null;
116
+ focusLayerRegistered = false;
117
+ overlayLayerRegistered = false;
118
+ constructor(config) {
119
+ this.instanceId = config.id?.trim() || createDatepickerId();
120
+ this.gridId = `${this.instanceId}-grid`;
121
+ this.overlayId = `${this.instanceId}-overlay`;
122
+ this.monthLabelId = `${this.instanceId}-month-label`;
123
+ this.dayRovingFocus = createRovingFocusController({ itemIds: [], loop: false });
124
+ this.monthRovingFocus = createRovingFocusController({ itemIds: [], loop: false });
125
+ this.yearRovingFocus = createRovingFocusController({ itemIds: [], loop: false });
126
+ this.dayActiveDescendant = createActiveDescendantController({
127
+ hostId: this.gridId,
128
+ itemIds: [],
129
+ loop: false,
130
+ });
131
+ this.config = this.resolveConfig(config, null);
132
+ const initialActiveDate = this.resolveInitialActiveDate(this.config.value, this.config.today);
133
+ const visibleMonth = this.config.adapter.startOfMonth(initialActiveDate);
134
+ this.state = {
135
+ activeDate: initialActiveDate,
136
+ disabled: this.config.disabled,
137
+ focusedSection: resolveInitialFocusedSection(this.config.initialView),
138
+ inputText: this.formatValueForInput(this.config.value),
139
+ lastCloseReason: null,
140
+ open: config.defaultOpen ?? false,
141
+ validationError: null,
142
+ value: this.config.value,
143
+ view: this.config.initialView,
144
+ visibleMonth,
145
+ };
146
+ this.focusVisibleDate = initialActiveDate;
147
+ this.yearPageStart = this.resolveCenteredYearPageStart(this.config.adapter.getYear(initialActiveDate));
148
+ this.rangeAnchorDate = this.extractSelectionAnchor(this.state.value);
149
+ if (this.state.open) {
150
+ const activeElement = this.resolveActiveElement();
151
+ this.restoreFocusTargetId = activeElement === null ? null : this.ensureElementId(activeElement);
152
+ this.registerOverlayLayer();
153
+ this.activateFocusLayer();
154
+ }
155
+ datepickerRegistry.add(this);
156
+ }
157
+ clear() {
158
+ if (this.destroyed) {
159
+ return;
160
+ }
161
+ const previousValue = this.state.value;
162
+ const nextValue = clearSelectionForMode(this.config.selectionMode);
163
+ if (selectionValuesEqual(this.config.adapter, this.config.selectionMode, previousValue, nextValue)) {
164
+ return;
165
+ }
166
+ this.state.value = nextValue;
167
+ this.rangeAnchorDate = null;
168
+ this.state.validationError = null;
169
+ this.state.inputText = this.formatValueForInput(nextValue);
170
+ const nextActive = this.resolveInitialActiveDate(nextValue, this.config.today);
171
+ this.applyActiveDate(nextActive, 'programmatic', true);
172
+ this.bumpVersion();
173
+ this.emit({
174
+ previousValue,
175
+ trigger: 'programmatic',
176
+ type: 'valueChange',
177
+ value: nextValue,
178
+ });
179
+ }
180
+ close(reason = 'programmatic') {
181
+ if (this.destroyed || !this.state.open) {
182
+ return;
183
+ }
184
+ this.emit({ reason, type: 'closeStart' });
185
+ this.state.open = false;
186
+ this.state.lastCloseReason = reason;
187
+ this.unregisterOverlayLayer();
188
+ this.deactivateFocusLayer();
189
+ this.bumpVersion();
190
+ this.emit({ reason, type: 'closed' });
191
+ }
192
+ commitInputText() {
193
+ if (this.destroyed || !this.config.allowManualInput) {
194
+ return false;
195
+ }
196
+ const inputText = this.state.inputText.trim();
197
+ const parsed = this.parseInputText(inputText);
198
+ if (parsed === null || !this.isStrictInputCommitValue(inputText, parsed)) {
199
+ this.state.validationError = 'invalid-input';
200
+ this.bumpVersion();
201
+ return false;
202
+ }
203
+ if (this.isDateDisabled(parsed)) {
204
+ this.state.validationError = 'out-of-range';
205
+ this.bumpVersion();
206
+ return false;
207
+ }
208
+ this.state.validationError = null;
209
+ this.selectDate(parsed, { trigger: 'text-input' });
210
+ return true;
211
+ }
212
+ destroy() {
213
+ if (this.destroyed) {
214
+ return;
215
+ }
216
+ this.destroyed = true;
217
+ this.unregisterOverlayLayer();
218
+ this.unregisterFocusLayer();
219
+ this.listenerSet.clear();
220
+ datepickerRegistry.delete(this);
221
+ }
222
+ formatDate(date, format = 'label') {
223
+ return this.config.adapter.format(date, format, this.config.locale);
224
+ }
225
+ getOutputs() {
226
+ if (this.cachedOutputs !== null && this.cachedOutputsVersion === this.version) {
227
+ return this.cachedOutputs;
228
+ }
229
+ const outputs = Object.freeze({
230
+ activeDate: this.state.activeDate,
231
+ cells: this.getCells(),
232
+ focusedSection: this.state.focusedSection,
233
+ getCellAttributes: (cellOrDate) => this.resolveCellAttributes(cellOrDate),
234
+ getGridAttributes: () => this.resolveGridAttributes(),
235
+ getHostAttributes: () => this.resolveHostAttributes(),
236
+ getMonthAttributes: (monthOrOption) => this.resolveMonthAttributes(monthOrOption),
237
+ getOverlayAttributes: () => this.resolveOverlayAttributes(),
238
+ getTriggerAttributes: () => this.resolveTriggerAttributes(),
239
+ getYearAttributes: (yearOrOption) => this.resolveYearAttributes(yearOrOption),
240
+ inputText: this.state.inputText,
241
+ labelMonthYear: this.config.adapter.format(this.state.visibleMonth, 'month-year', this.config.locale),
242
+ layout: this.resolveLayout(),
243
+ monthOptions: this.getMonthOptions(),
244
+ open: this.state.open,
245
+ validationError: this.state.validationError,
246
+ value: this.state.value,
247
+ view: this.state.view,
248
+ visibleMonth: this.state.visibleMonth,
249
+ weekdayLabels: this.getWeekdayLabels(),
250
+ yearOptions: this.getYearOptions(),
251
+ });
252
+ this.cachedOutputsVersion = this.version;
253
+ this.cachedOutputs = outputs;
254
+ return outputs;
255
+ }
256
+ getState() {
257
+ return Object.freeze({
258
+ activeDate: this.state.activeDate,
259
+ disabled: this.state.disabled,
260
+ focusedSection: this.state.focusedSection,
261
+ inputText: this.state.inputText,
262
+ lastCloseReason: this.state.lastCloseReason,
263
+ open: this.state.open,
264
+ validationError: this.state.validationError,
265
+ value: this.state.value,
266
+ view: this.state.view,
267
+ visibleMonth: this.state.visibleMonth,
268
+ });
269
+ }
270
+ goToNextMonth() {
271
+ this.nextMonth();
272
+ }
273
+ goToPrevMonth() {
274
+ this.prevMonth();
275
+ }
276
+ handleCellClick(date, options = {}) {
277
+ this.selectDate(date, {
278
+ shiftKey: options.shiftKey,
279
+ trigger: 'pointer',
280
+ });
281
+ }
282
+ handleCellPointerEnter(date) {
283
+ if (this.destroyed || this.config.selectionMode !== 'range' || this.rangeAnchorDate === null) {
284
+ return;
285
+ }
286
+ const normalized = normalizeDateInput(this.config.adapter, date, this.config.locale);
287
+ if (normalized === null) {
288
+ return;
289
+ }
290
+ this.hoverDate = normalized;
291
+ this.bumpVersion();
292
+ }
293
+ handleGridKeyDown(event) {
294
+ if (this.destroyed || this.state.disabled) {
295
+ return;
296
+ }
297
+ if (event.key === 'Escape') {
298
+ if (this.state.open) {
299
+ event.preventDefault();
300
+ this.close('escape');
301
+ }
302
+ return;
303
+ }
304
+ if (this.handlePageNavigation(event)) {
305
+ return;
306
+ }
307
+ const action = resolveGridNavigationKeyAction(event, {
308
+ direction: this.config.direction,
309
+ });
310
+ if (action === null) {
311
+ if (this.config.enableTypeahead) {
312
+ this.handleTypeahead(event.key);
313
+ }
314
+ return;
315
+ }
316
+ if (action.preventDefault) {
317
+ event.preventDefault();
318
+ }
319
+ if (action.type === 'exit') {
320
+ return;
321
+ }
322
+ if (action.type === 'activate') {
323
+ this.selectDate(this.state.activeDate, {
324
+ shiftKey: event.shiftKey === true,
325
+ trigger: 'keyboard',
326
+ });
327
+ return;
328
+ }
329
+ this.handleResolvedDayGridAction(action.type, event.shiftKey === true);
330
+ }
331
+ handleMonthGridKeyDown(event) {
332
+ this.handlePickerGridKeyDown(event, 'month');
333
+ }
334
+ handleOverlayKeyDown(event) {
335
+ if (this.destroyed || !this.config.trapFocus || event.key !== 'Tab') {
336
+ return;
337
+ }
338
+ const overlay = this.overlayElement;
339
+ if (overlay === null || !datepickerFocusHandoff.isTrapActive(this.instanceId)) {
340
+ return;
341
+ }
342
+ const focusableMemberIds = this.resolveTabbableOverlayMemberIds(overlay);
343
+ const firstMemberId = focusableMemberIds[0];
344
+ if (firstMemberId === undefined) {
345
+ return;
346
+ }
347
+ const lastMemberId = focusableMemberIds[focusableMemberIds.length - 1] ?? firstMemberId;
348
+ const activeElement = this.resolveActiveElement();
349
+ const activeElementId = activeElement !== null && overlay.contains(activeElement) ? this.ensureElementId(activeElement) : null;
350
+ let candidateId = null;
351
+ if (activeElementId === null) {
352
+ candidateId = event.shiftKey ? lastMemberId : firstMemberId;
353
+ }
354
+ else if (activeElementId === firstMemberId && event.shiftKey) {
355
+ candidateId = lastMemberId;
356
+ }
357
+ else if (activeElementId === lastMemberId && !event.shiftKey) {
358
+ candidateId = firstMemberId;
359
+ }
360
+ else {
361
+ datepickerFocusHandoff.recordFocus(this.instanceId, activeElementId);
362
+ return;
363
+ }
364
+ const resolvedId = datepickerFocusHandoff.resolveFocusCandidate(this.instanceId, candidateId);
365
+ if (resolvedId === null) {
366
+ return;
367
+ }
368
+ const nextFocusTarget = this.resolveElementById(resolvedId);
369
+ if (nextFocusTarget === null) {
370
+ return;
371
+ }
372
+ event.preventDefault();
373
+ nextFocusTarget.focus();
374
+ datepickerFocusHandoff.recordFocus(this.instanceId, resolvedId);
375
+ }
376
+ handleTriggerKeyDown(event) {
377
+ if (this.destroyed || this.state.disabled) {
378
+ return;
379
+ }
380
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
381
+ event.preventDefault();
382
+ this.open();
383
+ }
384
+ }
385
+ handleYearGridKeyDown(event) {
386
+ this.handlePickerGridKeyDown(event, 'year');
387
+ }
388
+ nextMonth() {
389
+ this.shiftVisibleMonth(1);
390
+ }
391
+ nextYear() {
392
+ this.shiftVisibleYear(1);
393
+ }
394
+ open() {
395
+ if (this.destroyed || this.state.disabled || this.state.open) {
396
+ return;
397
+ }
398
+ if (this.config.closeOthersOnOpen) {
399
+ for (const controller of datepickerRegistry) {
400
+ if (controller === this) {
401
+ continue;
402
+ }
403
+ controller.close('programmatic');
404
+ }
405
+ }
406
+ this.emit({ previous: false, type: 'openStart' });
407
+ this.state.open = true;
408
+ const activeElement = this.resolveActiveElement();
409
+ if (activeElement !== null) {
410
+ this.restoreFocusTargetId = this.ensureElementId(activeElement);
411
+ }
412
+ else if (this.triggerElement !== null) {
413
+ this.restoreFocusTargetId = this.ensureElementId(this.triggerElement);
414
+ }
415
+ else {
416
+ this.restoreFocusTargetId = null;
417
+ }
418
+ this.state.lastCloseReason = null;
419
+ if (!this.config.preserveViewOnOpenClose) {
420
+ this.state.view = this.config.initialView;
421
+ this.state.focusedSection = resolveInitialFocusedSection(this.state.view);
422
+ }
423
+ this.applyActiveDate(this.resolveInitialActiveDate(this.state.value, this.config.today), 'programmatic', true);
424
+ this.registerOverlayLayer();
425
+ this.activateFocusLayer();
426
+ this.focusCurrentOverlayTarget();
427
+ this.bumpVersion();
428
+ this.emit({ next: true, previous: false, type: 'opened' });
429
+ }
430
+ parseInputText(text) {
431
+ return this.config.adapter.parse(text, this.config.locale);
432
+ }
433
+ prevMonth() {
434
+ this.shiftVisibleMonth(-1);
435
+ }
436
+ prevYear() {
437
+ this.shiftVisibleYear(-1);
438
+ }
439
+ registerOverlay(element) {
440
+ this.overlayElement = element;
441
+ if (this.state.open) {
442
+ this.registerOverlayLayer();
443
+ this.registerFocusLayer();
444
+ this.activateFocusLayer();
445
+ this.focusCurrentOverlayTarget();
446
+ this.bumpVersion();
447
+ }
448
+ else if (element === null) {
449
+ this.unregisterFocusLayer();
450
+ }
451
+ }
452
+ registerTrigger(element) {
453
+ this.triggerElement = element;
454
+ }
455
+ selectDate(date, options = {}) {
456
+ if (this.destroyed || this.state.disabled) {
457
+ return;
458
+ }
459
+ const normalized = normalizeDateInput(this.config.adapter, date, this.config.locale);
460
+ if (normalized === null) {
461
+ this.state.validationError = 'invalid-value';
462
+ this.bumpVersion();
463
+ return;
464
+ }
465
+ if (this.isDateDisabled(normalized)) {
466
+ return;
467
+ }
468
+ const trigger = options.trigger ?? 'programmatic';
469
+ const previousValue = this.state.value;
470
+ this.state.validationError = null;
471
+ const nextValue = this.resolveNextSelection(normalized, options.shiftKey === true);
472
+ if (selectionValuesEqual(this.config.adapter, this.config.selectionMode, previousValue, nextValue)) {
473
+ if (this.state.validationError !== null) {
474
+ this.bumpVersion();
475
+ }
476
+ return;
477
+ }
478
+ this.state.value = nextValue;
479
+ this.state.inputText = this.formatValueForInput(nextValue);
480
+ this.rangeAnchorDate = this.extractSelectionAnchor(nextValue) ?? normalized;
481
+ this.applyActiveDate(normalized, trigger, true);
482
+ this.bumpVersion();
483
+ this.emit({
484
+ previousValue,
485
+ trigger,
486
+ type: 'valueChange',
487
+ value: nextValue,
488
+ });
489
+ if (this.config.closeOnSelect && this.state.open) {
490
+ this.close('select');
491
+ }
492
+ }
493
+ selectMonth(monthIndex) {
494
+ if (this.destroyed) {
495
+ return;
496
+ }
497
+ const normalizedMonth = Math.max(0, Math.min(11, Math.trunc(monthIndex)));
498
+ const nextMonth = this.config.adapter.createDate(this.config.adapter.getYear(this.state.visibleMonth), normalizedMonth, 1);
499
+ if (!hasSelectableDateInMonth(this.config.adapter, nextMonth, (date) => this.isDateDisabled(date))) {
500
+ return;
501
+ }
502
+ const previousMonth = this.state.visibleMonth;
503
+ this.state.visibleMonth = nextMonth;
504
+ const nextActive = clampDateToMonth(this.config.adapter, this.state.activeDate, nextMonth);
505
+ this.applyActiveDate(nextActive, 'programmatic', true);
506
+ this.bumpVersion();
507
+ this.emit({
508
+ previousMonth,
509
+ type: 'monthChange',
510
+ visibleMonth: nextMonth,
511
+ });
512
+ if (this.config.autoCommitView) {
513
+ this.setView('day');
514
+ }
515
+ }
516
+ selectYear(year) {
517
+ if (this.destroyed) {
518
+ return;
519
+ }
520
+ const normalizedYear = Math.trunc(year);
521
+ if (!hasSelectableDateInYear(this.config.adapter, normalizedYear, this.state.visibleMonth, (date) => this.isDateDisabled(date))) {
522
+ return;
523
+ }
524
+ const previousMonth = this.state.visibleMonth;
525
+ const previousYear = this.config.adapter.getYear(previousMonth);
526
+ const nextMonth = this.config.adapter.createDate(normalizedYear, this.config.adapter.getMonth(previousMonth), 1);
527
+ this.state.visibleMonth = nextMonth;
528
+ this.yearPageStart = this.resolveCenteredYearPageStart(normalizedYear);
529
+ const nextActive = clampDateToMonth(this.config.adapter, this.state.activeDate, nextMonth);
530
+ this.applyActiveDate(nextActive, 'programmatic', true);
531
+ this.bumpVersion();
532
+ this.emit({
533
+ previousMonth,
534
+ type: 'monthChange',
535
+ visibleMonth: nextMonth,
536
+ });
537
+ this.emit({
538
+ previousYear,
539
+ type: 'yearChange',
540
+ year: normalizedYear,
541
+ });
542
+ if (this.config.autoCommitView) {
543
+ this.setView('day');
544
+ }
545
+ }
546
+ setActiveDate(date, trigger = 'programmatic') {
547
+ if (this.destroyed) {
548
+ return;
549
+ }
550
+ const normalized = normalizeDateInput(this.config.adapter, date, this.config.locale);
551
+ if (normalized === null) {
552
+ return;
553
+ }
554
+ this.applyActiveDate(normalized, trigger, true);
555
+ this.bumpVersion();
556
+ }
557
+ setConfig(config) {
558
+ if (this.destroyed) {
559
+ return;
560
+ }
561
+ const previousMonth = this.state.visibleMonth;
562
+ const previousYear = this.config.adapter.getYear(previousMonth);
563
+ const previousTrapFocus = this.config.trapFocus;
564
+ this.config = this.resolveConfig(config, this.config);
565
+ this.state.disabled = this.config.disabled;
566
+ this.state.value = this.coerceSelectionWithinConstraints(this.state.value);
567
+ this.state.activeDate = this.resolveValidDate(this.state.activeDate, this.state.visibleMonth);
568
+ if (!isDateValueInMonth(this.config.adapter, this.state.activeDate, this.state.visibleMonth)) {
569
+ this.state.visibleMonth = this.config.adapter.startOfMonth(this.state.activeDate);
570
+ }
571
+ this.yearPageStart = this.resolveCenteredYearPageStart(this.config.adapter.getYear(this.state.activeDate));
572
+ this.state.inputText = this.formatValueForInput(this.state.value);
573
+ if (this.focusLayerRegistered && previousTrapFocus !== this.config.trapFocus) {
574
+ this.unregisterFocusLayer();
575
+ this.registerFocusLayer();
576
+ if (this.state.open) {
577
+ this.activateFocusLayer();
578
+ }
579
+ }
580
+ this.bumpVersion();
581
+ if (compareMonthIdentity(this.config.adapter, previousMonth, this.state.visibleMonth) !== 0) {
582
+ this.emit({
583
+ previousMonth,
584
+ type: 'monthChange',
585
+ visibleMonth: this.state.visibleMonth,
586
+ });
587
+ }
588
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
589
+ if (previousYear !== currentYear) {
590
+ this.emit({
591
+ previousYear,
592
+ type: 'yearChange',
593
+ year: currentYear,
594
+ });
595
+ }
596
+ }
597
+ setDisabled(disabled) {
598
+ if (this.destroyed || this.state.disabled === disabled) {
599
+ return;
600
+ }
601
+ this.state.disabled = disabled;
602
+ this.bumpVersion();
603
+ }
604
+ setFocusedSection(section) {
605
+ if (this.destroyed || this.state.focusedSection === section) {
606
+ return;
607
+ }
608
+ this.state.focusedSection = section;
609
+ this.bumpVersion();
610
+ }
611
+ setInputText(text) {
612
+ if (this.destroyed || !this.config.allowManualInput) {
613
+ return;
614
+ }
615
+ this.state.inputText = text;
616
+ this.state.validationError = null;
617
+ if (this.config.onPartialInputCommit) {
618
+ this.commitInputText();
619
+ return;
620
+ }
621
+ this.bumpVersion();
622
+ }
623
+ setOpen(open) {
624
+ if (open) {
625
+ this.open();
626
+ return;
627
+ }
628
+ this.close('programmatic');
629
+ }
630
+ setState(patch) {
631
+ if (this.destroyed) {
632
+ return;
633
+ }
634
+ const previousView = this.state.view;
635
+ const previousMonth = this.state.visibleMonth;
636
+ const previousYear = this.config.adapter.getYear(previousMonth);
637
+ const previousActive = this.state.activeDate;
638
+ const previousValue = this.state.value;
639
+ if (patch.view !== undefined) {
640
+ this.state.view = normalizeView(patch.view);
641
+ this.state.focusedSection = resolveInitialFocusedSection(this.state.view);
642
+ }
643
+ if (patch.disabled !== undefined) {
644
+ this.state.disabled = patch.disabled;
645
+ }
646
+ if (patch.visibleMonth !== undefined) {
647
+ const normalized = normalizeDateInput(this.config.adapter, patch.visibleMonth, this.config.locale);
648
+ if (normalized !== null) {
649
+ this.state.visibleMonth = this.config.adapter.startOfMonth(normalized);
650
+ }
651
+ }
652
+ if (patch.activeDate !== undefined) {
653
+ const normalized = normalizeDateInput(this.config.adapter, patch.activeDate, this.config.locale);
654
+ if (normalized !== null) {
655
+ this.state.activeDate = this.resolveValidDate(normalized, this.state.visibleMonth);
656
+ }
657
+ }
658
+ if (patch.value !== undefined) {
659
+ const normalized = normalizeSelectionInput(this.config.adapter, this.config.selectionMode, patch.value, this.config.locale);
660
+ this.state.value = this.coerceSelectionWithinConstraints(normalized.value);
661
+ }
662
+ if (patch.inputText !== undefined) {
663
+ this.state.inputText = patch.inputText;
664
+ }
665
+ else {
666
+ this.state.inputText = this.formatValueForInput(this.state.value);
667
+ }
668
+ this.bumpVersion();
669
+ if (previousView !== this.state.view) {
670
+ this.emit({
671
+ previousView,
672
+ type: 'viewChange',
673
+ view: this.state.view,
674
+ });
675
+ }
676
+ if (compareMonthIdentity(this.config.adapter, previousMonth, this.state.visibleMonth) !== 0) {
677
+ this.emit({
678
+ previousMonth,
679
+ type: 'monthChange',
680
+ visibleMonth: this.state.visibleMonth,
681
+ });
682
+ }
683
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
684
+ if (previousYear !== currentYear) {
685
+ this.emit({
686
+ previousYear,
687
+ type: 'yearChange',
688
+ year: currentYear,
689
+ });
690
+ }
691
+ if (!datesEqual(this.config.adapter, previousActive, this.state.activeDate)) {
692
+ this.emit({
693
+ activeDate: this.state.activeDate,
694
+ previousActiveDate: previousActive,
695
+ trigger: 'programmatic',
696
+ type: 'activeChange',
697
+ });
698
+ }
699
+ if (!selectionValuesEqual(this.config.adapter, this.config.selectionMode, previousValue, this.state.value)) {
700
+ this.emit({
701
+ previousValue,
702
+ trigger: 'programmatic',
703
+ type: 'valueChange',
704
+ value: this.state.value,
705
+ });
706
+ }
707
+ }
708
+ setValue(value) {
709
+ if (this.destroyed) {
710
+ return;
711
+ }
712
+ const normalized = normalizeSelectionInput(this.config.adapter, this.config.selectionMode, value, this.config.locale);
713
+ if (normalized.validationError !== null) {
714
+ this.state.validationError = normalized.validationError;
715
+ }
716
+ const coerced = this.coerceSelectionWithinConstraints(normalized.value);
717
+ if (selectionValuesEqual(this.config.adapter, this.config.selectionMode, this.state.value, coerced)) {
718
+ return;
719
+ }
720
+ const previousValue = this.state.value;
721
+ this.state.value = coerced;
722
+ this.state.validationError = normalized.validationError;
723
+ this.state.inputText = this.formatValueForInput(coerced);
724
+ const anchor = this.extractSelectionAnchor(coerced);
725
+ if (anchor !== null) {
726
+ this.applyActiveDate(anchor, 'programmatic', true);
727
+ }
728
+ this.bumpVersion();
729
+ this.emit({
730
+ previousValue,
731
+ trigger: 'programmatic',
732
+ type: 'valueChange',
733
+ value: coerced,
734
+ });
735
+ }
736
+ setView(view) {
737
+ if (this.destroyed) {
738
+ return;
739
+ }
740
+ const normalizedView = normalizeView(view);
741
+ if (this.state.view === normalizedView) {
742
+ return;
743
+ }
744
+ const previousView = this.state.view;
745
+ const previousMonth = this.state.visibleMonth;
746
+ const previousYear = this.config.adapter.getYear(previousMonth);
747
+ this.state.view = normalizedView;
748
+ this.state.focusedSection = resolveInitialFocusedSection(normalizedView);
749
+ if (normalizedView === 'year') {
750
+ this.yearPageStart = this.resolveCenteredYearPageStart(this.config.adapter.getYear(this.state.activeDate));
751
+ }
752
+ if (normalizedView === 'day' &&
753
+ !isDateValueInMonth(this.config.adapter, this.state.activeDate, this.state.visibleMonth)) {
754
+ this.state.visibleMonth = this.config.adapter.startOfMonth(this.state.activeDate);
755
+ }
756
+ this.bumpVersion();
757
+ this.emit({
758
+ previousView,
759
+ type: 'viewChange',
760
+ view: normalizedView,
761
+ });
762
+ if (compareMonthIdentity(this.config.adapter, previousMonth, this.state.visibleMonth) !== 0) {
763
+ this.emit({
764
+ previousMonth,
765
+ type: 'monthChange',
766
+ visibleMonth: this.state.visibleMonth,
767
+ });
768
+ }
769
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
770
+ if (previousYear !== currentYear) {
771
+ this.emit({
772
+ previousYear,
773
+ type: 'yearChange',
774
+ year: currentYear,
775
+ });
776
+ }
777
+ }
778
+ setVisibleMonth(valueOrYear, month) {
779
+ if (this.destroyed) {
780
+ return;
781
+ }
782
+ let nextMonth;
783
+ if (typeof valueOrYear === 'number') {
784
+ if (month === undefined) {
785
+ return;
786
+ }
787
+ nextMonth = this.config.adapter.createDate(Math.trunc(valueOrYear), Math.trunc(month), 1);
788
+ }
789
+ else {
790
+ const normalized = normalizeDateInput(this.config.adapter, valueOrYear, this.config.locale);
791
+ nextMonth = normalized === null ? null : this.config.adapter.startOfMonth(normalized);
792
+ }
793
+ if (nextMonth === null) {
794
+ return;
795
+ }
796
+ if (!hasSelectableDateInMonth(this.config.adapter, nextMonth, (date) => this.isDateDisabled(date))) {
797
+ return;
798
+ }
799
+ const previousMonth = this.state.visibleMonth;
800
+ const previousYear = this.config.adapter.getYear(previousMonth);
801
+ this.state.visibleMonth = this.config.adapter.startOfMonth(nextMonth);
802
+ this.yearPageStart = this.resolveCenteredYearPageStart(this.config.adapter.getYear(this.state.visibleMonth));
803
+ this.state.activeDate = this.resolveValidDate(this.state.activeDate, this.state.visibleMonth);
804
+ this.bumpVersion();
805
+ if (compareMonthIdentity(this.config.adapter, previousMonth, this.state.visibleMonth) !== 0) {
806
+ this.emit({
807
+ previousMonth,
808
+ type: 'monthChange',
809
+ visibleMonth: this.state.visibleMonth,
810
+ });
811
+ }
812
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
813
+ if (previousYear !== currentYear) {
814
+ this.emit({
815
+ previousYear,
816
+ type: 'yearChange',
817
+ year: currentYear,
818
+ });
819
+ }
820
+ }
821
+ setVisibleYear(year) {
822
+ this.selectYear(year);
823
+ }
824
+ showDaysPanel() {
825
+ this.setView('day');
826
+ }
827
+ showMonthsPanel() {
828
+ this.setView('month');
829
+ }
830
+ showYearsPanel() {
831
+ this.setView('year');
832
+ }
833
+ subscribe(listener) {
834
+ this.listenerSet.add(listener);
835
+ return () => {
836
+ this.listenerSet.delete(listener);
837
+ };
838
+ }
839
+ toggleOpen() {
840
+ if (this.state.open) {
841
+ this.close('programmatic');
842
+ return;
843
+ }
844
+ this.open();
845
+ }
846
+ applyActiveDate(nextDate, trigger, focusVisible) {
847
+ const resolved = this.resolveValidDate(nextDate, this.state.visibleMonth);
848
+ if (datesEqual(this.config.adapter, resolved, this.state.activeDate)) {
849
+ this.focusVisibleDate = focusVisible ? resolved : null;
850
+ this.bumpVersion();
851
+ return;
852
+ }
853
+ const previousActiveDate = this.state.activeDate;
854
+ this.state.activeDate = resolved;
855
+ this.focusVisibleDate = focusVisible ? resolved : null;
856
+ if (!isDateValueInMonth(this.config.adapter, resolved, this.state.visibleMonth)) {
857
+ const previousMonth = this.state.visibleMonth;
858
+ const previousYear = this.config.adapter.getYear(previousMonth);
859
+ this.state.visibleMonth = this.config.adapter.startOfMonth(resolved);
860
+ this.emit({
861
+ previousMonth,
862
+ type: 'monthChange',
863
+ visibleMonth: this.state.visibleMonth,
864
+ });
865
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
866
+ if (previousYear !== currentYear) {
867
+ this.emit({
868
+ previousYear,
869
+ type: 'yearChange',
870
+ year: currentYear,
871
+ });
872
+ }
873
+ }
874
+ this.emit({
875
+ activeDate: resolved,
876
+ previousActiveDate,
877
+ trigger,
878
+ type: 'activeChange',
879
+ });
880
+ this.bumpVersion();
881
+ }
882
+ applyPickerActiveDate(nextDate, trigger, focusVisible) {
883
+ const resolved = this.resolveValidDate(nextDate, this.config.adapter.startOfMonth(nextDate));
884
+ if (datesEqual(this.config.adapter, resolved, this.state.activeDate)) {
885
+ this.focusVisibleDate = focusVisible ? resolved : null;
886
+ this.bumpVersion();
887
+ return;
888
+ }
889
+ const previousActiveDate = this.state.activeDate;
890
+ this.state.activeDate = resolved;
891
+ this.focusVisibleDate = focusVisible ? resolved : null;
892
+ this.emit({
893
+ activeDate: resolved,
894
+ previousActiveDate,
895
+ trigger,
896
+ type: 'activeChange',
897
+ });
898
+ this.bumpVersion();
899
+ }
900
+ bumpVersion() {
901
+ this.version += 1;
902
+ }
903
+ coerceSelectionWithinConstraints(value) {
904
+ if (value === null) {
905
+ return clearSelectionForMode(this.config.selectionMode);
906
+ }
907
+ if (this.config.selectionMode === 'single') {
908
+ const date = value;
909
+ if (this.isDateDisabled(date)) {
910
+ this.state.validationError = 'out-of-range';
911
+ return null;
912
+ }
913
+ return date;
914
+ }
915
+ if (this.config.selectionMode === 'multiple') {
916
+ const values = value.filter((date) => !this.isDateDisabled(date));
917
+ if (value.length !== values.length) {
918
+ this.state.validationError = 'out-of-range';
919
+ }
920
+ return Object.freeze(values);
921
+ }
922
+ const range = normalizeRangeOrder(this.config.adapter, value);
923
+ if (range.start === null) {
924
+ return Object.freeze({ end: null, start: null });
925
+ }
926
+ if (this.isDateDisabled(range.start)) {
927
+ this.state.validationError = 'out-of-range';
928
+ return Object.freeze({ end: null, start: null });
929
+ }
930
+ if (range.end === null) {
931
+ return range;
932
+ }
933
+ if (this.isDateDisabled(range.end) || this.rangeContainsDisabledDate(range.start, range.end)) {
934
+ this.state.validationError = 'range-disabled';
935
+ return Object.freeze({ end: null, start: range.start });
936
+ }
937
+ return range;
938
+ }
939
+ emit(event) {
940
+ for (const listener of this.listenerSet) {
941
+ listener(event);
942
+ }
943
+ }
944
+ extractSelectionAnchor(value) {
945
+ if (value === null) {
946
+ return null;
947
+ }
948
+ if (Array.isArray(value)) {
949
+ return value.length > 0 ? value[value.length - 1] : null;
950
+ }
951
+ if (isRangeSelectionValue(value)) {
952
+ return value.end ?? value.start;
953
+ }
954
+ return value;
955
+ }
956
+ formatValueForInput(value) {
957
+ if (value === null) {
958
+ return '';
959
+ }
960
+ if (this.config.selectionMode === 'single') {
961
+ return this.config.adapter.format(value, 'input', this.config.locale);
962
+ }
963
+ if (this.config.selectionMode === 'multiple') {
964
+ return value
965
+ .map((date) => this.config.adapter.format(date, 'input', this.config.locale))
966
+ .join(', ');
967
+ }
968
+ const range = value;
969
+ if (range.start === null) {
970
+ return '';
971
+ }
972
+ if (range.end === null) {
973
+ return this.config.adapter.format(range.start, 'input', this.config.locale);
974
+ }
975
+ return `${this.config.adapter.format(range.start, 'input', this.config.locale)} – ${this.config.adapter.format(range.end, 'input', this.config.locale)}`;
976
+ }
977
+ isStrictInputCommitValue(inputText, parsed) {
978
+ const canonicalInputText = this.config.adapter.format(parsed, 'input', this.config.locale).trim();
979
+ return inputText === canonicalInputText;
980
+ }
981
+ getCells() {
982
+ if (this.cachedGridVersion === this.version) {
983
+ return this.cachedCells;
984
+ }
985
+ const cells = buildMonthGrid({
986
+ activeDate: this.state.activeDate,
987
+ adapter: this.config.adapter,
988
+ createCellId: (date) => `${this.instanceId}-cell-${toDateKey(this.config.adapter, date)}`,
989
+ fixedWeeks: this.config.fixedWeeks,
990
+ focusStrategy: this.config.focusStrategy,
991
+ focusedDate: this.focusVisibleDate,
992
+ inRange: (date) => rangeIncludesDate(this.config.adapter, this.state.value, date),
993
+ isDisabled: (date) => this.isDateDisabled(date),
994
+ isRangeEnd: (date) => {
995
+ const range = this.state.value;
996
+ return (isRangeSelectionValue(range) &&
997
+ range.end !== null &&
998
+ datesEqual(this.config.adapter, range.end, date));
999
+ },
1000
+ isRangeStart: (date) => {
1001
+ const range = this.state.value;
1002
+ return (isRangeSelectionValue(range) &&
1003
+ range.start !== null &&
1004
+ datesEqual(this.config.adapter, range.start, date));
1005
+ },
1006
+ isSelected: (date) => valueIncludesDate(this.config.adapter, this.config.selectionMode, this.state.value, date),
1007
+ locale: this.config.locale,
1008
+ showOutsideDays: this.config.showOutsideDays,
1009
+ today: this.config.today,
1010
+ visibleMonth: this.state.visibleMonth,
1011
+ weekStartsOn: this.config.weekStartsOn,
1012
+ });
1013
+ this.syncDayFocusControllers(cells);
1014
+ if (this.config.enableTypeahead) {
1015
+ this.typeahead.setItems(cells
1016
+ .filter((cell) => !cell.disabled && !cell.hidden)
1017
+ .map((cell) => ({ disabled: false, id: cell.id, text: cell.label })));
1018
+ }
1019
+ this.cachedCells = cells;
1020
+ this.cachedGridVersion = this.version;
1021
+ return cells;
1022
+ }
1023
+ getMonthOptions() {
1024
+ if (this.cachedMonthVersion === this.version) {
1025
+ return this.cachedMonthOptions;
1026
+ }
1027
+ const options = buildMonthOptions({
1028
+ activeDate: this.state.activeDate,
1029
+ adapter: this.config.adapter,
1030
+ createId: (month) => `${this.instanceId}-month-${month}`,
1031
+ disabledMonth: (month) => !hasSelectableDateInMonth(this.config.adapter, this.config.adapter.createDate(this.config.adapter.getYear(this.state.visibleMonth), month, 1), (date) => this.isDateDisabled(date)),
1032
+ focusedDate: this.focusVisibleDate,
1033
+ focusedSection: this.state.focusedSection,
1034
+ locale: this.config.locale,
1035
+ selectedMonth: (month) => {
1036
+ if (this.config.selectionMode === 'multiple') {
1037
+ return false;
1038
+ }
1039
+ const anchor = this.extractSelectionAnchor(this.state.value);
1040
+ return anchor !== null && this.config.adapter.getMonth(anchor) === month;
1041
+ },
1042
+ visibleMonth: this.state.visibleMonth,
1043
+ });
1044
+ this.syncMonthFocusController(options);
1045
+ this.cachedMonthOptions = options;
1046
+ this.cachedMonthVersion = this.version;
1047
+ return options;
1048
+ }
1049
+ getWeekdayLabels() {
1050
+ if (this.cachedWeekdayLabels.length > 0 && this.cachedGridVersion === this.version) {
1051
+ return this.cachedWeekdayLabels;
1052
+ }
1053
+ const start = this.config.adapter.startOfWeek(this.config.today, this.config.weekStartsOn);
1054
+ const labels = Array.from({ length: 7 }, (_, index) => this.config.adapter.format(this.config.adapter.addDays(start, index), 'weekday-short', this.config.locale));
1055
+ this.cachedWeekdayLabels = Object.freeze(labels);
1056
+ return this.cachedWeekdayLabels;
1057
+ }
1058
+ getYearOptions() {
1059
+ if (this.cachedYearVersion === this.version) {
1060
+ return this.cachedYearOptions;
1061
+ }
1062
+ const totalYears = this.config.yearPageSize;
1063
+ const options = buildYearOptions({
1064
+ activeDate: this.state.activeDate,
1065
+ adapter: this.config.adapter,
1066
+ createId: (year) => `${this.instanceId}-year-${year}`,
1067
+ disabledYear: (year) => !hasSelectableDateInYear(this.config.adapter, year, this.state.visibleMonth, (date) => this.isDateDisabled(date)),
1068
+ focusedDate: this.focusVisibleDate,
1069
+ focusedSection: this.state.focusedSection,
1070
+ locale: this.config.locale,
1071
+ selectedYear: (year) => {
1072
+ if (this.config.selectionMode === 'multiple') {
1073
+ return false;
1074
+ }
1075
+ const anchor = this.extractSelectionAnchor(this.state.value);
1076
+ return anchor !== null && this.config.adapter.getYear(anchor) === year;
1077
+ },
1078
+ startYear: this.yearPageStart,
1079
+ totalYears,
1080
+ visibleMonth: this.state.visibleMonth,
1081
+ });
1082
+ this.syncYearFocusController(options);
1083
+ this.cachedYearOptions = options;
1084
+ this.cachedYearVersion = this.version;
1085
+ return options;
1086
+ }
1087
+ handlePageNavigation(event) {
1088
+ if (event.key === 'PageUp' || event.key === 'PageDown') {
1089
+ event.preventDefault();
1090
+ const direction = event.key === 'PageDown' ? 1 : -1;
1091
+ const useYear = event.ctrlKey === true || event.metaKey === true || event.shiftKey === true;
1092
+ const nextActive = useYear
1093
+ ? this.config.adapter.addYears(this.state.activeDate, direction)
1094
+ : this.config.adapter.addMonths(this.state.activeDate, direction);
1095
+ this.applyActiveDate(nextActive, 'keyboard', true);
1096
+ return true;
1097
+ }
1098
+ return false;
1099
+ }
1100
+ handlePickerGridKeyDown(event, view) {
1101
+ if (this.destroyed || this.state.disabled) {
1102
+ return;
1103
+ }
1104
+ const options = view === 'month' ? this.getMonthOptions() : this.getYearOptions();
1105
+ const activeIndex = options.findIndex((option) => option.active);
1106
+ if (event.key === 'Escape') {
1107
+ const nextView = resolveViewForEscape(this.state.view);
1108
+ if (nextView === null) {
1109
+ this.close('escape');
1110
+ }
1111
+ else {
1112
+ this.setView(nextView);
1113
+ }
1114
+ event.preventDefault();
1115
+ return;
1116
+ }
1117
+ const action = resolveGridNavigationKeyAction(event, {
1118
+ direction: this.config.direction,
1119
+ });
1120
+ if (action === null) {
1121
+ return;
1122
+ }
1123
+ if (action.preventDefault) {
1124
+ event.preventDefault();
1125
+ }
1126
+ if (action.type === 'exit') {
1127
+ return;
1128
+ }
1129
+ if (action.type === 'activate') {
1130
+ const activeOption = options[Math.max(0, activeIndex)];
1131
+ if (activeOption === undefined || activeOption.disabled) {
1132
+ return;
1133
+ }
1134
+ if (view === 'month') {
1135
+ this.selectMonth(activeOption.index);
1136
+ }
1137
+ else {
1138
+ this.selectYear(activeOption.year);
1139
+ }
1140
+ return;
1141
+ }
1142
+ const currentOption = options[Math.max(0, activeIndex)];
1143
+ if (currentOption === undefined) {
1144
+ return;
1145
+ }
1146
+ const currentPosition = this.optionIndexToGridPosition(activeIndex, 4);
1147
+ const nextPosition = resolveNavigableGridCell(currentPosition, action.type, {
1148
+ bounds: {
1149
+ colCount: 4,
1150
+ rowCount: Math.ceil(options.length / 4),
1151
+ },
1152
+ cells: options.map((option, index) => ({
1153
+ col: index % 4,
1154
+ disabled: option.disabled,
1155
+ row: Math.floor(index / 4),
1156
+ })),
1157
+ });
1158
+ const yearDelta = view === 'year' ? this.resolveYearBoundaryDelta(action.type) : null;
1159
+ const monthDelta = view === 'month' ? this.resolveMonthBoundaryDelta(action.type) : null;
1160
+ const isBoundaryClamp = ((view === 'year' && yearDelta !== null) || (view === 'month' && monthDelta !== null)) &&
1161
+ nextPosition !== null &&
1162
+ nextPosition.row === currentPosition.row &&
1163
+ nextPosition.col === currentPosition.col;
1164
+ if (nextPosition === null || isBoundaryClamp) {
1165
+ if (view === 'year' && yearDelta !== null) {
1166
+ this.state.focusedSection = 'year';
1167
+ const fallbackYearOption = action.type === 'move-up' || action.type === 'move-down'
1168
+ ? this.findYearOptionInVerticalDirection(options, activeIndex, action.type)
1169
+ : null;
1170
+ if (fallbackYearOption !== null) {
1171
+ this.applyPickerActiveDate(fallbackYearOption.date, 'keyboard', true);
1172
+ return;
1173
+ }
1174
+ const targetYear = currentOption.year + yearDelta;
1175
+ if (!hasSelectableDateInYear(this.config.adapter, targetYear, this.state.visibleMonth, (date) => this.isDateDisabled(date))) {
1176
+ return;
1177
+ }
1178
+ this.shiftYearPageToInclude(targetYear);
1179
+ this.applyPickerActiveDate(this.config.adapter.createDate(targetYear, this.config.adapter.getMonth(this.state.visibleMonth), 1), 'keyboard', true);
1180
+ return;
1181
+ }
1182
+ if (view === 'month' && monthDelta !== null) {
1183
+ this.state.focusedSection = 'month';
1184
+ const targetMonth = this.config.adapter.addMonths(currentOption.date, monthDelta);
1185
+ if (!hasSelectableDateInMonth(this.config.adapter, targetMonth, (date) => this.isDateDisabled(date))) {
1186
+ return;
1187
+ }
1188
+ this.shiftMonthGridToTarget(targetMonth);
1189
+ this.applyPickerActiveDate(targetMonth, 'keyboard', true);
1190
+ }
1191
+ return;
1192
+ }
1193
+ const nextIndex = nextPosition.row * 4 + nextPosition.col;
1194
+ const nextOption = options[nextIndex];
1195
+ if (nextOption === undefined || nextOption.disabled) {
1196
+ return;
1197
+ }
1198
+ if (view === 'month') {
1199
+ this.state.focusedSection = 'month';
1200
+ }
1201
+ else {
1202
+ this.state.focusedSection = 'year';
1203
+ }
1204
+ this.applyPickerActiveDate(nextOption.date, 'keyboard', true);
1205
+ }
1206
+ findYearOptionInVerticalDirection(options, activeIndex, actionType) {
1207
+ if (actionType === 'move-up') {
1208
+ for (let index = activeIndex - 1; index >= 0; index -= 1) {
1209
+ const option = options[index];
1210
+ if (option !== undefined && !option.disabled) {
1211
+ return option;
1212
+ }
1213
+ }
1214
+ return null;
1215
+ }
1216
+ for (let index = activeIndex + 1; index < options.length; index += 1) {
1217
+ const option = options[index];
1218
+ if (option !== undefined && !option.disabled) {
1219
+ return option;
1220
+ }
1221
+ }
1222
+ return null;
1223
+ }
1224
+ handleResolvedDayGridAction(actionType, shiftKey) {
1225
+ const cells = this.getCells();
1226
+ const activeCell = cells.find((cell) => cell.active) ?? null;
1227
+ if (activeCell === null) {
1228
+ return;
1229
+ }
1230
+ if (actionType === 'move-left' ||
1231
+ actionType === 'move-right' ||
1232
+ actionType === 'move-up' ||
1233
+ actionType === 'move-down') {
1234
+ const delta = actionType === 'move-left'
1235
+ ? -1
1236
+ : actionType === 'move-right'
1237
+ ? 1
1238
+ : actionType === 'move-up'
1239
+ ? -7
1240
+ : 7;
1241
+ const nextDate = this.resolveDirectionalTarget(activeCell.date, delta);
1242
+ this.applyActiveDate(nextDate, 'keyboard', true);
1243
+ if (shiftKey &&
1244
+ this.config.selectionMode === 'range' &&
1245
+ this.config.enableRangeSelection) {
1246
+ this.commitRangeFromAnchor(nextDate);
1247
+ }
1248
+ return;
1249
+ }
1250
+ const nextPosition = resolveNavigableGridCell({
1251
+ col: activeCell.colIndex,
1252
+ row: activeCell.rowIndex,
1253
+ }, actionType, {
1254
+ bounds: {
1255
+ colCount: 7,
1256
+ rowCount: Math.ceil(cells.length / 7),
1257
+ },
1258
+ cells: cells.map((cell) => ({
1259
+ col: cell.colIndex,
1260
+ disabled: cell.disabled,
1261
+ row: cell.rowIndex,
1262
+ })),
1263
+ });
1264
+ if (nextPosition === null) {
1265
+ return;
1266
+ }
1267
+ const nextCell = cells.find((cell) => cell.colIndex === nextPosition.col && cell.rowIndex === nextPosition.row);
1268
+ if (nextCell === undefined || nextCell.disabled) {
1269
+ return;
1270
+ }
1271
+ this.applyActiveDate(nextCell.date, 'keyboard', true);
1272
+ }
1273
+ handleTypeahead(key) {
1274
+ if (!/^\d$/.test(key)) {
1275
+ return;
1276
+ }
1277
+ const result = this.typeahead.handleKey(key);
1278
+ if (result.activeId === null) {
1279
+ return;
1280
+ }
1281
+ const cell = this.getCells().find((item) => item.id === result.activeId);
1282
+ if (cell === undefined) {
1283
+ return;
1284
+ }
1285
+ this.applyActiveDate(cell.date, 'keyboard', true);
1286
+ }
1287
+ isDateDisabled(date) {
1288
+ if (this.state?.disabled === true) {
1289
+ return true;
1290
+ }
1291
+ if (this.config.min !== null && this.config.adapter.compare(date, this.config.min) < 0) {
1292
+ return true;
1293
+ }
1294
+ if (this.config.max !== null && this.config.adapter.compare(date, this.config.max) > 0) {
1295
+ return true;
1296
+ }
1297
+ return this.config.disableDate?.(date) ?? false;
1298
+ }
1299
+ rangeContainsDisabledDate(start, end) {
1300
+ let current = start;
1301
+ while (this.config.adapter.compare(current, end) <= 0) {
1302
+ if (this.isDateDisabled(current)) {
1303
+ return true;
1304
+ }
1305
+ current = this.config.adapter.addDays(current, 1);
1306
+ }
1307
+ return false;
1308
+ }
1309
+ registerOverlayLayer() {
1310
+ if (this.overlayLayerRegistered || this.config.overlayRuntime === null) {
1311
+ return;
1312
+ }
1313
+ this.config.overlayRuntime.registerLayer({
1314
+ containsTarget: (target) => (target instanceof Node && this.overlayElement?.contains(target) === true) ||
1315
+ (target instanceof Node && this.triggerElement?.contains(target) === true),
1316
+ dismissOnEscape: this.config.closeOnEscape,
1317
+ dismissOnOutsidePointer: this.config.closeOnOutsideClick,
1318
+ id: `${this.instanceId}-layer`,
1319
+ modal: false,
1320
+ onDismiss: (reason) => this.close(mapOverlayDismissReason(reason)),
1321
+ });
1322
+ this.overlayLayerRegistered = true;
1323
+ }
1324
+ activateFocusLayer() {
1325
+ this.registerFocusLayer();
1326
+ if (!this.focusLayerRegistered) {
1327
+ return;
1328
+ }
1329
+ datepickerFocusHandoff.activateLayer(this.instanceId, this.restoreFocusTargetId);
1330
+ }
1331
+ deactivateFocusLayer() {
1332
+ if (!this.focusLayerRegistered) {
1333
+ return;
1334
+ }
1335
+ const restoreFocusTargetId = datepickerFocusHandoff.deactivateLayer(this.instanceId);
1336
+ if (!this.config.restoreFocus || restoreFocusTargetId === null) {
1337
+ return;
1338
+ }
1339
+ const restoreFocusTarget = this.resolveElementById(restoreFocusTargetId) ?? this.triggerElement;
1340
+ restoreFocusTarget?.focus();
1341
+ }
1342
+ focusCurrentOverlayTarget() {
1343
+ if (!this.state.open || this.overlayElement === null) {
1344
+ return;
1345
+ }
1346
+ const targetId = this.resolveCurrentOverlayTargetId();
1347
+ if (targetId === null) {
1348
+ return;
1349
+ }
1350
+ const resolvedTargetId = datepickerFocusHandoff.resolveFocusCandidate(this.instanceId, targetId);
1351
+ if (resolvedTargetId === null) {
1352
+ return;
1353
+ }
1354
+ const target = this.resolveElementById(resolvedTargetId);
1355
+ if (target === null) {
1356
+ return;
1357
+ }
1358
+ target.focus();
1359
+ datepickerFocusHandoff.recordFocus(this.instanceId, resolvedTargetId);
1360
+ }
1361
+ resolveActiveElement() {
1362
+ const activeElement = this.config.ownerDocument?.activeElement;
1363
+ return activeElement instanceof HTMLElement ? activeElement : null;
1364
+ }
1365
+ resolveElementById(id) {
1366
+ if (id === null) {
1367
+ return null;
1368
+ }
1369
+ const element = this.config.ownerDocument?.getElementById(id);
1370
+ return element instanceof HTMLElement ? element : null;
1371
+ }
1372
+ resolveCellAttributes(cellOrDate) {
1373
+ const cell = 'id' in cellOrDate
1374
+ ? cellOrDate
1375
+ : this.getCells().find((candidate) => datesEqual(this.config.adapter, candidate.date, cellOrDate));
1376
+ if (cell === undefined) {
1377
+ return freezeAttributes({});
1378
+ }
1379
+ return freezeAttributes({
1380
+ 'aria-current': cell.today ? 'date' : null,
1381
+ 'aria-disabled': cell.disabled ? 'true' : null,
1382
+ 'aria-selected': cell.selected ? 'true' : 'false',
1383
+ 'data-active': cell.active ? 'true' : null,
1384
+ 'data-disabled': cell.disabled ? 'true' : null,
1385
+ 'data-focus-visible': cell.focusVisible ? 'true' : null,
1386
+ 'data-hidden': cell.hidden ? 'true' : null,
1387
+ 'data-in-month': cell.inMonth ? 'true' : null,
1388
+ 'data-in-range': cell.inRange ? 'true' : null,
1389
+ 'data-range-end': cell.rangeEnd ? 'true' : null,
1390
+ 'data-range-start': cell.rangeStart ? 'true' : null,
1391
+ 'data-selected': cell.selected ? 'true' : null,
1392
+ 'data-slot': 'datepicker-cell',
1393
+ id: cell.id,
1394
+ role: 'gridcell',
1395
+ tabindex: this.config.focusStrategy === 'roving'
1396
+ ? `${this.dayRovingFocus.getActiveId() === cell.id ? 0 : -1}`
1397
+ : '-1',
1398
+ });
1399
+ }
1400
+ resolveDirectionalTarget(currentDate, delta) {
1401
+ if (!this.config.skipDisabled) {
1402
+ return this.config.adapter.addDays(currentDate, delta);
1403
+ }
1404
+ return moveDateSkippingDisabled({
1405
+ adapter: this.config.adapter,
1406
+ delta,
1407
+ isDisabled: (date) => this.isDateDisabled(date),
1408
+ start: currentDate,
1409
+ });
1410
+ }
1411
+ resolveCurrentOverlayTargetId() {
1412
+ if (this.state.view === 'day') {
1413
+ if (this.config.focusStrategy === 'active-descendant') {
1414
+ return this.gridId;
1415
+ }
1416
+ return this.dayRovingFocus.getActiveId();
1417
+ }
1418
+ if (this.state.view === 'month') {
1419
+ return this.monthRovingFocus.getActiveId();
1420
+ }
1421
+ if (this.state.view === 'year') {
1422
+ return this.yearRovingFocus.getActiveId();
1423
+ }
1424
+ return null;
1425
+ }
1426
+ resolveGridAttributes() {
1427
+ const activeDescendantAttributes = this.config.focusStrategy === 'active-descendant'
1428
+ ? this.dayActiveDescendant.getHostAttributes()
1429
+ : null;
1430
+ return freezeAttributes({
1431
+ 'aria-activedescendant': activeDescendantAttributes?.['aria-activedescendant'] ?? null,
1432
+ 'aria-labelledby': this.monthLabelId,
1433
+ 'data-slot': 'datepicker-grid',
1434
+ id: this.gridId,
1435
+ role: 'grid',
1436
+ });
1437
+ }
1438
+ resolveHostAttributes() {
1439
+ return freezeAttributes({
1440
+ 'aria-describedby': this.config.ariaDescribedBy,
1441
+ 'aria-label': this.config.ariaLabel,
1442
+ 'aria-labelledby': this.config.ariaLabelledBy,
1443
+ 'data-disabled': this.state.disabled ? 'true' : null,
1444
+ 'data-open': this.state.open ? 'true' : 'false',
1445
+ 'data-slot': 'datepicker',
1446
+ 'data-view': this.state.view,
1447
+ dir: this.config.direction,
1448
+ role: 'group',
1449
+ });
1450
+ }
1451
+ resolveInitialActiveDate(value, fallback) {
1452
+ const anchor = this.extractSelectionAnchor(value);
1453
+ if (anchor !== null && !this.isDateDisabled(anchor)) {
1454
+ return anchor;
1455
+ }
1456
+ if (!this.isDateDisabled(fallback)) {
1457
+ return fallback;
1458
+ }
1459
+ const firstEnabled = findFirstEnabledDateInMonth(this.config.adapter, this.config.adapter.startOfMonth(fallback), (date) => this.isDateDisabled(date));
1460
+ if (firstEnabled !== null) {
1461
+ return firstEnabled;
1462
+ }
1463
+ if (this.config.min !== null && !this.isDateDisabled(this.config.min)) {
1464
+ return this.config.min;
1465
+ }
1466
+ if (this.config.max !== null && !this.isDateDisabled(this.config.max)) {
1467
+ return this.config.max;
1468
+ }
1469
+ return fallback;
1470
+ }
1471
+ resolveLayout() {
1472
+ if (!this.state.open || this.config.overlayMode === 'overlay') {
1473
+ return Object.freeze({
1474
+ mode: this.config.overlayMode,
1475
+ offsetX: 0,
1476
+ width: 0,
1477
+ });
1478
+ }
1479
+ const width = this.config.overlaySize;
1480
+ if (this.config.position === 'center') {
1481
+ return Object.freeze({ mode: this.config.overlayMode, offsetX: 0, width });
1482
+ }
1483
+ const startOffset = this.config.direction === 'rtl' ? -width : width;
1484
+ const endOffset = this.config.direction === 'rtl' ? width : -width;
1485
+ return Object.freeze({
1486
+ mode: this.config.overlayMode,
1487
+ offsetX: this.config.position === 'start' ? startOffset : endOffset,
1488
+ width,
1489
+ });
1490
+ }
1491
+ resolveMonthAttributes(monthOrOption) {
1492
+ const option = typeof monthOrOption === 'number'
1493
+ ? this.getMonthOptions().find((item) => item.index === monthOrOption)
1494
+ : monthOrOption;
1495
+ if (option === undefined) {
1496
+ return freezeAttributes({});
1497
+ }
1498
+ return freezeAttributes({
1499
+ 'aria-disabled': option.disabled ? 'true' : null,
1500
+ 'aria-selected': option.selected ? 'true' : 'false',
1501
+ 'data-active': option.active ? 'true' : null,
1502
+ 'data-disabled': option.disabled ? 'true' : null,
1503
+ 'data-focus-visible': option.focusVisible ? 'true' : null,
1504
+ 'data-selected': option.selected ? 'true' : null,
1505
+ 'data-slot': 'datepicker-month',
1506
+ id: option.id,
1507
+ role: 'gridcell',
1508
+ tabindex: `${this.monthRovingFocus.getActiveId() === option.id ? 0 : -1}`,
1509
+ });
1510
+ }
1511
+ resolveNextSelection(nextDate, shiftKey) {
1512
+ if (this.config.selectionMode === 'single') {
1513
+ if (this.config.allowDeselect &&
1514
+ this.state.value !== null &&
1515
+ !Array.isArray(this.state.value) &&
1516
+ datesEqual(this.config.adapter, this.state.value, nextDate)) {
1517
+ return null;
1518
+ }
1519
+ return nextDate;
1520
+ }
1521
+ if (this.config.selectionMode === 'range') {
1522
+ const currentRange = isRangeSelectionValue(this.state.value)
1523
+ ? this.state.value
1524
+ : Object.freeze({ end: null, start: null });
1525
+ if (shiftKey &&
1526
+ this.config.enableRangeSelection &&
1527
+ this.rangeAnchorDate !== null &&
1528
+ !this.rangeContainsDisabledDate(normalizeRangeOrder(this.config.adapter, {
1529
+ end: nextDate,
1530
+ start: this.rangeAnchorDate,
1531
+ }).start, normalizeRangeOrder(this.config.adapter, {
1532
+ end: nextDate,
1533
+ start: this.rangeAnchorDate,
1534
+ }).end)) {
1535
+ return normalizeRangeOrder(this.config.adapter, Object.freeze({ end: nextDate, start: this.rangeAnchorDate }));
1536
+ }
1537
+ if (currentRange.start === null || currentRange.end !== null) {
1538
+ return Object.freeze({ end: null, start: nextDate });
1539
+ }
1540
+ const normalizedRange = normalizeRangeOrder(this.config.adapter, Object.freeze({ end: nextDate, start: currentRange.start }));
1541
+ if (normalizedRange.start !== null &&
1542
+ normalizedRange.end !== null &&
1543
+ this.rangeContainsDisabledDate(normalizedRange.start, normalizedRange.end)) {
1544
+ return Object.freeze({ end: null, start: currentRange.start });
1545
+ }
1546
+ return normalizedRange;
1547
+ }
1548
+ const currentValues = Array.isArray(this.state.value)
1549
+ ? [...this.state.value]
1550
+ : [];
1551
+ const existingIndex = currentValues.findIndex((selected) => datesEqual(this.config.adapter, selected, nextDate));
1552
+ if (shiftKey && this.config.enableMultipleRangeSelection && this.rangeAnchorDate !== null) {
1553
+ const dates = [];
1554
+ const orderedRange = normalizeRangeOrder(this.config.adapter, {
1555
+ end: nextDate,
1556
+ start: this.rangeAnchorDate,
1557
+ });
1558
+ if (orderedRange.start !== null && orderedRange.end !== null) {
1559
+ let cursor = orderedRange.start;
1560
+ while (this.config.adapter.compare(cursor, orderedRange.end) <= 0) {
1561
+ if (!this.isDateDisabled(cursor)) {
1562
+ dates.push(cursor);
1563
+ }
1564
+ cursor = this.config.adapter.addDays(cursor, 1);
1565
+ }
1566
+ }
1567
+ const merged = Array.from(new Map([...currentValues, ...dates].map((date) => [toDateKey(this.config.adapter, date), date])).values());
1568
+ const sorted = merged.sort((left, right) => this.config.adapter.compare(left, right));
1569
+ if (this.config.maxSelections !== null && sorted.length > this.config.maxSelections) {
1570
+ this.state.validationError = 'max-selections';
1571
+ }
1572
+ return Object.freeze(sorted.slice(0, this.config.maxSelections ?? Number.MAX_SAFE_INTEGER));
1573
+ }
1574
+ if (existingIndex >= 0) {
1575
+ currentValues.splice(existingIndex, 1);
1576
+ return Object.freeze(currentValues);
1577
+ }
1578
+ if (this.config.maxSelections !== null && currentValues.length >= this.config.maxSelections) {
1579
+ this.state.validationError = 'max-selections';
1580
+ return Object.freeze(currentValues);
1581
+ }
1582
+ currentValues.push(nextDate);
1583
+ currentValues.sort((left, right) => this.config.adapter.compare(left, right));
1584
+ return Object.freeze(currentValues);
1585
+ }
1586
+ resolveOverlayAttributes() {
1587
+ return freezeAttributes({
1588
+ 'aria-describedby': this.config.ariaDescribedBy,
1589
+ 'aria-label': this.config.ariaLabel,
1590
+ 'aria-labelledby': this.config.ariaLabelledBy ?? this.monthLabelId,
1591
+ 'aria-modal': this.config.trapFocus ? 'true' : null,
1592
+ 'data-open': this.state.open ? 'true' : 'false',
1593
+ 'data-position': this.config.position,
1594
+ 'data-slot': 'datepicker-overlay',
1595
+ id: this.overlayId,
1596
+ role: 'dialog',
1597
+ });
1598
+ }
1599
+ resolveTriggerAttributes() {
1600
+ return freezeAttributes({
1601
+ 'aria-controls': this.overlayId,
1602
+ 'aria-expanded': this.state.open ? 'true' : 'false',
1603
+ 'aria-haspopup': 'dialog',
1604
+ 'data-open': this.state.open ? 'true' : 'false',
1605
+ 'data-slot': 'datepicker-trigger',
1606
+ });
1607
+ }
1608
+ resolveValidDate(date, monthContext) {
1609
+ const targetMonth = isDateValueInMonth(this.config.adapter, date, monthContext)
1610
+ ? monthContext
1611
+ : this.config.adapter.startOfMonth(date);
1612
+ let nextDate = date;
1613
+ if (!isDateValueInMonth(this.config.adapter, nextDate, targetMonth)) {
1614
+ nextDate = clampDateToMonth(this.config.adapter, nextDate, targetMonth);
1615
+ }
1616
+ if (!this.isDateDisabled(nextDate)) {
1617
+ return nextDate;
1618
+ }
1619
+ const fallback = findFirstEnabledDateInMonth(this.config.adapter, targetMonth, (candidate) => this.isDateDisabled(candidate));
1620
+ return fallback ?? nextDate;
1621
+ }
1622
+ resolveYearAttributes(yearOrOption) {
1623
+ const option = typeof yearOrOption === 'number'
1624
+ ? this.getYearOptions().find((item) => item.year === yearOrOption)
1625
+ : yearOrOption;
1626
+ if (option === undefined) {
1627
+ return freezeAttributes({});
1628
+ }
1629
+ return freezeAttributes({
1630
+ 'aria-disabled': option.disabled ? 'true' : null,
1631
+ 'aria-selected': option.selected ? 'true' : 'false',
1632
+ 'data-active': option.active ? 'true' : null,
1633
+ 'data-disabled': option.disabled ? 'true' : null,
1634
+ 'data-focus-visible': option.focusVisible ? 'true' : null,
1635
+ 'data-selected': option.selected ? 'true' : null,
1636
+ 'data-slot': 'datepicker-year',
1637
+ id: option.id,
1638
+ role: 'gridcell',
1639
+ tabindex: `${this.yearRovingFocus.getActiveId() === option.id ? 0 : -1}`,
1640
+ });
1641
+ }
1642
+ resolveConfig(nextConfig, previous) {
1643
+ const adapter = nextConfig.adapter ?? previous?.adapter ?? defaultDatepickerDateAdapter;
1644
+ const ownerDocument = nextConfig.ownerDocument ?? previous?.ownerDocument ?? (typeof document === 'undefined' ? null : document);
1645
+ const locale = nextConfig.locale ?? previous?.locale ?? resolveDefaultLocale(ownerDocument);
1646
+ const today = normalizeDateInput(adapter, nextConfig.today ?? previous?.today ?? null, locale) ??
1647
+ previous?.today ??
1648
+ adapter.today();
1649
+ const normalizedValue = nextConfig.value !== undefined || previous === null
1650
+ ? normalizeSelectionInput(adapter, normalizeSelectionMode(nextConfig.selectionMode ?? previous?.selectionMode), nextConfig.value ?? null, locale).value
1651
+ : previous.value;
1652
+ return Object.freeze({
1653
+ adapter,
1654
+ allowDeselect: nextConfig.allowDeselect ?? previous?.allowDeselect ?? false,
1655
+ allowManualInput: nextConfig.allowManualInput ?? previous?.allowManualInput ?? true,
1656
+ ariaDescribedBy: nextConfig.ariaDescribedBy ?? previous?.ariaDescribedBy ?? null,
1657
+ ariaLabel: nextConfig.ariaLabel ?? previous?.ariaLabel ?? null,
1658
+ ariaLabelledBy: nextConfig.ariaLabelledBy ?? previous?.ariaLabelledBy ?? null,
1659
+ autoCommitView: nextConfig.autoCommitView ?? previous?.autoCommitView ?? true,
1660
+ closeOnEscape: nextConfig.closeOnEscape ?? previous?.closeOnEscape ?? true,
1661
+ closeOnOutsideClick: nextConfig.closeOnOutsideClick ?? previous?.closeOnOutsideClick ?? true,
1662
+ closeOnSelect: nextConfig.closeOnSelect ?? previous?.closeOnSelect ?? false,
1663
+ closeOthersOnOpen: nextConfig.closeOthersOnOpen ?? previous?.closeOthersOnOpen ?? false,
1664
+ direction: normalizeDirection(nextConfig.direction ?? previous?.direction),
1665
+ disableDate: nextConfig.disableDate ?? previous?.disableDate ?? null,
1666
+ disabled: nextConfig.disabled ?? previous?.disabled ?? false,
1667
+ enableMultipleRangeSelection: nextConfig.enableMultipleRangeSelection ?? previous?.enableMultipleRangeSelection ?? true,
1668
+ enableRangeSelection: nextConfig.enableRangeSelection ?? previous?.enableRangeSelection ?? true,
1669
+ enableTypeahead: nextConfig.enableTypeahead ?? previous?.enableTypeahead ?? true,
1670
+ fixedWeeks: nextConfig.fixedWeeks ?? previous?.fixedWeeks ?? true,
1671
+ focusStrategy: nextConfig.focusStrategy ?? previous?.focusStrategy ?? 'roving',
1672
+ id: nextConfig.id?.trim() || previous?.id || this.instanceId,
1673
+ initialView: normalizeView(nextConfig.initialView ?? previous?.initialView),
1674
+ locale,
1675
+ max: normalizeDateInput(adapter, nextConfig.maxDate ?? nextConfig.max ?? previous?.max ?? null, locale),
1676
+ maxSelections: nextConfig.maxSelections ?? previous?.maxSelections ?? null,
1677
+ min: normalizeDateInput(adapter, nextConfig.minDate ?? nextConfig.min ?? previous?.min ?? null, locale),
1678
+ onPartialInputCommit: nextConfig.onPartialInputCommit ?? previous?.onPartialInputCommit ?? false,
1679
+ overlayMode: normalizeOverlayMode(nextConfig.overlayMode ?? previous?.overlayMode),
1680
+ overlayRuntime: nextConfig.overlayRuntime ??
1681
+ previous?.overlayRuntime ??
1682
+ (ownerDocument !== null ? createOverlayRuntime({ documentRef: readDocument(ownerDocument) }) : null),
1683
+ overlaySize: nextConfig.overlaySize ?? previous?.overlaySize ?? 320,
1684
+ ownerDocument,
1685
+ position: normalizePosition(nextConfig.position ?? previous?.position),
1686
+ preserveViewOnOpenClose: nextConfig.preserveViewOnOpenClose ?? previous?.preserveViewOnOpenClose ?? true,
1687
+ restoreFocus: nextConfig.restoreFocus ?? previous?.restoreFocus ?? true,
1688
+ selectionMode: normalizeSelectionMode(nextConfig.selectionMode ?? previous?.selectionMode),
1689
+ showOutsideDays: nextConfig.showOutsideDays ?? previous?.showOutsideDays ?? true,
1690
+ skipDisabled: nextConfig.skipDisabled ?? previous?.skipDisabled ?? true,
1691
+ today,
1692
+ trapFocus: nextConfig.trapFocus ?? previous?.trapFocus ?? false,
1693
+ value: normalizedValue,
1694
+ weekStartsOn: coerceWeekStartsOn(nextConfig.weekStartsOn ?? previous?.weekStartsOn ?? resolveLocaleWeekStartsOn(locale)),
1695
+ yearPageSize: ensureYearPageSize(nextConfig.yearPageSize ?? previous?.yearPageSize),
1696
+ });
1697
+ }
1698
+ registerFocusLayer() {
1699
+ if (this.focusLayerRegistered || this.overlayElement === null) {
1700
+ return;
1701
+ }
1702
+ datepickerFocusHandoff.registerLayer({
1703
+ layerId: this.instanceId,
1704
+ members: () => {
1705
+ const overlay = this.overlayElement;
1706
+ if (overlay === null) {
1707
+ return [];
1708
+ }
1709
+ return this.resolveFocusableMemberIds(overlay);
1710
+ },
1711
+ restoreFocus: this.config.restoreFocus,
1712
+ trapFocus: this.config.trapFocus,
1713
+ });
1714
+ this.focusLayerRegistered = true;
1715
+ }
1716
+ unregisterFocusLayer() {
1717
+ if (!this.focusLayerRegistered) {
1718
+ return;
1719
+ }
1720
+ datepickerFocusHandoff.unregisterLayer(this.instanceId);
1721
+ this.focusLayerRegistered = false;
1722
+ }
1723
+ resolveFocusableMemberIds(container) {
1724
+ const focusableElements = resolveFocusableElements(container);
1725
+ const memberIds = [];
1726
+ const seenIds = new Set();
1727
+ const registerMember = (element) => {
1728
+ const id = this.ensureElementId(element);
1729
+ if (seenIds.has(id)) {
1730
+ return;
1731
+ }
1732
+ seenIds.add(id);
1733
+ memberIds.push(id);
1734
+ };
1735
+ registerMember(container);
1736
+ for (const element of focusableElements) {
1737
+ registerMember(element);
1738
+ }
1739
+ return memberIds;
1740
+ }
1741
+ resolveTabbableOverlayMemberIds(container) {
1742
+ const focusableElements = resolveFocusableElements(container);
1743
+ const memberIds = [];
1744
+ const seenIds = new Set();
1745
+ for (const element of focusableElements) {
1746
+ const id = this.ensureElementId(element);
1747
+ if (seenIds.has(id)) {
1748
+ continue;
1749
+ }
1750
+ seenIds.add(id);
1751
+ memberIds.push(id);
1752
+ }
1753
+ return memberIds;
1754
+ }
1755
+ ensureElementId(element) {
1756
+ const existingId = element.id.trim();
1757
+ if (existingId.length > 0) {
1758
+ return existingId;
1759
+ }
1760
+ const generatedId = createDatepickerFocusableId();
1761
+ element.id = generatedId;
1762
+ return generatedId;
1763
+ }
1764
+ optionIndexToGridPosition(index, columnCount) {
1765
+ return Object.freeze({
1766
+ col: Math.max(0, index) % columnCount,
1767
+ row: Math.floor(Math.max(0, index) / columnCount),
1768
+ });
1769
+ }
1770
+ resolveCenteredYearPageStart(year) {
1771
+ return year - Math.floor(this.config.yearPageSize / 2);
1772
+ }
1773
+ resolveYearBoundaryDelta(actionType) {
1774
+ if (actionType === 'move-left') {
1775
+ return -1;
1776
+ }
1777
+ if (actionType === 'move-right') {
1778
+ return 1;
1779
+ }
1780
+ if (actionType === 'move-up') {
1781
+ return -4;
1782
+ }
1783
+ if (actionType === 'move-down') {
1784
+ return 4;
1785
+ }
1786
+ return null;
1787
+ }
1788
+ resolveMonthBoundaryDelta(actionType) {
1789
+ if (actionType === 'move-left') {
1790
+ return -1;
1791
+ }
1792
+ if (actionType === 'move-right') {
1793
+ return 1;
1794
+ }
1795
+ if (actionType === 'move-up') {
1796
+ return -4;
1797
+ }
1798
+ if (actionType === 'move-down') {
1799
+ return 4;
1800
+ }
1801
+ return null;
1802
+ }
1803
+ shiftYearPageToInclude(targetYear) {
1804
+ const previousMonth = this.state.visibleMonth;
1805
+ const previousYear = this.config.adapter.getYear(previousMonth);
1806
+ let nextStart = this.yearPageStart;
1807
+ const pageSize = this.config.yearPageSize;
1808
+ while (targetYear < nextStart) {
1809
+ nextStart -= pageSize;
1810
+ }
1811
+ while (targetYear > nextStart + pageSize - 1) {
1812
+ nextStart += pageSize;
1813
+ }
1814
+ if (nextStart === this.yearPageStart) {
1815
+ return;
1816
+ }
1817
+ this.yearPageStart = nextStart;
1818
+ this.state.visibleMonth = this.config.adapter.createDate(targetYear, this.config.adapter.getMonth(this.state.visibleMonth), 1);
1819
+ this.emit({
1820
+ previousMonth,
1821
+ type: 'monthChange',
1822
+ visibleMonth: this.state.visibleMonth,
1823
+ });
1824
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
1825
+ if (previousYear !== currentYear) {
1826
+ this.emit({
1827
+ previousYear,
1828
+ type: 'yearChange',
1829
+ year: currentYear,
1830
+ });
1831
+ }
1832
+ }
1833
+ shiftMonthGridToTarget(targetMonth) {
1834
+ const normalizedTargetMonth = this.config.adapter.startOfMonth(targetMonth);
1835
+ if (!hasSelectableDateInMonth(this.config.adapter, normalizedTargetMonth, (date) => this.isDateDisabled(date))) {
1836
+ return;
1837
+ }
1838
+ const previousMonth = this.state.visibleMonth;
1839
+ const previousYear = this.config.adapter.getYear(previousMonth);
1840
+ this.state.visibleMonth = normalizedTargetMonth;
1841
+ this.emit({
1842
+ previousMonth,
1843
+ type: 'monthChange',
1844
+ visibleMonth: this.state.visibleMonth,
1845
+ });
1846
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
1847
+ if (previousYear !== currentYear) {
1848
+ this.emit({
1849
+ previousYear,
1850
+ type: 'yearChange',
1851
+ year: currentYear,
1852
+ });
1853
+ }
1854
+ }
1855
+ syncDayFocusControllers(cells) {
1856
+ const itemIds = cells.map((cell) => cell.id);
1857
+ const disabledIds = cells.filter((cell) => cell.disabled).map((cell) => cell.id);
1858
+ const activeCellId = cells.find((cell) => cell.active)?.id ?? null;
1859
+ this.dayRovingFocus.setItemIds(itemIds);
1860
+ this.dayRovingFocus.setDisabledIds(disabledIds);
1861
+ this.dayRovingFocus.setActiveId(activeCellId);
1862
+ this.dayActiveDescendant.setItemIds(itemIds);
1863
+ this.dayActiveDescendant.setDisabledIds(disabledIds);
1864
+ this.dayActiveDescendant.setActiveId(activeCellId);
1865
+ }
1866
+ syncMonthFocusController(options) {
1867
+ const itemIds = options.map((option) => option.id);
1868
+ const disabledIds = options.filter((option) => option.disabled).map((option) => option.id);
1869
+ const activeId = options.find((option) => option.active)?.id ?? null;
1870
+ this.monthRovingFocus.setItemIds(itemIds);
1871
+ this.monthRovingFocus.setDisabledIds(disabledIds);
1872
+ this.monthRovingFocus.setActiveId(activeId);
1873
+ }
1874
+ syncYearFocusController(options) {
1875
+ const itemIds = options.map((option) => option.id);
1876
+ const disabledIds = options.filter((option) => option.disabled).map((option) => option.id);
1877
+ const activeId = options.find((option) => option.active)?.id ?? null;
1878
+ this.yearRovingFocus.setItemIds(itemIds);
1879
+ this.yearRovingFocus.setDisabledIds(disabledIds);
1880
+ this.yearRovingFocus.setActiveId(activeId);
1881
+ }
1882
+ shiftVisibleMonth(direction) {
1883
+ const nextMonth = this.config.adapter.addMonths(this.state.visibleMonth, direction);
1884
+ if (!hasSelectableDateInMonth(this.config.adapter, nextMonth, (date) => this.isDateDisabled(date))) {
1885
+ return;
1886
+ }
1887
+ const previousMonth = this.state.visibleMonth;
1888
+ const previousYear = this.config.adapter.getYear(previousMonth);
1889
+ this.state.visibleMonth = this.config.adapter.startOfMonth(nextMonth);
1890
+ const nextActive = clampDateToMonth(this.config.adapter, this.state.activeDate, this.state.visibleMonth);
1891
+ this.applyActiveDate(nextActive, 'keyboard', true);
1892
+ this.bumpVersion();
1893
+ this.emit({
1894
+ previousMonth,
1895
+ type: 'monthChange',
1896
+ visibleMonth: this.state.visibleMonth,
1897
+ });
1898
+ const currentYear = this.config.adapter.getYear(this.state.visibleMonth);
1899
+ if (previousYear !== currentYear) {
1900
+ this.emit({
1901
+ previousYear,
1902
+ type: 'yearChange',
1903
+ year: currentYear,
1904
+ });
1905
+ }
1906
+ }
1907
+ shiftVisibleYear(direction) {
1908
+ const nextYear = this.config.adapter.getYear(this.state.visibleMonth) + direction;
1909
+ this.selectYear(nextYear);
1910
+ }
1911
+ commitRangeFromAnchor(endDate) {
1912
+ if (this.rangeAnchorDate === null) {
1913
+ return;
1914
+ }
1915
+ const previousValue = this.state.value;
1916
+ const nextValue = normalizeRangeOrder(this.config.adapter, {
1917
+ end: endDate,
1918
+ start: this.rangeAnchorDate,
1919
+ });
1920
+ if (nextValue.start === null || nextValue.end === null) {
1921
+ return;
1922
+ }
1923
+ if (this.rangeContainsDisabledDate(nextValue.start, nextValue.end)) {
1924
+ return;
1925
+ }
1926
+ this.state.value = nextValue;
1927
+ this.state.inputText = this.formatValueForInput(nextValue);
1928
+ this.bumpVersion();
1929
+ this.emit({
1930
+ previousValue,
1931
+ trigger: 'keyboard',
1932
+ type: 'valueChange',
1933
+ value: nextValue,
1934
+ });
1935
+ }
1936
+ unregisterOverlayLayer() {
1937
+ if (!this.overlayLayerRegistered || this.config.overlayRuntime === null) {
1938
+ return;
1939
+ }
1940
+ this.config.overlayRuntime.unregisterLayer(`${this.instanceId}-layer`);
1941
+ this.overlayLayerRegistered = false;
1942
+ }
1943
+ }
1944
+ export function createDatepickerController(config = {}) {
1945
+ return new DatepickerController(config);
1946
+ }
1947
+ export { defaultDatepickerDateAdapter, normalizeDateInput } from './datepicker.adapters';
1948
+ //# sourceMappingURL=tng-datepicker.js.map