@oslokommune/punkt-elements 19.8.0 → 19.9.0

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 (224) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/accordion-CMCCpxtz.cjs +47 -0
  3. package/dist/{accordion-BJh6X1Ek.js → accordion-Cp2o6wTY.js} +10 -9
  4. package/dist/{alert-WME3geaX.js → alert-CAPiwi4F.js} +5 -5
  5. package/dist/{alert-CKqtC9AM.cjs → alert-QfloIr-o.cjs} +1 -1
  6. package/dist/{async-directive-BGoo_npW.cjs → async-directive-DpyKp6qv.cjs} +1 -1
  7. package/dist/{async-directive-DsdqP_X7.js → async-directive-yh8hG41Q.js} +2 -2
  8. package/dist/{backlink-BRqhwUr2.cjs → backlink-CsFeerTH.cjs} +1 -1
  9. package/dist/{backlink-BslFewn-.js → backlink-ogvv493Y.js} +3 -3
  10. package/dist/base-elements/input-element.d.ts +17 -0
  11. package/dist/base-elements/option-group-element.d.ts +61 -0
  12. package/dist/base-elements/polyfill-form-value.test.d.ts +0 -0
  13. package/dist/{breadcrumbs-Cr3GugPv.js → breadcrumbs-53QVnTFP.js} +2 -2
  14. package/dist/{breadcrumbs-DG4i5UCh.cjs → breadcrumbs-D03id_ha.cjs} +1 -1
  15. package/dist/{button-B78im4wB.js → button-CeRr7oIn.js} +5 -5
  16. package/dist/{button-BNVTj7nB.cjs → button-Dh97H-58.cjs} +1 -1
  17. package/dist/{calendar-TcxvS-1_.cjs → calendar-C5QwPrbk.cjs} +1 -1
  18. package/dist/{calendar-BLaRXUBl.js → calendar-D9w8M99G.js} +3 -3
  19. package/dist/{card-CaLEuumN.cjs → card--NuWdTbG.cjs} +1 -1
  20. package/dist/{card-CYKhPUNI.js → card-_PlgXmgZ.js} +6 -6
  21. package/dist/{checkbox-Tq8ESzwH.js → checkbox-G8k3vg85.js} +39 -35
  22. package/dist/checkbox-QIcdn09A.cjs +40 -0
  23. package/dist/checkbox-group-DoueGGi5.js +50 -0
  24. package/dist/checkbox-group-nLazAkcg.cjs +1 -0
  25. package/dist/checkbox-group.d.ts +1 -0
  26. package/dist/{class-map-C4hdfSfY.js → class-map-Bf-QzUpN.js} +1 -1
  27. package/dist/{class-map-Drv0_anQ.cjs → class-map-Bh6toDeV.cjs} +1 -1
  28. package/dist/{combobox-89Ohd1Oe.cjs → combobox-ClXhKwR9.cjs} +1 -1
  29. package/dist/{combobox-Cpb67TV0.js → combobox-Cz_rq7Mm.js} +13 -13
  30. package/dist/components/checkbox/checkbox.d.ts +1 -0
  31. package/dist/components/checkbox-group/checkbox-group-context.d.ts +5 -0
  32. package/dist/components/checkbox-group/checkbox-group.d.ts +15 -0
  33. package/dist/components/checkbox-group/checkbox-group.test.d.ts +0 -0
  34. package/dist/components/checkbox-group/index.d.ts +3 -0
  35. package/dist/components/index.d.ts +2 -0
  36. package/dist/components/radio-group/index.d.ts +3 -0
  37. package/dist/components/radio-group/radio-group-context.d.ts +7 -0
  38. package/dist/components/radio-group/radio-group.d.ts +14 -0
  39. package/dist/components/radio-group/radio-group.test.d.ts +0 -0
  40. package/dist/components/radiobutton/radiobutton.d.ts +1 -0
  41. package/dist/{consent-CleJMbVH.js → consent-CZMxUavj.js} +2 -2
  42. package/dist/{consent-Dl-R0iC3.cjs → consent-DimzcQxt.cjs} +1 -1
  43. package/dist/consume-Bp12si68.js +61 -0
  44. package/dist/consume-kEjgTrAp.cjs +1 -0
  45. package/dist/{datepicker-DBEKa1aJ.js → datepicker-C7ABsx7H.js} +12 -12
  46. package/dist/{datepicker-B9NCJX4o.cjs → datepicker-usIF9WY7.cjs} +2 -2
  47. package/dist/{directive-helpers-BRrz3Bcj.cjs → directive-helpers-Bv70GjQh.cjs} +1 -1
  48. package/dist/{directive-helpers-D96YEsht.js → directive-helpers-CwxFepWh.js} +1 -1
  49. package/dist/directives/slot-content.d.ts +4 -3
  50. package/dist/docs/components/checkbox-group.d.ts +13 -0
  51. package/dist/docs/components/radio-group.d.ts +13 -0
  52. package/dist/{element-BOmr9YLv.cjs → element-C6WTyJFE.cjs} +1 -1
  53. package/dist/{element-mwwC2Rra.js → element-DBugHPKM.js} +1 -0
  54. package/dist/{element-with-slot-DQv0cbbD.js → element-with-slot-DDLS-aLw.js} +6 -3
  55. package/dist/element-with-slot-DhiAuSjM.cjs +1 -0
  56. package/dist/{fileupload-2YAbjYuU.cjs → fileupload-2uTFjd81.cjs} +1 -1
  57. package/dist/{fileupload-DqWCXMPz.js → fileupload-BR9H6fET.js} +8 -8
  58. package/dist/{footer-DdLls3F-.js → footer-B3DMvo8-.js} +4 -4
  59. package/dist/{footer-vy9NqT65.cjs → footer-CyxRYvT-.cjs} +1 -1
  60. package/dist/{header-Dp0Lq7wS.js → header-BKeSkfB7.js} +11 -11
  61. package/dist/{header-9Y6Q6Cfq.cjs → header-Drj3lTrv.cjs} +1 -1
  62. package/dist/{header-menu-BfkCp8jw.cjs → header-menu-B_5hCNUl.cjs} +1 -1
  63. package/dist/{header-menu-ANj5nL2M.js → header-menu-buYE90FU.js} +4 -4
  64. package/dist/{heading-zoz-QZJ9.cjs → heading-DUYOvlxs.cjs} +1 -1
  65. package/dist/{heading-CUrZvkrM.js → heading-WBB5MO9i.js} +2 -2
  66. package/dist/{helptext-BCkwSz1m.cjs → helptext-C7cyihnu.cjs} +1 -1
  67. package/dist/{helptext-DCT_9dw1.js → helptext-DadLkcoO.js} +4 -4
  68. package/dist/{icon-JigbxxF7.js → icon-Bao96Zb8.js} +1 -1
  69. package/dist/{icon-Dnv6LtPC.cjs → icon-jPhyZD1z.cjs} +1 -1
  70. package/dist/if-defined-CiML3MTM.cjs +1 -0
  71. package/dist/{if-defined-Bebu3LXY.js → if-defined-O59_I-e-.js} +1 -1
  72. package/dist/{input-element-lLzfwdim.js → input-element-BBygBEei.js} +54 -17
  73. package/dist/input-element-C4SelHLD.cjs +1 -0
  74. package/dist/{input-wrapper-CTMf4kCK.cjs → input-wrapper-BT6B3Kco.cjs} +6 -2
  75. package/dist/{input-wrapper-C9K-_7g-.js → input-wrapper-DrSCuw6U.js} +12 -8
  76. package/dist/{link-LO_5_La8.cjs → link-CEP7w1et.cjs} +1 -1
  77. package/dist/{link-CmZwAw5u.js → link-CVnIn74-.js} +4 -4
  78. package/dist/{linkcard-UKsxADrP.cjs → linkcard-D8vitdrQ.cjs} +1 -1
  79. package/dist/{linkcard-CQluSyJJ.js → linkcard-IWiVvRKy.js} +4 -4
  80. package/dist/{listbox-Ct9jIV_5.js → listbox-BDesthOp.js} +4 -4
  81. package/dist/{listbox-g-vHHLkD.cjs → listbox-CvduK_YU.cjs} +1 -1
  82. package/dist/{loader-C_PH05aw.cjs → loader-B0hFoBtF.cjs} +1 -1
  83. package/dist/{loader-DzctJ9jW.js → loader-CAsUPHjl.js} +4 -4
  84. package/dist/{messagebox-D17GYxb9.js → messagebox-6_iNN5F3.js} +4 -4
  85. package/dist/{messagebox-X01V2l_N.cjs → messagebox-C19L2Duq.cjs} +1 -1
  86. package/dist/{modal-BymjMIJi.cjs → modal-CoEPzt4p.cjs} +1 -1
  87. package/dist/{modal-CELmA0-8.js → modal-D7MolBXb.js} +5 -5
  88. package/dist/option-group-element-BIuotbRu.js +135 -0
  89. package/dist/option-group-element-BYS7s-is.cjs +23 -0
  90. package/dist/pkt-accordion.cjs +1 -1
  91. package/dist/pkt-accordion.js +1 -1
  92. package/dist/pkt-alert.cjs +1 -1
  93. package/dist/pkt-alert.js +1 -1
  94. package/dist/pkt-backlink.cjs +1 -1
  95. package/dist/pkt-backlink.js +1 -1
  96. package/dist/pkt-breadcrumbs.cjs +1 -1
  97. package/dist/pkt-breadcrumbs.js +1 -1
  98. package/dist/pkt-button.cjs +1 -1
  99. package/dist/pkt-button.js +1 -1
  100. package/dist/pkt-calendar.cjs +1 -1
  101. package/dist/pkt-calendar.js +1 -1
  102. package/dist/pkt-card.cjs +1 -1
  103. package/dist/pkt-card.js +1 -1
  104. package/dist/pkt-checkbox-group.cjs +1 -0
  105. package/dist/pkt-checkbox-group.js +2 -0
  106. package/dist/pkt-checkbox.cjs +1 -1
  107. package/dist/pkt-checkbox.js +1 -1
  108. package/dist/pkt-combobox.cjs +1 -1
  109. package/dist/pkt-combobox.js +1 -1
  110. package/dist/pkt-consent.cjs +1 -1
  111. package/dist/pkt-consent.js +1 -1
  112. package/dist/pkt-datepicker.cjs +1 -1
  113. package/dist/pkt-datepicker.js +1 -1
  114. package/dist/pkt-fileupload.cjs +1 -1
  115. package/dist/pkt-fileupload.js +1 -1
  116. package/dist/pkt-footer.cjs +1 -1
  117. package/dist/pkt-footer.js +1 -1
  118. package/dist/pkt-header-menu.cjs +1 -1
  119. package/dist/pkt-header-menu.js +1 -1
  120. package/dist/pkt-header.cjs +1 -1
  121. package/dist/pkt-header.js +1 -1
  122. package/dist/pkt-heading.cjs +1 -1
  123. package/dist/pkt-heading.js +1 -1
  124. package/dist/pkt-helptext.cjs +1 -1
  125. package/dist/pkt-helptext.js +1 -1
  126. package/dist/pkt-icon.cjs +1 -1
  127. package/dist/pkt-icon.js +1 -1
  128. package/dist/pkt-index.cjs +1 -1
  129. package/dist/pkt-index.js +37 -35
  130. package/dist/pkt-input-wrapper.cjs +1 -1
  131. package/dist/pkt-input-wrapper.js +1 -1
  132. package/dist/pkt-link.cjs +1 -1
  133. package/dist/pkt-link.js +1 -1
  134. package/dist/pkt-linkcard.cjs +1 -1
  135. package/dist/pkt-linkcard.js +1 -1
  136. package/dist/pkt-listbox.cjs +1 -1
  137. package/dist/pkt-listbox.js +1 -1
  138. package/dist/pkt-loader.cjs +1 -1
  139. package/dist/pkt-loader.js +1 -1
  140. package/dist/pkt-messagebox.cjs +1 -1
  141. package/dist/pkt-messagebox.js +1 -1
  142. package/dist/pkt-modal.cjs +1 -1
  143. package/dist/pkt-modal.js +1 -1
  144. package/dist/{pkt-options-controller-CpEZcomb.cjs → pkt-options-controller-B3vVKnAG.cjs} +1 -1
  145. package/dist/{pkt-options-controller-D7l18Iw0.js → pkt-options-controller-ClFrUWZK.js} +1 -1
  146. package/dist/pkt-progressbar.cjs +1 -1
  147. package/dist/pkt-progressbar.js +1 -1
  148. package/dist/pkt-radio-group.cjs +1 -0
  149. package/dist/pkt-radio-group.js +2 -0
  150. package/dist/pkt-radiobutton.cjs +1 -1
  151. package/dist/pkt-radiobutton.js +1 -1
  152. package/dist/pkt-searchinput.cjs +1 -1
  153. package/dist/pkt-searchinput.js +1 -1
  154. package/dist/pkt-select.cjs +1 -1
  155. package/dist/pkt-select.js +1 -1
  156. package/dist/pkt-tabs.cjs +1 -1
  157. package/dist/pkt-tabs.js +1 -1
  158. package/dist/pkt-tag.cjs +1 -1
  159. package/dist/pkt-tag.js +1 -1
  160. package/dist/pkt-textarea.cjs +1 -1
  161. package/dist/pkt-textarea.js +1 -1
  162. package/dist/pkt-textinput.cjs +1 -1
  163. package/dist/pkt-textinput.js +1 -1
  164. package/dist/pkt-timepicker.cjs +1 -1
  165. package/dist/pkt-timepicker.js +1 -1
  166. package/dist/{progressbar-CIXNSwTy.cjs → progressbar-CnX4fFfR.cjs} +1 -1
  167. package/dist/{progressbar-DH5lIBpx.js → progressbar-DZHiq7B0.js} +5 -5
  168. package/dist/provide-Cz18sYug.cjs +1 -0
  169. package/dist/provide-_MnlMGnw.js +111 -0
  170. package/dist/radio-group-kOzlGn8T.cjs +1 -0
  171. package/dist/radio-group-lhymYEZX.js +52 -0
  172. package/dist/radio-group.d.ts +1 -0
  173. package/dist/radiobutton-BgHCcDJv.cjs +38 -0
  174. package/dist/{radiobutton-l_3oGDbb.js → radiobutton-CFd8TgV9.js} +39 -35
  175. package/dist/{ref-Emu5PNxk.cjs → ref-CB7e9nMY.cjs} +1 -1
  176. package/dist/{ref-CCSGk4MP.js → ref-y_5qv9vU.js} +2 -2
  177. package/dist/{repeat-CdxGS-_3.js → repeat-CV9S4zya.js} +2 -2
  178. package/dist/{repeat-Dh9YKoeV.cjs → repeat-CnnigVcj.cjs} +1 -1
  179. package/dist/{searchinput-Co8tcq-l.js → searchinput-Bkcr3Fby.js} +4 -4
  180. package/dist/{searchinput-CbClDE-d.cjs → searchinput-Mdc9Ihld.cjs} +1 -1
  181. package/dist/{select-Cz80KV3g.js → select-B6CN2Ymq.js} +7 -7
  182. package/dist/{select-CZk57p1_.cjs → select-BowvpCNa.cjs} +1 -1
  183. package/dist/shared-strings/nb.d.ts +1 -0
  184. package/dist/shared-types/index.d.ts +4 -2
  185. package/dist/shared-types/option-group.d.ts +50 -0
  186. package/dist/{tabs-B8blHQrL.js → tabs-DZvtVA0y.js} +9 -8
  187. package/dist/tabs-Dm8xcPRs.cjs +40 -0
  188. package/dist/{tag-Bns7Xj1O.js → tag-C33BPIOm.js} +5 -5
  189. package/dist/{tag-B5dUg7AD.cjs → tag-DyVZrqOQ.cjs} +1 -1
  190. package/dist/tests/component-registry.d.ts +4 -0
  191. package/dist/tests/option-contexts.d.ts +7 -0
  192. package/dist/{textarea-Dwjx-Crl.cjs → textarea-DoH3Il8v.cjs} +1 -1
  193. package/dist/{textarea-Dfvx1I60.js → textarea-hW0kV5gj.js} +9 -9
  194. package/dist/{textinput-7zTPlXVj.js → textinput-B6G4dVuL.js} +8 -8
  195. package/dist/{textinput-BeraGRzb.cjs → textinput-CQknclmV.cjs} +1 -1
  196. package/dist/{timepicker-C0dSH4Ij.js → timepicker-Cd2QI6ua.js} +8 -8
  197. package/dist/{timepicker-DarB9eA6.cjs → timepicker-DB3gor6g.cjs} +1 -1
  198. package/package.json +2 -2
  199. package/src/components/checkbox/checkbox.test.ts +99 -0
  200. package/src/components/checkbox/checkbox.ts +14 -5
  201. package/src/components/checkbox-group/checkbox-group-context.ts +8 -0
  202. package/src/components/checkbox-group/checkbox-group.test.ts +406 -0
  203. package/src/components/checkbox-group/checkbox-group.ts +69 -0
  204. package/src/components/checkbox-group/index.ts +4 -0
  205. package/src/components/combobox/combobox.ts +1 -1
  206. package/src/components/datepicker/datepicker.ts +1 -1
  207. package/src/components/index.ts +2 -0
  208. package/src/components/input-wrapper/input-wrapper.ts +5 -1
  209. package/src/components/input-wrapper/wrapper-parity.test.ts +27 -2
  210. package/src/components/radio-group/index.ts +4 -0
  211. package/src/components/radio-group/radio-group-context.ts +8 -0
  212. package/src/components/radio-group/radio-group.test.ts +530 -0
  213. package/src/components/radio-group/radio-group.ts +65 -0
  214. package/src/components/radiobutton/radiobutton.test.ts +95 -0
  215. package/src/components/radiobutton/radiobutton.ts +11 -5
  216. package/dist/accordion-5lSFQToJ.cjs +0 -47
  217. package/dist/checkbox-D69leWDh.cjs +0 -40
  218. package/dist/consume-BDnMAp2g.js +0 -167
  219. package/dist/consume-Le4HztPo.cjs +0 -1
  220. package/dist/element-with-slot-D7sTqOr3.cjs +0 -1
  221. package/dist/if-defined-2mkIDgOm.cjs +0 -1
  222. package/dist/input-element-CSfDGslx.cjs +0 -1
  223. package/dist/radiobutton-BPr22VmL.cjs +0 -38
  224. package/dist/tabs-CjX-GWvM.cjs +0 -40
@@ -0,0 +1,406 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { fireEvent } from '@testing-library/dom'
4
+ import { vi } from 'vitest'
5
+ import type { LitElement } from 'lit'
6
+ import { CustomElementFor } from '../../tests/component-registry'
7
+ import '@/components/textinput'
8
+ import './checkbox-group'
9
+
10
+ expect.extend(toHaveNoViolations)
11
+
12
+ type TCheckboxGroup = CustomElementFor<'pkt-checkbox-group'>
13
+ type TCheckbox = CustomElementFor<'pkt-checkbox'> & {
14
+ internals: { validity: ValidityState; validationMessage: string }
15
+ }
16
+
17
+ const settle = async () => {
18
+ for (let i = 0; i < 2; i++) {
19
+ const elements = Array.from(
20
+ document.querySelectorAll<LitElement>('pkt-checkbox-group, pkt-input-wrapper, pkt-checkbox'),
21
+ )
22
+ await Promise.all(elements.map((el) => el.updateComplete))
23
+ await new Promise((resolve) => setTimeout(resolve, 0))
24
+ }
25
+ }
26
+
27
+ const mount = async (markup: string) => {
28
+ document.body.innerHTML = markup
29
+ await settle()
30
+ return document.querySelector('pkt-checkbox-group') as TCheckboxGroup
31
+ }
32
+
33
+ const checkbox = (id: string) => document.getElementById(id) as TCheckbox
34
+ const inner = (id: string) => checkbox(id).querySelector('input') as HTMLInputElement
35
+
36
+ const toggle = async (id: string) => {
37
+ fireEvent.click(inner(id))
38
+ await settle()
39
+ }
40
+
41
+ const options = `
42
+ <pkt-checkbox id="enkelt" value="enkelt" label="Enkeltrom"></pkt-checkbox>
43
+ <pkt-checkbox id="dobbelt" value="dobbelt" label="Dobbeltrom"></pkt-checkbox>
44
+ <pkt-checkbox id="varsler" value="varsler" label="Send meg varsler" isSwitch></pkt-checkbox>
45
+ `
46
+
47
+ describe('pkt-checkbox-group', () => {
48
+ afterEach(() => {
49
+ document.body.innerHTML = ''
50
+ })
51
+
52
+ describe('rendering', () => {
53
+ test('renders the checkboxes inside the fieldset options of an input wrapper', async () => {
54
+ const group = await mount(
55
+ `<pkt-checkbox-group id="rom" label="Romtype">${options}</pkt-checkbox-group>`,
56
+ )
57
+
58
+ const container = group.querySelector('fieldset .pkt-inputwrapper__options')
59
+ expect(group.querySelector('legend')).toHaveTextContent('Romtype')
60
+ expect(checkbox('enkelt').parentElement).toBe(container)
61
+ expect(checkbox('varsler').parentElement).toBe(container)
62
+ })
63
+
64
+ test('allows switches in the group', async () => {
65
+ await mount(`<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`)
66
+
67
+ expect(inner('varsler')).toHaveAttribute('role', 'switch')
68
+ })
69
+ })
70
+
71
+ describe('name and defaults', () => {
72
+ test('gives checkboxes without a name the group name', async () => {
73
+ await mount(`<pkt-checkbox-group name="rom" label="Romtype">${options}</pkt-checkbox-group>`)
74
+
75
+ expect(checkbox('enkelt')).toHaveAttribute('name', 'rom')
76
+ expect(checkbox('dobbelt')).toHaveAttribute('name', 'rom')
77
+ })
78
+
79
+ test('keeps a name set on the checkbox', async () => {
80
+ await mount(
81
+ `<pkt-checkbox-group name="rom" label="Romtype">
82
+ <pkt-checkbox id="enkelt" name="eget" value="enkelt" label="Enkeltrom"></pkt-checkbox>
83
+ </pkt-checkbox-group>`,
84
+ )
85
+
86
+ expect(checkbox('enkelt')).toHaveAttribute('name', 'eget')
87
+ })
88
+
89
+ test('turns on tile and error state for every checkbox', async () => {
90
+ await mount(
91
+ `<pkt-checkbox-group label="Romtype" hasTile hasError>${options}</pkt-checkbox-group>`,
92
+ )
93
+
94
+ expect(checkbox('dobbelt').querySelector('.pkt-input-check__input')).toHaveClass(
95
+ 'pkt-input-check__input--tile',
96
+ )
97
+ expect(inner('dobbelt')).toHaveClass('pkt-input-check__input-checkbox--error')
98
+ })
99
+
100
+ test('does not pass required on to the checkboxes', async () => {
101
+ await mount(`<pkt-checkbox-group label="Romtype" required>${options}</pkt-checkbox-group>`)
102
+
103
+ expect(checkbox('enkelt').required).toBe(false)
104
+ })
105
+ })
106
+
107
+ describe('value', () => {
108
+ test('is the values of the checked checkboxes in DOM order', async () => {
109
+ const group = await mount(
110
+ `<pkt-checkbox-group label="Romtype">
111
+ <pkt-checkbox id="enkelt" value="enkelt" label="Enkeltrom" checked></pkt-checkbox>
112
+ <pkt-checkbox id="dobbelt" value="dobbelt" label="Dobbeltrom"></pkt-checkbox>
113
+ <pkt-checkbox id="suite" value="suite" label="Suite" checked></pkt-checkbox>
114
+ </pkt-checkbox-group>`,
115
+ )
116
+
117
+ expect(group.value).toEqual(['enkelt', 'suite'])
118
+ })
119
+
120
+ test('keeps DOM order regardless of the order they were checked in', async () => {
121
+ const group = await mount(
122
+ `<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`,
123
+ )
124
+
125
+ await toggle('varsler')
126
+ await toggle('enkelt')
127
+
128
+ expect(group.value).toEqual(['enkelt', 'varsler'])
129
+ })
130
+
131
+ test('leaves out disabled checkboxes', async () => {
132
+ const group = await mount(
133
+ `<pkt-checkbox-group label="Romtype">
134
+ <pkt-checkbox id="enkelt" value="enkelt" label="Enkeltrom" checked></pkt-checkbox>
135
+ <pkt-checkbox id="dobbelt" value="dobbelt" label="Dobbeltrom" checked disabled></pkt-checkbox>
136
+ </pkt-checkbox-group>`,
137
+ )
138
+
139
+ expect(group.value).toEqual(['enkelt'])
140
+ })
141
+
142
+ test('uses on for a checked checkbox without a value', async () => {
143
+ const group = await mount(
144
+ `<pkt-checkbox-group label="Vilkår">
145
+ <pkt-checkbox id="godta" label="Jeg godtar" checked></pkt-checkbox>
146
+ </pkt-checkbox-group>`,
147
+ )
148
+
149
+ expect(group.value).toEqual(['on'])
150
+ })
151
+
152
+ test('checks the checkboxes listed in the value attribute', async () => {
153
+ const group = await mount(
154
+ `<pkt-checkbox-group label="Romtype" value="enkelt,varsler">${options}</pkt-checkbox-group>`,
155
+ )
156
+
157
+ expect(checkbox('enkelt').checked).toBe(true)
158
+ expect(checkbox('dobbelt').checked).toBeFalsy()
159
+ expect(checkbox('varsler').checked).toBe(true)
160
+ expect(group.value).toEqual(['enkelt', 'varsler'])
161
+ })
162
+
163
+ test('checks the matching checkboxes when set from code, without firing events', async () => {
164
+ const group = await mount(
165
+ `<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`,
166
+ )
167
+ const changed = vi.fn()
168
+ group.addEventListener('change', changed)
169
+ group.addEventListener('value-change', changed)
170
+
171
+ group.value = ['dobbelt']
172
+ await settle()
173
+
174
+ expect(checkbox('dobbelt').checked).toBe(true)
175
+ expect(inner('dobbelt').checked).toBe(true)
176
+ expect(checkbox('enkelt').checked).toBeFalsy()
177
+ expect(changed).not.toHaveBeenCalled()
178
+ })
179
+ })
180
+
181
+ describe('events', () => {
182
+ test('fires value-change on the group with all checked values', async () => {
183
+ const group = await mount(
184
+ `<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`,
185
+ )
186
+ const fromGroup: unknown[] = []
187
+ group.addEventListener('value-change', (event) => {
188
+ if (event.target === group) fromGroup.push((event as CustomEvent).detail)
189
+ })
190
+
191
+ await toggle('dobbelt')
192
+ await toggle('enkelt')
193
+ await toggle('dobbelt')
194
+
195
+ expect(fromGroup).toEqual([['dobbelt'], ['enkelt', 'dobbelt'], ['enkelt']])
196
+ })
197
+
198
+ test('fires change from the group with all checked values as its value', async () => {
199
+ const group = await mount(
200
+ `<form><pkt-checkbox-group name="rom" label="Romtype">${options}</pkt-checkbox-group></form>`,
201
+ )
202
+ const values: unknown[] = []
203
+ group.addEventListener('change', (event) => {
204
+ expect(event.target).toBe(group)
205
+ values.push((event.target as TCheckboxGroup).value)
206
+ })
207
+ const formChanges = vi.fn()
208
+ group.closest('form')!.addEventListener('change', formChanges)
209
+
210
+ await toggle('dobbelt')
211
+ await toggle('enkelt')
212
+
213
+ expect(values).toEqual([['dobbelt'], ['enkelt', 'dobbelt']])
214
+ expect(formChanges).toHaveBeenCalledTimes(2)
215
+ expect(formChanges.mock.calls.every(([event]) => event.target === group)).toBe(true)
216
+ })
217
+
218
+ test('still fires the events on the checkbox itself', async () => {
219
+ await mount(`<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`)
220
+ const changed = vi.fn()
221
+ checkbox('enkelt').addEventListener('change', changed)
222
+
223
+ await toggle('enkelt')
224
+
225
+ expect(changed).toHaveBeenCalledTimes(1)
226
+ })
227
+
228
+ test('focus() moves focus to the first checked checkbox, or the first one', async () => {
229
+ const group = await mount(
230
+ `<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`,
231
+ )
232
+
233
+ group.focus()
234
+ expect(document.activeElement).toBe(inner('enkelt'))
235
+
236
+ group.value = ['dobbelt']
237
+ await settle()
238
+ group.focus()
239
+ expect(document.activeElement).toBe(inner('dobbelt'))
240
+ })
241
+ })
242
+
243
+ describe('validation', () => {
244
+ test('makes every checkbox invalid with a custom error until one is checked', async () => {
245
+ await mount(`<pkt-checkbox-group label="Romtype" required>${options}</pkt-checkbox-group>`)
246
+
247
+ for (const id of ['enkelt', 'dobbelt', 'varsler']) {
248
+ expect(checkbox(id).internals.validity.customError).toBe(true)
249
+ expect(checkbox(id).internals.validity.valueMissing).toBe(false)
250
+ expect(checkbox(id).internals.validationMessage).toBe('Velg minst ett alternativ')
251
+ }
252
+
253
+ await toggle('dobbelt')
254
+
255
+ for (const id of ['enkelt', 'dobbelt', 'varsler']) {
256
+ expect(checkbox(id).internals.validity.valid).toBe(true)
257
+ }
258
+
259
+ await toggle('dobbelt')
260
+
261
+ expect(checkbox('enkelt').internals.validity.customError).toBe(true)
262
+ })
263
+
264
+ test('counts a checked value set from code', async () => {
265
+ const group = await mount(
266
+ `<pkt-checkbox-group label="Romtype" required>${options}</pkt-checkbox-group>`,
267
+ )
268
+
269
+ group.value = ['enkelt']
270
+ await settle()
271
+
272
+ expect(checkbox('dobbelt').internals.validity.valid).toBe(true)
273
+ })
274
+
275
+ test('clears the custom error when the group is no longer required', async () => {
276
+ const group = await mount(
277
+ `<pkt-checkbox-group label="Romtype" required>${options}</pkt-checkbox-group>`,
278
+ )
279
+
280
+ group.required = false
281
+ await settle()
282
+
283
+ expect(checkbox('enkelt').internals.validity.valid).toBe(true)
284
+ })
285
+
286
+ test('keeps the rule of a required checkbox in a required group', async () => {
287
+ await mount(
288
+ `<pkt-checkbox-group label="Vilkår" required>
289
+ <pkt-checkbox id="vilkar" value="vilkar" label="Vilkår" required></pkt-checkbox>
290
+ <pkt-checkbox id="nyhetsbrev" value="nyhetsbrev" label="Nyhetsbrev"></pkt-checkbox>
291
+ </pkt-checkbox-group>`,
292
+ )
293
+
294
+ await toggle('nyhetsbrev')
295
+
296
+ expect(checkbox('nyhetsbrev').internals.validity.valid).toBe(true)
297
+ expect(checkbox('vilkar').internals.validity.valueMissing).toBe(true)
298
+ })
299
+
300
+ test('leaves the checkboxes valid when the group is not required', async () => {
301
+ await mount(`<pkt-checkbox-group label="Romtype">${options}</pkt-checkbox-group>`)
302
+
303
+ expect(checkbox('enkelt').internals.validity.valid).toBe(true)
304
+ })
305
+ })
306
+
307
+ describe('disabled', () => {
308
+ test('disables the fieldset', async () => {
309
+ const group = await mount(
310
+ `<pkt-checkbox-group label="Romtype" disabled>${options}</pkt-checkbox-group>`,
311
+ )
312
+
313
+ expect(group.querySelector('fieldset')).toHaveAttribute('disabled', '')
314
+ })
315
+ })
316
+
317
+ describe('a text field for an other option', () => {
318
+ const settleAll = async () => {
319
+ for (let i = 0; i < 3; i++) {
320
+ const elements = Array.from(document.querySelectorAll<LitElement>('*')).filter(
321
+ (el) => 'updateComplete' in el,
322
+ )
323
+ await Promise.all(elements.map((el) => el.updateComplete))
324
+ await new Promise((resolve) => setTimeout(resolve, 0))
325
+ }
326
+ }
327
+
328
+ const mountOther = async () => {
329
+ document.body.innerHTML = `
330
+ <form id="skjema">
331
+ <pkt-checkbox-group id="gruppe" name="varsling" label="Spørsmål">
332
+ <pkt-checkbox id="epost" value="epost" label="E-post"></pkt-checkbox>
333
+ <pkt-checkbox id="annet" value="annet" label="Annet"></pkt-checkbox>
334
+ <pkt-textinput id="annet-tekst" name="varsling-annet" label="Hva annet?" hidden></pkt-textinput>
335
+ </pkt-checkbox-group>
336
+ <button>Utenfor</button>
337
+ </form>`
338
+ await settleAll()
339
+ const group = document.getElementById('gruppe') as HTMLElement & { value: unknown }
340
+ const field = document.getElementById('annet-tekst') as HTMLElement
341
+ group.addEventListener('change', (event) => {
342
+ if (event.target === group) field.hidden = !String(group.value).includes('annet')
343
+ })
344
+ return { group, field, input: () => field.querySelector('input') as HTMLInputElement }
345
+ }
346
+
347
+ test('shows the text field inside the group when the other option is chosen', async () => {
348
+ const { field } = await mountOther()
349
+
350
+ fireEvent.click(document.getElementById('annet')!.querySelector('input')!)
351
+ await settleAll()
352
+
353
+ expect(field.hidden).toBe(false)
354
+ expect(field.closest('.pkt-inputwrapper__options')).toBeInTheDocument()
355
+ })
356
+
357
+ test('keeps the text field out of the group value, and lets its events through', async () => {
358
+ const { group, input } = await mountOther()
359
+ fireEvent.click(document.getElementById('annet')!.querySelector('input')!)
360
+ await settleAll()
361
+ const targets: string[] = []
362
+ group.addEventListener('change', (event) => targets.push((event.target as Element).id))
363
+
364
+ input().value = 'Brevdue'
365
+ fireEvent.input(input())
366
+ fireEvent.change(input())
367
+ await settleAll()
368
+
369
+ expect(group.value).toEqual(['annet'])
370
+ expect(targets).toEqual(['annet-tekst'])
371
+ expect(
372
+ Array.from(new FormData(document.getElementById('skjema') as HTMLFormElement).entries()),
373
+ ).toEqual([
374
+ ['varsling', 'annet'],
375
+ ['varsling-annet', 'Brevdue'],
376
+ ])
377
+ })
378
+
379
+ test('fires blur only when focus leaves the group, also from the text field', async () => {
380
+ const { group, input } = await mountOther()
381
+ fireEvent.click(document.getElementById('annet')!.querySelector('input')!)
382
+ await settleAll()
383
+ const blurs: Element[] = []
384
+ group.addEventListener('blur', (event) => {
385
+ if (event.target === group) blurs.push(event.target as Element)
386
+ })
387
+
388
+ document.getElementById('annet')!.querySelector('input')!.focus()
389
+ input().focus()
390
+ expect(blurs).toEqual([])
391
+
392
+ document.querySelector('button')!.focus()
393
+ expect(blurs).toEqual([group])
394
+ })
395
+ })
396
+
397
+ describe('accessibility', () => {
398
+ test('has no axe violations', async () => {
399
+ const group = await mount(
400
+ `<pkt-checkbox-group id="rom" label="Romtype" helptext="Velg alle som passer">${options}</pkt-checkbox-group>`,
401
+ )
402
+
403
+ expect(await axe(group)).toHaveNoViolations()
404
+ })
405
+ })
406
+ })
@@ -0,0 +1,69 @@
1
+ import { provide } from '@lit/context'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import {
4
+ optionChecked,
5
+ optionDisabled,
6
+ optionValue,
7
+ PktOptionGroupElement,
8
+ type TOption,
9
+ } from '@/base-elements/option-group-element'
10
+ import { checkboxGroupContext, type CheckboxGroupContext } from './checkbox-group-context'
11
+ import '@/components/checkbox'
12
+
13
+ export class PktCheckboxGroup extends PktOptionGroupElement {
14
+ protected readonly optionSelector = 'pkt-checkbox'
15
+
16
+ @provide({ context: checkboxGroupContext })
17
+ @state()
18
+ protected groupContext: CheckboxGroupContext = { name: '', hasTile: false, hasError: false }
19
+
20
+ private storedValue: string[] | null = null
21
+ private validityApplied = false
22
+
23
+ @property({ type: String })
24
+ get value(): string[] {
25
+ return this.options()
26
+ .filter((option) => optionChecked(option) && !optionDisabled(option))
27
+ .map(optionValue)
28
+ }
29
+
30
+ set value(newValue: string | string[] | null | undefined) {
31
+ const oldValue = this.value
32
+ this.storedValue = Array.isArray(newValue) ? newValue : newValue ? newValue.split(',') : []
33
+ this.applyStoredValue(this.options())
34
+ this.requestUpdate('value', oldValue)
35
+ }
36
+
37
+ protected get hasStoredValue(): boolean {
38
+ return this.storedValue !== null
39
+ }
40
+
41
+ protected storeCurrentValue(): void {
42
+ this.storedValue = this.value
43
+ }
44
+
45
+ protected applyStoredValue(options: TOption[]): void {
46
+ options.forEach((option) => {
47
+ const checked = !!this.storedValue?.includes(optionValue(option))
48
+ if (optionChecked(option) !== checked) option.checked = checked
49
+ })
50
+ }
51
+
52
+ protected syncValidity(): void {
53
+ if (!this.required && !this.validityApplied) return
54
+ this.validityApplied = this.required
55
+ const options = this.options()
56
+ const missing =
57
+ this.required && !options.some((option) => optionChecked(option) && !optionDisabled(option))
58
+ const message = missing ? this.pktStrings.get('validation').checkboxGroupValueMissing : ''
59
+ options.forEach((option) => option.setGroupValidity?.(message))
60
+ }
61
+ }
62
+
63
+ try {
64
+ customElement('pkt-checkbox-group')(PktCheckboxGroup)
65
+ } catch (e) {
66
+ console.warn('Forsøker å definere <pkt-checkbox-group>, men den er allerede definert')
67
+ }
68
+
69
+ export default PktCheckboxGroup
@@ -0,0 +1,4 @@
1
+ import { PktCheckboxGroup } from './checkbox-group'
2
+
3
+ export { PktCheckboxGroup }
4
+ export default PktCheckboxGroup
@@ -313,7 +313,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
313
313
  this.inputRef.value.value = ''
314
314
  }
315
315
 
316
- this.internals.setFormValue('')
316
+ this.setFormValue('')
317
317
  this.internals.ariaInvalid = 'false'
318
318
  this.requestUpdate()
319
319
  }
@@ -612,7 +612,7 @@ export class PktDatepicker extends PktInputElement<Props> {
612
612
  public clearInputValue() {
613
613
  this._value = []
614
614
  this.value = ''
615
- this.internals.setFormValue(this.value)
615
+ this.setFormValue(this.value)
616
616
  this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))
617
617
  this.dispatchEvent(
618
618
  new CustomEvent('value-change', {
@@ -9,6 +9,7 @@ export { PktCard } from '@/components/card'
9
9
  export { PktCombobox } from './combobox'
10
10
  export { PktConsent } from './consent'
11
11
  export { PktCheckbox } from '@/components/checkbox'
12
+ export { PktCheckboxGroup } from '@/components/checkbox-group'
12
13
  export { PktFileUpload } from '@/components/fileupload'
13
14
  export { PktDateTags } from '@/components/datepicker/date-tags.js'
14
15
  export { PktDatepicker } from '@/components/datepicker/datepicker.js'
@@ -31,6 +32,7 @@ export { PktMessagebox } from '@/components/messagebox'
31
32
  export { PktModal } from '@/components/modal'
32
33
  export { PktProgressbar } from '@/components/progressbar'
33
34
  export { PktRadioButton } from '@/components/radiobutton'
35
+ export { PktRadioGroup } from '@/components/radio-group'
34
36
  // TODO: Avklar om RadioButton kan eksporteres som *kun* PktRadiobutton
35
37
  export { PktRadioButton as PktRadiobutton } from '@/components/radiobutton'
36
38
  export { PktTag } from '@/components/tag'
@@ -232,7 +232,11 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
232
232
 
233
233
  const wrapperElement = () => {
234
234
  return this.hasFieldset
235
- ? html`<fieldset class="pkt-inputwrapper__fieldset" aria-describedby="${describedBy}">
235
+ ? html`<fieldset
236
+ class="pkt-inputwrapper__fieldset"
237
+ aria-describedby="${describedBy}"
238
+ ?disabled=${this.disabled}
239
+ >
236
240
  ${inputContent()}
237
241
  </fieldset>`
238
242
  : html`<div class="pkt-inputwrapper__fieldset">${inputContent()}</div>`
@@ -36,6 +36,14 @@ const COMPONENTS = {
36
36
  elements: 'packages/elements/src/components/fileupload/fileupload.ts',
37
37
  react: 'packages/react/src/components/fileupload/FileUpload.tsx',
38
38
  },
39
+ radioGroup: {
40
+ elements: 'packages/elements/src/base-elements/option-group-element.ts',
41
+ react: 'packages/react/src/components/radiogroup/RadioGroup.tsx',
42
+ },
43
+ checkboxGroup: {
44
+ elements: 'packages/elements/src/base-elements/option-group-element.ts',
45
+ react: 'packages/react/src/components/checkboxgroup/CheckboxGroup.tsx',
46
+ },
39
47
  } as const
40
48
 
41
49
  // Props som med vilje ikke videresendes, med begrunnelse.
@@ -44,16 +52,22 @@ const OMITTED: Record<string, Partial<Record<keyof typeof COMPONENTS, string>>>
44
52
  select: 'ingenting å telle i en select',
45
53
  timepicker: 'ingenting å telle',
46
54
  fileupload: 'ingenting å telle',
55
+ radioGroup: 'ingenting å telle',
56
+ checkboxGroup: 'ingenting å telle',
47
57
  },
48
58
  counterCurrent: {
49
59
  select: 'se counter',
50
60
  timepicker: 'se counter',
51
61
  fileupload: 'se counter',
62
+ radioGroup: 'se counter',
63
+ checkboxGroup: 'se counter',
52
64
  },
53
65
  counterMaxLength: {
54
66
  select: 'se counter',
55
67
  timepicker: 'se counter',
56
68
  fileupload: 'se counter',
69
+ radioGroup: 'se counter',
70
+ checkboxGroup: 'se counter',
57
71
  },
58
72
  counterPosition: {
59
73
  textinput: 'teller over feltet er aldri støttet',
@@ -63,6 +77,8 @@ const OMITTED: Record<string, Partial<Record<keyof typeof COMPONENTS, string>>>
63
77
  datepicker: 'teller over feltet er aldri støttet',
64
78
  timepicker: 'teller over feltet er aldri støttet',
65
79
  fileupload: 'teller over feltet er aldri støttet',
80
+ radioGroup: 'ingenting å telle',
81
+ checkboxGroup: 'ingenting å telle',
66
82
  },
67
83
  layout: {
68
84
  textinput: 'layout gjelder alternativer i en gruppe, ikke ett felt',
@@ -76,9 +92,18 @@ const OMITTED: Record<string, Partial<Record<keyof typeof COMPONENTS, string>>>
76
92
  hasFieldset: {
77
93
  fileupload: 'ikke støttet',
78
94
  },
79
- inline: { fileupload: 'ikke et enkelt felt på en linje' },
95
+ inline: {
96
+ fileupload: 'ikke et enkelt felt på en linje',
97
+ radioGroup: 'ikke et enkelt felt på en linje, retningen styres av layout',
98
+ checkboxGroup: 'ikke et enkelt felt på en linje, retningen styres av layout',
99
+ },
80
100
  useWrapper: { fileupload: 'wrapperen rendres bare når label er satt' },
81
- ariaDescribedby: { combobox: 'ikke implementert ennå', fileupload: 'ikke implementert ennå' },
101
+ ariaDescribedby: {
102
+ combobox: 'ikke implementert ennå',
103
+ fileupload: 'ikke implementert ennå',
104
+ radioGroup: 'fieldset beskrives av hjelpeteksten og feilmeldingen',
105
+ checkboxGroup: 'fieldset beskrives av hjelpeteksten og feilmeldingen',
106
+ },
82
107
  errorMessage: { fileupload: 'egen feilvisning' },
83
108
  size: { fileupload: 'har ikke størrelsesvarianter' },
84
109
  role: {},
@@ -0,0 +1,4 @@
1
+ import { PktRadioGroup } from './radio-group'
2
+
3
+ export { PktRadioGroup }
4
+ export default PktRadioGroup
@@ -0,0 +1,8 @@
1
+ import { createContext } from '@lit/context'
2
+ import type { IPktOptionGroupContext } from '@/base-elements/input-element'
3
+
4
+ export interface RadioGroupContext extends IPktOptionGroupContext {
5
+ required: boolean
6
+ }
7
+
8
+ export const radioGroupContext = createContext<RadioGroupContext>(Symbol('pkt-radio-group-context'))