@oslokommune/punkt-elements 15.4.5 → 16.0.2

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 (73) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{card-CnPjrdre.js → card-CmfUyl_s.js} +1 -1
  3. package/dist/{card-5S2r9UD1.cjs → card-Db9QSEqh.cjs} +1 -1
  4. package/dist/{checkbox-D98_NjcU.cjs → checkbox-Cpyay9_l.cjs} +1 -1
  5. package/dist/{checkbox-BSz71IeT.js → checkbox-D6nltMuc.js} +1 -1
  6. package/dist/combobox-Bv37b6cI.cjs +135 -0
  7. package/dist/combobox-CoO8T-F-.js +818 -0
  8. package/dist/{datepicker-SEKblnRR.cjs → datepicker-CrvQ5Y5w.cjs} +1 -1
  9. package/dist/{datepicker-nnyTW0vf.js → datepicker-DbsIuC5Z.js} +2 -2
  10. package/dist/index.d.ts +157 -90
  11. package/dist/{input-element-Bkv6Yxld.js → input-element-BGNbdzy2.js} +1 -1
  12. package/dist/{input-element-DM0tY799.cjs → input-element-CSDVA3Y6.cjs} +1 -1
  13. package/dist/listbox-Dm2mKp6_.cjs +101 -0
  14. package/dist/listbox-OdkIn9_A.js +431 -0
  15. package/dist/pkt-card.cjs +1 -1
  16. package/dist/pkt-card.js +1 -1
  17. package/dist/pkt-checkbox.cjs +1 -1
  18. package/dist/pkt-checkbox.js +1 -1
  19. package/dist/pkt-combobox.cjs +1 -1
  20. package/dist/pkt-combobox.js +1 -1
  21. package/dist/pkt-datepicker.cjs +1 -1
  22. package/dist/pkt-datepicker.js +2 -2
  23. package/dist/pkt-header.cjs +1 -1
  24. package/dist/pkt-header.js +1 -1
  25. package/dist/pkt-index.cjs +1 -1
  26. package/dist/pkt-index.js +9 -9
  27. package/dist/pkt-listbox.cjs +1 -1
  28. package/dist/pkt-listbox.js +1 -1
  29. package/dist/pkt-options-controller-BogGk-6J.cjs +1 -0
  30. package/dist/{pkt-options-controller-BcGywCmf.js → pkt-options-controller-Z-bPox7n.js} +2 -2
  31. package/dist/pkt-radiobutton.cjs +1 -1
  32. package/dist/pkt-radiobutton.js +1 -1
  33. package/dist/pkt-select.cjs +1 -1
  34. package/dist/pkt-select.js +1 -1
  35. package/dist/pkt-tag.cjs +1 -1
  36. package/dist/pkt-tag.js +1 -1
  37. package/dist/pkt-textarea.cjs +1 -1
  38. package/dist/pkt-textarea.js +1 -1
  39. package/dist/pkt-textinput.cjs +1 -1
  40. package/dist/pkt-textinput.js +1 -1
  41. package/dist/{radiobutton-95wp024h.cjs → radiobutton-CNHCpKn0.cjs} +1 -1
  42. package/dist/{radiobutton-CTFAV5GU.js → radiobutton-DgC27mb0.js} +1 -1
  43. package/dist/{select-YLvYAQX6.js → select-7VuYtPZv.js} +2 -2
  44. package/dist/{select-CZ_Lx5W6.cjs → select-PWPy5gTB.cjs} +1 -1
  45. package/dist/{tag-68q0_Sn0.js → tag-DZPqFiem.js} +37 -33
  46. package/dist/tag-DmbgBCKu.cjs +27 -0
  47. package/dist/{textarea-CuTsE1WX.cjs → textarea-CO7Ikug5.cjs} +1 -1
  48. package/dist/{textarea-DhWH99qN.js → textarea-VpCEjVFx.js} +1 -1
  49. package/dist/{textinput-BCi9p0Du.js → textinput-C2AZ9ss2.js} +1 -1
  50. package/dist/{textinput-st4Vml5J.cjs → textinput-DRFZU3dA.cjs} +1 -1
  51. package/package.json +4 -4
  52. package/src/components/card/card.ts +1 -0
  53. package/src/components/combobox/combobox-base.ts +158 -0
  54. package/src/components/combobox/combobox-handlers.ts +419 -0
  55. package/src/components/combobox/combobox-types.ts +10 -0
  56. package/src/components/combobox/combobox-utils.ts +135 -0
  57. package/src/components/combobox/combobox-value.ts +248 -0
  58. package/src/components/combobox/combobox.accessibility.test.ts +243 -0
  59. package/src/components/combobox/{combobox.test.ts → combobox.core.test.ts} +104 -46
  60. package/src/components/combobox/combobox.interaction.test.ts +436 -0
  61. package/src/components/combobox/combobox.selection.test.ts +543 -0
  62. package/src/components/combobox/combobox.ts +260 -734
  63. package/src/components/listbox/index.ts +2 -0
  64. package/src/components/listbox/listbox.interaction.test.ts +580 -0
  65. package/src/components/listbox/listbox.test.ts +32 -6
  66. package/src/components/listbox/listbox.ts +109 -126
  67. package/src/components/tag/tag.ts +3 -0
  68. package/dist/combobox-C5YcNVSZ.cjs +0 -128
  69. package/dist/combobox-cer7PLSE.js +0 -533
  70. package/dist/listbox-C7NEa9SU.cjs +0 -96
  71. package/dist/listbox-Cykec1bj.js +0 -361
  72. package/dist/pkt-options-controller-BnTmkl3g.cjs +0 -1
  73. package/dist/tag-BnT5onW2.cjs +0 -26
@@ -0,0 +1,436 @@
1
+ import '@testing-library/jest-dom'
2
+ import { fireEvent } from '@testing-library/dom'
3
+
4
+ import './combobox'
5
+ import { PktCombobox } from './combobox'
6
+ import type { IPktComboboxOption } from './combobox'
7
+
8
+ const waitForCustomElements = async () => {
9
+ await customElements.whenDefined('pkt-combobox')
10
+ }
11
+
12
+ const createCombobox = async (comboboxProps = '') => {
13
+ const container = document.createElement('div')
14
+ container.innerHTML = `
15
+ <pkt-combobox ${comboboxProps}></pkt-combobox>
16
+ `
17
+ document.body.appendChild(container)
18
+ await waitForCustomElements()
19
+ return container
20
+ }
21
+
22
+ const defaultOptions: IPktComboboxOption[] = [
23
+ { value: 'apple', label: 'Apple' },
24
+ { value: 'banana', label: 'Banana' },
25
+ { value: 'cherry', label: 'Cherry' },
26
+ { value: 'date', label: 'Date' },
27
+ ]
28
+
29
+ afterEach(() => {
30
+ document.body.innerHTML = ''
31
+ })
32
+
33
+ describe('PktCombobox', () => {
34
+ describe('Keyboard navigation', () => {
35
+ test('opens dropdown with Enter on arrow button', async () => {
36
+ const container = await createCombobox('id="test" name="test" label="Test"')
37
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
38
+ await combobox.updateComplete
39
+
40
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
41
+ fireEvent.keyDown(arrowButton!, { key: 'Enter' })
42
+ await combobox.updateComplete
43
+
44
+ expect(combobox['_isOptionsOpen']).toBe(true)
45
+ })
46
+
47
+ test('opens dropdown with Space on arrow button', async () => {
48
+ const container = await createCombobox('id="test" name="test" label="Test"')
49
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
50
+ await combobox.updateComplete
51
+
52
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
53
+ fireEvent.keyDown(arrowButton!, { key: ' ' })
54
+ await combobox.updateComplete
55
+
56
+ expect(combobox['_isOptionsOpen']).toBe(true)
57
+ })
58
+
59
+ test('opens dropdown with ArrowDown on arrow button', async () => {
60
+ const container = await createCombobox('id="test" name="test" label="Test"')
61
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
62
+ await combobox.updateComplete
63
+
64
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
65
+ fireEvent.keyDown(arrowButton!, { key: 'ArrowDown' })
66
+ await combobox.updateComplete
67
+
68
+ expect(combobox['_isOptionsOpen']).toBe(true)
69
+ })
70
+
71
+ test('toggles dropdown closed with Enter on arrow button', async () => {
72
+ const container = await createCombobox('id="test" name="test" label="Test"')
73
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
74
+ await combobox.updateComplete
75
+
76
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
77
+
78
+ // Open
79
+ fireEvent.keyDown(arrowButton!, { key: 'Enter' })
80
+ await combobox.updateComplete
81
+ expect(combobox['_isOptionsOpen']).toBe(true)
82
+
83
+ // Close
84
+ fireEvent.keyDown(arrowButton!, { key: 'Enter' })
85
+ await combobox.updateComplete
86
+ expect(combobox['_isOptionsOpen']).toBe(false)
87
+ })
88
+
89
+ test('does not toggle on non-toggle keys', async () => {
90
+ const container = await createCombobox('id="test" name="test" label="Test"')
91
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
92
+ await combobox.updateComplete
93
+
94
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
95
+ fireEvent.keyDown(arrowButton!, { key: 'Escape' })
96
+ await combobox.updateComplete
97
+
98
+ expect(combobox['_isOptionsOpen']).toBe(false)
99
+ })
100
+
101
+ test('submits value with Enter in text input', async () => {
102
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
103
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
104
+ combobox.options = [...defaultOptions]
105
+ await combobox.updateComplete
106
+
107
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
108
+ fireEvent.focus(input)
109
+ await combobox.updateComplete
110
+
111
+ input.value = 'apple'
112
+ fireEvent.keyDown(input, { key: 'Enter' })
113
+ await combobox.updateComplete
114
+
115
+ expect(combobox['_value']).toContain('apple')
116
+ })
117
+
118
+ test('closes dropdown with Escape in text input', async () => {
119
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
120
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
121
+ combobox.options = [...defaultOptions]
122
+ await combobox.updateComplete
123
+
124
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
125
+ fireEvent.focus(input)
126
+ await combobox.updateComplete
127
+ expect(combobox['_isOptionsOpen']).toBe(true)
128
+
129
+ fireEvent.keyDown(input, { key: 'Escape' })
130
+ await combobox.updateComplete
131
+
132
+ expect(combobox['_isOptionsOpen']).toBe(false)
133
+ })
134
+
135
+ test('does not open dropdown when disabled', async () => {
136
+ const container = await createCombobox('id="test" name="test" label="Test" disabled')
137
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
138
+ await combobox.updateComplete
139
+
140
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
141
+ fireEvent.keyDown(arrowButton!, { key: 'Enter' })
142
+ await combobox.updateComplete
143
+
144
+ expect(combobox['_isOptionsOpen']).toBe(false)
145
+ })
146
+ })
147
+
148
+ describe('Focus handling', () => {
149
+ test('opens dropdown on input focus', async () => {
150
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
151
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
152
+ combobox.options = [...defaultOptions]
153
+ await combobox.updateComplete
154
+
155
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
156
+ fireEvent.focus(input)
157
+ await combobox.updateComplete
158
+
159
+ expect(combobox['_isOptionsOpen']).toBe(true)
160
+ expect(combobox['_inputFocus']).toBe(true)
161
+ })
162
+
163
+ test('populates input with current value on focus in single-select', async () => {
164
+ const container = await createCombobox(
165
+ 'id="test" name="test" label="Test" allow-user-input value="apple"',
166
+ )
167
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
168
+ combobox.options = [...defaultOptions]
169
+ await combobox.updateComplete
170
+
171
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
172
+ fireEvent.focus(input)
173
+ await combobox.updateComplete
174
+
175
+ expect(input.value).toBe('Apple')
176
+ })
177
+
178
+ test('handles blur correctly', async () => {
179
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
180
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
181
+ await combobox.updateComplete
182
+
183
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
184
+ fireEvent.focus(input)
185
+ await combobox.updateComplete
186
+
187
+ fireEvent.blur(input)
188
+ await combobox.updateComplete
189
+
190
+ expect(combobox['_inputFocus']).toBe(false)
191
+ })
192
+
193
+ test('opens dropdown on input container click', async () => {
194
+ const container = await createCombobox('id="test" name="test" label="Test"')
195
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
196
+ await combobox.updateComplete
197
+
198
+ const inputDiv = combobox.querySelector('.pkt-combobox__input')
199
+ fireEvent.click(inputDiv!)
200
+ await combobox.updateComplete
201
+
202
+ expect(combobox['_isOptionsOpen']).toBe(true)
203
+ })
204
+
205
+ test('does not open when disabled and input container is clicked', async () => {
206
+ const container = await createCombobox('id="test" name="test" label="Test" disabled')
207
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
208
+ await combobox.updateComplete
209
+
210
+ const inputDiv = combobox.querySelector('.pkt-combobox__input')
211
+ fireEvent.click(inputDiv!)
212
+ await combobox.updateComplete
213
+
214
+ expect(combobox['_isOptionsOpen']).toBe(false)
215
+ })
216
+ })
217
+
218
+ describe('Focus-out behavior', () => {
219
+ test('closes dropdown when clicking outside combobox', async () => {
220
+ const container = await createCombobox('id="test" name="test" label="Test"')
221
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
222
+ await combobox.updateComplete
223
+
224
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
225
+ fireEvent.click(arrowButton!)
226
+ await combobox.updateComplete
227
+ expect(combobox['_isOptionsOpen']).toBe(true)
228
+
229
+ fireEvent.click(document.body)
230
+ await combobox.updateComplete
231
+
232
+ expect(combobox['_isOptionsOpen']).toBe(false)
233
+ })
234
+
235
+ test('selects matching option on focus-out when allowUserInput is off', async () => {
236
+ const container = await createCombobox('id="test" name="test" label="Test" typeahead')
237
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
238
+ combobox.options = [...defaultOptions]
239
+ await combobox.updateComplete
240
+
241
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
242
+ fireEvent.focus(input)
243
+ await combobox.updateComplete
244
+
245
+ input.value = 'Apple'
246
+ combobox['_isOptionsOpen'] = true
247
+ await combobox.updateComplete
248
+ ;(combobox as any).closeAndProcessInput()
249
+ await combobox.updateComplete
250
+
251
+ expect(combobox['_value']).toContain('apple')
252
+ })
253
+
254
+ test('adds custom value on focus-out when allowUserInput is on', async () => {
255
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
256
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
257
+ combobox.options = [...defaultOptions]
258
+ await combobox.updateComplete
259
+
260
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
261
+ fireEvent.focus(input)
262
+ await combobox.updateComplete
263
+
264
+ input.value = 'NewFruit'
265
+ combobox['_isOptionsOpen'] = true
266
+ await combobox.updateComplete
267
+ ;(combobox as any).closeAndProcessInput()
268
+ await combobox.updateComplete
269
+
270
+ expect(combobox['_value']).toContain('NewFruit')
271
+ })
272
+ })
273
+
274
+ describe('Search and filtering', () => {
275
+ test('filters options when typing in typeahead mode', async () => {
276
+ const container = await createCombobox('id="test" name="test" label="Test" typeahead')
277
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
278
+ combobox.options = [...defaultOptions]
279
+ await combobox.updateComplete
280
+
281
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
282
+ fireEvent.focus(input)
283
+ await combobox.updateComplete
284
+
285
+ // Type to filter
286
+ input.value = 'app'
287
+ fireEvent.input(input, { target: { value: 'app' } })
288
+ await combobox.updateComplete
289
+
290
+ const listbox = combobox.querySelector('pkt-listbox') as any
291
+ await listbox?.updateComplete
292
+
293
+ // Internal _options should be filtered
294
+ const filteredCount = combobox['_options'].length
295
+ expect(filteredCount).toBeLessThan(defaultOptions.length)
296
+ })
297
+
298
+ test('shows no-match message when search has no results and allowUserInput is off', async () => {
299
+ const container = await createCombobox('id="test" name="test" label="Test" typeahead')
300
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
301
+ combobox.options = [...defaultOptions]
302
+ await combobox.updateComplete
303
+
304
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
305
+ fireEvent.focus(input)
306
+ await combobox.updateComplete
307
+
308
+ input.value = 'zzzzz'
309
+ fireEvent.input(input, { target: { value: 'zzzzz' } })
310
+ await combobox.updateComplete
311
+
312
+ const listbox = combobox.querySelector('pkt-listbox') as any
313
+ await listbox?.updateComplete
314
+
315
+ const visibleOptions = combobox.querySelectorAll('.pkt-listbox__option')
316
+ expect(visibleOptions.length).toBe(0)
317
+ })
318
+
319
+ test('shows add-value banner when search has no exact match and allowUserInput is on', async () => {
320
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
321
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
322
+ combobox.options = [...defaultOptions]
323
+ await combobox.updateComplete
324
+
325
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
326
+ fireEvent.focus(input)
327
+ await combobox.updateComplete
328
+
329
+ input.value = 'NewFruit'
330
+ fireEvent.input(input, { target: { value: 'NewFruit' } })
331
+ await combobox.updateComplete
332
+
333
+ const listbox = combobox.querySelector('pkt-listbox') as any
334
+ await listbox?.updateComplete
335
+
336
+ const addBanner = combobox.querySelector('.pkt-listbox__banner--new-option')
337
+ expect(addBanner).toBeInTheDocument()
338
+ })
339
+
340
+ test('dispatches search event on internal search state change', async () => {
341
+ const container = await createCombobox('id="test" name="test" label="Test" include-search')
342
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
343
+ await combobox.updateComplete
344
+
345
+ let searchEventDetail: string | null = null
346
+ combobox.addEventListener('search', (e: Event) => {
347
+ searchEventDetail = (e as CustomEvent).detail
348
+ })
349
+
350
+ combobox['_search'] = 'test query'
351
+ await combobox.updateComplete
352
+
353
+ expect(searchEventDetail).toBe('test query')
354
+ })
355
+
356
+ test('resets search when option is selected via toggleValue', async () => {
357
+ const container = await createCombobox('id="test" name="test" label="Test" typeahead')
358
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
359
+ combobox.options = [...defaultOptions]
360
+ await combobox.updateComplete
361
+
362
+ // Set some search state
363
+ combobox['_search'] = 'app'
364
+ await combobox.updateComplete
365
+
366
+ // Select an option
367
+ ;(combobox as any).toggleValue('apple')
368
+ await combobox.updateComplete
369
+
370
+ expect(combobox['_search']).toBe('')
371
+ })
372
+ })
373
+
374
+ describe('Listbox search (includeSearch)', () => {
375
+ test('passes includeSearch to listbox', async () => {
376
+ const container = await createCombobox('id="test" name="test" label="Test" include-search')
377
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
378
+ await combobox.updateComplete
379
+
380
+ const listbox = combobox.querySelector('pkt-listbox')
381
+ expect(listbox?.hasAttribute('include-search')).toBe(true)
382
+ })
383
+
384
+ test('passes searchPlaceholder to listbox', async () => {
385
+ const container = await createCombobox(
386
+ 'id="test" name="test" label="Test" include-search search-placeholder="Søk her..."',
387
+ )
388
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
389
+ await combobox.updateComplete
390
+
391
+ const listbox = combobox.querySelector('pkt-listbox') as any
392
+ expect(listbox?.searchPlaceholder).toBe('Søk her...')
393
+ })
394
+
395
+ test('updates search state on listbox search-change event', async () => {
396
+ const container = await createCombobox('id="test" name="test" label="Test" include-search')
397
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
398
+ combobox.options = [...defaultOptions]
399
+ await combobox.updateComplete
400
+
401
+ // Open dropdown first
402
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
403
+ fireEvent.click(arrowButton!)
404
+ await combobox.updateComplete
405
+
406
+ // Simulate search input in the listbox search field
407
+ const listbox = combobox.querySelector('pkt-listbox') as any
408
+ await listbox?.updateComplete
409
+
410
+ const searchInput = combobox.querySelector('[role="searchbox"]') as HTMLInputElement
411
+ if (searchInput) {
412
+ fireEvent.input(searchInput, { target: { value: 'app' } })
413
+ await combobox.updateComplete
414
+ }
415
+
416
+ expect(combobox['_search']).toBe('app')
417
+ })
418
+ })
419
+
420
+ describe('Disconnected callback cleanup', () => {
421
+ test('cleans up body click handler on disconnect', async () => {
422
+ const container = await createCombobox('id="test" name="test" label="Test"')
423
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
424
+ await combobox.updateComplete
425
+
426
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
427
+ fireEvent.click(arrowButton!)
428
+ await combobox.updateComplete
429
+ expect(combobox['_isOptionsOpen']).toBe(true)
430
+
431
+ combobox.remove()
432
+
433
+ expect(() => fireEvent.click(document.body)).not.toThrow()
434
+ })
435
+ })
436
+ })