@oslokommune/punkt-elements 14.0.2 → 14.0.4

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 (30) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/calendar-BtShW7ER.cjs +90 -0
  3. package/dist/{calendar-Bz27nuTP.js → calendar-yxjSI4wd.js} +766 -682
  4. package/dist/datepicker-D0q75U1Z.js +1463 -0
  5. package/dist/datepicker-DDV382Uu.cjs +271 -0
  6. package/dist/index.d.ts +118 -83
  7. package/dist/pkt-calendar.cjs +1 -1
  8. package/dist/pkt-calendar.js +1 -1
  9. package/dist/pkt-datepicker.cjs +1 -1
  10. package/dist/pkt-datepicker.js +2 -2
  11. package/dist/pkt-index.cjs +1 -1
  12. package/dist/pkt-index.js +3 -3
  13. package/package.json +4 -4
  14. package/src/components/calendar/calendar.ts +372 -414
  15. package/src/components/calendar/helpers/calendar-grid.ts +93 -0
  16. package/src/components/calendar/helpers/date-validation.ts +86 -0
  17. package/src/components/calendar/helpers/index.ts +49 -0
  18. package/src/components/calendar/helpers/keyboard-navigation.ts +54 -0
  19. package/src/components/calendar/helpers/selection-manager.ts +184 -0
  20. package/src/components/datepicker/datepicker-base.ts +151 -0
  21. package/src/components/datepicker/datepicker-multiple.ts +7 -114
  22. package/src/components/datepicker/datepicker-range.ts +21 -141
  23. package/src/components/datepicker/datepicker-single.ts +7 -115
  24. package/src/components/datepicker/datepicker-types.ts +56 -0
  25. package/src/components/datepicker/datepicker-utils.test.ts +730 -0
  26. package/src/components/datepicker/datepicker-utils.ts +338 -9
  27. package/src/components/datepicker/datepicker.ts +25 -1
  28. package/dist/calendar-Dz1Cnzx5.cjs +0 -115
  29. package/dist/datepicker-CnCOXI2x.cjs +0 -289
  30. package/dist/datepicker-DsqM01iU.js +0 -1355
@@ -0,0 +1,730 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import {
3
+ valueUtils,
4
+ inputTypeUtils,
5
+ formUtils,
6
+ calendarUtils,
7
+ eventUtils,
8
+ cssUtils,
9
+ dateProcessingUtils,
10
+ keyboardUtils,
11
+ } from './datepicker-utils'
12
+ import { createRef } from 'lit/directives/ref.js'
13
+
14
+ describe('datepicker-utils', () => {
15
+ describe('valueUtils', () => {
16
+ describe('normalizeNameForMultiple', () => {
17
+ it('should return null when name is null', () => {
18
+ expect(valueUtils.normalizeNameForMultiple(null, false, false)).toBe(null)
19
+ expect(valueUtils.normalizeNameForMultiple(null, true, false)).toBe(null)
20
+ expect(valueUtils.normalizeNameForMultiple(null, false, true)).toBe(null)
21
+ })
22
+
23
+ it('should add [] suffix for multiple inputs without []', () => {
24
+ expect(valueUtils.normalizeNameForMultiple('dates', true, false)).toBe('dates[]')
25
+ })
26
+
27
+ it('should add [] suffix for range inputs without []', () => {
28
+ expect(valueUtils.normalizeNameForMultiple('daterange', false, true)).toBe('daterange[]')
29
+ })
30
+
31
+ it('should not add [] suffix when already present', () => {
32
+ expect(valueUtils.normalizeNameForMultiple('dates[]', true, false)).toBe('dates[]')
33
+ expect(valueUtils.normalizeNameForMultiple('range[]', false, true)).toBe('range[]')
34
+ })
35
+
36
+ it('should not modify name for single inputs', () => {
37
+ expect(valueUtils.normalizeNameForMultiple('singledate', false, false)).toBe('singledate')
38
+ })
39
+ })
40
+
41
+ describe('validateRangeOrder', () => {
42
+ it('should return true for incomplete ranges', () => {
43
+ expect(valueUtils.validateRangeOrder([])).toBe(true)
44
+ expect(valueUtils.validateRangeOrder(['2024-01-01'])).toBe(true)
45
+ })
46
+
47
+ it('should return true for valid date ranges', () => {
48
+ expect(valueUtils.validateRangeOrder(['2024-01-01', '2024-01-31'])).toBe(true)
49
+ expect(valueUtils.validateRangeOrder(['2024-01-01', '2024-01-01'])).toBe(true)
50
+ })
51
+
52
+ it('should return false for invalid date ranges', () => {
53
+ expect(valueUtils.validateRangeOrder(['2024-01-31', '2024-01-01'])).toBe(false)
54
+ })
55
+ })
56
+
57
+ describe('sortDates', () => {
58
+ it('should sort dates chronologically', () => {
59
+ const dates = ['2024-03-15', '2024-01-10', '2024-02-20']
60
+ const sorted = valueUtils.sortDates(dates)
61
+ expect(sorted).toEqual(['2024-01-10', '2024-02-20', '2024-03-15'])
62
+ })
63
+
64
+ it('should handle empty array', () => {
65
+ expect(valueUtils.sortDates([])).toEqual([])
66
+ })
67
+ })
68
+
69
+ describe('filterSelectableDates', () => {
70
+ it('should filter dates outside min/max range', () => {
71
+ const dates = ['2024-01-01', '2024-01-15', '2024-01-31']
72
+ const filtered = valueUtils.filterSelectableDates(dates, '2024-01-10', '2024-01-20')
73
+ expect(filtered).toEqual(['2024-01-15'])
74
+ })
75
+
76
+ it('should filter excluded dates', () => {
77
+ const dates = ['2024-01-01', '2024-01-02', '2024-01-03']
78
+ const filtered = valueUtils.filterSelectableDates(
79
+ dates,
80
+ null,
81
+ null,
82
+ ['2024-01-02'],
83
+ undefined,
84
+ )
85
+ expect(filtered).toEqual(['2024-01-01', '2024-01-03'])
86
+ })
87
+
88
+ it('should filter excluded weekdays', () => {
89
+ const dates = ['2024-01-01', '2024-01-06', '2024-01-07'] // Mon, Sat, Sun
90
+ const filtered = valueUtils.filterSelectableDates(dates, null, null, undefined, ['0', '6']) // Exclude weekends
91
+ expect(filtered).toEqual(['2024-01-01'])
92
+ })
93
+ })
94
+ })
95
+
96
+ describe('inputTypeUtils', () => {
97
+ describe('getInputType', () => {
98
+ it('should return "text" for iOS devices', () => {
99
+ // Mock isIOS to return true
100
+ vi.mock('shared-utils/device-utils', () => ({
101
+ isIOS: () => true,
102
+ }))
103
+ // Note: This test is limited by how vitest handles module mocking
104
+ // In a real scenario, you'd need proper mocking setup
105
+ })
106
+
107
+ it('should return "date" for non-iOS devices', () => {
108
+ // The function returns based on isIOS() which is mocked elsewhere
109
+ const type = inputTypeUtils.getInputType()
110
+ expect(['text', 'date']).toContain(type)
111
+ })
112
+ })
113
+ })
114
+
115
+ describe('formUtils', () => {
116
+ describe('submitForm', () => {
117
+ it('should call requestSubmit on the form if available', () => {
118
+ const mockForm = { requestSubmit: vi.fn() }
119
+ const element = {
120
+ internals: { form: mockForm },
121
+ } as any
122
+
123
+ formUtils.submitForm(element)
124
+ expect(mockForm.requestSubmit).toHaveBeenCalledOnce()
125
+ })
126
+
127
+ it('should not throw if form is not available', () => {
128
+ const element = {} as any
129
+ expect(() => formUtils.submitForm(element)).not.toThrow()
130
+ })
131
+ })
132
+
133
+ describe('submitFormOrFallback', () => {
134
+ it('should submit form if internals has form', () => {
135
+ const mockForm = { requestSubmit: vi.fn() }
136
+ const fallback = vi.fn()
137
+ const internals = { form: mockForm }
138
+
139
+ formUtils.submitFormOrFallback(internals, fallback)
140
+ expect(mockForm.requestSubmit).toHaveBeenCalledOnce()
141
+ expect(fallback).not.toHaveBeenCalled()
142
+ })
143
+
144
+ it('should call fallback if no form available', () => {
145
+ const fallback = vi.fn()
146
+ formUtils.submitFormOrFallback(null, fallback)
147
+ expect(fallback).toHaveBeenCalledOnce()
148
+ })
149
+
150
+ it('should call fallback if internals has no form', () => {
151
+ const fallback = vi.fn()
152
+ const internals = {}
153
+ formUtils.submitFormOrFallback(internals, fallback)
154
+ expect(fallback).toHaveBeenCalledOnce()
155
+ })
156
+ })
157
+
158
+ describe('validateDateInput', () => {
159
+ it('should return early if input has no value', () => {
160
+ const input = { value: '' } as HTMLInputElement
161
+ const internals = { setValidity: vi.fn() }
162
+ formUtils.validateDateInput(input, internals)
163
+ expect(internals.setValidity).not.toHaveBeenCalled()
164
+ })
165
+
166
+ it('should set rangeUnderflow validity when value is below minimum', () => {
167
+ const input = { value: '2024-01-01' } as HTMLInputElement
168
+ const internals = { setValidity: vi.fn() }
169
+ const strings = { forms: { messages: { rangeUnderflow: 'Too early' } } }
170
+
171
+ formUtils.validateDateInput(input, internals, '2024-01-10', null, strings)
172
+ expect(internals.setValidity).toHaveBeenCalledWith(
173
+ { rangeUnderflow: true },
174
+ 'Too early',
175
+ input,
176
+ )
177
+ })
178
+
179
+ it('should set rangeOverflow validity when value is above maximum', () => {
180
+ const input = { value: '2024-01-31' } as HTMLInputElement
181
+ const internals = { setValidity: vi.fn() }
182
+ const strings = { forms: { messages: { rangeOverflow: 'Too late' } } }
183
+
184
+ formUtils.validateDateInput(input, internals, null, '2024-01-20', strings)
185
+ expect(internals.setValidity).toHaveBeenCalledWith(
186
+ { rangeOverflow: true },
187
+ 'Too late',
188
+ input,
189
+ )
190
+ })
191
+
192
+ it('should use default messages when strings not provided', () => {
193
+ const input = { value: '2024-01-01' } as HTMLInputElement
194
+ const internals = { setValidity: vi.fn() }
195
+
196
+ formUtils.validateDateInput(input, internals, '2024-01-10', null)
197
+ expect(internals.setValidity).toHaveBeenCalledWith(
198
+ { rangeUnderflow: true },
199
+ 'Value is below minimum',
200
+ input,
201
+ )
202
+ })
203
+ })
204
+ })
205
+
206
+ describe('calendarUtils', () => {
207
+ describe('addToSelected', () => {
208
+ it('should return early if target has no value', () => {
209
+ const event = { target: { value: '' } } as any
210
+ const calendarRef = createRef()
211
+ calendarUtils.addToSelected(event, calendarRef as any)
212
+ // Should not throw
213
+ })
214
+
215
+ it('should clear input value after processing', () => {
216
+ const mockCalendar = { handleDateSelect: vi.fn() }
217
+ const target = { value: '2024-01-15' }
218
+ const event = { target } as any
219
+ const calendarRef = { value: mockCalendar } as any
220
+
221
+ calendarUtils.addToSelected(event, calendarRef)
222
+ expect(target.value).toBe('')
223
+ })
224
+
225
+ it('should call handleDateSelect with valid date', () => {
226
+ const mockCalendar = { handleDateSelect: vi.fn() }
227
+ const target = { value: '2024-01-15' }
228
+ const event = { target } as any
229
+ const calendarRef = { value: mockCalendar } as any
230
+
231
+ calendarUtils.addToSelected(event, calendarRef)
232
+ expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
233
+ })
234
+
235
+ it('should respect min/max constraints', () => {
236
+ const mockCalendar = { handleDateSelect: vi.fn() }
237
+ const target = { value: '2024-01-05' }
238
+ const event = { target } as any
239
+ const calendarRef = { value: mockCalendar } as any
240
+
241
+ calendarUtils.addToSelected(event, calendarRef, '2024-01-10', '2024-01-20')
242
+ expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
243
+ expect(target.value).toBe('')
244
+ })
245
+ })
246
+
247
+ describe('handleCalendarPosition', () => {
248
+ it('should return early if refs are not available', () => {
249
+ const popupRef = createRef()
250
+ const inputRef = createRef()
251
+ expect(() => calendarUtils.handleCalendarPosition(popupRef as any, inputRef as any)).not.toThrow()
252
+ })
253
+
254
+ it('should position calendar below input by default', () => {
255
+ const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 300 }) }
256
+ const mockInput = {
257
+ getBoundingClientRect: () => ({ height: 40, top: 100 }),
258
+ parentElement: null,
259
+ }
260
+ const popupRef = { value: mockPopup } as any
261
+ const inputRef = { value: mockInput } as any
262
+
263
+ calendarUtils.handleCalendarPosition(popupRef, inputRef)
264
+ expect(mockPopup.style.top).toBe('100%')
265
+ })
266
+
267
+ it('should position calendar above input if not enough space below', () => {
268
+ const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 400 }) }
269
+ const mockInput = {
270
+ getBoundingClientRect: () => ({ height: 40, top: window.innerHeight - 100 }),
271
+ parentElement: null,
272
+ }
273
+ const popupRef = { value: mockPopup } as any
274
+ const inputRef = { value: mockInput } as any
275
+
276
+ calendarUtils.handleCalendarPosition(popupRef, inputRef)
277
+ expect(mockPopup.style.top).toContain('calc(100%')
278
+ expect(mockPopup.style.top).toContain('px')
279
+ })
280
+
281
+ it('should account for counter when hasCounter is true', () => {
282
+ const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 300 }) }
283
+ const mockInput = {
284
+ getBoundingClientRect: () => ({ height: 40, top: 100 }),
285
+ parentElement: null,
286
+ }
287
+ const popupRef = { value: mockPopup } as any
288
+ const inputRef = { value: mockInput } as any
289
+
290
+ calendarUtils.handleCalendarPosition(popupRef, inputRef, true)
291
+ expect(mockPopup.style.top).toBe('calc(100% - 30px)')
292
+ })
293
+ })
294
+ })
295
+
296
+ describe('eventUtils', () => {
297
+ describe('createDocumentClickListener', () => {
298
+ it('should return a function', () => {
299
+ const listener = eventUtils.createDocumentClickListener(
300
+ createRef() as any,
301
+ null,
302
+ createRef() as any,
303
+ () => true,
304
+ vi.fn(),
305
+ vi.fn(),
306
+ )
307
+ expect(typeof listener).toBe('function')
308
+ })
309
+
310
+ it('should call onBlur and hideCalendar when clicking outside', () => {
311
+ const onBlur = vi.fn()
312
+ const hideCalendar = vi.fn()
313
+ const inputRef = { value: { contains: () => false } } as any
314
+ const btnRef = { value: { contains: () => false } } as any
315
+
316
+ const listener = eventUtils.createDocumentClickListener(
317
+ inputRef,
318
+ null,
319
+ btnRef,
320
+ () => true,
321
+ onBlur,
322
+ hideCalendar,
323
+ )
324
+
325
+ const event = {
326
+ target: document.createElement('div'),
327
+ } as any
328
+
329
+ listener(event)
330
+ expect(onBlur).toHaveBeenCalledOnce()
331
+ expect(hideCalendar).toHaveBeenCalledOnce()
332
+ })
333
+
334
+ it('should not call handlers when clicking inside input', () => {
335
+ const onBlur = vi.fn()
336
+ const hideCalendar = vi.fn()
337
+ const inputRef = { value: { contains: () => true } } as any
338
+ const btnRef = { value: { contains: () => false } } as any
339
+
340
+ const listener = eventUtils.createDocumentClickListener(
341
+ inputRef,
342
+ null,
343
+ btnRef,
344
+ () => true,
345
+ onBlur,
346
+ hideCalendar,
347
+ )
348
+
349
+ const event = {
350
+ target: document.createElement('div'),
351
+ } as any
352
+
353
+ listener(event)
354
+ expect(onBlur).not.toHaveBeenCalled()
355
+ expect(hideCalendar).not.toHaveBeenCalled()
356
+ })
357
+ })
358
+
359
+ describe('createDocumentKeydownListener', () => {
360
+ it('should return a function', () => {
361
+ const listener = eventUtils.createDocumentKeydownListener(() => true, vi.fn())
362
+ expect(typeof listener).toBe('function')
363
+ })
364
+
365
+ it('should call hideCalendar on Escape key', () => {
366
+ const hideCalendar = vi.fn()
367
+ const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)
368
+
369
+ const event = { key: 'Escape' } as KeyboardEvent
370
+ listener(event)
371
+ expect(hideCalendar).toHaveBeenCalledOnce()
372
+ })
373
+
374
+ it('should not call hideCalendar if calendar is not open', () => {
375
+ const hideCalendar = vi.fn()
376
+ const listener = eventUtils.createDocumentKeydownListener(() => false, hideCalendar)
377
+
378
+ const event = { key: 'Escape' } as KeyboardEvent
379
+ listener(event)
380
+ expect(hideCalendar).not.toHaveBeenCalled()
381
+ })
382
+
383
+ it('should not call hideCalendar on other keys', () => {
384
+ const hideCalendar = vi.fn()
385
+ const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)
386
+
387
+ const event = { key: 'Enter' } as KeyboardEvent
388
+ listener(event)
389
+ expect(hideCalendar).not.toHaveBeenCalled()
390
+ })
391
+ })
392
+
393
+ describe('handleFocusOut', () => {
394
+ it('should call onBlur and hideCalendar when focus leaves element', () => {
395
+ const onBlur = vi.fn()
396
+ const hideCalendar = vi.fn()
397
+ const element = { contains: () => false } as any
398
+ const event = { target: document.createElement('div') } as any
399
+
400
+ eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
401
+ expect(onBlur).toHaveBeenCalledOnce()
402
+ expect(hideCalendar).toHaveBeenCalledOnce()
403
+ })
404
+
405
+ it('should not call handlers when focus stays within element', () => {
406
+ const onBlur = vi.fn()
407
+ const hideCalendar = vi.fn()
408
+ const element = { contains: () => true } as any
409
+ const event = { target: document.createElement('div') } as any
410
+
411
+ eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
412
+ expect(onBlur).not.toHaveBeenCalled()
413
+ expect(hideCalendar).not.toHaveBeenCalled()
414
+ })
415
+ })
416
+ })
417
+
418
+ describe('cssUtils', () => {
419
+ describe('getInputClasses', () => {
420
+ it('should return base classes', () => {
421
+ const classes = cssUtils.getInputClasses(false, false, false, false)
422
+ expect(classes['pkt-input']).toBe(true)
423
+ expect(classes['pkt-datepicker__input']).toBe(true)
424
+ })
425
+
426
+ it('should include fullwidth class when fullwidth is true', () => {
427
+ const classes = cssUtils.getInputClasses(true, false, false, false)
428
+ expect(classes['pkt-input--fullwidth']).toBe(true)
429
+ })
430
+
431
+ it('should include hasrangelabels class when showRangeLabels is true', () => {
432
+ const classes = cssUtils.getInputClasses(false, true, false, false)
433
+ expect(classes['pkt-datepicker--hasrangelabels']).toBe(true)
434
+ })
435
+
436
+ it('should include multiple class when multiple is true', () => {
437
+ const classes = cssUtils.getInputClasses(false, false, true, false)
438
+ expect(classes['pkt-datepicker--multiple']).toBe(true)
439
+ })
440
+
441
+ it('should include range class when range is true', () => {
442
+ const classes = cssUtils.getInputClasses(false, false, false, true)
443
+ expect(classes['pkt-datepicker--range']).toBe(true)
444
+ })
445
+
446
+ it('should include ios-readonly-hack when readonly is false and inputType is text', () => {
447
+ const classes = cssUtils.getInputClasses(false, false, false, false, false, 'text')
448
+ expect(classes['ios-readonly-hack']).toBe(true)
449
+ })
450
+ })
451
+
452
+ describe('getButtonClasses', () => {
453
+ it('should return button classes', () => {
454
+ const classes = cssUtils.getButtonClasses()
455
+ expect(classes['pkt-input-icon']).toBe(true)
456
+ expect(classes['pkt-btn']).toBe(true)
457
+ expect(classes['pkt-btn--icon-only']).toBe(true)
458
+ expect(classes['pkt-btn--tertiary']).toBe(true)
459
+ expect(classes['pkt-datepicker__calendar-button']).toBe(true)
460
+ })
461
+ })
462
+
463
+ describe('getRangeLabelClasses', () => {
464
+ it('should return correct classes when showRangeLabels is true', () => {
465
+ const classes = cssUtils.getRangeLabelClasses(true)
466
+ expect(classes['pkt-input-prefix']).toBe(true)
467
+ expect(classes['pkt-hide']).toBe(false)
468
+ })
469
+
470
+ it('should return correct classes when showRangeLabels is false', () => {
471
+ const classes = cssUtils.getRangeLabelClasses(false)
472
+ expect(classes['pkt-input-prefix']).toBe(false)
473
+ expect(classes['pkt-hide']).toBe(true)
474
+ })
475
+ })
476
+ })
477
+
478
+ describe('dateProcessingUtils', () => {
479
+ describe('processDateSelection', () => {
480
+ it('should return first date for single selection', () => {
481
+ const result = dateProcessingUtils.processDateSelection(['2024-01-15'], false, false)
482
+ expect(result).toBe('2024-01-15')
483
+ })
484
+
485
+ it('should return empty string when no dates for single selection', () => {
486
+ const result = dateProcessingUtils.processDateSelection([], false, false)
487
+ expect(result).toBe('')
488
+ })
489
+
490
+ it('should return comma-separated dates for multiple selection', () => {
491
+ const result = dateProcessingUtils.processDateSelection(
492
+ ['2024-01-15', '2024-01-20'],
493
+ true,
494
+ false,
495
+ )
496
+ expect(result).toBe('2024-01-15,2024-01-20')
497
+ })
498
+
499
+ it('should return comma-separated dates for range selection', () => {
500
+ const result = dateProcessingUtils.processDateSelection(
501
+ ['2024-01-15', '2024-01-20'],
502
+ false,
503
+ true,
504
+ )
505
+ expect(result).toBe('2024-01-15,2024-01-20')
506
+ })
507
+ })
508
+
509
+ describe('updateInputValues', () => {
510
+ it('should return early if inputRef has no value', () => {
511
+ const inputRef = { value: null } as any
512
+ expect(() =>
513
+ dateProcessingUtils.updateInputValues(inputRef, null, [], false, false, vi.fn()),
514
+ ).not.toThrow()
515
+ })
516
+
517
+ it('should update both inputs for range', () => {
518
+ const input = { value: '' } as any
519
+ const inputTo = { value: '' } as any
520
+ const inputRef = { value: input } as any
521
+ const inputRefTo = { value: inputTo } as any
522
+ const manageValidity = vi.fn()
523
+
524
+ dateProcessingUtils.updateInputValues(
525
+ inputRef,
526
+ inputRefTo,
527
+ ['2024-01-15', '2024-01-20'],
528
+ true,
529
+ false,
530
+ manageValidity,
531
+ )
532
+
533
+ expect(input.value).toBe('2024-01-15')
534
+ expect(inputTo.value).toBe('2024-01-20')
535
+ expect(manageValidity).toHaveBeenCalledTimes(2)
536
+ })
537
+
538
+ it('should update single input for non-multiple, non-range', () => {
539
+ const input = { value: '' } as any
540
+ const inputRef = { value: input } as any
541
+ const manageValidity = vi.fn()
542
+
543
+ dateProcessingUtils.updateInputValues(
544
+ inputRef,
545
+ null,
546
+ ['2024-01-15'],
547
+ false,
548
+ false,
549
+ manageValidity,
550
+ )
551
+
552
+ expect(input.value).toBe('2024-01-15')
553
+ expect(manageValidity).toHaveBeenCalledOnce()
554
+ })
555
+
556
+ it('should not update input for multiple selection', () => {
557
+ const input = { value: 'initial' } as any
558
+ const inputRef = { value: input } as any
559
+ const manageValidity = vi.fn()
560
+
561
+ dateProcessingUtils.updateInputValues(
562
+ inputRef,
563
+ null,
564
+ ['2024-01-15'],
565
+ false,
566
+ true,
567
+ manageValidity,
568
+ )
569
+
570
+ expect(input.value).toBe('initial')
571
+ expect(manageValidity).not.toHaveBeenCalled()
572
+ })
573
+ })
574
+
575
+ describe('processRangeBlur', () => {
576
+ it('should call manageValidity and handleDateSelect when target has value', () => {
577
+ const target = { value: '2024-01-15' } as any
578
+ const event = { target } as any
579
+ const mockCalendar = { handleDateSelect: vi.fn() }
580
+ const calendarRef = { value: mockCalendar } as any
581
+ const clearInputValue = vi.fn()
582
+ const manageValidity = vi.fn()
583
+
584
+ dateProcessingUtils.processRangeBlur(
585
+ event,
586
+ ['2024-01-10', '2024-01-20'],
587
+ calendarRef,
588
+ clearInputValue,
589
+ manageValidity,
590
+ )
591
+
592
+ expect(manageValidity).toHaveBeenCalledWith(target)
593
+ expect(clearInputValue).not.toHaveBeenCalled()
594
+ expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
595
+ })
596
+
597
+ it('should clear input value when target is empty but values[0] exists', () => {
598
+ const target = { value: '' } as any
599
+ const event = { target } as any
600
+ const mockCalendar = { handleDateSelect: vi.fn() }
601
+ const calendarRef = { value: mockCalendar } as any
602
+ const clearInputValue = vi.fn()
603
+ const manageValidity = vi.fn()
604
+
605
+ dateProcessingUtils.processRangeBlur(
606
+ event,
607
+ ['2024-01-10'],
608
+ calendarRef,
609
+ clearInputValue,
610
+ manageValidity,
611
+ )
612
+
613
+ expect(clearInputValue).toHaveBeenCalled()
614
+ expect(manageValidity).not.toHaveBeenCalled()
615
+ expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
616
+ })
617
+ })
618
+ })
619
+
620
+ describe('keyboardUtils', () => {
621
+ describe('handleInputKeydown', () => {
622
+ it('should call toggleCalendar on Space key', () => {
623
+ const toggleCalendar = vi.fn()
624
+ const event = { key: ' ', preventDefault: vi.fn() } as any
625
+
626
+ keyboardUtils.handleInputKeydown(event, toggleCalendar)
627
+ expect(event.preventDefault).toHaveBeenCalled()
628
+ expect(toggleCalendar).toHaveBeenCalledWith(event)
629
+ })
630
+
631
+ it('should call submitForm on Enter key when provided', () => {
632
+ const toggleCalendar = vi.fn()
633
+ const submitForm = vi.fn()
634
+ const event = { key: 'Enter', preventDefault: vi.fn() } as any
635
+
636
+ keyboardUtils.handleInputKeydown(event, toggleCalendar, submitForm)
637
+ expect(event.preventDefault).toHaveBeenCalled()
638
+ expect(submitForm).toHaveBeenCalled()
639
+ expect(toggleCalendar).not.toHaveBeenCalled()
640
+ })
641
+
642
+ it('should call focusNextInput on Enter when submitForm not provided', () => {
643
+ const toggleCalendar = vi.fn()
644
+ const focusNextInput = vi.fn()
645
+ const event = { key: 'Enter', preventDefault: vi.fn() } as any
646
+
647
+ keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, focusNextInput)
648
+ expect(event.preventDefault).toHaveBeenCalled()
649
+ expect(focusNextInput).toHaveBeenCalled()
650
+ })
651
+
652
+ it('should call blurInput on Enter when neither submitForm nor focusNextInput provided', () => {
653
+ const toggleCalendar = vi.fn()
654
+ const blurInput = vi.fn()
655
+ const event = { key: 'Enter', preventDefault: vi.fn() } as any
656
+
657
+ keyboardUtils.handleInputKeydown(
658
+ event,
659
+ toggleCalendar,
660
+ undefined,
661
+ undefined,
662
+ blurInput,
663
+ )
664
+ expect(event.preventDefault).toHaveBeenCalled()
665
+ expect(blurInput).toHaveBeenCalled()
666
+ })
667
+
668
+ it('should call commaHandler on comma key when provided', () => {
669
+ const toggleCalendar = vi.fn()
670
+ const commaHandler = vi.fn()
671
+ const event = { key: ',', preventDefault: vi.fn() } as any
672
+
673
+ keyboardUtils.handleInputKeydown(
674
+ event,
675
+ toggleCalendar,
676
+ undefined,
677
+ undefined,
678
+ undefined,
679
+ commaHandler,
680
+ )
681
+ expect(event.preventDefault).toHaveBeenCalled()
682
+ expect(commaHandler).toHaveBeenCalledWith(event)
683
+ })
684
+
685
+ it('should call blurInput on comma key when commaHandler not provided', () => {
686
+ const toggleCalendar = vi.fn()
687
+ const blurInput = vi.fn()
688
+ const event = { key: ',', preventDefault: vi.fn() } as any
689
+
690
+ keyboardUtils.handleInputKeydown(
691
+ event,
692
+ toggleCalendar,
693
+ undefined,
694
+ undefined,
695
+ blurInput,
696
+ )
697
+ expect(event.preventDefault).toHaveBeenCalled()
698
+ expect(blurInput).toHaveBeenCalled()
699
+ })
700
+ })
701
+
702
+ describe('handleButtonKeydown', () => {
703
+ it('should call toggleCalendar on Enter key', () => {
704
+ const toggleCalendar = vi.fn()
705
+ const event = { key: 'Enter', preventDefault: vi.fn() } as any
706
+
707
+ keyboardUtils.handleButtonKeydown(event, toggleCalendar)
708
+ expect(event.preventDefault).toHaveBeenCalled()
709
+ expect(toggleCalendar).toHaveBeenCalledWith(event)
710
+ })
711
+
712
+ it('should call toggleCalendar on Space key', () => {
713
+ const toggleCalendar = vi.fn()
714
+ const event = { key: ' ', preventDefault: vi.fn() } as any
715
+
716
+ keyboardUtils.handleButtonKeydown(event, toggleCalendar)
717
+ expect(event.preventDefault).toHaveBeenCalled()
718
+ expect(toggleCalendar).toHaveBeenCalledWith(event)
719
+ })
720
+
721
+ it('should not call toggleCalendar on other keys', () => {
722
+ const toggleCalendar = vi.fn()
723
+ const event = { key: 'a', preventDefault: vi.fn() } as any
724
+
725
+ keyboardUtils.handleButtonKeydown(event, toggleCalendar)
726
+ expect(toggleCalendar).not.toHaveBeenCalled()
727
+ })
728
+ })
729
+ })
730
+ })