@oslokommune/punkt-elements 13.4.0 → 13.4.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 (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{alert-B0OUR9oD.js → alert-B07oUpkq.js} +5 -4
  3. package/dist/{alert-BH0lJ2ny.cjs → alert-DQNBDKjT.cjs} +1 -1
  4. package/dist/{backlink-B13JTp9D.js → backlink-C2jbzu0U.js} +16 -15
  5. package/dist/backlink-JbBNi3qg.cjs +13 -0
  6. package/dist/{button-CDocR7iN.cjs → button-B8rdtaHB.cjs} +1 -1
  7. package/dist/{button-DrrEvUy9.js → button-DhispFOY.js} +1 -0
  8. package/dist/{card-uccD6Pnv.cjs → card-BUITGoqX.cjs} +10 -10
  9. package/dist/{card-BI1NZONj.js → card-Dtw26f7i.js} +96 -76
  10. package/dist/checkbox-Gn7Wtk9h.cjs +31 -0
  11. package/dist/checkbox-ym7z6cpt.js +142 -0
  12. package/dist/{combobox-BhcqC30d.cjs → combobox-DjO0RMUB.cjs} +1 -1
  13. package/dist/{combobox-D9dGKWuZ.js → combobox-yE4aYhTi.js} +1 -1
  14. package/dist/{consent-CftYu8Di.js → consent-BpcQFvbi.js} +1 -1
  15. package/dist/{consent-DrS71kvz.cjs → consent-hYeFWNFr.cjs} +1 -1
  16. package/dist/index.d.ts +3 -0
  17. package/dist/pkt-alert.cjs +1 -1
  18. package/dist/pkt-alert.js +1 -1
  19. package/dist/pkt-backlink.cjs +1 -1
  20. package/dist/pkt-backlink.js +1 -1
  21. package/dist/pkt-button.cjs +1 -1
  22. package/dist/pkt-button.js +1 -1
  23. package/dist/pkt-card.cjs +1 -1
  24. package/dist/pkt-card.js +1 -1
  25. package/dist/pkt-checkbox.cjs +1 -1
  26. package/dist/pkt-checkbox.js +1 -1
  27. package/dist/pkt-combobox.cjs +1 -1
  28. package/dist/pkt-combobox.js +1 -1
  29. package/dist/pkt-consent.cjs +1 -1
  30. package/dist/pkt-consent.js +1 -1
  31. package/dist/pkt-index.cjs +1 -1
  32. package/dist/pkt-index.js +7 -7
  33. package/package.json +2 -2
  34. package/src/components/accordion/accordion.test.ts +0 -4
  35. package/src/components/alert/alert.test.ts +348 -0
  36. package/src/components/alert/alert.ts +1 -0
  37. package/src/components/backlink/backlink.test.ts +286 -0
  38. package/src/components/backlink/backlink.ts +5 -3
  39. package/src/components/button/button.test.ts +514 -0
  40. package/src/components/button/button.ts +2 -0
  41. package/src/components/card/card.test.ts +592 -0
  42. package/src/components/card/card.ts +24 -1
  43. package/src/components/checkbox/checkbox.test.ts +535 -0
  44. package/src/components/checkbox/checkbox.ts +44 -1
  45. package/src/components/combobox/combobox.test.ts +737 -0
  46. package/src/components/combobox/combobox.ts +1 -1
  47. package/dist/backlink-C5jQRMwJ.cjs +0 -13
  48. package/dist/checkbox-CTRbpbye.js +0 -120
  49. package/dist/checkbox-wJ26voZd.cjs +0 -30
@@ -0,0 +1,737 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { fireEvent } from '@testing-library/dom'
4
+
5
+ expect.extend(toHaveNoViolations)
6
+
7
+ import './combobox'
8
+ import { PktCombobox } from './combobox'
9
+ import type { IPktComboboxOption } from './combobox'
10
+
11
+ const waitForCustomElements = async () => {
12
+ await customElements.whenDefined('pkt-combobox')
13
+ }
14
+
15
+ // Helper function to create combobox markup
16
+ const createCombobox = async (comboboxProps = '', options = '') => {
17
+ const container = document.createElement('div')
18
+ container.innerHTML = `
19
+ <pkt-combobox ${comboboxProps}>
20
+ ${options}
21
+ </pkt-combobox>
22
+ `
23
+ document.body.appendChild(container)
24
+ await waitForCustomElements()
25
+ return container
26
+ }
27
+
28
+ // Helper function to create options
29
+ const createOptions = (optionData: { value: string; label?: string; disabled?: boolean }[]) => {
30
+ return optionData
31
+ .map(
32
+ (opt) =>
33
+ `<option value="${opt.value}" ${opt.disabled ? 'disabled' : ''}>${opt.label || opt.value}</option>`,
34
+ )
35
+ .join('')
36
+ }
37
+
38
+ // Cleanup after each test
39
+ afterEach(() => {
40
+ document.body.innerHTML = ''
41
+ })
42
+
43
+ describe('PktCombobox', () => {
44
+ describe('Rendering and basic functionality', () => {
45
+ test('renders without errors', async () => {
46
+ const container = await createCombobox('id="test-combobox" name="test" label="Test Combobox"')
47
+
48
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
49
+ expect(combobox).toBeInTheDocument()
50
+
51
+ await combobox.updateComplete
52
+ expect(combobox).toBeTruthy()
53
+
54
+ const wrapper = combobox.querySelector('pkt-input-wrapper')
55
+ const comboboxDiv = combobox.querySelector('.pkt-combobox')
56
+ expect(wrapper).toBeInTheDocument()
57
+ expect(comboboxDiv).toBeInTheDocument()
58
+ })
59
+
60
+ test('renders with correct structure', async () => {
61
+ const container = await createCombobox('id="test" name="test" label="Test Label"')
62
+
63
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
64
+ await combobox.updateComplete
65
+
66
+ const wrapper = combobox.querySelector('pkt-input-wrapper')
67
+ const comboboxDiv = wrapper?.querySelector('.pkt-combobox')
68
+ const inputDiv = comboboxDiv?.querySelector('.pkt-combobox__input')
69
+ const arrowButton = inputDiv?.querySelector('.pkt-combobox__arrow')
70
+ const listbox = comboboxDiv?.querySelector('pkt-listbox')
71
+
72
+ expect(wrapper).toBeInTheDocument()
73
+ expect(comboboxDiv).toBeInTheDocument()
74
+ expect(inputDiv).toBeInTheDocument()
75
+ expect(arrowButton).toBeInTheDocument()
76
+ expect(listbox).toBeInTheDocument()
77
+ })
78
+
79
+ test('renders arrow button with correct attributes', async () => {
80
+ const container = await createCombobox('id="test-arrow" name="test" label="Test"')
81
+
82
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
83
+ await combobox.updateComplete
84
+
85
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
86
+ const icon = arrowButton?.querySelector('pkt-icon')
87
+
88
+ expect(arrowButton?.getAttribute('id')).toBe('test-arrow-arrow')
89
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('false')
90
+ expect(arrowButton?.getAttribute('aria-controls')).toBe('test-arrow-listbox')
91
+ expect(arrowButton?.getAttribute('aria-haspopup')).toBe('listbox')
92
+ expect(arrowButton?.getAttribute('aria-label')).toBe('Åpne liste')
93
+ expect(arrowButton?.getAttribute('role')).toBe('button')
94
+ expect(icon?.getAttribute('name')).toBe('chevron-thin-down')
95
+ })
96
+ })
97
+
98
+ describe('Properties and attributes', () => {
99
+ test('applies default properties correctly', async () => {
100
+ const container = await createCombobox('id="test" name="test"')
101
+
102
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
103
+ await combobox.updateComplete
104
+
105
+ expect(combobox.value).toBe(undefined)
106
+ expect(combobox.allowUserInput).toBe(false)
107
+ expect(combobox.typeahead).toBe(false)
108
+ expect(combobox.includeSearch).toBe(false)
109
+ expect(combobox.multiple).toBe(false)
110
+ expect(combobox.disabled).toBe(false)
111
+ expect(combobox.options).toEqual([])
112
+ expect(combobox.tagPlacement).toBe(null)
113
+ expect(combobox.maxlength).toBe(null)
114
+ expect(combobox.displayValueAs).toBe('label')
115
+ })
116
+
117
+ test('handles multiple property correctly', async () => {
118
+ const container = await createCombobox('id="test" name="test" multiple')
119
+
120
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
121
+ await combobox.updateComplete
122
+
123
+ expect(combobox.multiple).toBe(true)
124
+
125
+ const listbox = combobox.querySelector('pkt-listbox')
126
+ expect(listbox?.hasAttribute('ismultiselect')).toBe(true)
127
+ })
128
+
129
+ test('handles allowUserInput property correctly', async () => {
130
+ const container = await createCombobox('id="test" name="test" allowUserInput')
131
+
132
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
133
+ await combobox.updateComplete
134
+
135
+ expect(combobox.allowUserInput).toBe(true)
136
+
137
+ const input = combobox.querySelector('input[type="text"]')
138
+ expect(input).toBeInTheDocument()
139
+ expect(input?.getAttribute('role')).toBe('combobox')
140
+ })
141
+
142
+ test('handles typeahead property correctly', async () => {
143
+ const container = await createCombobox('id="test" name="test" typeahead')
144
+
145
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
146
+ await combobox.updateComplete
147
+
148
+ expect(combobox.typeahead).toBe(true)
149
+
150
+ const input = combobox.querySelector('input[type="text"]')
151
+ expect(input).toBeInTheDocument()
152
+ expect(input?.getAttribute('aria-autocomplete')).toBe('both')
153
+ })
154
+
155
+ test('handles includeSearch property correctly', async () => {
156
+ const container = await createCombobox('id="test" name="test" includeSearch')
157
+
158
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
159
+ await combobox.updateComplete
160
+
161
+ expect(combobox.includeSearch).toBe(true)
162
+
163
+ const listbox = combobox.querySelector('pkt-listbox')
164
+ expect(listbox?.hasAttribute('includesearch')).toBe(true)
165
+ })
166
+
167
+ test('handles disabled property correctly', async () => {
168
+ const container = await createCombobox('id="test" name="test" disabled')
169
+
170
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
171
+ await combobox.updateComplete
172
+
173
+ expect(combobox.disabled).toBe(true)
174
+
175
+ const inputDiv = combobox.querySelector('.pkt-combobox__input')
176
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
177
+ expect(inputDiv).toHaveClass('pkt-combobox__input--disabled')
178
+ expect(arrowButton?.getAttribute('tabindex')).toBe('-1')
179
+ expect(arrowButton?.hasAttribute('disabled')).toBe(true)
180
+ })
181
+
182
+ test('handles fullwidth property correctly', async () => {
183
+ const container = await createCombobox('id="test" name="test" fullwidth')
184
+
185
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
186
+ await combobox.updateComplete
187
+
188
+ expect(combobox.fullwidth).toBe(true)
189
+
190
+ const inputDiv = combobox.querySelector('.pkt-combobox__input')
191
+ expect(inputDiv).toHaveClass('pkt-combobox__input--fullwidth')
192
+ })
193
+ })
194
+
195
+ describe('Options handling', () => {
196
+ test('handles options provided via property', async () => {
197
+ const testOptions: IPktComboboxOption[] = [
198
+ { value: 'option1', label: 'Option 1' },
199
+ { value: 'option2', label: 'Option 2' },
200
+ { value: 'option3', label: 'Option 3', disabled: true },
201
+ ]
202
+
203
+ const container = await createCombobox('id="test" name="test"')
204
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
205
+
206
+ // Set options via property
207
+ combobox.options = testOptions
208
+ await combobox.updateComplete
209
+
210
+ // Ensure listbox has also updated
211
+ const listbox = combobox.querySelector('pkt-listbox') as any
212
+ await listbox?.updateComplete
213
+
214
+ expect(combobox.options).toHaveLength(3)
215
+ expect(combobox.options[0].value).toBe('option1')
216
+ expect(combobox.options[0].label).toBe('Option 1')
217
+ expect(combobox.options[2].disabled).toBe(true)
218
+
219
+ expect(listbox?.options).toEqual(
220
+ expect.arrayContaining([expect.objectContaining({ value: 'option1', label: 'Option 1' })]),
221
+ )
222
+ })
223
+
224
+ test('handles options provided via slot', async () => {
225
+ const optionsMarkup = createOptions([
226
+ { value: 'value1', label: 'Label 1' },
227
+ { value: 'value2', label: 'Label 2' },
228
+ ])
229
+
230
+ const container = await createCombobox('id="test" name="test"', optionsMarkup)
231
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
232
+ await combobox.updateComplete
233
+
234
+ // Options should be extracted from slot
235
+ expect(combobox.options.length).toBeGreaterThan(0)
236
+ expect(combobox.options.some((opt) => opt.value === 'value1')).toBe(true)
237
+ expect(combobox.options.some((opt) => opt.value === 'value2')).toBe(true)
238
+ })
239
+
240
+ test('handles defaultOptions property', async () => {
241
+ const defaultOptions: IPktComboboxOption[] = [
242
+ { value: 'default1', label: 'Default 1' },
243
+ { value: 'default2', label: 'Default 2' },
244
+ ]
245
+
246
+ const container = await createCombobox('id="test" name="test"')
247
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
248
+
249
+ combobox.defaultOptions = defaultOptions
250
+ await combobox.updateComplete
251
+
252
+ expect(combobox.options).toHaveLength(2)
253
+ expect(combobox.options[0].value).toBe('default1')
254
+ expect(combobox.options[1].value).toBe('default2')
255
+ })
256
+
257
+ test('preserves userAdded options when defaultOptions change', async () => {
258
+ const container = await createCombobox('id="test" name="test" allowUserInput')
259
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
260
+ await combobox.updateComplete
261
+
262
+ // Add a user option
263
+ const userOption: IPktComboboxOption = {
264
+ value: 'user-added',
265
+ label: 'User Added',
266
+ userAdded: true,
267
+ }
268
+ combobox.options = [userOption]
269
+ await combobox.updateComplete
270
+
271
+ // Set default options
272
+ const defaultOptions: IPktComboboxOption[] = [{ value: 'default1', label: 'Default 1' }]
273
+ combobox.defaultOptions = defaultOptions
274
+ await combobox.updateComplete
275
+
276
+ // Should have both user-added and default options
277
+ expect(combobox.options).toHaveLength(2)
278
+ expect(combobox.options.some((opt) => opt.userAdded)).toBe(true)
279
+ expect(combobox.options.some((opt) => opt.value === 'default1')).toBe(true)
280
+ })
281
+ })
282
+
283
+ describe('Value handling', () => {
284
+ test('handles single value correctly', async () => {
285
+ const container = await createCombobox('id="test" name="test" value="test-value"')
286
+
287
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
288
+ await combobox.updateComplete
289
+
290
+ expect(combobox.value).toBe('test-value')
291
+
292
+ // Internal _value should be an array with single item
293
+ expect(combobox['_value']).toEqual(['test-value'])
294
+ })
295
+
296
+ test('handles multiple values correctly', async () => {
297
+ const container = await createCombobox('id="test" name="test" value="value1,value2" multiple')
298
+
299
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
300
+ await combobox.updateComplete
301
+
302
+ expect(combobox.value).toEqual(['value1', 'value2'])
303
+ expect(combobox['_value']).toEqual(['value1', 'value2'])
304
+ })
305
+
306
+ test('handles array value correctly', async () => {
307
+ const container = await createCombobox('id="test" name="test" multiple')
308
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
309
+
310
+ combobox.value = ['array1', 'array2']
311
+ await combobox.updateComplete
312
+
313
+ expect(combobox['_value']).toEqual(['array1', 'array2'])
314
+ })
315
+
316
+ test('limits to single value when not multiple', async () => {
317
+ const container = await createCombobox('id="test" name="test"')
318
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
319
+
320
+ combobox.value = ['value1', 'value2']
321
+ await combobox.updateComplete
322
+
323
+ // Should only keep the first value when not multiple
324
+ expect(combobox['_value']).toEqual(['value1'])
325
+ })
326
+ })
327
+
328
+ describe('Placeholder functionality', () => {
329
+ test('shows placeholder when no value selected', async () => {
330
+ const container = await createCombobox('id="test" name="test" placeholder="Select an option"')
331
+
332
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
333
+ await combobox.updateComplete
334
+
335
+ expect(combobox.placeholder).toBe('Select an option')
336
+
337
+ const placeholder = combobox.querySelector('.pkt-combobox__placeholder')
338
+ expect(placeholder).toBeInTheDocument()
339
+ expect(placeholder?.textContent).toBe('Select an option')
340
+ })
341
+
342
+ test('hides placeholder when value is selected', async () => {
343
+ const testOptions: IPktComboboxOption[] = [{ value: 'option1', label: 'Option 1' }]
344
+
345
+ const container = await createCombobox(
346
+ 'id="test" name="test" placeholder="Select an option" value="option1"',
347
+ )
348
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
349
+ combobox.options = testOptions
350
+ await combobox.updateComplete
351
+
352
+ const placeholder = combobox.querySelector('.pkt-combobox__placeholder')
353
+ expect(placeholder).not.toBeInTheDocument()
354
+ })
355
+
356
+ test('shows placeholder in multiple mode with outside tag placement', async () => {
357
+ const container = await createCombobox(
358
+ 'id="test" name="test" placeholder="Select options" multiple tagPlacement="outside"',
359
+ )
360
+
361
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
362
+ await combobox.updateComplete
363
+
364
+ const placeholder = combobox.querySelector('.pkt-combobox__placeholder')
365
+ expect(placeholder).toBeInTheDocument()
366
+ expect(placeholder?.textContent).toBe('Select options')
367
+ })
368
+ })
369
+
370
+ describe('Tag placement functionality', () => {
371
+ test('renders tags inside input by default in multiple mode', async () => {
372
+ const testOptions: IPktComboboxOption[] = [{ value: 'option1', label: 'Option 1' }]
373
+
374
+ const container = await createCombobox('id="test" name="test" multiple value="option1"')
375
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
376
+ combobox.options = testOptions
377
+ await combobox.updateComplete
378
+
379
+ const outsideTags = combobox.querySelector('.pkt-combobox__tags-outside')
380
+ expect(outsideTags).not.toBeInTheDocument()
381
+ })
382
+
383
+ test('renders tags outside input when tagPlacement is outside', async () => {
384
+ const testOptions: IPktComboboxOption[] = [{ value: 'option1', label: 'Option 1' }]
385
+
386
+ const container = await createCombobox(
387
+ 'id="test" name="test" multiple tagPlacement="outside" value="option1"',
388
+ )
389
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
390
+ combobox.options = testOptions
391
+ await combobox.updateComplete
392
+
393
+ const outsideTags = combobox.querySelector('.pkt-combobox__tags-outside')
394
+ expect(outsideTags).toBeInTheDocument()
395
+ })
396
+ })
397
+
398
+ describe('Input field functionality', () => {
399
+ test('renders hidden input when not allowUserInput or typeahead', async () => {
400
+ const container = await createCombobox('id="test" name="test-name"')
401
+
402
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
403
+ await combobox.updateComplete
404
+
405
+ const hiddenInput = combobox.querySelector('input[type="hidden"]')
406
+ const textInput = combobox.querySelector('input[type="text"]')
407
+
408
+ expect(hiddenInput).toBeInTheDocument()
409
+ expect(textInput).not.toBeInTheDocument()
410
+ expect(hiddenInput?.getAttribute('id')).toBe('test-input')
411
+ expect(hiddenInput?.getAttribute('name')).toBe('test-name-input')
412
+ })
413
+
414
+ test('renders text input when allowUserInput is true', async () => {
415
+ const container = await createCombobox('id="test" name="test-name" allowUserInput')
416
+
417
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
418
+ await combobox.updateComplete
419
+
420
+ const textInput = combobox.querySelector('input[type="text"]')
421
+ const hiddenInput = combobox.querySelector('input[type="hidden"]')
422
+
423
+ expect(textInput).toBeInTheDocument()
424
+ expect(hiddenInput).not.toBeInTheDocument()
425
+ expect(textInput?.getAttribute('id')).toBe('test-input')
426
+ expect(textInput?.getAttribute('name')).toBe('test-name-input')
427
+ expect(textInput?.getAttribute('role')).toBe('combobox')
428
+ expect(textInput?.getAttribute('aria-controls')).toBe('test-listbox')
429
+ })
430
+
431
+ test('renders text input when typeahead is true', async () => {
432
+ const container = await createCombobox('id="test" name="test" typeahead')
433
+
434
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
435
+ await combobox.updateComplete
436
+
437
+ const textInput = combobox.querySelector('input[type="text"]')
438
+ expect(textInput).toBeInTheDocument()
439
+ expect(textInput?.getAttribute('aria-autocomplete')).toBe('both')
440
+ })
441
+
442
+ test('sets correct aria-autocomplete for allowUserInput', async () => {
443
+ const container = await createCombobox('id="test" name="test" allowUserInput')
444
+
445
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
446
+ await combobox.updateComplete
447
+
448
+ const textInput = combobox.querySelector('input[type="text"]')
449
+ expect(textInput?.getAttribute('aria-autocomplete')).toBe('list')
450
+ })
451
+ })
452
+
453
+ describe('Dropdown functionality', () => {
454
+ test('opens dropdown when arrow button is clicked', async () => {
455
+ const container = await createCombobox('id="test" name="test"')
456
+
457
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
458
+ await combobox.updateComplete
459
+
460
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
461
+ const inputDiv = combobox.querySelector('.pkt-combobox__input')
462
+
463
+ expect(combobox['_isOptionsOpen']).toBe(false)
464
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('false')
465
+ expect(inputDiv).not.toHaveClass('pkt-combobox__input--open')
466
+
467
+ // Click arrow to open
468
+ fireEvent.click(arrowButton!)
469
+ await combobox.updateComplete
470
+
471
+ expect(combobox['_isOptionsOpen']).toBe(true)
472
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('true')
473
+ expect(inputDiv).toHaveClass('pkt-combobox__input--open')
474
+ })
475
+
476
+ test('toggles dropdown state with multiple clicks', async () => {
477
+ const container = await createCombobox('id="test" name="test"')
478
+
479
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
480
+ await combobox.updateComplete
481
+
482
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
483
+
484
+ // Click to open
485
+ fireEvent.click(arrowButton!)
486
+ await combobox.updateComplete
487
+ expect(combobox['_isOptionsOpen']).toBe(true)
488
+
489
+ // Click to close
490
+ fireEvent.click(arrowButton!)
491
+ await combobox.updateComplete
492
+ expect(combobox['_isOptionsOpen']).toBe(false)
493
+ })
494
+
495
+ test('does not open when disabled', async () => {
496
+ const container = await createCombobox('id="test" name="test" disabled')
497
+
498
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
499
+ await combobox.updateComplete
500
+
501
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
502
+
503
+ // Try to click when disabled
504
+ fireEvent.click(arrowButton!)
505
+ await combobox.updateComplete
506
+
507
+ expect(combobox['_isOptionsOpen']).toBe(false)
508
+ })
509
+ })
510
+
511
+ describe('Search functionality', () => {
512
+ test('handles search input when includeSearch is true', async () => {
513
+ const container = await createCombobox('id="test" name="test" includeSearch')
514
+
515
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
516
+ await combobox.updateComplete
517
+
518
+ const listbox = combobox.querySelector('pkt-listbox')
519
+ expect(listbox?.hasAttribute('includesearch')).toBe(true)
520
+ })
521
+
522
+ test('sets search placeholder correctly', async () => {
523
+ const container = await createCombobox(
524
+ 'id="test" name="test" includeSearch searchPlaceholder="Search items..."',
525
+ )
526
+
527
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
528
+ await combobox.updateComplete
529
+
530
+ expect(combobox.searchPlaceholder).toBe('Search items...')
531
+
532
+ const listbox = combobox.querySelector('pkt-listbox') as any
533
+ expect(listbox?.searchPlaceholder).toBe('Search items...')
534
+ })
535
+ })
536
+
537
+ describe('Keyboard navigation', () => {
538
+ test('handles keyboard events on arrow button', async () => {
539
+ const container = await createCombobox('id="test" name="test"')
540
+
541
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
542
+ await combobox.updateComplete
543
+
544
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
545
+
546
+ // Test Enter key
547
+ fireEvent.keyDown(arrowButton!, { key: 'Enter' })
548
+ await combobox.updateComplete
549
+
550
+ expect(combobox['_isOptionsOpen']).toBe(true)
551
+
552
+ // Test Space key
553
+ fireEvent.keyDown(arrowButton!, { key: ' ' })
554
+ await combobox.updateComplete
555
+
556
+ expect(combobox['_isOptionsOpen']).toBe(false)
557
+ })
558
+
559
+ test('handles input focus for text inputs', async () => {
560
+ const container = await createCombobox('id="test" name="test" allowUserInput')
561
+
562
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
563
+ await combobox.updateComplete
564
+
565
+ const textInput = combobox.querySelector('input[type="text"]')
566
+
567
+ // Test focus event
568
+ fireEvent.focus(textInput!)
569
+ await combobox.updateComplete
570
+
571
+ // Test blur event
572
+ fireEvent.blur(textInput!)
573
+ await combobox.updateComplete
574
+
575
+ // Should not throw errors
576
+ expect(combobox).toBeInTheDocument()
577
+ })
578
+ })
579
+
580
+ describe('Max length functionality', () => {
581
+ test('handles maxlength property correctly', async () => {
582
+ const container = await createCombobox('id="test" name="test" multiple maxlength="3"')
583
+
584
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
585
+ await combobox.updateComplete
586
+
587
+ expect(combobox.maxlength).toBe(3)
588
+
589
+ const listbox = combobox.querySelector('pkt-listbox') as any
590
+ expect(listbox?.maxLength).toBe(3)
591
+ })
592
+
593
+ test('disables user input when max is reached', async () => {
594
+ const testOptions: IPktComboboxOption[] = [
595
+ { value: 'option1', label: 'Option 1' },
596
+ { value: 'option2', label: 'Option 2' },
597
+ { value: 'option3', label: 'Option 3' },
598
+ ]
599
+
600
+ const container = await createCombobox(
601
+ 'id="test" name="test" multiple allowUserInput maxlength="2"',
602
+ )
603
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
604
+ combobox.options = testOptions
605
+ combobox.value = ['option1', 'option2'] // Set to max
606
+ await combobox.updateComplete
607
+
608
+ const listbox = combobox.querySelector('pkt-listbox') as any
609
+ await listbox?.updateComplete
610
+
611
+ expect(listbox?.maxIsReached).toBe(true)
612
+ })
613
+ })
614
+
615
+ describe('Error handling', () => {
616
+ test('applies error styling when hasError is true', async () => {
617
+ const container = await createCombobox('id="test" name="test"')
618
+
619
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
620
+ combobox.hasError = true
621
+ await combobox.updateComplete
622
+
623
+ const inputDiv = combobox.querySelector('.pkt-combobox__input')
624
+ expect(inputDiv).toHaveClass('pkt-combobox__input--error')
625
+ })
626
+
627
+ test('passes error state to input wrapper', async () => {
628
+ const container = await createCombobox('id="test" name="test" error-message="Test error"')
629
+
630
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
631
+ combobox.hasError = true
632
+ await combobox.updateComplete
633
+
634
+ const wrapper = combobox.querySelector('pkt-input-wrapper')
635
+ expect(wrapper?.hasAttribute('haserror')).toBe(true)
636
+ })
637
+ })
638
+
639
+ describe('Accessibility', () => {
640
+ test('has no accessibility violations', async () => {
641
+ const container = await createCombobox(
642
+ 'id="accessible-combobox" name="test" label="Accessible Combobox"',
643
+ )
644
+
645
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
646
+ await combobox.updateComplete
647
+
648
+ const results = await axe(combobox)
649
+ expect(results).toHaveNoViolations()
650
+ })
651
+
652
+ test('sets correct ARIA attributes on arrow button', async () => {
653
+ const container = await createCombobox('id="test-aria" name="test" label="Test"')
654
+
655
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
656
+ await combobox.updateComplete
657
+
658
+ const arrowButton = combobox.querySelector('.pkt-combobox__arrow')
659
+ expect(arrowButton?.getAttribute('aria-controls')).toBe('test-aria-listbox')
660
+ expect(arrowButton?.getAttribute('aria-haspopup')).toBe('listbox')
661
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('false')
662
+ expect(arrowButton?.getAttribute('aria-label')).toBe('Åpne liste')
663
+ })
664
+
665
+ test('sets correct ARIA attributes on text input', async () => {
666
+ const container = await createCombobox(
667
+ 'id="test-input" name="test" label="Test Label" allowUserInput',
668
+ )
669
+
670
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
671
+ await combobox.updateComplete
672
+
673
+ const textInput = combobox.querySelector('input[type="text"]')
674
+ expect(textInput?.getAttribute('role')).toBe('combobox')
675
+ expect(textInput?.getAttribute('aria-controls')).toBe('test-input-listbox')
676
+ expect(textInput?.getAttribute('aria-label')).toBe('Test Label')
677
+ expect(textInput?.getAttribute('aria-autocomplete')).toBe('list')
678
+ })
679
+
680
+ test('sets correct ARIA attributes for multiple selection', async () => {
681
+ const container = await createCombobox('id="test" name="test" allowUserInput multiple')
682
+
683
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
684
+ await combobox.updateComplete
685
+
686
+ const textInput = combobox.querySelector('input[type="text"]')
687
+ expect(textInput?.getAttribute('aria-multiselectable')).toBe('true')
688
+ })
689
+
690
+ test('sets correct aria-activedescendant when value is selected', async () => {
691
+ const testOptions: IPktComboboxOption[] = [{ value: 'option1', label: 'Option 1' }]
692
+
693
+ const container = await createCombobox('id="test" name="test" allowUserInput value="option1"')
694
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
695
+ combobox.options = testOptions
696
+ await combobox.updateComplete
697
+
698
+ const textInput = combobox.querySelector('input[type="text"]')
699
+ expect(textInput?.getAttribute('aria-activedescendant')).toBe('test-listbox--1')
700
+ })
701
+ })
702
+
703
+ describe('Event handling', () => {
704
+ test('dispatches search event when search value changes', async () => {
705
+ const container = await createCombobox('id="test" name="test" includeSearch')
706
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
707
+ await combobox.updateComplete
708
+
709
+ const searchEventSpy = jest.fn()
710
+ combobox.addEventListener('search', searchEventSpy)
711
+
712
+ // Simulate search change
713
+ combobox['_search'] = 'test search'
714
+ await combobox.updateComplete
715
+
716
+ expect(searchEventSpy).toHaveBeenCalledWith(
717
+ expect.objectContaining({
718
+ detail: 'test search',
719
+ }),
720
+ )
721
+ })
722
+
723
+ test('handles listbox events correctly', async () => {
724
+ const container = await createCombobox('id="test" name="test"')
725
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
726
+ await combobox.updateComplete
727
+
728
+ const listbox = combobox.querySelector('pkt-listbox')
729
+
730
+ // Test close-options event
731
+ fireEvent(listbox!, new CustomEvent('close-options'))
732
+ await combobox.updateComplete
733
+
734
+ expect(combobox['_isOptionsOpen']).toBe(false)
735
+ })
736
+ })
737
+ })