@oslokommune/punkt-elements 16.17.3 → 16.19.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 (195) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{accordion-CU2tJ5vn.js → accordion-BjYsEbSM.js} +19 -19
  3. package/dist/{accordion-DDZkjXFb.cjs → accordion-DaeWPWod.cjs} +5 -5
  4. package/dist/{alert-ePJuDtmm.js → alert-BkzC5mVu.js} +22 -22
  5. package/dist/{alert-BEEnTiCb.cjs → alert-D_45dWPn.cjs} +6 -6
  6. package/dist/{async-directive-CaQiqjyV.js → async-directive-Cz6tcqge.js} +2 -2
  7. package/dist/async-directive-DO1QtVmf.cjs +1 -0
  8. package/dist/{backlink-CiwXrQYD.js → backlink-BXAdgAte.js} +6 -6
  9. package/dist/{backlink-Ba03LlbB.cjs → backlink-C42-3OG3.cjs} +2 -2
  10. package/dist/{breadcrumbs-CD1zib3s.cjs → breadcrumbs-CkjeLh3U.cjs} +6 -6
  11. package/dist/{breadcrumbs-CpuVqWhF.js → breadcrumbs-Dkb9TxMp.js} +15 -15
  12. package/dist/{button-BIpd1xJh.js → button-CR-KWhTT.js} +21 -21
  13. package/dist/{button-D-TOBaJB.cjs → button-Dwv11n6R.cjs} +7 -7
  14. package/dist/{calendar-DkjXla0I.cjs → calendar-Buvxe4l_.cjs} +11 -11
  15. package/dist/{calendar-Ch1dgnBo.js → calendar-DAq6iJdx.js} +50 -50
  16. package/dist/{card-CEDbo1t7.js → card-5PydU247.js} +43 -43
  17. package/dist/{card-C3POnP3O.cjs → card-C__IlsUa.cjs} +26 -26
  18. package/dist/{checkbox-CF5mSBR5.js → checkbox-CC1X7fw4.js} +23 -23
  19. package/dist/{checkbox-BerAYcfK.cjs → checkbox-DzMc0AJX.cjs} +10 -10
  20. package/dist/class-map-B8Ja0q7n.cjs +1 -0
  21. package/dist/{class-map-C8HuhNzZ.js → class-map-DH9hlJny.js} +7 -8
  22. package/dist/{combobox-DjvRMKOD.js → combobox-D-DY7YcW.js} +42 -42
  23. package/dist/{combobox-DYttdLJ4.cjs → combobox-x4tBGP3F.cjs} +17 -17
  24. package/dist/components/heading/heading.d.ts +5 -8
  25. package/dist/components/icon/icon.d.ts +6 -1
  26. package/dist/components/icon/icon.test.d.ts +2 -0
  27. package/dist/{consent-B5dIDyLD.cjs → consent-BB3g_ByO.cjs} +3 -3
  28. package/dist/{consent-DSHrt9eS.js → consent-Y14WZ0JS.js} +8 -8
  29. package/dist/{datepicker-D3QQbd_H.cjs → datepicker-BFI-3stE.cjs} +20 -20
  30. package/dist/{datepicker-M_jcY3wp.js → datepicker-CRk7aiwH.js} +57 -57
  31. package/dist/{directive-helpers-Dkv2gKgs.js → directive-helpers-Cep_4cpj.js} +1 -1
  32. package/dist/{directive-helpers-D1QSxY17.cjs → directive-helpers-f36YZFOG.cjs} +1 -1
  33. package/dist/element-CXAtr6Gu.cjs +2 -0
  34. package/dist/{element-cZ85T_aa.js → element-CduG0XyF.js} +47 -23
  35. package/dist/{element-with-slot-4J2o3SeU.js → element-with-slot-BVj9MXzi.js} +5 -6
  36. package/dist/element-with-slot-Cmxb9h4H.cjs +1 -0
  37. package/dist/{fileupload-IXSGrqiH.js → fileupload-C8muj8qd.js} +149 -149
  38. package/dist/{fileupload-qc0fHcR-.cjs → fileupload-YtsrgQTY.cjs} +40 -40
  39. package/dist/{header-C1hDYMRX.cjs → header-CjBip0zj.cjs} +57 -57
  40. package/dist/{header-BGknkrEt.js → header-DONTGbD9.js} +130 -130
  41. package/dist/{header-menu-zhGYdnvz.js → header-menu-BU-ylGrJ.js} +45 -45
  42. package/dist/{header-menu-CPI1_5oZ.cjs → header-menu-CnW7nzmF.cjs} +24 -24
  43. package/dist/heading-3o4Dk0Wn.js +80 -0
  44. package/dist/heading-C40-NX0r.cjs +1 -0
  45. package/dist/{helptext-BdqoW19r.js → helptext-C2aQBBo_.js} +12 -12
  46. package/dist/{helptext-KWXjKljV.cjs → helptext-DNLg43eI.cjs} +4 -4
  47. package/dist/icon-Dw6I0HDV.cjs +1 -0
  48. package/dist/{icon-Co72KtgF.js → icon-LaKyBKON.js} +39 -19
  49. package/dist/if-defined-LTjAScFG.cjs +1 -0
  50. package/dist/{if-defined-Cjj8qN-Z.js → if-defined-VShASPdu.js} +1 -1
  51. package/dist/{input-element-D0egtEnr.cjs → input-element-BHQVt4Q9.cjs} +1 -1
  52. package/dist/{input-element-B1ciW1N2.js → input-element-BrmkXIXi.js} +30 -30
  53. package/dist/{input-wrapper-DVU68NcJ.js → input-wrapper-B6WowlWG.js} +33 -33
  54. package/dist/{input-wrapper-DOIWggEv.cjs → input-wrapper-Dhkt20WW.cjs} +17 -17
  55. package/dist/{link-DMKjKawh.cjs → link-B5ur0Rym.cjs} +4 -4
  56. package/dist/{link-F51Ho7CV.js → link-ClUNY__5.js} +13 -13
  57. package/dist/linkcard-BIcCW-qw.cjs +13 -0
  58. package/dist/{linkcard-E07qJZMP.js → linkcard-DNJm5FvA.js} +15 -15
  59. package/dist/{listbox-0NHJvNjz.js → listbox-DXMIBv3y.js} +35 -35
  60. package/dist/{listbox-CcThiNBY.cjs → listbox-DbEobJNx.cjs} +14 -14
  61. package/dist/{loader-q_eO6zaE.cjs → loader-Uap9qDsV.cjs} +5 -5
  62. package/dist/{loader-teN65oWo.js → loader-gKdQpOOQ.js} +12 -12
  63. package/dist/{messagebox-D1QeQqb8.cjs → messagebox-BqBxXK8c.cjs} +5 -5
  64. package/dist/{messagebox-Ct9lfe0Z.js → messagebox-XRFecqyS.js} +15 -15
  65. package/dist/{modal-Ca-6xVkg.js → modal-CWgeT-RP.js} +19 -19
  66. package/dist/{modal-B7-0F5q7.cjs → modal-CrYHKOmr.cjs} +8 -8
  67. package/dist/pkt-accordion.cjs +1 -1
  68. package/dist/pkt-accordion.js +1 -1
  69. package/dist/pkt-alert.cjs +1 -1
  70. package/dist/pkt-alert.js +1 -1
  71. package/dist/pkt-backlink.cjs +1 -1
  72. package/dist/pkt-backlink.js +1 -1
  73. package/dist/pkt-breadcrumbs.cjs +1 -1
  74. package/dist/pkt-breadcrumbs.js +1 -1
  75. package/dist/pkt-button.cjs +1 -1
  76. package/dist/pkt-button.js +1 -1
  77. package/dist/pkt-calendar.cjs +1 -1
  78. package/dist/pkt-calendar.js +1 -1
  79. package/dist/pkt-card.cjs +1 -1
  80. package/dist/pkt-card.js +1 -1
  81. package/dist/pkt-checkbox.cjs +1 -1
  82. package/dist/pkt-checkbox.js +1 -1
  83. package/dist/pkt-combobox.cjs +1 -1
  84. package/dist/pkt-combobox.js +1 -1
  85. package/dist/pkt-consent.cjs +1 -1
  86. package/dist/pkt-consent.js +1 -1
  87. package/dist/pkt-datepicker.cjs +1 -1
  88. package/dist/pkt-datepicker.js +1 -1
  89. package/dist/pkt-fileupload.cjs +1 -1
  90. package/dist/pkt-fileupload.js +1 -1
  91. package/dist/pkt-header-menu.cjs +1 -1
  92. package/dist/pkt-header-menu.js +1 -1
  93. package/dist/pkt-header.cjs +1 -1
  94. package/dist/pkt-header.js +1 -1
  95. package/dist/pkt-heading.cjs +1 -1
  96. package/dist/pkt-heading.js +1 -1
  97. package/dist/pkt-helptext.cjs +1 -1
  98. package/dist/pkt-helptext.js +1 -1
  99. package/dist/pkt-icon.cjs +1 -1
  100. package/dist/pkt-icon.js +1 -1
  101. package/dist/pkt-index.cjs +1 -1
  102. package/dist/pkt-index.js +32 -32
  103. package/dist/pkt-input-wrapper.cjs +1 -1
  104. package/dist/pkt-input-wrapper.js +1 -1
  105. package/dist/pkt-link.cjs +1 -1
  106. package/dist/pkt-link.js +1 -1
  107. package/dist/pkt-linkcard.cjs +1 -1
  108. package/dist/pkt-linkcard.js +1 -1
  109. package/dist/pkt-listbox.cjs +1 -1
  110. package/dist/pkt-listbox.js +1 -1
  111. package/dist/pkt-loader.cjs +1 -1
  112. package/dist/pkt-loader.js +1 -1
  113. package/dist/pkt-messagebox.cjs +1 -1
  114. package/dist/pkt-messagebox.js +1 -1
  115. package/dist/pkt-modal.cjs +1 -1
  116. package/dist/pkt-modal.js +1 -1
  117. package/dist/{pkt-options-controller-CCB15L_G.js → pkt-options-controller-BTNtjEIS.js} +1 -1
  118. package/dist/{pkt-options-controller-CeAQ-D-i.cjs → pkt-options-controller-CBM8xHwR.cjs} +1 -1
  119. package/dist/pkt-progressbar.cjs +1 -1
  120. package/dist/pkt-progressbar.js +1 -1
  121. package/dist/pkt-radiobutton.cjs +1 -1
  122. package/dist/pkt-radiobutton.js +1 -1
  123. package/dist/pkt-searchinput.cjs +1 -1
  124. package/dist/pkt-searchinput.js +1 -1
  125. package/dist/pkt-select.cjs +1 -1
  126. package/dist/pkt-select.js +1 -1
  127. package/dist/pkt-tabs.cjs +1 -1
  128. package/dist/pkt-tabs.js +1 -1
  129. package/dist/pkt-tag.cjs +1 -1
  130. package/dist/pkt-tag.js +1 -1
  131. package/dist/pkt-textarea.cjs +1 -1
  132. package/dist/pkt-textarea.js +1 -1
  133. package/dist/pkt-textinput.cjs +1 -1
  134. package/dist/pkt-textinput.js +1 -1
  135. package/dist/pkt-timepicker.cjs +1 -1
  136. package/dist/pkt-timepicker.js +1 -1
  137. package/dist/{progressbar-o-M17HPA.js → progressbar-B0P_vmGD.js} +23 -24
  138. package/dist/progressbar-DADdvDl5.cjs +17 -0
  139. package/dist/{radiobutton-B1Xg6XzP.cjs → radiobutton-BfURufrZ.cjs} +8 -8
  140. package/dist/{radiobutton-BAn1V_mQ.js → radiobutton-C6zERHoy.js} +20 -20
  141. package/dist/{ref-RS8Uv6uC.js → ref-BgoHEsEt.js} +7 -8
  142. package/dist/ref-CBdWMlde.cjs +1 -0
  143. package/dist/{repeat-DV1O43YU.js → repeat-CKIii8Mi.js} +15 -16
  144. package/dist/repeat-CRRgmzVn.cjs +1 -0
  145. package/dist/{searchinput-BVIaoetB.cjs → searchinput-BrxfRzez.cjs} +13 -13
  146. package/dist/{searchinput-CIseS5tL.js → searchinput-fPvXHu2O.js} +31 -31
  147. package/dist/{select-6XCVj1lP.js → select-BIgFPp42.js} +13 -13
  148. package/dist/{select-DO0xGhQQ.cjs → select-DzZHv6Bz.cjs} +3 -3
  149. package/dist/shared-types/index.d.ts +1 -1
  150. package/dist/shared-types/skin.d.ts +4 -0
  151. package/dist/{tabs-ClI1XQDt.js → tabs-BJx6wwXD.js} +29 -29
  152. package/dist/{tabs-CN_RIMWg.cjs → tabs-DxQy8U-0.cjs} +7 -7
  153. package/dist/{tag-DWtajZk6.cjs → tag-CBFz0R9N.cjs} +5 -5
  154. package/dist/{tag-hl59gziI.js → tag-CzlnHhho.js} +19 -19
  155. package/dist/textarea-M_S9WK7s.cjs +46 -0
  156. package/dist/{textarea-DBSNSvrX.js → textarea-qHtkhelz.js} +24 -25
  157. package/dist/{textinput-BZZnFDk3.js → textinput-CZrBM-Fj.js} +26 -26
  158. package/dist/{textinput-Dsc-8-BB.cjs → textinput-C_6kvf87.cjs} +12 -12
  159. package/dist/{timepicker-DDQ7CfOy.js → timepicker-O0iCeJyW.js} +51 -51
  160. package/dist/{timepicker-C2Ua6vHY.cjs → timepicker-WiYLAncb.cjs} +20 -20
  161. package/package.json +2 -2
  162. package/src/components/card/card.ts +1 -1
  163. package/src/components/heading/heading.test.ts +79 -70
  164. package/src/components/heading/heading.ts +40 -78
  165. package/src/components/icon/icon.test.ts +101 -0
  166. package/src/components/icon/icon.ts +40 -1
  167. package/dist/async-directive-B9P1Qa0k.cjs +0 -1
  168. package/dist/class-map-CJ-msbHs.cjs +0 -1
  169. package/dist/directive-3THFsVew.js +0 -28
  170. package/dist/directive-CJ2giQBZ.cjs +0 -1
  171. package/dist/element-DjtxO-1r.cjs +0 -2
  172. package/dist/element-with-slot-CWoTGA1B.cjs +0 -1
  173. package/dist/heading-BxvE80bb.js +0 -73
  174. package/dist/heading-DRJTS27k.cjs +0 -1
  175. package/dist/icon-Dr8sfT2X.cjs +0 -1
  176. package/dist/if-defined-XKOD_t_V.cjs +0 -1
  177. package/dist/linkcard-CBmJbSoV.cjs +0 -13
  178. package/dist/progressbar-Ca7pRjmt.cjs +0 -17
  179. package/dist/ref-BaJ0mBT_.cjs +0 -1
  180. package/dist/repeat-DaDFvx86.cjs +0 -1
  181. package/dist/textarea-CV6z3LDf.cjs +0 -46
  182. /package/dist/{booleanish-CRaKZ75t.js → booleanish-BmcOU-43.js} +0 -0
  183. /package/dist/{booleanish-B5EFqFpJ.cjs → booleanish-jJLlnr6-.cjs} +0 -0
  184. /package/dist/{classutils-z8YsPlOf.cjs → classutils-CVTzKF0J.cjs} +0 -0
  185. /package/dist/{classutils-DEL164Ur.js → classutils-lVbGcvjN.js} +0 -0
  186. /package/dist/{converters-Bffq_YDm.cjs → converters-C_66kHUN.cjs} +0 -0
  187. /package/dist/{converters-jauYkvNh.js → converters-DSQXaDQ_.js} +0 -0
  188. /package/dist/{dialog-polyfill.esm-B7W3teD0.js → dialog-polyfill.esm-0dVT8G1o.js} +0 -0
  189. /package/dist/{dialog-polyfill.esm-BnAQvMQO.cjs → dialog-polyfill.esm-CYs2AHP1.cjs} +0 -0
  190. /package/dist/{focus-modality-controller-pzSZkDkH.js → focus-modality-controller-CaUqYdk0.js} +0 -0
  191. /package/dist/{focus-modality-controller-BSZAQk8y.cjs → focus-modality-controller-wI5SR8wH.cjs} +0 -0
  192. /package/dist/{input-wrapper-CQGoyLdZ.cjs → input-wrapper-CYCCx8MP.cjs} +0 -0
  193. /package/dist/{input-wrapper-DwSsP39J.js → input-wrapper-kw2y6Jp_.js} +0 -0
  194. /package/dist/{utils-BOOZ0ppt.js → utils-CUqCKGvp.js} +0 -0
  195. /package/dist/{utils-CM67Oudc.cjs → utils-CjAbIttF.cjs} +0 -0
@@ -22,6 +22,9 @@ const createHeading = async (headingProps = '', content = 'Test Heading') => {
22
22
  return container
23
23
  }
24
24
 
25
+ const innerHeading = (host: PktHeading): HTMLHeadingElement | null =>
26
+ host.querySelector('h1, h2, h3, h4, h5, h6')
27
+
25
28
  // Cleanup after each test
26
29
  afterEach(() => {
27
30
  document.body.innerHTML = ''
@@ -34,7 +37,7 @@ describe('PktHeading', () => {
34
37
  const heading = container.querySelector('pkt-heading') as PktHeading
35
38
 
36
39
  expect(heading).toBeInTheDocument()
37
- expect(heading.shadowRoot).toBeTruthy()
40
+ expect(heading.shadowRoot).toBeNull()
38
41
  })
39
42
 
40
43
  test('renders with default properties', async () => {
@@ -47,19 +50,33 @@ describe('PktHeading', () => {
47
50
  expect(heading.align).toBe(undefined)
48
51
  })
49
52
 
50
- test('renders content in shadow DOM slot', async () => {
53
+ test('renders content inside a semantic heading element', async () => {
51
54
  const container = await createHeading('', 'Custom Heading Text')
52
55
  const heading = container.querySelector('pkt-heading') as PktHeading
53
56
  await heading.updateComplete
54
57
 
55
- const slot = heading.shadowRoot?.querySelector('slot')
56
- expect(slot).toBeInTheDocument()
57
- expect(heading.textContent).toContain('Custom Heading Text')
58
+ const inner = innerHeading(heading)
59
+ expect(inner).toBeInTheDocument()
60
+ expect(inner?.tagName).toBe('H2')
61
+ expect(inner?.textContent).toContain('Custom Heading Text')
62
+ })
63
+
64
+ test('uses the matching h-tag for the given level', async () => {
65
+ const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
66
+
67
+ for (const level of levels) {
68
+ const container = await createHeading(`level="${level}"`)
69
+ const heading = container.querySelector('pkt-heading') as PktHeading
70
+ await heading.updateComplete
71
+
72
+ expect(innerHeading(heading)?.tagName).toBe(`H${level}`)
73
+ document.body.innerHTML = ''
74
+ }
58
75
  })
59
76
  })
60
77
 
61
78
  describe('Properties and attributes', () => {
62
- test('applies default properties correctly', async () => {
79
+ test('applies default reflected attributes correctly', async () => {
63
80
  const container = await createHeading()
64
81
  const heading = container.querySelector('pkt-heading') as PktHeading
65
82
  await heading.updateComplete
@@ -104,7 +121,7 @@ describe('PktHeading', () => {
104
121
  await heading.updateComplete
105
122
 
106
123
  expect(heading.visuallyHidden).toBe(true)
107
- expect(heading.hasAttribute('visuallyHidden')).toBe(true)
124
+ expect(heading.hasAttribute('visuallyhidden')).toBe(true)
108
125
  })
109
126
 
110
127
  test('sets align property correctly', async () => {
@@ -123,17 +140,16 @@ describe('PktHeading', () => {
123
140
  })
124
141
 
125
142
  describe('CSS classes and styling', () => {
126
- test('applies default CSS classes', async () => {
143
+ test('applies pkt-heading class to the inner heading element, not the host', async () => {
127
144
  const container = await createHeading()
128
145
  const heading = container.querySelector('pkt-heading') as PktHeading
129
146
  await heading.updateComplete
130
147
 
131
- expect(heading.classList.contains('pkt-heading')).toBe(true)
132
- expect(heading.classList.contains('pkt-heading--medium')).toBe(false)
133
- expect(heading.classList.contains('pkt-heading--start')).toBe(false)
148
+ expect(heading.classList.contains('pkt-heading')).toBe(false)
149
+ expect(innerHeading(heading)?.classList.contains('pkt-heading')).toBe(true)
134
150
  })
135
151
 
136
- test('applies size-specific CSS classes', async () => {
152
+ test('applies size-specific class to the inner heading', async () => {
137
153
  const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']
138
154
 
139
155
  for (const size of sizes) {
@@ -141,20 +157,20 @@ describe('PktHeading', () => {
141
157
  const heading = container.querySelector('pkt-heading') as PktHeading
142
158
  await heading.updateComplete
143
159
 
144
- expect(heading.classList.contains(`pkt-heading--${size}`)).toBe(true)
160
+ expect(innerHeading(heading)?.classList.contains(`pkt-heading--${size}`)).toBe(true)
145
161
  document.body.innerHTML = ''
146
162
  }
147
163
  })
148
164
 
149
- test('applies visually hidden class when enabled', async () => {
165
+ test('applies visually hidden class to the inner heading when enabled', async () => {
150
166
  const container = await createHeading('visuallyHidden="true"')
151
167
  const heading = container.querySelector('pkt-heading') as PktHeading
152
168
  await heading.updateComplete
153
169
 
154
- expect(heading.classList.contains('pkt-sr-only')).toBe(true)
170
+ expect(innerHeading(heading)?.classList.contains('pkt-sr-only')).toBe(true)
155
171
  })
156
172
 
157
- test('applies alignment-specific CSS classes', async () => {
173
+ test('applies alignment-specific class to the inner heading', async () => {
158
174
  const alignments = ['start', 'center', 'end'] as const
159
175
 
160
176
  for (const align of alignments) {
@@ -162,7 +178,7 @@ describe('PktHeading', () => {
162
178
  const heading = container.querySelector('pkt-heading') as PktHeading
163
179
  await heading.updateComplete
164
180
 
165
- expect(heading.classList.contains(`pkt-heading--${align}`)).toBe(true)
181
+ expect(innerHeading(heading)?.classList.contains(`pkt-heading--${align}`)).toBe(true)
166
182
  document.body.innerHTML = ''
167
183
  }
168
184
  })
@@ -172,55 +188,47 @@ describe('PktHeading', () => {
172
188
  const heading = container.querySelector('pkt-heading') as PktHeading
173
189
  await heading.updateComplete
174
190
 
175
- expect(heading.classList.contains('pkt-heading--small')).toBe(true)
176
- expect(heading.classList.contains('pkt-heading--center')).toBe(true)
191
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(true)
192
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--center')).toBe(true)
177
193
 
178
- // Change properties
179
194
  heading.size = 'large'
180
195
  heading.align = 'end'
181
196
  await heading.updateComplete
182
197
 
183
- expect(heading.classList.contains('pkt-heading--small')).toBe(false)
184
- expect(heading.classList.contains('pkt-heading--center')).toBe(false)
185
- expect(heading.classList.contains('pkt-heading--large')).toBe(true)
186
- expect(heading.classList.contains('pkt-heading--end')).toBe(true)
198
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(false)
199
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--center')).toBe(false)
200
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--large')).toBe(true)
201
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--end')).toBe(true)
187
202
  })
188
203
  })
189
204
 
190
- describe('ARIA and accessibility attributes', () => {
191
- test('sets role and aria-level attributes on connection', async () => {
192
- const container = await createHeading('level="3"')
193
- const heading = container.querySelector('pkt-heading') as PktHeading
194
- await heading.updateComplete
195
-
196
- expect(heading.getAttribute('role')).toBe('heading')
197
- expect(heading.getAttribute('aria-level')).toBe('3')
198
- })
199
-
200
- test('updates aria-level when level property changes', async () => {
201
- const container = await createHeading('level="2"')
205
+ describe('Dynamic level changes', () => {
206
+ test('swaps the inner heading tag when the level changes via property', async () => {
207
+ const container = await createHeading('level="2"', 'Dynamic')
202
208
  const heading = container.querySelector('pkt-heading') as PktHeading
203
209
  await heading.updateComplete
204
210
 
205
- expect(heading.getAttribute('aria-level')).toBe('2')
211
+ expect(innerHeading(heading)?.tagName).toBe('H2')
212
+ expect(innerHeading(heading)?.textContent).toContain('Dynamic')
206
213
 
207
214
  heading.level = 4
208
215
  await heading.updateComplete
209
216
 
210
- expect(heading.getAttribute('aria-level')).toBe('4')
217
+ expect(innerHeading(heading)?.tagName).toBe('H4')
218
+ expect(innerHeading(heading)?.textContent).toContain('Dynamic')
211
219
  })
212
220
 
213
- test('updates aria-level when level attribute changes', async () => {
221
+ test('swaps the inner heading tag when the level attribute changes', async () => {
214
222
  const container = await createHeading('level="1"')
215
223
  const heading = container.querySelector('pkt-heading') as PktHeading
216
224
  await heading.updateComplete
217
225
 
218
- expect(heading.getAttribute('aria-level')).toBe('1')
226
+ expect(innerHeading(heading)?.tagName).toBe('H1')
219
227
 
220
228
  heading.setAttribute('level', '5')
221
229
  await heading.updateComplete
222
230
 
223
- expect(heading.getAttribute('aria-level')).toBe('5')
231
+ expect(innerHeading(heading)?.tagName).toBe('H5')
224
232
  expect(heading.level).toBe(5)
225
233
  })
226
234
  })
@@ -235,28 +243,27 @@ describe('PktHeading', () => {
235
243
  await heading.updateComplete
236
244
 
237
245
  expect(heading.level).toBe(level)
238
- expect(heading.getAttribute('aria-level')).toBe(String(level))
246
+ expect(innerHeading(heading)?.tagName).toBe(`H${level}`)
239
247
  document.body.innerHTML = ''
240
248
  }
241
249
  })
242
250
 
243
- test('handles invalid levels gracefully', async () => {
251
+ test('warns and falls back to h2 for invalid levels', async () => {
244
252
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
245
253
 
246
254
  const container = await createHeading()
247
255
  const heading = container.querySelector('pkt-heading') as PktHeading
248
256
  await heading.updateComplete
249
257
 
250
- // Test invalid level via property
251
258
  heading.level = 0 as TPktHeadingLevel
252
259
  await heading.updateComplete
253
-
254
260
  expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 0. Must be between 1 and 6.')
261
+ expect(innerHeading(heading)?.tagName).toBe('H2')
255
262
 
256
263
  heading.level = 7 as TPktHeadingLevel
257
264
  await heading.updateComplete
258
-
259
265
  expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 7. Must be between 1 and 6.')
266
+ expect(innerHeading(heading)?.tagName).toBe('H2')
260
267
 
261
268
  consoleSpy.mockRestore()
262
269
  })
@@ -268,13 +275,13 @@ describe('PktHeading', () => {
268
275
  const heading = container.querySelector('pkt-heading') as PktHeading
269
276
  await heading.updateComplete
270
277
 
271
- expect(heading.classList.contains('pkt-heading--small')).toBe(true)
278
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(true)
272
279
 
273
280
  heading.size = 'xlarge'
274
281
  await heading.updateComplete
275
282
 
276
- expect(heading.classList.contains('pkt-heading--small')).toBe(false)
277
- expect(heading.classList.contains('pkt-heading--xlarge')).toBe(true)
283
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--small')).toBe(false)
284
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--xlarge')).toBe(true)
278
285
  })
279
286
 
280
287
  test('updates classes when visuallyHidden property changes', async () => {
@@ -282,12 +289,12 @@ describe('PktHeading', () => {
282
289
  const heading = container.querySelector('pkt-heading') as PktHeading
283
290
  await heading.updateComplete
284
291
 
285
- expect(heading.classList.contains('pkt-sr-only')).toBe(false)
292
+ expect(innerHeading(heading)?.classList.contains('pkt-sr-only')).toBe(false)
286
293
 
287
294
  heading.visuallyHidden = true
288
295
  await heading.updateComplete
289
296
 
290
- expect(heading.classList.contains('pkt-sr-only')).toBe(true)
297
+ expect(innerHeading(heading)?.classList.contains('pkt-sr-only')).toBe(true)
291
298
  })
292
299
 
293
300
  test('updates classes when align property changes', async () => {
@@ -295,34 +302,37 @@ describe('PktHeading', () => {
295
302
  const heading = container.querySelector('pkt-heading') as PktHeading
296
303
  await heading.updateComplete
297
304
 
298
- expect(heading.classList.contains('pkt-heading--start')).toBe(true)
305
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--start')).toBe(true)
299
306
 
300
307
  heading.align = 'center'
301
308
  await heading.updateComplete
302
309
 
303
- expect(heading.classList.contains('pkt-heading--start')).toBe(false)
304
- expect(heading.classList.contains('pkt-heading--center')).toBe(true)
310
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--start')).toBe(false)
311
+ expect(innerHeading(heading)?.classList.contains('pkt-heading--center')).toBe(true)
305
312
  })
306
313
  })
307
314
 
308
315
  describe('Content rendering', () => {
309
- test('renders simple text content', async () => {
316
+ test('renders simple text content inside the h-tag', async () => {
310
317
  const container = await createHeading('', 'Simple Heading')
311
318
  const heading = container.querySelector('pkt-heading') as PktHeading
319
+ await heading.updateComplete
312
320
 
313
- expect(heading.textContent).toContain('Simple Heading')
321
+ expect(innerHeading(heading)?.textContent).toContain('Simple Heading')
314
322
  })
315
323
 
316
- test('renders HTML content safely', async () => {
324
+ test('renders nested HTML content inside the h-tag', async () => {
317
325
  const container = await createHeading('', '<strong>Bold</strong> heading')
318
326
  const heading = container.querySelector('pkt-heading') as PktHeading
327
+ await heading.updateComplete
319
328
 
320
- expect(heading.innerHTML).toContain('<strong>Bold</strong>')
321
- expect(heading.innerHTML).toContain('heading')
322
- expect(heading.querySelector('strong')).toBeInTheDocument()
329
+ const inner = innerHeading(heading)
330
+ expect(inner?.querySelector('strong')).toBeInTheDocument()
331
+ expect(inner?.textContent).toContain('Bold')
332
+ expect(inner?.textContent).toContain('heading')
323
333
  })
324
334
 
325
- test('renders multiple child elements', async () => {
335
+ test('renders multiple child elements inside the h-tag', async () => {
326
336
  const container = document.createElement('div')
327
337
  container.innerHTML = `
328
338
  <pkt-heading>
@@ -336,10 +346,11 @@ describe('PktHeading', () => {
336
346
  const heading = container.querySelector('pkt-heading') as PktHeading
337
347
  await heading.updateComplete
338
348
 
339
- expect(heading.querySelector('span')).toBeInTheDocument()
340
- expect(heading.querySelector('em')).toBeInTheDocument()
341
- expect(heading.textContent).toContain('Part 1')
342
- expect(heading.textContent).toContain('Part 2')
349
+ const inner = innerHeading(heading)
350
+ expect(inner?.querySelector('span')).toBeInTheDocument()
351
+ expect(inner?.querySelector('em')).toBeInTheDocument()
352
+ expect(inner?.textContent).toContain('Part 1')
353
+ expect(inner?.textContent).toContain('Part 2')
343
354
  })
344
355
  })
345
356
 
@@ -423,11 +434,11 @@ describe('PktHeading', () => {
423
434
  await Promise.all([...headings].map((h) => h.updateComplete))
424
435
 
425
436
  expect(headings[0].level).toBe(1)
426
- expect(headings[0].size).toBe('xlarge')
437
+ expect(innerHeading(headings[0])?.tagName).toBe('H1')
427
438
  expect(headings[1].level).toBe(2)
428
- expect(headings[1].size).toBe('large')
439
+ expect(innerHeading(headings[1])?.tagName).toBe('H2')
429
440
  expect(headings[2].level).toBe(3)
430
- expect(headings[2].size).toBe('medium')
441
+ expect(innerHeading(headings[2])?.tagName).toBe('H3')
431
442
  })
432
443
 
433
444
  test('maintains independence between multiple instances', async () => {
@@ -443,12 +454,10 @@ describe('PktHeading', () => {
443
454
  const heading2 = container.querySelector('#h2') as PktHeading
444
455
  await Promise.all([heading1.updateComplete, heading2.updateComplete])
445
456
 
446
- // Change properties on first heading
447
457
  heading1.size = 'xlarge'
448
458
  heading1.align = 'center'
449
459
  await heading1.updateComplete
450
460
 
451
- // Verify second heading is unaffected
452
461
  expect(heading2.size).toBe('small')
453
462
  expect(heading2.align).toBe(undefined)
454
463
  expect(heading1.size).toBe('xlarge')
@@ -1,5 +1,6 @@
1
- import { PktShadowElement } from '@/base-elements/element'
2
- import { html, PropertyValues } from 'lit'
1
+ import { PktElementWithSlot } from '@/base-elements/element-with-slot'
2
+ import { slotContent } from '@/directives/slot-content'
3
+ import { html, PropertyValues, TemplateResult } from 'lit'
3
4
  import { customElement, property } from 'lit/decorators.js'
4
5
  import type { THeadingLevel, THeadingSize, THeadingWeight } from 'shared-types'
5
6
 
@@ -15,7 +16,7 @@ export interface IPktHeading {
15
16
  align?: 'start' | 'center' | 'end'
16
17
  }
17
18
 
18
- export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHeading {
19
+ export class PktHeading extends PktElementWithSlot<IPktHeading> implements IPktHeading {
19
20
  @property({ type: String, reflect: true }) size: TPktHeadingSize | undefined = undefined
20
21
  @property({ type: Number, reflect: true }) level: TPktHeadingLevel = 2
21
22
  @property({ type: String, reflect: true }) weight: TPktHeadingWeight | undefined = undefined
@@ -23,32 +24,13 @@ export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHea
23
24
  @property({ type: String, reflect: true }) align: 'start' | 'center' | 'end' | undefined =
24
25
  undefined
25
26
 
26
- connectedCallback(): void {
27
- super.connectedCallback()
28
- this.setAttribute('role', 'heading')
29
- this.setAttribute('aria-level', String(this.level))
30
-
31
- this.updateHostClasses()
32
- }
33
-
34
- attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
35
- super.attributeChangedCallback(name, _old, value)
36
- if (name === 'level' && value) {
37
- this.setLevel(Number(value) as TPktHeadingLevel)
38
- }
39
- if (name === 'visuallyHidden') {
40
- this.visuallyHidden = value !== null && value !== 'false'
41
- }
42
- if (name === 'size' || name === 'visuallyHidden' || name === 'align' || name === 'weight') {
43
- this.updateHostClasses()
44
- }
45
- }
46
-
47
27
  protected updated(_changedProperties: PropertyValues): void {
48
28
  super.updated(_changedProperties)
49
- if (_changedProperties.has('level')) {
50
- this.setLevel(this.level)
51
29
 
30
+ if (_changedProperties.has('level')) {
31
+ if (this.level < 1 || this.level > 6) {
32
+ console.warn(`Invalid heading level: ${this.level}. Must be between 1 and 6.`)
33
+ }
52
34
  if (!this.hasAttribute('size')) {
53
35
  this.size = this.defaultSizeForLevel
54
36
  }
@@ -56,29 +38,9 @@ export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHea
56
38
  this.weight = this.defaultWeightForLevel
57
39
  }
58
40
  }
59
- if (
60
- !this.hasAttribute('size') &&
61
- (_changedProperties.has('level') || this.size === undefined)
62
- ) {
63
- this.size = this.defaultSizeForLevel
64
- }
65
-
66
- if (
67
- _changedProperties.has('size') ||
68
- _changedProperties.has('visuallyHidden') ||
69
- _changedProperties.has('align') ||
70
- _changedProperties.has('weight')
71
- ) {
72
- this.updateHostClasses()
73
- }
74
- }
75
41
 
76
- private setLevel(level: TPktHeadingLevel): void {
77
- if (level >= 1 && level <= 6) {
78
- this.level = level
79
- this.setAttribute('aria-level', String(level))
80
- } else {
81
- console.warn(`Invalid heading level: ${level}. Must be between 1 and 6.`)
42
+ if (!this.hasAttribute('size') && this.size === undefined) {
43
+ this.size = this.defaultSizeForLevel
82
44
  }
83
45
  }
84
46
 
@@ -93,7 +55,6 @@ export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHea
93
55
  case 4:
94
56
  return 'small'
95
57
  case 5:
96
- return 'xsmall'
97
58
  case 6:
98
59
  return 'xsmall'
99
60
  default:
@@ -104,15 +65,11 @@ export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHea
104
65
  private get defaultWeightForLevel(): TPktHeadingWeight {
105
66
  switch (this.level) {
106
67
  case 1:
107
- return 'regular'
108
68
  case 2:
109
69
  return 'regular'
110
70
  case 3:
111
- return 'medium'
112
71
  case 4:
113
- return 'medium'
114
72
  case 5:
115
- return 'medium'
116
73
  case 6:
117
74
  return 'medium'
118
75
  default:
@@ -120,34 +77,39 @@ export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHea
120
77
  }
121
78
  }
122
79
 
123
- private updateHostClasses() {
124
- // Remove all possible classes first
125
- this.classList.remove(
80
+ private get headingClasses(): string {
81
+ return [
126
82
  'pkt-heading',
127
- 'pkt-heading--xsmall',
128
- 'pkt-heading--small',
129
- 'pkt-heading--medium',
130
- 'pkt-heading--large',
131
- 'pkt-heading--xlarge',
132
- 'pkt-sr-only',
133
- 'pkt-heading--start',
134
- 'pkt-heading--center',
135
- 'pkt-heading--end',
136
- 'pkt-heading--light',
137
- 'pkt-heading--regular',
138
- 'pkt-heading--medium',
139
- 'pkt-heading--bold',
140
- )
141
- // Add current classes
142
- this.classList.add('pkt-heading')
143
- if (this.size) this.classList.add(`pkt-heading--${this.size}`)
144
- if (this.weight) this.classList.add(`pkt-heading--fw-${this.weight}`)
145
- if (this.visuallyHidden) this.classList.add('pkt-sr-only')
146
- if (this.align) this.classList.add(`pkt-heading--${this.align}`)
83
+ this.size && `pkt-heading--${this.size}`,
84
+ this.weight && `pkt-heading--fw-${this.weight}`,
85
+ this.visuallyHidden && 'pkt-sr-only',
86
+ this.align && `pkt-heading--${this.align}`,
87
+ ]
88
+ .filter(Boolean)
89
+ .join(' ')
147
90
  }
148
91
 
149
- render() {
150
- return html`<slot></slot>`
92
+ render(): TemplateResult {
93
+ const classes = this.headingClasses
94
+ const content = slotContent(this)
95
+ const safeLevel = this.level >= 1 && this.level <= 6 ? this.level : 2
96
+
97
+ switch (safeLevel) {
98
+ case 1:
99
+ return html`<h1 class=${classes}>${content}</h1>`
100
+ case 2:
101
+ return html`<h2 class=${classes}>${content}</h2>`
102
+ case 3:
103
+ return html`<h3 class=${classes}>${content}</h3>`
104
+ case 4:
105
+ return html`<h4 class=${classes}>${content}</h4>`
106
+ case 5:
107
+ return html`<h5 class=${classes}>${content}</h5>`
108
+ case 6:
109
+ return html`<h6 class=${classes}>${content}</h6>`
110
+ default:
111
+ return html`<h2 class=${classes}>${content}</h2>`
112
+ }
151
113
  }
152
114
  }
153
115
 
@@ -16,6 +16,8 @@ expect.extend(toHaveNoViolations)
16
16
  export interface IconTestConfig extends BaseTestConfig {
17
17
  name?: string
18
18
  path?: string
19
+ 'aria-hidden'?: string
20
+ 'aria-label'?: string
19
21
  }
20
22
 
21
23
  // Use shared framework
@@ -326,6 +328,105 @@ describe('PktIcon', () => {
326
328
  const results = await axe(container)
327
329
  expect(results).toHaveNoViolations()
328
330
  })
331
+
332
+ describe('aria-hidden', () => {
333
+ test('defaults to true on the SVG', async () => {
334
+ const { icon } = await createIconTest({ name: 'arrow-right' })
335
+ await icon.updateComplete
336
+ await new Promise((resolve) => setTimeout(resolve, 0))
337
+ await icon.updateComplete
338
+
339
+ const svg = icon.querySelector('svg')
340
+ expect(svg?.getAttribute('aria-hidden')).toBe('true')
341
+ expect(svg?.getAttribute('aria-label')).toBeNull()
342
+ })
343
+
344
+ test('aria-hidden="false" attribute renders aria-hidden="false" on the SVG with name fallback as aria-label', async () => {
345
+ const { icon } = await createIconTest({
346
+ name: 'arrow-right',
347
+ 'aria-hidden': 'false',
348
+ })
349
+ await icon.updateComplete
350
+ await new Promise((resolve) => setTimeout(resolve, 0))
351
+ await icon.updateComplete
352
+
353
+ const svg = icon.querySelector('svg')
354
+ expect(svg?.getAttribute('aria-hidden')).toBe('false')
355
+ expect(svg?.getAttribute('aria-label')).toBe('arrow-right')
356
+ })
357
+
358
+ test('aria-hidden="true" attribute renders aria-hidden="true" on the SVG', async () => {
359
+ const { icon } = await createIconTest({
360
+ name: 'arrow-right',
361
+ 'aria-hidden': 'true',
362
+ })
363
+ await icon.updateComplete
364
+ await new Promise((resolve) => setTimeout(resolve, 0))
365
+ await icon.updateComplete
366
+
367
+ const svg = icon.querySelector('svg')
368
+ expect(svg?.getAttribute('aria-hidden')).toBe('true')
369
+ expect(svg?.getAttribute('aria-label')).toBeNull()
370
+ })
371
+
372
+ test('updates the SVG when the ariaHidden property changes', async () => {
373
+ const { icon } = await createIconTest({ name: 'arrow-right' })
374
+ await icon.updateComplete
375
+ await new Promise((resolve) => setTimeout(resolve, 0))
376
+ await icon.updateComplete
377
+
378
+ icon.ariaHidden = 'false'
379
+ await icon.updateComplete
380
+
381
+ const svg = icon.querySelector('svg')
382
+ expect(svg?.getAttribute('aria-hidden')).toBe('false')
383
+ })
384
+ })
385
+
386
+ describe('aria-label', () => {
387
+ test('aria-label alone makes aria-hidden default to false on the SVG', async () => {
388
+ const { icon } = await createIconTest({
389
+ name: 'arrow-right',
390
+ 'aria-label': 'Gå til forsiden',
391
+ })
392
+ await icon.updateComplete
393
+ await new Promise((resolve) => setTimeout(resolve, 0))
394
+ await icon.updateComplete
395
+
396
+ const svg = icon.querySelector('svg')
397
+ expect(svg?.getAttribute('aria-hidden')).toBe('false')
398
+ expect(svg?.getAttribute('aria-label')).toBe('Gå til forsiden')
399
+ })
400
+
401
+ test('aria-label plus explicit aria-hidden="true" keeps the icon hidden and omits the label', async () => {
402
+ const { icon } = await createIconTest({
403
+ name: 'arrow-right',
404
+ 'aria-label': 'Lukk',
405
+ 'aria-hidden': 'true',
406
+ })
407
+ await icon.updateComplete
408
+ await new Promise((resolve) => setTimeout(resolve, 0))
409
+ await icon.updateComplete
410
+
411
+ const svg = icon.querySelector('svg')
412
+ expect(svg?.getAttribute('aria-hidden')).toBe('true')
413
+ expect(svg?.getAttribute('aria-label')).toBeNull()
414
+ })
415
+
416
+ test('setting ariaLabel via JS flips aria-hidden to false on the SVG', async () => {
417
+ const { icon } = await createIconTest({ name: 'arrow-right' })
418
+ await icon.updateComplete
419
+ await new Promise((resolve) => setTimeout(resolve, 0))
420
+ await icon.updateComplete
421
+
422
+ icon.ariaLabel = 'Lukk'
423
+ await icon.updateComplete
424
+
425
+ const svg = icon.querySelector('svg')
426
+ expect(svg?.getAttribute('aria-hidden')).toBe('false')
427
+ expect(svg?.getAttribute('aria-label')).toBe('Lukk')
428
+ })
429
+ })
329
430
  })
330
431
 
331
432
  describe('Integration scenarios', () => {