@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.
- package/CHANGELOG.md +31 -0
- package/dist/{calendar-xYEsKilS.cjs → calendar-KZlln5k8.cjs} +2 -2
- package/dist/{calendar-CkAAgviq.js → calendar-T_iFl5mR.js} +2 -2
- package/dist/{checkbox-Cg7wnn8U.cjs → checkbox-BS0iHeSP.cjs} +1 -1
- package/dist/{checkbox-XvlUQmg5.js → checkbox-DCaSNEjy.js} +1 -1
- package/dist/{checkbox-group-BF29zyLd.cjs → checkbox-group-BZhrLn4e.cjs} +1 -1
- package/dist/{checkbox-group-CRIV5CX0.js → checkbox-group-GsfZSfMs.js} +2 -2
- package/dist/{combobox-DuXavWEY.cjs → combobox-DoUwp_6k.cjs} +2 -1
- package/dist/{combobox-CDJcIkNJ.js → combobox-_2jd4CHR.js} +4 -3
- package/dist/components/datepicker/datepicker-popup.d.ts +0 -1
- package/dist/components/datepicker/datepicker-utils.d.ts +2 -13
- package/dist/components/datepicker/datepicker.d.ts +32 -4
- package/dist/components/datepicker/index.d.ts +1 -0
- package/dist/{datepicker-DHdFA8Dx.js → datepicker-D60fq9D5.js} +227 -472
- package/dist/datepicker-DErgngTf.cjs +286 -0
- package/dist/{fileupload-Cjde62AR.cjs → fileupload-Cn7CvQtX.cjs} +1 -1
- package/dist/{fileupload-BQw5nlzQ.js → fileupload-dmzxRqGF.js} +4 -4
- package/dist/{header-YbbGgmsD.cjs → header-BQCGdOPT.cjs} +1 -1
- package/dist/{header-CZnKVVt4.js → header-Djjhqu7w.js} +2 -2
- package/dist/{helptext-CQaNvbiv.cjs → helptext-4xBIABJJ.cjs} +1 -1
- package/dist/{helptext-kKmVQkwo.js → helptext-CNg695BI.js} +1 -1
- package/dist/{input-element-CfZIyfon.cjs → input-element-BPzDBWdd.cjs} +1 -1
- package/dist/{input-element-DE0FzjgP.js → input-element-DsWlt-6T.js} +1 -1
- package/dist/{input-wrapper-DYg1VHPv.js → input-wrapper-BBUcHEKx.js} +2 -2
- package/dist/{input-wrapper-CbYMk21W.cjs → input-wrapper-EKGhSUKU.cjs} +1 -1
- package/dist/{listbox-df8gHweY.js → listbox-BcJbeoAT.js} +1 -1
- package/dist/{listbox-Du8llYth.cjs → listbox-gjzS_z0P.cjs} +1 -1
- package/dist/{modal-SBJ8aGvM.js → modal-CvqH9R3w.js} +1 -1
- package/dist/{modal-B3rnE83a.cjs → modal-Du-2AmLz.cjs} +1 -1
- package/dist/{option-group-element-dN0ntPZk.cjs → option-group-element-DgqdU9HX.cjs} +1 -1
- package/dist/{option-group-element-DtSw52Dr.js → option-group-element-Dr2FR6v7.js} +2 -2
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-checkbox-group.cjs +1 -1
- package/dist/pkt-checkbox-group.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-fileupload.cjs +1 -1
- package/dist/pkt-fileupload.js +1 -1
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +18 -18
- package/dist/pkt-input-wrapper.cjs +1 -1
- package/dist/pkt-input-wrapper.js +1 -1
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-modal.cjs +1 -1
- package/dist/pkt-modal.js +1 -1
- package/dist/pkt-progressbar.cjs +1 -1
- package/dist/pkt-progressbar.js +1 -1
- package/dist/pkt-radio-group.cjs +1 -1
- package/dist/pkt-radio-group.js +1 -1
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-searchinput.cjs +1 -1
- package/dist/pkt-searchinput.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/pkt-timepicker.cjs +1 -1
- package/dist/pkt-timepicker.js +1 -1
- package/dist/{progressbar-Wyq_Q1Ur.cjs → progressbar-BKA9zFnE.cjs} +1 -1
- package/dist/{progressbar-DlbkKRgP.js → progressbar-aKjj5VZg.js} +1 -1
- package/dist/{radio-group-u1IH7g-4.cjs → radio-group-BLVe7aMS.cjs} +1 -1
- package/dist/{radio-group-bRF8fnPW.js → radio-group-DDymupLt.js} +2 -2
- package/dist/{radiobutton-Bf6aFfYt.js → radiobutton-CnExI3Vj.js} +1 -1
- package/dist/{radiobutton-86ZvvDmJ.cjs → radiobutton-XX3zhblA.cjs} +1 -1
- package/dist/{searchinput-D5l64Mkb.cjs → searchinput-C8UHexyf.cjs} +1 -1
- package/dist/{searchinput-DN8kDZQ1.js → searchinput-DUJlxKHH.js} +1 -1
- package/dist/{select-CXIEM2qY.js → select-C4RDEc9h.js} +2 -2
- package/dist/{select-C7kAlSxs.cjs → select-CbVmSYmi.cjs} +1 -1
- package/dist/shared-types/datepicker.d.ts +76 -0
- package/dist/shared-types/index.d.ts +2 -2
- package/dist/shared-utils/datepicker-utils.d.ts +18 -0
- package/dist/shared-utils/forms/focus-within.d.ts +13 -0
- package/dist/{textarea-CRcSfWgX.cjs → textarea-D4yCdiFY.cjs} +1 -1
- package/dist/{textarea-12le8b4I.js → textarea-WktT3N3w.js} +2 -2
- package/dist/{textinput-CC0zxUBT.js → textinput-BPrC2neL.js} +2 -2
- package/dist/{textinput-Dooq2n8W.cjs → textinput-C6AcbxnQ.cjs} +1 -1
- package/dist/{timepicker-Cz81VQu3.js → timepicker-D77gssQg.js} +2 -2
- package/dist/{timepicker-BM4I5w-i.cjs → timepicker-DaqrwM-g.cjs} +1 -1
- package/dist/{utils-Dn5ykFtS.js → utils-BtXhdgpe.js} +2 -2
- package/dist/utils-dl1XA5EE.cjs +1 -0
- package/package.json +2 -2
- package/src/components/calendar/calendar.selection.test.ts +16 -0
- package/src/components/calendar/calendar.ts +4 -3
- package/src/components/combobox/combobox.interaction.test.ts +23 -0
- package/src/components/combobox/combobox.ts +1 -0
- package/src/components/datepicker/datepicker-multiple.ts +2 -6
- package/src/components/datepicker/datepicker-popup.ts +3 -6
- package/src/components/datepicker/datepicker-utils.test.ts +40 -203
- package/src/components/datepicker/datepicker-utils.ts +3 -94
- package/src/components/datepicker/datepicker.accessibility.test.ts +2 -2
- package/src/components/datepicker/datepicker.core.test.ts +236 -0
- package/src/components/datepicker/datepicker.ts +179 -115
- package/src/components/datepicker/index.ts +1 -0
- package/dist/datepicker-Cnrbqm62.cjs +0 -294
- 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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
88
|
+
input.focus()
|
|
89
89
|
await datepicker.updateComplete
|
|
90
|
-
|
|
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(
|