@oslokommune/punkt-elements 19.7.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 (232) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/accordion-CMCCpxtz.cjs +47 -0
  3. package/dist/{accordion-BJh6X1Ek.js → accordion-Cp2o6wTY.js} +10 -9
  4. package/dist/{alert-C0PiYnot.js → alert-CAPiwi4F.js} +10 -5
  5. package/dist/alert-QfloIr-o.cjs +39 -0
  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-BfdvXLBz.cjs → combobox-ClXhKwR9.cjs} +1 -1
  29. package/dist/{combobox-CMpYhgln.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/messagebox/messagebox.d.ts +2 -6
  37. package/dist/components/radio-group/index.d.ts +3 -0
  38. package/dist/components/radio-group/radio-group-context.d.ts +7 -0
  39. package/dist/components/radio-group/radio-group.d.ts +14 -0
  40. package/dist/components/radio-group/radio-group.test.d.ts +0 -0
  41. package/dist/components/radiobutton/radiobutton.d.ts +1 -0
  42. package/dist/{consent-CleJMbVH.js → consent-CZMxUavj.js} +2 -2
  43. package/dist/{consent-Dl-R0iC3.cjs → consent-DimzcQxt.cjs} +1 -1
  44. package/dist/consume-Bp12si68.js +61 -0
  45. package/dist/consume-kEjgTrAp.cjs +1 -0
  46. package/dist/{datepicker-Bs7Z3EFn.js → datepicker-C7ABsx7H.js} +12 -12
  47. package/dist/{datepicker-CWHYYj-H.cjs → datepicker-usIF9WY7.cjs} +2 -2
  48. package/dist/{directive-helpers-BRrz3Bcj.cjs → directive-helpers-Bv70GjQh.cjs} +1 -1
  49. package/dist/{directive-helpers-D96YEsht.js → directive-helpers-CwxFepWh.js} +1 -1
  50. package/dist/directives/slot-content.d.ts +4 -3
  51. package/dist/docs/components/checkbox-group.d.ts +13 -0
  52. package/dist/docs/components/radio-group.d.ts +13 -0
  53. package/dist/{element-BOmr9YLv.cjs → element-C6WTyJFE.cjs} +1 -1
  54. package/dist/{element-mwwC2Rra.js → element-DBugHPKM.js} +1 -0
  55. package/dist/{element-with-slot-DQv0cbbD.js → element-with-slot-DDLS-aLw.js} +6 -3
  56. package/dist/element-with-slot-DhiAuSjM.cjs +1 -0
  57. package/dist/{fileupload-CMIFZWJ5.cjs → fileupload-2uTFjd81.cjs} +1 -1
  58. package/dist/{fileupload-BQFJx1ok.js → fileupload-BR9H6fET.js} +8 -8
  59. package/dist/{footer-DdLls3F-.js → footer-B3DMvo8-.js} +4 -4
  60. package/dist/{footer-vy9NqT65.cjs → footer-CyxRYvT-.cjs} +1 -1
  61. package/dist/{header-Cc1Cy0U3.js → header-BKeSkfB7.js} +11 -11
  62. package/dist/{header-C4Uojzoy.cjs → header-Drj3lTrv.cjs} +1 -1
  63. package/dist/{header-menu-BfkCp8jw.cjs → header-menu-B_5hCNUl.cjs} +1 -1
  64. package/dist/{header-menu-ANj5nL2M.js → header-menu-buYE90FU.js} +4 -4
  65. package/dist/{heading-zoz-QZJ9.cjs → heading-DUYOvlxs.cjs} +1 -1
  66. package/dist/{heading-CUrZvkrM.js → heading-WBB5MO9i.js} +2 -2
  67. package/dist/{helptext-BCkwSz1m.cjs → helptext-C7cyihnu.cjs} +1 -1
  68. package/dist/{helptext-DCT_9dw1.js → helptext-DadLkcoO.js} +4 -4
  69. package/dist/{icon-JigbxxF7.js → icon-Bao96Zb8.js} +1 -1
  70. package/dist/{icon-Dnv6LtPC.cjs → icon-jPhyZD1z.cjs} +1 -1
  71. package/dist/if-defined-CiML3MTM.cjs +1 -0
  72. package/dist/{if-defined-Bebu3LXY.js → if-defined-O59_I-e-.js} +1 -1
  73. package/dist/{input-element-lLzfwdim.js → input-element-BBygBEei.js} +54 -17
  74. package/dist/input-element-C4SelHLD.cjs +1 -0
  75. package/dist/{input-wrapper-J3mEKhvR.cjs → input-wrapper-BT6B3Kco.cjs} +6 -2
  76. package/dist/{input-wrapper-BoTugCvh.js → input-wrapper-DrSCuw6U.js} +12 -8
  77. package/dist/{link-LO_5_La8.cjs → link-CEP7w1et.cjs} +1 -1
  78. package/dist/{link-CmZwAw5u.js → link-CVnIn74-.js} +4 -4
  79. package/dist/{linkcard-UKsxADrP.cjs → linkcard-D8vitdrQ.cjs} +1 -1
  80. package/dist/{linkcard-CQluSyJJ.js → linkcard-IWiVvRKy.js} +4 -4
  81. package/dist/{listbox-Ct9jIV_5.js → listbox-BDesthOp.js} +4 -4
  82. package/dist/{listbox-g-vHHLkD.cjs → listbox-CvduK_YU.cjs} +1 -1
  83. package/dist/{loader-C_PH05aw.cjs → loader-B0hFoBtF.cjs} +1 -1
  84. package/dist/{loader-DzctJ9jW.js → loader-CAsUPHjl.js} +4 -4
  85. package/dist/{messagebox-DPFBuisE.js → messagebox-6_iNN5F3.js} +9 -49
  86. package/dist/messagebox-C19L2Duq.cjs +13 -0
  87. package/dist/{modal-BymjMIJi.cjs → modal-CoEPzt4p.cjs} +1 -1
  88. package/dist/{modal-CELmA0-8.js → modal-D7MolBXb.js} +5 -5
  89. package/dist/option-group-element-BIuotbRu.js +135 -0
  90. package/dist/option-group-element-BYS7s-is.cjs +23 -0
  91. package/dist/pkt-accordion.cjs +1 -1
  92. package/dist/pkt-accordion.js +1 -1
  93. package/dist/pkt-alert.cjs +1 -1
  94. package/dist/pkt-alert.js +1 -1
  95. package/dist/pkt-backlink.cjs +1 -1
  96. package/dist/pkt-backlink.js +1 -1
  97. package/dist/pkt-breadcrumbs.cjs +1 -1
  98. package/dist/pkt-breadcrumbs.js +1 -1
  99. package/dist/pkt-button.cjs +1 -1
  100. package/dist/pkt-button.js +1 -1
  101. package/dist/pkt-calendar.cjs +1 -1
  102. package/dist/pkt-calendar.js +1 -1
  103. package/dist/pkt-card.cjs +1 -1
  104. package/dist/pkt-card.js +1 -1
  105. package/dist/pkt-checkbox-group.cjs +1 -0
  106. package/dist/pkt-checkbox-group.js +2 -0
  107. package/dist/pkt-checkbox.cjs +1 -1
  108. package/dist/pkt-checkbox.js +1 -1
  109. package/dist/pkt-combobox.cjs +1 -1
  110. package/dist/pkt-combobox.js +1 -1
  111. package/dist/pkt-consent.cjs +1 -1
  112. package/dist/pkt-consent.js +1 -1
  113. package/dist/pkt-datepicker.cjs +1 -1
  114. package/dist/pkt-datepicker.js +1 -1
  115. package/dist/pkt-fileupload.cjs +1 -1
  116. package/dist/pkt-fileupload.js +1 -1
  117. package/dist/pkt-footer.cjs +1 -1
  118. package/dist/pkt-footer.js +1 -1
  119. package/dist/pkt-header-menu.cjs +1 -1
  120. package/dist/pkt-header-menu.js +1 -1
  121. package/dist/pkt-header.cjs +1 -1
  122. package/dist/pkt-header.js +1 -1
  123. package/dist/pkt-heading.cjs +1 -1
  124. package/dist/pkt-heading.js +1 -1
  125. package/dist/pkt-helptext.cjs +1 -1
  126. package/dist/pkt-helptext.js +1 -1
  127. package/dist/pkt-icon.cjs +1 -1
  128. package/dist/pkt-icon.js +1 -1
  129. package/dist/pkt-index.cjs +1 -1
  130. package/dist/pkt-index.js +37 -35
  131. package/dist/pkt-input-wrapper.cjs +1 -1
  132. package/dist/pkt-input-wrapper.js +1 -1
  133. package/dist/pkt-link.cjs +1 -1
  134. package/dist/pkt-link.js +1 -1
  135. package/dist/pkt-linkcard.cjs +1 -1
  136. package/dist/pkt-linkcard.js +1 -1
  137. package/dist/pkt-listbox.cjs +1 -1
  138. package/dist/pkt-listbox.js +1 -1
  139. package/dist/pkt-loader.cjs +1 -1
  140. package/dist/pkt-loader.js +1 -1
  141. package/dist/pkt-messagebox.cjs +1 -1
  142. package/dist/pkt-messagebox.js +1 -1
  143. package/dist/pkt-modal.cjs +1 -1
  144. package/dist/pkt-modal.js +1 -1
  145. package/dist/{pkt-options-controller-CpEZcomb.cjs → pkt-options-controller-B3vVKnAG.cjs} +1 -1
  146. package/dist/{pkt-options-controller-D7l18Iw0.js → pkt-options-controller-ClFrUWZK.js} +1 -1
  147. package/dist/pkt-progressbar.cjs +1 -1
  148. package/dist/pkt-progressbar.js +1 -1
  149. package/dist/pkt-radio-group.cjs +1 -0
  150. package/dist/pkt-radio-group.js +2 -0
  151. package/dist/pkt-radiobutton.cjs +1 -1
  152. package/dist/pkt-radiobutton.js +1 -1
  153. package/dist/pkt-searchinput.cjs +1 -1
  154. package/dist/pkt-searchinput.js +1 -1
  155. package/dist/pkt-select.cjs +1 -1
  156. package/dist/pkt-select.js +1 -1
  157. package/dist/pkt-tabs.cjs +1 -1
  158. package/dist/pkt-tabs.js +1 -1
  159. package/dist/pkt-tag.cjs +1 -1
  160. package/dist/pkt-tag.js +1 -1
  161. package/dist/pkt-textarea.cjs +1 -1
  162. package/dist/pkt-textarea.js +1 -1
  163. package/dist/pkt-textinput.cjs +1 -1
  164. package/dist/pkt-textinput.js +1 -1
  165. package/dist/pkt-timepicker.cjs +1 -1
  166. package/dist/pkt-timepicker.js +1 -1
  167. package/dist/{progressbar-CIXNSwTy.cjs → progressbar-CnX4fFfR.cjs} +1 -1
  168. package/dist/{progressbar-DH5lIBpx.js → progressbar-DZHiq7B0.js} +5 -5
  169. package/dist/provide-Cz18sYug.cjs +1 -0
  170. package/dist/provide-_MnlMGnw.js +111 -0
  171. package/dist/radio-group-kOzlGn8T.cjs +1 -0
  172. package/dist/radio-group-lhymYEZX.js +52 -0
  173. package/dist/radio-group.d.ts +1 -0
  174. package/dist/radiobutton-BgHCcDJv.cjs +38 -0
  175. package/dist/{radiobutton-l_3oGDbb.js → radiobutton-CFd8TgV9.js} +39 -35
  176. package/dist/{ref-Emu5PNxk.cjs → ref-CB7e9nMY.cjs} +1 -1
  177. package/dist/{ref-CCSGk4MP.js → ref-y_5qv9vU.js} +2 -2
  178. package/dist/{repeat-CdxGS-_3.js → repeat-CV9S4zya.js} +2 -2
  179. package/dist/{repeat-Dh9YKoeV.cjs → repeat-CnnigVcj.cjs} +1 -1
  180. package/dist/{searchinput-Co8tcq-l.js → searchinput-Bkcr3Fby.js} +4 -4
  181. package/dist/{searchinput-CbClDE-d.cjs → searchinput-Mdc9Ihld.cjs} +1 -1
  182. package/dist/{select-DYjKMmHa.js → select-B6CN2Ymq.js} +7 -7
  183. package/dist/{select-CZYQ5Dmx.cjs → select-BowvpCNa.cjs} +1 -1
  184. package/dist/shared-strings/nb.d.ts +1 -0
  185. package/dist/shared-types/index.d.ts +7 -3
  186. package/dist/shared-types/messagebox.d.ts +32 -0
  187. package/dist/shared-types/option-group.d.ts +50 -0
  188. package/dist/shared-types/skin.d.ts +1 -4
  189. package/dist/{tabs-B8blHQrL.js → tabs-DZvtVA0y.js} +9 -8
  190. package/dist/tabs-Dm8xcPRs.cjs +40 -0
  191. package/dist/{tag-Bns7Xj1O.js → tag-C33BPIOm.js} +5 -5
  192. package/dist/{tag-B5dUg7AD.cjs → tag-DyVZrqOQ.cjs} +1 -1
  193. package/dist/tests/component-registry.d.ts +4 -0
  194. package/dist/tests/option-contexts.d.ts +7 -0
  195. package/dist/{textarea-H1Lt2o6K.cjs → textarea-DoH3Il8v.cjs} +1 -1
  196. package/dist/{textarea-C2oaRlfb.js → textarea-hW0kV5gj.js} +9 -9
  197. package/dist/{textinput-C0eu70sH.js → textinput-B6G4dVuL.js} +8 -8
  198. package/dist/{textinput-C2Tx-OrN.cjs → textinput-CQknclmV.cjs} +1 -1
  199. package/dist/{timepicker-B8TwchA2.js → timepicker-Cd2QI6ua.js} +8 -8
  200. package/dist/{timepicker-DZG_gHIa.cjs → timepicker-DB3gor6g.cjs} +1 -1
  201. package/package.json +2 -2
  202. package/src/components/alert/alert.ts +1 -1
  203. package/src/components/checkbox/checkbox.test.ts +99 -0
  204. package/src/components/checkbox/checkbox.ts +14 -5
  205. package/src/components/checkbox-group/checkbox-group-context.ts +8 -0
  206. package/src/components/checkbox-group/checkbox-group.test.ts +406 -0
  207. package/src/components/checkbox-group/checkbox-group.ts +69 -0
  208. package/src/components/checkbox-group/index.ts +4 -0
  209. package/src/components/combobox/combobox.ts +1 -1
  210. package/src/components/datepicker/datepicker.ts +1 -1
  211. package/src/components/index.ts +2 -0
  212. package/src/components/input-wrapper/input-wrapper.ts +5 -1
  213. package/src/components/input-wrapper/wrapper-parity.test.ts +27 -2
  214. package/src/components/messagebox/messagebox.test.ts +1 -1
  215. package/src/components/messagebox/messagebox.ts +6 -12
  216. package/src/components/radio-group/index.ts +4 -0
  217. package/src/components/radio-group/radio-group-context.ts +8 -0
  218. package/src/components/radio-group/radio-group.test.ts +530 -0
  219. package/src/components/radio-group/radio-group.ts +65 -0
  220. package/src/components/radiobutton/radiobutton.test.ts +95 -0
  221. package/src/components/radiobutton/radiobutton.ts +11 -5
  222. package/dist/accordion-5lSFQToJ.cjs +0 -47
  223. package/dist/alert-IaZzEgyI.cjs +0 -39
  224. package/dist/checkbox-D69leWDh.cjs +0 -40
  225. package/dist/consume-BDnMAp2g.js +0 -167
  226. package/dist/consume-Le4HztPo.cjs +0 -1
  227. package/dist/element-with-slot-D7sTqOr3.cjs +0 -1
  228. package/dist/if-defined-2mkIDgOm.cjs +0 -1
  229. package/dist/input-element-CSfDGslx.cjs +0 -1
  230. package/dist/messagebox-JWKMJEFc.cjs +0 -13
  231. package/dist/radiobutton-BPr22VmL.cjs +0 -38
  232. package/dist/tabs-CjX-GWvM.cjs +0 -40
@@ -1,6 +1,11 @@
1
- import { customElement, property } from 'lit/decorators.js'
1
+ import { consume } from '@lit/context'
2
+ import { customElement, property, state } from 'lit/decorators.js'
2
3
 
3
4
  import { PktInputElement } from '@/base-elements/input-element'
5
+ import {
6
+ checkboxGroupContext,
7
+ type CheckboxGroupContext,
8
+ } from '@/components/checkbox-group/checkbox-group-context'
4
9
  import { Ref, createRef, ref } from 'lit/directives/ref.js'
5
10
  import { html, nothing, PropertyValues } from 'lit'
6
11
  import { classMap } from 'lit/directives/class-map.js'
@@ -46,6 +51,10 @@ export class PktCheckbox extends PktInputElement<Props> {
46
51
  /** @deprecated Bruk `strings={{ forms: { required: '…' } }}` eller `setPktStrings()`. */
47
52
  @property({ type: String }) requiredText?: string
48
53
 
54
+ @consume({ context: checkboxGroupContext, subscribe: true })
55
+ @state()
56
+ private groupContext?: CheckboxGroupContext
57
+
49
58
  connectedCallback() {
50
59
  super.connectedCallback()
51
60
  }
@@ -91,15 +100,16 @@ export class PktCheckbox extends PktInputElement<Props> {
91
100
  }
92
101
 
93
102
  render() {
103
+ const hasTile = this.hasTile || !!this.groupContext?.hasTile
94
104
  const inputTileClasses = classMap({
95
105
  'pkt-input-check__input': true,
96
- 'pkt-input-check__input--tile': this.hasTile,
97
- 'pkt-input-check__input--tile-disabled': this.isDisabled && this.hasTile,
106
+ 'pkt-input-check__input--tile': hasTile,
107
+ 'pkt-input-check__input--tile-disabled': this.isDisabled && hasTile,
98
108
  })
99
109
 
100
110
  const inputCheckBoxClasses = classMap({
101
111
  'pkt-input-check__input-checkbox': true,
102
- 'pkt-input-check__input-checkbox--error': this.hasError,
112
+ 'pkt-input-check__input-checkbox--error': this.hasError || !!this.groupContext?.hasError,
103
113
  })
104
114
 
105
115
  const labelClasses = classMap({
@@ -185,7 +195,6 @@ export class PktCheckbox extends PktInputElement<Props> {
185
195
 
186
196
  private handleInput(e: Event) {
187
197
  e.stopImmediatePropagation()
188
- this.onInput()
189
198
  }
190
199
 
191
200
  private handleClick(e: Event) {
@@ -0,0 +1,8 @@
1
+ import { createContext } from '@lit/context'
2
+ import type { IPktOptionGroupContext } from '@/base-elements/input-element'
3
+
4
+ export type CheckboxGroupContext = IPktOptionGroupContext
5
+
6
+ export const checkboxGroupContext = createContext<CheckboxGroupContext>(
7
+ Symbol('pkt-checkbox-group-context'),
8
+ )
@@ -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: {},
@@ -72,7 +72,7 @@ describe('PktMessagebox', () => {
72
72
  expect(messagebox.closable).toBe(false)
73
73
  expect(messagebox.compact).toBe(false)
74
74
  expect(messagebox.title).toBe('')
75
- expect(messagebox.skin).toBeUndefined()
75
+ expect(messagebox.skin).toBe('beige')
76
76
  })
77
77
 
78
78
  test('sets properties correctly', async () => {