@oslokommune/punkt-elements 18.7.1 → 18.7.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 (187) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{accordion-NO9LYXaN.cjs → accordion-CsrLQXEA.cjs} +1 -1
  3. package/dist/{accordion-BI-Qiznz.js → accordion-DoxhiwK-.js} +5 -5
  4. package/dist/{alert-HFqn-cvb.cjs → alert-DH3VqNqh.cjs} +1 -1
  5. package/dist/{alert-CayKowTW.js → alert-DxAGxSX_.js} +6 -6
  6. package/dist/{async-directive-CAZoZMez.js → async-directive-BSwJ8-GL.js} +2 -2
  7. package/dist/{async-directive-DlB0Sf4j.cjs → async-directive-CfPXShXu.cjs} +1 -1
  8. package/dist/{backlink-DYO8qA9C.cjs → backlink-CDH824db.cjs} +1 -1
  9. package/dist/{backlink-BHtxOlTw.js → backlink-CGc7r9CS.js} +3 -3
  10. package/dist/{breadcrumbs-HG9wzGtr.cjs → breadcrumbs-B37tsIoh.cjs} +1 -1
  11. package/dist/{breadcrumbs-BlRDuZbZ.js → breadcrumbs-DRGhIXmN.js} +2 -2
  12. package/dist/{button-CIb0IxTg.cjs → button-Cx6Xdp6t.cjs} +1 -1
  13. package/dist/{button-DWMHtdWk.js → button-D6QhHPWH.js} +5 -5
  14. package/dist/{calendar-CBghTpkZ.js → calendar-42o_q-jI.js} +3 -3
  15. package/dist/{calendar-CA1mz04B.cjs → calendar-CAwIY7x_.cjs} +1 -1
  16. package/dist/{card-CsSNvjZV.cjs → card-CB72gwBC.cjs} +1 -1
  17. package/dist/{card-PXDm9JXv.js → card-cyZE94EO.js} +6 -6
  18. package/dist/{checkbox-DReZOEy2.js → checkbox-C6JdANel.js} +4 -4
  19. package/dist/{checkbox-CMu4pF8-.cjs → checkbox-CddODhna.cjs} +1 -1
  20. package/dist/{class-map-BL_YL_Ed.js → class-map-DSoj1J68.js} +1 -1
  21. package/dist/{class-map-BEib48YL.cjs → class-map-DupQbnhE.cjs} +1 -1
  22. package/dist/{combobox-DcBFiuwP.js → combobox-Cqqokx73.js} +172 -128
  23. package/dist/combobox-N3NP77le.cjs +137 -0
  24. package/dist/components/combobox/combobox-base.d.ts +14 -12
  25. package/dist/components/combobox/combobox-handlers.d.ts +1 -0
  26. package/dist/components/combobox/combobox-utils.d.ts +3 -1
  27. package/dist/components/combobox/combobox-value.d.ts +5 -0
  28. package/dist/components/combobox/combobox.attributes.test.d.ts +0 -0
  29. package/dist/components/combobox/combobox.d.ts +31 -2
  30. package/dist/components/combobox/combobox.preselection.test.d.ts +0 -0
  31. package/dist/components/combobox/index.d.ts +1 -1
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/listbox/listbox.d.ts +3 -1
  34. package/dist/{consent-Yt1MpkUy.js → consent-CsgKWDsR.js} +2 -2
  35. package/dist/{consent-D1SMUJLs.cjs → consent-DnE6IPEj.cjs} +1 -1
  36. package/dist/{datepicker-NSWMVoGi.cjs → datepicker-BzRhOIkG.cjs} +1 -1
  37. package/dist/{datepicker-DboWQcrh.js → datepicker-CtSYgxMs.js} +11 -11
  38. package/dist/{directive-helpers-Ddr8wUxL.cjs → directive-helpers-2mcpDDv5.cjs} +1 -1
  39. package/dist/{directive-helpers-CohhRNQz.js → directive-helpers-ChEduoup.js} +1 -1
  40. package/dist/{element-DRc10X2R.cjs → element-D_mCyFeX.cjs} +1 -1
  41. package/dist/{element-e2MAMNEP.js → element-J8nynuVk.js} +1 -0
  42. package/dist/{element-with-slot-GwTT3WRD.js → element-with-slot-BOuXlWSY.js} +2 -2
  43. package/dist/{element-with-slot-DgtT8lCH.cjs → element-with-slot-C66Jck5e.cjs} +1 -1
  44. package/dist/{fileupload-BYVtnSaI.cjs → fileupload-3OtW4Xlo.cjs} +1 -1
  45. package/dist/{fileupload-CqxWUH0y.js → fileupload-PJWEBD88.js} +8 -8
  46. package/dist/{footer-YrlXRq7e.js → footer-ByAxlj9_.js} +4 -4
  47. package/dist/{footer-CLSihvYS.cjs → footer-Ca3DM544.cjs} +1 -1
  48. package/dist/{header-DsaPCLjJ.js → header-CxfDtS3Y.js} +11 -11
  49. package/dist/{header-DzVHM7gZ.cjs → header-D2SD2JLt.cjs} +1 -1
  50. package/dist/{header-menu-wcnhBSzs.cjs → header-menu-B9ABa0WW.cjs} +1 -1
  51. package/dist/{header-menu-DKAcUFzh.js → header-menu-CIuu8_UC.js} +4 -4
  52. package/dist/{heading-OfJKjUi2.cjs → heading-4ZHACOeL.cjs} +1 -1
  53. package/dist/{heading-C23xQS4l.js → heading-BHWdVbvN.js} +2 -2
  54. package/dist/{helptext-DyHzv2ZG.cjs → helptext-6HsH-I6a.cjs} +1 -1
  55. package/dist/{helptext-DqdOdYRj.js → helptext-BESB9Vlq.js} +4 -4
  56. package/dist/{icon-DBFTFd-J.cjs → icon-BzxBkRyE.cjs} +1 -1
  57. package/dist/{icon-jEFbKTB_.js → icon-DkVqRWbC.js} +1 -1
  58. package/dist/{if-defined-r8tKY4tG.js → if-defined-BILLSM7i.js} +1 -1
  59. package/dist/if-defined-BbUZeoaQ.cjs +1 -0
  60. package/dist/{input-element-kvI_5er5.cjs → input-element-DhIEFeuP.cjs} +1 -1
  61. package/dist/{input-element-BiUfK_gU.js → input-element-Qb0w3ChW.js} +2 -2
  62. package/dist/{input-wrapper-Bwh720ke.js → input-wrapper-Bg06k-cj.js} +6 -6
  63. package/dist/{input-wrapper-wyvCnWPp.cjs → input-wrapper-D64Qn-QH.cjs} +1 -1
  64. package/dist/{link-BWAhHUku.cjs → link-Bk7aDZCC.cjs} +1 -1
  65. package/dist/{link-BvjWWWlc.js → link-C7fOEv66.js} +4 -4
  66. package/dist/{linkcard-6CbGf4I_.js → linkcard-Bguo9oCv.js} +4 -4
  67. package/dist/{linkcard-D6fRaZSc.cjs → linkcard-rZpQuO1Z.cjs} +1 -1
  68. package/dist/{listbox-CsP0qbU1.js → listbox-CrQboPG6.js} +80 -63
  69. package/dist/listbox-kV4OO1Qa.cjs +111 -0
  70. package/dist/{loader-B5t78Yk7.cjs → loader-BAn68gBb.cjs} +1 -1
  71. package/dist/{loader-BS7EJqxD.js → loader-C3gwO0i3.js} +4 -4
  72. package/dist/{messagebox-Bt0IvUlD.js → messagebox-DAE59dMX.js} +4 -4
  73. package/dist/{messagebox-DBVUz-DN.cjs → messagebox-WH2HjT5I.cjs} +1 -1
  74. package/dist/{modal-C1g2fiUq.cjs → modal-DZm_fn3K.cjs} +1 -1
  75. package/dist/{modal-z5RzZ-fr.js → modal-DdC1_qT3.js} +5 -5
  76. package/dist/pkt-accordion.cjs +1 -1
  77. package/dist/pkt-accordion.js +1 -1
  78. package/dist/pkt-alert.cjs +1 -1
  79. package/dist/pkt-alert.js +1 -1
  80. package/dist/pkt-backlink.cjs +1 -1
  81. package/dist/pkt-backlink.js +1 -1
  82. package/dist/pkt-breadcrumbs.cjs +1 -1
  83. package/dist/pkt-breadcrumbs.js +1 -1
  84. package/dist/pkt-button.cjs +1 -1
  85. package/dist/pkt-button.js +1 -1
  86. package/dist/pkt-calendar.cjs +1 -1
  87. package/dist/pkt-calendar.js +1 -1
  88. package/dist/pkt-card.cjs +1 -1
  89. package/dist/pkt-card.js +1 -1
  90. package/dist/pkt-checkbox.cjs +1 -1
  91. package/dist/pkt-checkbox.js +1 -1
  92. package/dist/pkt-combobox.cjs +1 -1
  93. package/dist/pkt-combobox.js +1 -1
  94. package/dist/pkt-consent.cjs +1 -1
  95. package/dist/pkt-consent.js +1 -1
  96. package/dist/pkt-datepicker.cjs +1 -1
  97. package/dist/pkt-datepicker.js +1 -1
  98. package/dist/pkt-fileupload.cjs +1 -1
  99. package/dist/pkt-fileupload.js +1 -1
  100. package/dist/pkt-footer.cjs +1 -1
  101. package/dist/pkt-footer.js +1 -1
  102. package/dist/pkt-header-menu.cjs +1 -1
  103. package/dist/pkt-header-menu.js +1 -1
  104. package/dist/pkt-header.cjs +1 -1
  105. package/dist/pkt-header.js +1 -1
  106. package/dist/pkt-heading.cjs +1 -1
  107. package/dist/pkt-heading.js +1 -1
  108. package/dist/pkt-helptext.cjs +1 -1
  109. package/dist/pkt-helptext.js +1 -1
  110. package/dist/pkt-icon.cjs +1 -1
  111. package/dist/pkt-icon.js +1 -1
  112. package/dist/pkt-index.cjs +1 -1
  113. package/dist/pkt-index.js +34 -34
  114. package/dist/pkt-input-wrapper.cjs +1 -1
  115. package/dist/pkt-input-wrapper.js +1 -1
  116. package/dist/pkt-link.cjs +1 -1
  117. package/dist/pkt-link.js +1 -1
  118. package/dist/pkt-linkcard.cjs +1 -1
  119. package/dist/pkt-linkcard.js +1 -1
  120. package/dist/pkt-listbox.cjs +1 -1
  121. package/dist/pkt-listbox.js +1 -1
  122. package/dist/pkt-loader.cjs +1 -1
  123. package/dist/pkt-loader.js +1 -1
  124. package/dist/pkt-messagebox.cjs +1 -1
  125. package/dist/pkt-messagebox.js +1 -1
  126. package/dist/pkt-modal.cjs +1 -1
  127. package/dist/pkt-modal.js +1 -1
  128. package/dist/{pkt-options-controller-AFYyv3-b.cjs → pkt-options-controller-DXAAVcBJ.cjs} +1 -1
  129. package/dist/{pkt-options-controller-DKkmDDzT.js → pkt-options-controller-XcIgx_v1.js} +1 -1
  130. package/dist/pkt-progressbar.cjs +1 -1
  131. package/dist/pkt-progressbar.js +1 -1
  132. package/dist/pkt-radiobutton.cjs +1 -1
  133. package/dist/pkt-radiobutton.js +1 -1
  134. package/dist/pkt-searchinput.cjs +1 -1
  135. package/dist/pkt-searchinput.js +1 -1
  136. package/dist/pkt-select.cjs +1 -1
  137. package/dist/pkt-select.js +1 -1
  138. package/dist/pkt-tabs.cjs +1 -1
  139. package/dist/pkt-tabs.js +1 -1
  140. package/dist/pkt-tag.cjs +1 -1
  141. package/dist/pkt-tag.js +1 -1
  142. package/dist/pkt-textarea.cjs +1 -1
  143. package/dist/pkt-textarea.js +1 -1
  144. package/dist/pkt-textinput.cjs +1 -1
  145. package/dist/pkt-textinput.js +1 -1
  146. package/dist/pkt-timepicker.cjs +1 -1
  147. package/dist/pkt-timepicker.js +1 -1
  148. package/dist/{progressbar-BPHLC7ce.js → progressbar-D1x6MwdY.js} +5 -5
  149. package/dist/{progressbar-BpcsDCnG.cjs → progressbar-DsE4-qaW.cjs} +1 -1
  150. package/dist/{radiobutton-bGdAEz26.js → radiobutton-B2P48MMf.js} +4 -4
  151. package/dist/{radiobutton-B6pzZQZV.cjs → radiobutton-CdwYbHnx.cjs} +1 -1
  152. package/dist/{ref-BkV4Cd8U.cjs → ref-AuNEkS5a.cjs} +1 -1
  153. package/dist/{ref-lE1mGUOL.js → ref-DNowxdYH.js} +2 -2
  154. package/dist/{repeat-uBgVG8Ch.cjs → repeat-B3ksuY2F.cjs} +1 -1
  155. package/dist/{repeat-CCnTfedv.js → repeat-CZ1oMXnW.js} +2 -2
  156. package/dist/{searchinput-DrUcPajl.js → searchinput-CmyWnNr6.js} +4 -4
  157. package/dist/{searchinput-CiPB8xvh.cjs → searchinput-vi7twt5b.cjs} +1 -1
  158. package/dist/{select-ay8WHI7p.cjs → select-TAwBNhhf.cjs} +1 -1
  159. package/dist/{select-D7r2s7Cw.js → select-c15YAQbr.js} +7 -7
  160. package/dist/shared-strings/nb.d.ts +1 -0
  161. package/dist/{tabs-DqzPOdi9.cjs → tabs-hiTrWsXq.cjs} +1 -1
  162. package/dist/{tabs-DHvCjUi4.js → tabs-kxsX1ZEf.js} +4 -4
  163. package/dist/{tag-Cojf4jgA.cjs → tag-CGlxtBOH.cjs} +1 -1
  164. package/dist/{tag-D2Xvsy1i.js → tag-bztxjcF0.js} +5 -5
  165. package/dist/{textarea-s4IDLsBi.cjs → textarea-BHGZhMN4.cjs} +1 -1
  166. package/dist/{textarea-CzAL8e6L.js → textarea-BPQVrOp9.js} +9 -9
  167. package/dist/{textinput-B9w4jhIP.cjs → textinput-Cv4tal5W.cjs} +1 -1
  168. package/dist/{textinput-DT3HR9eU.js → textinput-DHGUOsIA.js} +8 -8
  169. package/dist/{timepicker-C-Ip5Jgl.cjs → timepicker-BBUYnYzM.cjs} +1 -1
  170. package/dist/{timepicker-BE0UouVr.js → timepicker-C8DHbOpM.js} +8 -8
  171. package/package.json +2 -2
  172. package/src/components/combobox/combobox-base.ts +42 -28
  173. package/src/components/combobox/combobox-handlers.ts +11 -5
  174. package/src/components/combobox/combobox-utils.ts +9 -1
  175. package/src/components/combobox/combobox-value.ts +32 -16
  176. package/src/components/combobox/combobox.accessibility.test.ts +29 -3
  177. package/src/components/combobox/combobox.attributes.test.ts +82 -0
  178. package/src/components/combobox/combobox.core.test.ts +46 -1
  179. package/src/components/combobox/combobox.interaction.test.ts +167 -0
  180. package/src/components/combobox/combobox.preselection.test.ts +271 -0
  181. package/src/components/combobox/combobox.ts +143 -79
  182. package/src/components/combobox/index.ts +6 -1
  183. package/src/components/index.ts +1 -0
  184. package/src/components/listbox/listbox.ts +56 -29
  185. package/dist/combobox-BZm_12qR.cjs +0 -140
  186. package/dist/if-defined-BeVQnl3C.cjs +0 -1
  187. package/dist/listbox-D95b5zAd.cjs +0 -106
@@ -175,6 +175,37 @@ describe('PktCombobox', () => {
175
175
  expect(input.value).toBe('Apple')
176
176
  })
177
177
 
178
+ test('closes the list when focus leaves the combobox', async () => {
179
+ const outside = document.createElement('button')
180
+ document.body.appendChild(outside)
181
+
182
+ const container = await createCombobox('id="test" name="test" label="Test"')
183
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
184
+ combobox.isOpen = true
185
+ await combobox.updateComplete
186
+
187
+ expect(combobox['_isOptionsOpen']).toBe(true)
188
+
189
+ const trigger = combobox.querySelector('[role="combobox"]') as HTMLElement
190
+ trigger.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outside }))
191
+ await combobox.updateComplete
192
+
193
+ expect(combobox['_isOptionsOpen']).toBe(false)
194
+ })
195
+
196
+ test('keeps the list open when focus moves within the combobox', async () => {
197
+ const container = await createCombobox('id="test" name="test" label="Test"')
198
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
199
+ combobox.isOpen = true
200
+ await combobox.updateComplete
201
+
202
+ const trigger = combobox.querySelector('[role="combobox"]') as HTMLElement
203
+ trigger.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: trigger }))
204
+ await combobox.updateComplete
205
+
206
+ expect(combobox['_isOptionsOpen']).toBe(true)
207
+ })
208
+
178
209
  test('handles blur correctly', async () => {
179
210
  const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
180
211
  const combobox = container.querySelector('pkt-combobox') as PktCombobox
@@ -271,6 +302,142 @@ describe('PktCombobox', () => {
271
302
  })
272
303
  })
273
304
 
305
+ describe('Focus on open', () => {
306
+ const openAndSettle = async (combobox: PktCombobox) => {
307
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
308
+ fireEvent.click(arrowButton!)
309
+ await combobox.updateComplete
310
+ await new Promise((resolve) => setTimeout(resolve, 0))
311
+ }
312
+
313
+ test('moves focus to the first option when the dropdown opens', async () => {
314
+ const container = await createCombobox('id="test" name="test" label="Test"')
315
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
316
+ combobox.options = [...defaultOptions]
317
+ await combobox.updateComplete
318
+
319
+ await openAndSettle(combobox)
320
+
321
+ const active = document.activeElement as HTMLElement
322
+ expect(active?.getAttribute('role')).toBe('option')
323
+ expect(active?.dataset.value).toBe('apple')
324
+ })
325
+
326
+ test('moves focus to the selected option when the dropdown opens', async () => {
327
+ const container = await createCombobox('id="test" name="test" label="Test" value="cherry"')
328
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
329
+ combobox.options = [...defaultOptions]
330
+ await combobox.updateComplete
331
+
332
+ await openAndSettle(combobox)
333
+
334
+ const active = document.activeElement as HTMLElement
335
+ expect(active?.getAttribute('role')).toBe('option')
336
+ expect(active?.dataset.value).toBe('cherry')
337
+ })
338
+ })
339
+
340
+ describe('Focus after committing a value', () => {
341
+ const commit = async (combobox: PktCombobox, typed: string) => {
342
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
343
+ fireEvent.focus(input)
344
+ await combobox.updateComplete
345
+ input.value = typed
346
+ fireEvent.input(input)
347
+ await combobox.updateComplete
348
+ fireEvent.keyDown(input, { key: 'Enter' })
349
+ await combobox.updateComplete
350
+ await new Promise((resolve) => setTimeout(resolve, 0))
351
+ }
352
+
353
+ test('moves focus out of the text input after adding a user value', async () => {
354
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
355
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
356
+ combobox.options = [...defaultOptions]
357
+ await combobox.updateComplete
358
+
359
+ await commit(combobox, 'NewFruit')
360
+
361
+ const input = combobox.querySelector('input[type="text"]')
362
+ expect(document.activeElement).not.toBe(input)
363
+ expect(document.activeElement).toBe(combobox.querySelector('.pkt-combobox__input'))
364
+ expect(combobox['_isOptionsOpen']).toBe(false)
365
+ })
366
+
367
+ test('moves focus out of the text input after selecting a typeahead match', async () => {
368
+ const container = await createCombobox('id="test" name="test" label="Test" typeahead')
369
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
370
+ combobox.options = [...defaultOptions]
371
+ await combobox.updateComplete
372
+
373
+ await commit(combobox, 'apple')
374
+
375
+ const input = combobox.querySelector('input[type="text"]')
376
+ expect(document.activeElement).not.toBe(input)
377
+ expect(document.activeElement).toBe(combobox.querySelector('.pkt-combobox__input'))
378
+ expect(combobox['_value']).toContain('apple')
379
+ })
380
+ })
381
+
382
+ describe('Selection announcement', () => {
383
+ const liveText = (container: HTMLElement) =>
384
+ container.querySelector('[aria-live="polite"]')?.textContent?.trim()
385
+
386
+ test('announces the displayed label, not the form value', async () => {
387
+ const container = await createCombobox('id="test" name="test" label="Test" typeahead')
388
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
389
+ combobox.options = [{ value: 'NO', label: 'Norge' }]
390
+ await combobox.updateComplete
391
+
392
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
393
+ input.focus()
394
+ input.value = 'NO'
395
+ fireEvent.input(input)
396
+ await combobox.updateComplete
397
+ fireEvent.keyDown(input, { key: 'Enter' })
398
+ await combobox.updateComplete
399
+ await new Promise((resolve) => setTimeout(resolve, 0))
400
+
401
+ expect(input.value).toBe('Norge')
402
+ expect(liveText(container)).toBe('Norge er valgt')
403
+ })
404
+
405
+ test('announces the raw value when displayValueAs is value', async () => {
406
+ const container = await createCombobox(
407
+ 'id="test" name="test" label="Test" typeahead display-value-as="value"',
408
+ )
409
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
410
+ combobox.options = [{ value: 'NO', label: 'Norge' }]
411
+ await combobox.updateComplete
412
+
413
+ const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
414
+ input.focus()
415
+ input.value = 'NO'
416
+ fireEvent.input(input)
417
+ await combobox.updateComplete
418
+ fireEvent.keyDown(input, { key: 'Enter' })
419
+ await combobox.updateComplete
420
+ await new Promise((resolve) => setTimeout(resolve, 0))
421
+
422
+ expect(liveText(container)).toBe('NO er valgt')
423
+ })
424
+
425
+ test('announces the prefix in select-only mode, matching the display', async () => {
426
+ const container = await createCombobox(
427
+ 'id="test" name="test" label="Test" display-value-as="prefixAndValue"',
428
+ )
429
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
430
+ combobox.options = [{ value: 'NO', label: 'Norge', prefix: '+47' }]
431
+ await combobox.updateComplete
432
+
433
+ combobox.toggleValue('NO')
434
+ await combobox.updateComplete
435
+
436
+ expect(combobox.querySelector('.pkt-combobox__value')?.textContent?.trim()).toBe('+47 NO')
437
+ expect(liveText(container)).toBe('+47 NO er valgt')
438
+ })
439
+ })
440
+
274
441
  describe('Search and filtering', () => {
275
442
  test('filters options when typing in typeahead mode', async () => {
276
443
  const container = await createCombobox('id="test" name="test" label="Test" typeahead')
@@ -0,0 +1,271 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ import './combobox'
4
+ import { PktCombobox } from './combobox'
5
+ import type { IPktComboboxOption } from './combobox'
6
+
7
+ const bydeler: IPktComboboxOption[] = [
8
+ { value: 'bfr', label: 'Frogner' },
9
+ { value: 'bga', label: 'Grünerløkka' },
10
+ { value: 'bsa', label: 'Sagene' },
11
+ { value: 'bgo', label: 'Gamle Oslo' },
12
+ ]
13
+
14
+ const createCombobox = async (attrs: string, options?: IPktComboboxOption[]) => {
15
+ const container = document.createElement('div')
16
+ container.innerHTML = `<pkt-combobox ${attrs}></pkt-combobox>`
17
+ document.body.appendChild(container)
18
+ await customElements.whenDefined('pkt-combobox')
19
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
20
+ if (options) combobox.options = options.map((o) => ({ ...o }))
21
+ await combobox.updateComplete
22
+ return combobox
23
+ }
24
+
25
+ /** Values the listbox renders as selected. */
26
+ const selectedInList = (combobox: PktCombobox) =>
27
+ Array.from(combobox.querySelectorAll('[role="option"]'))
28
+ .filter((el) => el.getAttribute('aria-selected') === 'true')
29
+ .map((el) => el.getAttribute('data-value'))
30
+
31
+ const tagLabels = (combobox: PktCombobox) =>
32
+ Array.from(combobox.querySelectorAll('pkt-tag')).map((el) => el.textContent?.trim())
33
+
34
+ afterEach(() => {
35
+ document.body.innerHTML = ''
36
+ })
37
+
38
+ describe('PktCombobox preselection', () => {
39
+ describe('Preselected via the value attribute', () => {
40
+ test('marks the preselected options in both the tag list and the listbox', async () => {
41
+ const combobox = await createCombobox(
42
+ 'id="t" name="t" label="L" multiple tag-placement="outside" value="bfr,bga,bsa"',
43
+ bydeler,
44
+ )
45
+
46
+ expect(combobox['_value']).toEqual(['bfr', 'bga', 'bsa'])
47
+ expect(selectedInList(combobox)).toEqual(['bfr', 'bga', 'bsa'])
48
+ expect(tagLabels(combobox)).toHaveLength(3)
49
+ })
50
+
51
+ test('keeps the existing selection in the listbox when another option is picked', async () => {
52
+ const combobox = await createCombobox(
53
+ 'id="t" name="t" label="L" multiple tag-placement="outside" value="bfr,bga,bsa"',
54
+ bydeler,
55
+ )
56
+
57
+ combobox.toggleValue('bgo')
58
+ await combobox.updateComplete
59
+
60
+ expect(combobox['_value']).toEqual(['bfr', 'bga', 'bsa', 'bgo'])
61
+ expect(selectedInList(combobox)).toEqual(['bfr', 'bga', 'bsa', 'bgo'])
62
+ expect(tagLabels(combobox)).toHaveLength(4)
63
+ })
64
+
65
+ test('keeps the listbox in sync when an option is deselected', async () => {
66
+ const combobox = await createCombobox(
67
+ 'id="t" name="t" label="L" multiple value="bfr,bga,bsa"',
68
+ bydeler,
69
+ )
70
+
71
+ combobox.toggleValue('bga')
72
+ await combobox.updateComplete
73
+
74
+ expect(combobox['_value']).toEqual(['bfr', 'bsa'])
75
+ expect(selectedInList(combobox)).toEqual(['bfr', 'bsa'])
76
+ })
77
+ })
78
+
79
+ describe('Preselected via options.selected', () => {
80
+ test('seeds the value from options marked selected', async () => {
81
+ const combobox = await createCombobox('id="t" name="t" label="L" multiple typeahead', [
82
+ { value: 'HODEPINE', selected: true },
83
+ { value: 'KVALME' },
84
+ { value: 'INGEN', selected: true },
85
+ ])
86
+
87
+ expect(combobox['_value']).toEqual(['HODEPINE', 'INGEN'])
88
+ expect(selectedInList(combobox)).toEqual(['HODEPINE', 'INGEN'])
89
+ expect(tagLabels(combobox)).toHaveLength(2)
90
+ })
91
+
92
+ test('adds to the preselected values rather than replacing them', async () => {
93
+ const combobox = await createCombobox('id="t" name="t" label="L" multiple typeahead', [
94
+ { value: 'HODEPINE', selected: true },
95
+ { value: 'KVALME' },
96
+ { value: 'INGEN', selected: true },
97
+ ])
98
+
99
+ combobox.toggleValue('KVALME')
100
+ await combobox.updateComplete
101
+
102
+ expect(combobox['_value']).toEqual(['HODEPINE', 'INGEN', 'KVALME'])
103
+ expect(selectedInList(combobox)).toEqual(['HODEPINE', 'KVALME', 'INGEN'])
104
+ expect(tagLabels(combobox)).toHaveLength(3)
105
+ })
106
+
107
+ test('takes only the first preselected option in single-select mode', async () => {
108
+ const combobox = await createCombobox('id="t" name="t" label="L"', [
109
+ { value: 'HODEPINE', selected: true },
110
+ { value: 'KVALME' },
111
+ ])
112
+
113
+ expect(combobox['_value']).toEqual(['HODEPINE'])
114
+ })
115
+ })
116
+
117
+ describe('Preselected via defaultValue', () => {
118
+ test('seeds the value from defaultValue', async () => {
119
+ const combobox = await createCombobox('id="t" name="t" label="L" multiple', bydeler)
120
+ combobox.defaultValue = ['bfr', 'bga']
121
+ await combobox.updateComplete
122
+
123
+ expect(combobox['_value']).toEqual(['bfr', 'bga'])
124
+ expect(selectedInList(combobox)).toEqual(['bfr', 'bga'])
125
+ expect(tagLabels(combobox)).toHaveLength(2)
126
+ })
127
+
128
+ test('lets value take precedence over defaultValue', async () => {
129
+ const combobox = await createCombobox('id="t" name="t" label="L" multiple value="bsa"', bydeler)
130
+ combobox.defaultValue = ['bfr', 'bga']
131
+ await combobox.updateComplete
132
+
133
+ expect(combobox['_value']).toEqual(['bsa'])
134
+ })
135
+
136
+ test('does not reapply defaultValue after the selection is cleared', async () => {
137
+ const combobox = await createCombobox('id="t" name="t" label="L" multiple', bydeler)
138
+ combobox.defaultValue = ['bfr']
139
+ await combobox.updateComplete
140
+ expect(combobox['_value']).toEqual(['bfr'])
141
+
142
+ combobox.toggleValue('bfr')
143
+ await combobox.updateComplete
144
+
145
+ expect(combobox['_value']).toEqual([])
146
+ expect(selectedInList(combobox)).toEqual([])
147
+ })
148
+
149
+ test('restores defaultValue on form reset', async () => {
150
+ const container = document.createElement('div')
151
+ container.innerHTML = `<form><pkt-combobox id="t" name="t" label="L" multiple></pkt-combobox></form>`
152
+ document.body.appendChild(container)
153
+ await customElements.whenDefined('pkt-combobox')
154
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
155
+ combobox.options = bydeler.map((o) => ({ ...o }))
156
+ combobox.defaultValue = ['bfr']
157
+ await combobox.updateComplete
158
+
159
+ combobox.toggleValue('bga')
160
+ await combobox.updateComplete
161
+ expect(combobox['_value']).toEqual(['bfr', 'bga'])
162
+ ;(container.querySelector('form') as HTMLFormElement).reset()
163
+ await combobox.updateComplete
164
+
165
+ expect(combobox['_value']).toEqual(['bfr'])
166
+ })
167
+ })
168
+
169
+ describe('Form reset', () => {
170
+ const mountInForm = async (attrs: string, options: IPktComboboxOption[]) => {
171
+ const container = document.createElement('div')
172
+ container.innerHTML = `<form><pkt-combobox ${attrs}></pkt-combobox></form>`
173
+ document.body.appendChild(container)
174
+ await customElements.whenDefined('pkt-combobox')
175
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
176
+ combobox.options = options.map((o) => ({ ...o }))
177
+ await combobox.updateComplete
178
+ const form = container.querySelector('form') as HTMLFormElement
179
+ return { combobox, form }
180
+ }
181
+
182
+ test('clears a selected option', async () => {
183
+ const { combobox, form } = await mountInForm('id="t" name="t" label="L" multiple', bydeler)
184
+ combobox.toggleValue('bfr')
185
+ await combobox.updateComplete
186
+
187
+ form.reset()
188
+ await combobox.updateComplete
189
+
190
+ expect(combobox['_value']).toEqual([])
191
+ expect(tagLabels(combobox)).toHaveLength(0)
192
+ })
193
+
194
+ test('clears a user-added value', async () => {
195
+ const { combobox, form } = await mountInForm(
196
+ 'id="t" name="t" label="L" multiple allow-user-input',
197
+ bydeler,
198
+ )
199
+ combobox.toggleValue('egendefinert')
200
+ await combobox.updateComplete
201
+ expect(combobox['_value']).toEqual(['egendefinert'])
202
+
203
+ form.reset()
204
+ await combobox.updateComplete
205
+
206
+ expect(combobox['_value']).toEqual([])
207
+ expect(tagLabels(combobox)).toHaveLength(0)
208
+ expect(combobox.options.some((option) => option.userAdded)).toBe(false)
209
+ })
210
+
211
+ test('clears a user-added value alongside a selected option', async () => {
212
+ const { combobox, form } = await mountInForm(
213
+ 'id="t" name="t" label="L" multiple allow-user-input',
214
+ bydeler,
215
+ )
216
+ combobox.toggleValue('bfr')
217
+ await combobox.updateComplete
218
+ combobox.toggleValue('egendefinert')
219
+ await combobox.updateComplete
220
+ expect(combobox['_value']).toEqual(['bfr', 'egendefinert'])
221
+
222
+ form.reset()
223
+ await combobox.updateComplete
224
+
225
+ expect(combobox['_value']).toEqual([])
226
+ expect(selectedInList(combobox)).toEqual([])
227
+ })
228
+ })
229
+
230
+ describe('Selection state after picking an option', () => {
231
+ test('marks the option in the listbox on the first click', async () => {
232
+ const combobox = await createCombobox(
233
+ 'id="t" name="t" label="L" multiple maxlength="3" include-search',
234
+ bydeler,
235
+ )
236
+
237
+ combobox.toggleValue('bfr')
238
+ await combobox.updateComplete
239
+
240
+ expect(combobox['_value']).toEqual(['bfr'])
241
+ expect(selectedInList(combobox)).toEqual(['bfr'])
242
+ expect(tagLabels(combobox)).toHaveLength(1)
243
+ })
244
+
245
+ test('keeps value and listbox in sync across repeated toggles', async () => {
246
+ const combobox = await createCombobox('id="t" name="t" label="L" multiple', bydeler)
247
+
248
+ for (const value of ['bfr', 'bga', 'bfr', 'bsa']) {
249
+ combobox.toggleValue(value)
250
+ await combobox.updateComplete
251
+ }
252
+
253
+ expect(combobox['_value']).toEqual(['bga', 'bsa'])
254
+ expect(selectedInList(combobox)).toEqual(['bga', 'bsa'])
255
+ expect(tagLabels(combobox)).toHaveLength(2)
256
+ })
257
+
258
+ test('keeps selection when all options are preselected', async () => {
259
+ const combobox = await createCombobox(
260
+ 'id="t" name="t" label="L" multiple value="bfr,bga,bsa,bgo"',
261
+ bydeler,
262
+ )
263
+
264
+ combobox.toggleValue('bfr')
265
+ await combobox.updateComplete
266
+
267
+ expect(combobox['_value']).toEqual(['bga', 'bsa', 'bgo'])
268
+ expect(selectedInList(combobox)).toEqual(['bga', 'bsa', 'bgo'])
269
+ })
270
+ })
271
+ })