@oslokommune/punkt-elements 20.1.0 → 20.1.3

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 (108) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{calendar-xYEsKilS.cjs → calendar-KZlln5k8.cjs} +2 -2
  3. package/dist/{calendar-CkAAgviq.js → calendar-T_iFl5mR.js} +2 -2
  4. package/dist/{checkbox-Cg7wnn8U.cjs → checkbox-BS0iHeSP.cjs} +1 -1
  5. package/dist/{checkbox-XvlUQmg5.js → checkbox-DCaSNEjy.js} +1 -1
  6. package/dist/{checkbox-group-BF29zyLd.cjs → checkbox-group-BZhrLn4e.cjs} +1 -1
  7. package/dist/{checkbox-group-CRIV5CX0.js → checkbox-group-GsfZSfMs.js} +2 -2
  8. package/dist/{combobox-DuXavWEY.cjs → combobox-DoUwp_6k.cjs} +2 -1
  9. package/dist/{combobox-CDJcIkNJ.js → combobox-_2jd4CHR.js} +4 -3
  10. package/dist/components/datepicker/datepicker-popup.d.ts +0 -1
  11. package/dist/components/datepicker/datepicker-utils.d.ts +2 -13
  12. package/dist/components/datepicker/datepicker.d.ts +32 -4
  13. package/dist/components/datepicker/index.d.ts +1 -0
  14. package/dist/{datepicker-DHdFA8Dx.js → datepicker-D60fq9D5.js} +227 -472
  15. package/dist/datepicker-DErgngTf.cjs +286 -0
  16. package/dist/{fileupload-Cjde62AR.cjs → fileupload-Cn7CvQtX.cjs} +1 -1
  17. package/dist/{fileupload-BQw5nlzQ.js → fileupload-dmzxRqGF.js} +4 -4
  18. package/dist/{header-YbbGgmsD.cjs → header-BQCGdOPT.cjs} +1 -1
  19. package/dist/{header-CZnKVVt4.js → header-Djjhqu7w.js} +2 -2
  20. package/dist/{helptext-CQaNvbiv.cjs → helptext-4xBIABJJ.cjs} +1 -1
  21. package/dist/{helptext-kKmVQkwo.js → helptext-CNg695BI.js} +1 -1
  22. package/dist/{input-element-CfZIyfon.cjs → input-element-BPzDBWdd.cjs} +1 -1
  23. package/dist/{input-element-DE0FzjgP.js → input-element-DsWlt-6T.js} +1 -1
  24. package/dist/{input-wrapper-DYg1VHPv.js → input-wrapper-BBUcHEKx.js} +2 -2
  25. package/dist/{input-wrapper-CbYMk21W.cjs → input-wrapper-EKGhSUKU.cjs} +1 -1
  26. package/dist/{listbox-df8gHweY.js → listbox-BcJbeoAT.js} +1 -1
  27. package/dist/{listbox-Du8llYth.cjs → listbox-gjzS_z0P.cjs} +1 -1
  28. package/dist/{modal-SBJ8aGvM.js → modal-CvqH9R3w.js} +1 -1
  29. package/dist/{modal-B3rnE83a.cjs → modal-Du-2AmLz.cjs} +1 -1
  30. package/dist/{option-group-element-dN0ntPZk.cjs → option-group-element-DgqdU9HX.cjs} +1 -1
  31. package/dist/{option-group-element-DtSw52Dr.js → option-group-element-Dr2FR6v7.js} +2 -2
  32. package/dist/pkt-calendar.cjs +1 -1
  33. package/dist/pkt-calendar.js +1 -1
  34. package/dist/pkt-checkbox-group.cjs +1 -1
  35. package/dist/pkt-checkbox-group.js +1 -1
  36. package/dist/pkt-checkbox.cjs +1 -1
  37. package/dist/pkt-checkbox.js +1 -1
  38. package/dist/pkt-combobox.cjs +1 -1
  39. package/dist/pkt-combobox.js +1 -1
  40. package/dist/pkt-datepicker.cjs +1 -1
  41. package/dist/pkt-datepicker.js +1 -1
  42. package/dist/pkt-fileupload.cjs +1 -1
  43. package/dist/pkt-fileupload.js +1 -1
  44. package/dist/pkt-header.cjs +1 -1
  45. package/dist/pkt-header.js +1 -1
  46. package/dist/pkt-helptext.cjs +1 -1
  47. package/dist/pkt-helptext.js +1 -1
  48. package/dist/pkt-index.cjs +1 -1
  49. package/dist/pkt-index.js +18 -18
  50. package/dist/pkt-input-wrapper.cjs +1 -1
  51. package/dist/pkt-input-wrapper.js +1 -1
  52. package/dist/pkt-listbox.cjs +1 -1
  53. package/dist/pkt-listbox.js +1 -1
  54. package/dist/pkt-modal.cjs +1 -1
  55. package/dist/pkt-modal.js +1 -1
  56. package/dist/pkt-progressbar.cjs +1 -1
  57. package/dist/pkt-progressbar.js +1 -1
  58. package/dist/pkt-radio-group.cjs +1 -1
  59. package/dist/pkt-radio-group.js +1 -1
  60. package/dist/pkt-radiobutton.cjs +1 -1
  61. package/dist/pkt-radiobutton.js +1 -1
  62. package/dist/pkt-searchinput.cjs +1 -1
  63. package/dist/pkt-searchinput.js +1 -1
  64. package/dist/pkt-select.cjs +1 -1
  65. package/dist/pkt-select.js +1 -1
  66. package/dist/pkt-textarea.cjs +1 -1
  67. package/dist/pkt-textarea.js +1 -1
  68. package/dist/pkt-textinput.cjs +1 -1
  69. package/dist/pkt-textinput.js +1 -1
  70. package/dist/pkt-timepicker.cjs +1 -1
  71. package/dist/pkt-timepicker.js +1 -1
  72. package/dist/{progressbar-Wyq_Q1Ur.cjs → progressbar-BKA9zFnE.cjs} +1 -1
  73. package/dist/{progressbar-DlbkKRgP.js → progressbar-aKjj5VZg.js} +1 -1
  74. package/dist/{radio-group-u1IH7g-4.cjs → radio-group-BLVe7aMS.cjs} +1 -1
  75. package/dist/{radio-group-bRF8fnPW.js → radio-group-DDymupLt.js} +2 -2
  76. package/dist/{radiobutton-Bf6aFfYt.js → radiobutton-CnExI3Vj.js} +1 -1
  77. package/dist/{radiobutton-86ZvvDmJ.cjs → radiobutton-XX3zhblA.cjs} +1 -1
  78. package/dist/{searchinput-D5l64Mkb.cjs → searchinput-C8UHexyf.cjs} +1 -1
  79. package/dist/{searchinput-DN8kDZQ1.js → searchinput-DUJlxKHH.js} +1 -1
  80. package/dist/{select-CXIEM2qY.js → select-C4RDEc9h.js} +2 -2
  81. package/dist/{select-C7kAlSxs.cjs → select-CbVmSYmi.cjs} +1 -1
  82. package/dist/shared-types/datepicker.d.ts +76 -0
  83. package/dist/shared-types/index.d.ts +2 -2
  84. package/dist/shared-utils/datepicker-utils.d.ts +18 -0
  85. package/dist/shared-utils/forms/focus-within.d.ts +13 -0
  86. package/dist/{textarea-CRcSfWgX.cjs → textarea-D4yCdiFY.cjs} +1 -1
  87. package/dist/{textarea-12le8b4I.js → textarea-WktT3N3w.js} +2 -2
  88. package/dist/{textinput-CC0zxUBT.js → textinput-BPrC2neL.js} +2 -2
  89. package/dist/{textinput-Dooq2n8W.cjs → textinput-C6AcbxnQ.cjs} +1 -1
  90. package/dist/{timepicker-Cz81VQu3.js → timepicker-D77gssQg.js} +2 -2
  91. package/dist/{timepicker-BM4I5w-i.cjs → timepicker-DaqrwM-g.cjs} +1 -1
  92. package/dist/{utils-Dn5ykFtS.js → utils-BtXhdgpe.js} +2 -2
  93. package/dist/utils-dl1XA5EE.cjs +1 -0
  94. package/package.json +2 -2
  95. package/src/components/calendar/calendar.selection.test.ts +16 -0
  96. package/src/components/calendar/calendar.ts +4 -3
  97. package/src/components/combobox/combobox.interaction.test.ts +23 -0
  98. package/src/components/combobox/combobox.ts +1 -0
  99. package/src/components/datepicker/datepicker-multiple.ts +2 -6
  100. package/src/components/datepicker/datepicker-popup.ts +3 -6
  101. package/src/components/datepicker/datepicker-utils.test.ts +40 -203
  102. package/src/components/datepicker/datepicker-utils.ts +3 -94
  103. package/src/components/datepicker/datepicker.accessibility.test.ts +2 -2
  104. package/src/components/datepicker/datepicker.core.test.ts +236 -0
  105. package/src/components/datepicker/datepicker.ts +179 -115
  106. package/src/components/datepicker/index.ts +1 -0
  107. package/dist/datepicker-Cnrbqm62.cjs +0 -294
  108. package/dist/utils-63cRpWut.cjs +0 -1
@@ -4,11 +4,11 @@ import {
4
4
  inputTypeUtils,
5
5
  formUtils,
6
6
  calendarUtils,
7
- eventUtils,
8
7
  cssUtils,
9
8
  dateProcessingUtils,
10
9
  keyboardUtils,
11
10
  } from './datepicker-utils'
11
+ import { completeRangeWithTo } from 'shared-utils/datepicker-utils'
12
12
  import { createRef } from 'lit/directives/ref.js'
13
13
 
14
14
  const mockIsIOS = vi.hoisted(() => vi.fn(() => false))
@@ -132,53 +132,6 @@ describe('datepicker-utils', () => {
132
132
  expect(fallback).toHaveBeenCalledOnce()
133
133
  })
134
134
  })
135
-
136
- describe('validateDateInput', () => {
137
- it('should return early if input has no value', () => {
138
- const input = { value: '' } as HTMLInputElement
139
- const internals = { setValidity: vi.fn() }
140
- formUtils.validateDateInput(input, internals)
141
- expect(internals.setValidity).not.toHaveBeenCalled()
142
- })
143
-
144
- it('should set rangeUnderflow validity when value is below minimum', () => {
145
- const input = { value: '2024-01-01' } as HTMLInputElement
146
- const internals = { setValidity: vi.fn() }
147
- const messages = { dateUnderflow: 'Too early' }
148
-
149
- formUtils.validateDateInput(input, internals, '2024-01-10', null, messages)
150
- expect(internals.setValidity).toHaveBeenCalledWith(
151
- { rangeUnderflow: true },
152
- 'Too early',
153
- input,
154
- )
155
- })
156
-
157
- it('should set rangeOverflow validity when value is above maximum', () => {
158
- const input = { value: '2024-01-31' } as HTMLInputElement
159
- const internals = { setValidity: vi.fn() }
160
- const messages = { dateOverflow: 'Too late' }
161
-
162
- formUtils.validateDateInput(input, internals, null, '2024-01-20', messages)
163
- expect(internals.setValidity).toHaveBeenCalledWith(
164
- { rangeOverflow: true },
165
- 'Too late',
166
- input,
167
- )
168
- })
169
-
170
- it('should use default messages when strings not provided', () => {
171
- const input = { value: '2024-01-01' } as HTMLInputElement
172
- const internals = { setValidity: vi.fn() }
173
-
174
- formUtils.validateDateInput(input, internals, '2024-01-10', null)
175
- expect(internals.setValidity).toHaveBeenCalledWith(
176
- { rangeUnderflow: true },
177
- 'For tidlig dato',
178
- input,
179
- )
180
- })
181
- })
182
135
  })
183
136
 
184
137
  describe('calendarUtils', () => {
@@ -270,127 +223,33 @@ describe('datepicker-utils', () => {
270
223
  calendarUtils.handleCalendarPosition(popupRef, inputRef, true)
271
224
  expect(mockPopup.style.top).toBe('calc(100% - 30px)')
272
225
  })
273
- })
274
- })
275
-
276
- describe('eventUtils', () => {
277
- describe('createDocumentClickListener', () => {
278
- it('should return a function', () => {
279
- const listener = eventUtils.createDocumentClickListener(
280
- createRef() as any,
281
- null,
282
- createRef() as any,
283
- () => true,
284
- vi.fn(),
285
- vi.fn(),
286
- )
287
- expect(typeof listener).toBe('function')
288
- })
289
-
290
- it('should call onBlur and hideCalendar when clicking outside', () => {
291
- const onBlur = vi.fn()
292
- const hideCalendar = vi.fn()
293
- const inputRef = { value: { contains: () => false } } as any
294
- const btnRef = { value: { contains: () => false } } as any
295
-
296
- const listener = eventUtils.createDocumentClickListener(
297
- inputRef,
298
- null,
299
- btnRef,
300
- () => true,
301
- onBlur,
302
- hideCalendar,
303
- )
304
-
305
- const event = {
306
- target: document.createElement('div'),
307
- } as any
308
-
309
- listener(event)
310
- expect(onBlur).toHaveBeenCalledOnce()
311
- expect(hideCalendar).toHaveBeenCalledOnce()
312
- })
313
-
314
- it('should not call handlers when clicking inside input', () => {
315
- const onBlur = vi.fn()
316
- const hideCalendar = vi.fn()
317
- const inputRef = { value: { contains: () => true } } as any
318
- const btnRef = { value: { contains: () => false } } as any
319
-
320
- const listener = eventUtils.createDocumentClickListener(
321
- inputRef,
322
- null,
323
- btnRef,
324
- () => true,
325
- onBlur,
326
- hideCalendar,
327
- )
328
-
329
- const event = {
330
- target: document.createElement('div'),
331
- } as any
332
-
333
- listener(event)
334
- expect(onBlur).not.toHaveBeenCalled()
335
- expect(hideCalendar).not.toHaveBeenCalled()
336
- })
337
- })
338
226
 
339
- describe('createDocumentKeydownListener', () => {
340
- it('should return a function', () => {
341
- const listener = eventUtils.createDocumentKeydownListener(() => true, vi.fn())
342
- expect(typeof listener).toBe('function')
343
- })
344
-
345
- it('should call hideCalendar on Escape key', () => {
346
- const hideCalendar = vi.fn()
347
- const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)
348
-
349
- const event = { key: 'Escape' } as KeyboardEvent
350
- listener(event)
351
- expect(hideCalendar).toHaveBeenCalledOnce()
352
- })
353
-
354
- it('should not call hideCalendar if calendar is not open', () => {
355
- const hideCalendar = vi.fn()
356
- const listener = eventUtils.createDocumentKeydownListener(() => false, hideCalendar)
357
-
358
- const event = { key: 'Escape' } as KeyboardEvent
359
- listener(event)
360
- expect(hideCalendar).not.toHaveBeenCalled()
361
- })
362
-
363
- it('should not call hideCalendar on other keys', () => {
364
- const hideCalendar = vi.fn()
365
- const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)
366
-
367
- const event = { key: 'Enter' } as KeyboardEvent
368
- listener(event)
369
- expect(hideCalendar).not.toHaveBeenCalled()
370
- })
371
- })
227
+ describe('with the popup margin', () => {
228
+ const position = (inputTop: number) => {
229
+ const popup = document.createElement('div')
230
+ popup.style.marginTop = '8px'
231
+ popup.getBoundingClientRect = () => ({ height: 300 }) as DOMRect
232
+ document.body.appendChild(popup)
233
+ const input = {
234
+ getBoundingClientRect: () => ({ height: 40, top: inputTop }),
235
+ parentElement: null,
236
+ }
237
+ calendarUtils.handleCalendarPosition({ value: popup } as any, { value: input } as any)
238
+ popup.remove()
239
+ return popup.style.top
240
+ }
372
241
 
373
- describe('handleFocusOut', () => {
374
- it('should call onBlur and hideCalendar when focus leaves element', () => {
375
- const onBlur = vi.fn()
376
- const hideCalendar = vi.fn()
377
- const element = { contains: () => false } as any
378
- const event = { target: document.createElement('div') } as any
242
+ it('flips above when the margin pushes the popup off screen', () => {
243
+ expect(position(window.innerHeight - 40 - 300 - 4)).toBe('calc(100% - 356px)')
244
+ })
379
245
 
380
- eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
381
- expect(onBlur).toHaveBeenCalledOnce()
382
- expect(hideCalendar).toHaveBeenCalledOnce()
383
- })
246
+ it('stays below when the popup and margin fit', () => {
247
+ expect(position(window.innerHeight - 40 - 300 - 8)).toBe('100%')
248
+ })
384
249
 
385
- it('should not call handlers when focus stays within element', () => {
386
- const onBlur = vi.fn()
387
- const hideCalendar = vi.fn()
388
- const element = { contains: () => true } as any
389
- const event = { target: document.createElement('div') } as any
390
-
391
- eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
392
- expect(onBlur).not.toHaveBeenCalled()
393
- expect(hideCalendar).not.toHaveBeenCalled()
250
+ it('stays below when there is no room above either', () => {
251
+ expect(position(200)).toBe('100%')
252
+ })
394
253
  })
395
254
  })
396
255
  })
@@ -561,49 +420,27 @@ describe('datepicker-utils', () => {
561
420
  expect(manageValidity).not.toHaveBeenCalled()
562
421
  })
563
422
  })
423
+ })
564
424
 
565
- describe('processRangeBlur', () => {
566
- it('should call manageValidity and handleDateSelect when target has value', () => {
567
- const target = { value: '2024-01-15' } as any
568
- const event = { target } as any
569
- const mockCalendar = { handleDateSelect: vi.fn() }
570
- const calendarRef = { value: mockCalendar } as any
571
- const clearInputValue = vi.fn()
572
- const manageValidity = vi.fn()
573
-
574
- dateProcessingUtils.processRangeBlur(
575
- event,
576
- ['2024-01-10', '2024-01-20'],
577
- calendarRef,
578
- clearInputValue,
579
- manageValidity,
580
- )
425
+ describe('completeRangeWithTo', () => {
426
+ it('completes the range with a waiting to-date', () => {
427
+ expect(completeRangeWithTo(['2026-11-03'], '2026-11-12')).toEqual(['2026-11-03', '2026-11-12'])
428
+ })
581
429
 
582
- expect(manageValidity).toHaveBeenCalledWith(target)
583
- expect(clearInputValue).not.toHaveBeenCalled()
584
- expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
585
- })
430
+ it('allows a one-day range', () => {
431
+ expect(completeRangeWithTo(['2026-11-12'], '2026-11-12')).toEqual(['2026-11-12', '2026-11-12'])
432
+ })
586
433
 
587
- it('should clear input value when target is empty but values[0] exists', () => {
588
- const target = { value: '' } as any
589
- const event = { target } as any
590
- const mockCalendar = { handleDateSelect: vi.fn() }
591
- const calendarRef = { value: mockCalendar } as any
592
- const clearInputValue = vi.fn()
593
- const manageValidity = vi.fn()
434
+ it('keeps the selection when the date comes after the to-date', () => {
435
+ expect(completeRangeWithTo(['2026-11-20'], '2026-11-12')).toEqual(['2026-11-20'])
436
+ })
594
437
 
595
- dateProcessingUtils.processRangeBlur(
596
- event,
597
- ['2024-01-10'],
598
- calendarRef,
599
- clearInputValue,
600
- manageValidity,
601
- )
438
+ it('keeps the selection when an excluded date lies in between', () => {
439
+ expect(completeRangeWithTo(['2026-11-03'], '2026-11-12', ['2026-11-06'])).toEqual(['2026-11-03'])
440
+ })
602
441
 
603
- expect(clearInputValue).toHaveBeenCalled()
604
- expect(manageValidity).not.toHaveBeenCalled()
605
- expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
606
- })
442
+ it('keeps the selection when there is no to-date', () => {
443
+ expect(completeRangeWithTo(['2026-11-03'], null)).toEqual(['2026-11-03'])
607
444
  })
608
445
  })
609
446
 
@@ -1,7 +1,6 @@
1
- import { fromISOToDate, newDate } from 'shared-utils/date-utils'
1
+ import { newDate } from 'shared-utils/date-utils'
2
2
  import { Ref } from 'lit/directives/ref.js'
3
3
  import { PktCalendar } from '@/components/calendar/calendar'
4
- import { nb, type TPktStrings } from 'shared-strings'
5
4
  import {
6
5
  validateRangeOrder,
7
6
  sortDates,
@@ -45,7 +44,7 @@ export const inputTypeUtils = {
45
44
  type TFormOwner = { form?: Pick<HTMLFormElement, 'requestSubmit'> | null }
46
45
 
47
46
  /**
48
- * Form and validation utilities (Lit-specific — uses ElementInternals)
47
+ * Form utilities (Lit-specific — uses ElementInternals)
49
48
  */
50
49
  export const formUtils = {
51
50
  submitForm(element: HTMLElement & { internals?: TFormOwner }): void {
@@ -63,25 +62,6 @@ export const formUtils = {
63
62
  fallbackAction()
64
63
  }
65
64
  },
66
-
67
- validateDateInput(
68
- input: HTMLInputElement,
69
- internals: Pick<ElementInternals, 'setValidity'>,
70
- min?: string | null,
71
- max?: string | null,
72
- messages?: Partial<TPktStrings['validation']>,
73
- ): void {
74
- const value = input.value
75
- if (!value) return
76
-
77
- const validation = { ...nb.validation, ...messages }
78
-
79
- if (min && min > value) {
80
- internals.setValidity({ rangeUnderflow: true }, validation.dateUnderflow, input)
81
- } else if (max && max < value) {
82
- internals.setValidity({ rangeOverflow: true }, validation.dateOverflow, input)
83
- }
84
- },
85
65
  }
86
66
 
87
67
  /**
@@ -124,58 +104,6 @@ export const calendarUtils = {
124
104
  },
125
105
  }
126
106
 
127
- /**
128
- * Event handling utilities (Lit-specific — uses Lit Ref types)
129
- */
130
- export const eventUtils = {
131
- createDocumentClickListener(
132
- inputRef: Ref<HTMLInputElement>,
133
- inputRefTo: Ref<HTMLInputElement> | null,
134
- btnRef: Ref<HTMLButtonElement>,
135
- getCalendarOpen: () => boolean,
136
- onBlur: () => void,
137
- hideCalendar: () => void,
138
- ): (e: MouseEvent) => void {
139
- return (e: MouseEvent) => {
140
- if (
141
- inputRef?.value &&
142
- btnRef?.value &&
143
- !inputRef.value.contains(e.target as Node) &&
144
- !(inputRefTo?.value && inputRefTo.value.contains(e.target as Node)) &&
145
- !btnRef.value.contains(e.target as Node) &&
146
- !(e.target as Element).closest('.pkt-calendar-popup') &&
147
- getCalendarOpen()
148
- ) {
149
- onBlur()
150
- hideCalendar()
151
- }
152
- }
153
- },
154
-
155
- createDocumentKeydownListener(
156
- getCalendarOpen: () => boolean,
157
- hideCalendar: () => void,
158
- ): (e: KeyboardEvent) => void {
159
- return (e: KeyboardEvent) => {
160
- if (e.key === 'Escape' && getCalendarOpen()) {
161
- hideCalendar()
162
- }
163
- }
164
- },
165
-
166
- handleFocusOut(
167
- event: FocusEvent,
168
- element: HTMLElement,
169
- onBlur: () => void,
170
- hideCalendar: () => void,
171
- ): void {
172
- if (!element.contains(event.target as Node)) {
173
- onBlur()
174
- hideCalendar()
175
- }
176
- },
177
- }
178
-
179
107
  /**
180
108
  * CSS class utilities
181
109
  * Delegates to shared-utils/datepicker-utils
@@ -189,7 +117,7 @@ export const cssUtils = {
189
117
  /**
190
118
  * Date value processing utilities
191
119
  * processDateSelection delegates to shared-utils.
192
- * updateInputValues and processRangeBlur stay Lit-specific (use Lit Ref).
120
+ * updateInputValues stays Lit-specific (uses Lit Ref).
193
121
  */
194
122
  export const dateProcessingUtils = {
195
123
  processDateSelection: sharedProcessDateSelection,
@@ -214,25 +142,6 @@ export const dateProcessingUtils = {
214
142
  manageValidity(inputRef.value)
215
143
  }
216
144
  },
217
-
218
- processRangeBlur(
219
- event: Event,
220
- values: string[],
221
- calendarRef: Ref<PktCalendar>,
222
- clearInputValue: () => void,
223
- manageValidity: (input: HTMLInputElement) => void,
224
- ): void {
225
- const target = event.target as HTMLInputElement
226
- if (target.value) {
227
- manageValidity(target)
228
- const date = fromISOToDate(target.value)
229
- if (date) {
230
- calendarRef?.value?.handleDateSelect(date)
231
- }
232
- } else if (values[0]) {
233
- clearInputValue()
234
- }
235
- },
236
145
  }
237
146
 
238
147
  /**
@@ -85,9 +85,9 @@ describe('PktDatepicker', () => {
85
85
  })
86
86
 
87
87
  const input = datepicker.querySelector('input') as HTMLInputElement
88
- fireEvent.focus(input)
88
+ input.focus()
89
89
  await datepicker.updateComplete
90
- fireEvent.blur(input)
90
+ input.blur()
91
91
  await datepicker.updateComplete
92
92
 
93
93
  expect(focusEventFired).toBe(true)
@@ -10,6 +10,7 @@ import '../calendar/calendar'
10
10
  import { PktDatepicker } from './datepicker'
11
11
  import { PktCalendar } from '../calendar/calendar'
12
12
  import { PktDatepickerPopup } from './datepicker-popup'
13
+ import type { PktInputWrapper } from '../input-wrapper/input-wrapper'
13
14
 
14
15
  const waitForCustomElements = async () => {
15
16
  await customElements.whenDefined('pkt-datepicker')
@@ -140,6 +141,10 @@ describe('PktDatepicker', () => {
140
141
  expect(datepicker.range).toBe(false)
141
142
  expect(datepicker.maxlength).toBe(null)
142
143
  expect(datepicker.showRangeLabels).toBe(false)
144
+ expect(datepicker.dateformat).toBe('dd.MM.yyyy')
145
+ expect(datepicker.weeknumbers).toBe(false)
146
+ expect(datepicker.withcontrols).toBe(false)
147
+ expect(datepicker.calendarOpen).toBe(false)
143
148
  })
144
149
 
145
150
  test('handles value property correctly', async () => {
@@ -194,6 +199,237 @@ describe('PktDatepicker', () => {
194
199
  expect(label?.textContent).toContain(testLabel)
195
200
  })
196
201
 
202
+ describe('counter', () => {
203
+ const counterFor = async (attributes: string) => {
204
+ const container = await createDatepicker(attributes)
205
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
206
+ await datepicker.updateComplete
207
+ const wrapper = datepicker.querySelector('pkt-input-wrapper') as PktInputWrapper
208
+ await wrapper.updateComplete
209
+ return datepicker.querySelector('.pkt-input__counter')
210
+ }
211
+
212
+ test('shows the counter for multiple dates with a limit', async () => {
213
+ expect(await counterFor('multiple maxlength="3"')).toBeTruthy()
214
+ })
215
+
216
+ test('hides the counter when counter is false', async () => {
217
+ expect(await counterFor('multiple maxlength="3" counter="false"')).toBeNull()
218
+ })
219
+
220
+ test('shows the counter when counter is set without a limit', async () => {
221
+ expect(await counterFor('counter')).toBeTruthy()
222
+ })
223
+
224
+ test('shows no counter when maxlength is 0', async () => {
225
+ expect(await counterFor('multiple maxlength="0"')).toBeNull()
226
+ })
227
+ })
228
+
229
+ test('keeps the input enabled when maxlength is 0', async () => {
230
+ const container = await createDatepicker('multiple maxlength="0"')
231
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
232
+ await datepicker.updateComplete
233
+
234
+ expect(datepicker.querySelector('input.pkt-datepicker__input')).not.toHaveAttribute('disabled')
235
+ })
236
+
237
+ describe('focus and blur', () => {
238
+ const setup = async (attributes = '') => {
239
+ const container = await createDatepicker(attributes)
240
+ const outside = container.appendChild(document.createElement('button'))
241
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
242
+ await datepicker.updateComplete
243
+ const events: string[] = []
244
+ datepicker.addEventListener('focus', () => events.push('focus'))
245
+ datepicker.addEventListener('blur', () => events.push('blur'))
246
+ const input = datepicker.querySelector('input.pkt-datepicker__input') as HTMLInputElement
247
+ const button = datepicker.querySelector('.pkt-datepicker__calendar-button') as HTMLElement
248
+ return { datepicker, input, button, outside, events }
249
+ }
250
+
251
+ test('fires one focus and one blur per visit, however focus moves inside', async () => {
252
+ const { input, button, outside, events } = await setup()
253
+ input.focus()
254
+ button.focus()
255
+ input.focus()
256
+ outside.focus()
257
+
258
+ expect(events).toEqual(['focus', 'blur'])
259
+ })
260
+
261
+ test('ignores focus events while focus is already inside', async () => {
262
+ const { input, events } = await setup()
263
+ input.focus()
264
+ input.dispatchEvent(
265
+ new FocusEvent('focusin', { bubbles: true, relatedTarget: document.createElement('span') }),
266
+ )
267
+
268
+ expect(events).toEqual(['focus'])
269
+ })
270
+
271
+ test('returns focus to the field after picking a date', async () => {
272
+ const { datepicker, input, button, events } = await setup(
273
+ 'today="2026-11-09" currentmonth="2026-11-01"',
274
+ )
275
+ input.focus()
276
+ fireEvent.click(button)
277
+ const popup = datepicker.querySelector('pkt-datepicker-popup') as PktDatepickerPopup
278
+ await popup.updateComplete
279
+ await (popup.calendarRef.value as PktCalendar).updateComplete
280
+ const date = datepicker.querySelector(
281
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
282
+ ) as HTMLElement
283
+ date.focus()
284
+ fireEvent.click(date)
285
+ await datepicker.updateComplete
286
+
287
+ expect(document.activeElement).toBe(input)
288
+ expect(events).toEqual(['focus'])
289
+ })
290
+ })
291
+
292
+ describe('emptying range fields', () => {
293
+ const setup = async (value = '2026-11-05,2026-11-12') => {
294
+ const container = await createDatepicker(
295
+ `id="dp" range value="${value}" today="2026-11-09" currentmonth="2026-11-01"`,
296
+ )
297
+ const outside = container.appendChild(document.createElement('button'))
298
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
299
+ await datepicker.updateComplete
300
+ const from = datepicker.querySelector('#dp-input') as HTMLInputElement
301
+ const to = datepicker.querySelector('#dp-to') as HTMLInputElement
302
+ const type = async (input: HTMLInputElement, text: string, next: HTMLElement) => {
303
+ input.focus()
304
+ input.value = text
305
+ next.focus()
306
+ await datepicker.updateComplete
307
+ }
308
+ const pick = async (date: string) => {
309
+ fireEvent.click(datepicker.querySelector('.pkt-datepicker__calendar-button')!)
310
+ const popup = datepicker.querySelector('pkt-datepicker-popup') as PktDatepickerPopup
311
+ await popup.updateComplete
312
+ await (popup.calendarRef.value as PktCalendar).updateComplete
313
+ fireEvent.click(datepicker.querySelector(`[data-date="${date}"]`)!)
314
+ await datepicker.updateComplete
315
+ return popup
316
+ }
317
+ return { datepicker, from, to, outside, type, pick }
318
+ }
319
+
320
+ test('keeps the from-date when the to-date is emptied', async () => {
321
+ const { datepicker, from, to, outside, type } = await setup()
322
+ const changes: string[] = []
323
+ const valueChanges: unknown[] = []
324
+ datepicker.addEventListener('change', () => changes.push(datepicker.value))
325
+ datepicker.addEventListener('value-change', (e) =>
326
+ valueChanges.push((e as CustomEvent).detail),
327
+ )
328
+ await type(to, '', outside)
329
+
330
+ expect([from.value, to.value]).toEqual(['2026-11-05', ''])
331
+ expect(datepicker.value).toBe('2026-11-05')
332
+ expect(valueChanges).toEqual([['2026-11-05']])
333
+ expect(changes).toEqual([])
334
+ })
335
+
336
+ test('keeps the to-date in its field while focus stays inside', async () => {
337
+ const { datepicker, from, to, type } = await setup()
338
+ await type(from, '', to)
339
+
340
+ expect([from.value, to.value]).toEqual(['', '2026-11-12'])
341
+ expect(datepicker.value).toBe('')
342
+ })
343
+
344
+ test('clears a to-date with no from-date when focus leaves', async () => {
345
+ const { datepicker, from, to, outside, type } = await setup()
346
+ await type(from, '', to)
347
+ outside.focus()
348
+ await datepicker.updateComplete
349
+
350
+ expect([from.value, to.value]).toEqual(['', ''])
351
+ expect(datepicker.value).toBe('')
352
+ })
353
+
354
+ test('completes the range when a new from-date is typed', async () => {
355
+ const { datepicker, from, to, outside, type } = await setup()
356
+ await type(from, '', to)
357
+ await type(from, '2026-11-03', outside)
358
+
359
+ expect([from.value, to.value]).toEqual(['2026-11-03', '2026-11-12'])
360
+ expect(datepicker.value).toBe('2026-11-03,2026-11-12')
361
+ })
362
+
363
+ test('keeps a to-date typed before the from-date', async () => {
364
+ const { datepicker, from, to, outside, type } = await setup('')
365
+ await type(to, '2026-11-12', from)
366
+
367
+ expect([from.value, to.value]).toEqual(['', '2026-11-12'])
368
+
369
+ await type(from, '2026-11-03', outside)
370
+
371
+ expect(datepicker.value).toBe('2026-11-03,2026-11-12')
372
+ })
373
+
374
+ test('completes the range when a from-date is picked in the calendar', async () => {
375
+ const { datepicker, from, to, type, pick } = await setup()
376
+ await type(from, '', to)
377
+ const popup = await pick('2026-11-03')
378
+
379
+ expect([from.value, to.value]).toEqual(['2026-11-03', '2026-11-12'])
380
+ expect(datepicker.value).toBe('2026-11-03,2026-11-12')
381
+ expect(popup.open).toBe(false)
382
+ })
383
+
384
+ test('shows only complete parts of the range in the calendar while typing', async () => {
385
+ const { datepicker, from, to } = await setup()
386
+ fireEvent.click(datepicker.querySelector('.pkt-datepicker__calendar-button')!)
387
+ const popup = datepicker.querySelector('pkt-datepicker-popup') as PktDatepickerPopup
388
+ await popup.updateComplete
389
+ const calendar = popup.calendarRef.value as PktCalendar
390
+ const edit = async (input: HTMLInputElement, text: string) => {
391
+ input.value = text
392
+ input.dispatchEvent(new Event('input', { bubbles: true }))
393
+ await datepicker.updateComplete
394
+ await popup.updateComplete
395
+ return calendar.selected
396
+ }
397
+
398
+ expect(await edit(from, '')).toEqual([])
399
+ expect(await edit(from, '2026-11-03')).toEqual(['2026-11-03', '2026-11-12'])
400
+ expect(await edit(to, '')).toEqual(['2026-11-03'])
401
+ })
402
+
403
+ test('starts a new range when the picked date comes after the to-date', async () => {
404
+ const { datepicker, from, to, type, pick } = await setup()
405
+ await type(from, '', to)
406
+ await pick('2026-11-20')
407
+
408
+ expect([from.value, to.value]).toEqual(['2026-11-20', ''])
409
+ expect(datepicker.value).toBe('2026-11-20')
410
+ })
411
+ })
412
+
413
+ describe('calendar position', () => {
414
+ const popupTopAfterOpening = async (attributes: string) => {
415
+ const container = await createDatepicker(attributes)
416
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
417
+ await datepicker.updateComplete
418
+ fireEvent.click(datepicker.querySelector('.pkt-datepicker__calendar-button')!)
419
+ const popup = datepicker.querySelector('.pkt-calendar-popup') as HTMLElement
420
+ await vi.waitFor(() => expect(popup.style.top).not.toBe(''))
421
+ return popup.style.top
422
+ }
423
+
424
+ test('places the popup below the field when it opens', async () => {
425
+ expect(await popupTopAfterOpening('')).toBe('100%')
426
+ })
427
+
428
+ test('places the popup above the counter when there is one', async () => {
429
+ expect(await popupTopAfterOpening('multiple maxlength="3"')).toBe('calc(100% - 30px)')
430
+ })
431
+ })
432
+
197
433
  test('handles dateformat property correctly', async () => {
198
434
  const customFormat = 'MM/dd/yyyy'
199
435
  const container = await createDatepicker(