@oslokommune/punkt-elements 17.0.9 → 17.1.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 (201) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/{accordion-DEUHQ4JD.js → accordion-DZq8Dnvh.js} +5 -5
  3. package/dist/{accordion-CbXDxOho.cjs → accordion-DlNfuooe.cjs} +1 -1
  4. package/dist/{alert-CbtVGrHD.js → alert-CbN5Bfdd.js} +5 -5
  5. package/dist/{alert-CkUnbPdA.cjs → alert-D7YR5Ru2.cjs} +1 -1
  6. package/dist/{async-directive-HLXOyE4A.cjs → async-directive-Cj6po9Zy.cjs} +1 -1
  7. package/dist/{async-directive-Cw_7VW_-.js → async-directive-DaiMzAU4.js} +2 -2
  8. package/dist/{backlink-DDGlLzk9.cjs → backlink-CxSKQSoA.cjs} +1 -1
  9. package/dist/{backlink-wMNLuQ0X.js → backlink-eAD8xH5b.js} +3 -3
  10. package/dist/base-elements/input-element.d.ts +9 -4
  11. package/dist/booleanish-BF8l4scU.cjs +1 -0
  12. package/dist/{booleanish-MfutDT_4.js → booleanish-OQJTYSrw.js} +8 -1
  13. package/dist/{breadcrumbs-CVTLNNsL.js → breadcrumbs-CJP75InB.js} +2 -2
  14. package/dist/{breadcrumbs-D3tfWqvC.cjs → breadcrumbs-CYKfjd4Q.cjs} +1 -1
  15. package/dist/{button-Cod17rKo.js → button-D_M6dpeU.js} +5 -5
  16. package/dist/{button-gvDXtBMC.cjs → button-o0BRKpis.cjs} +1 -1
  17. package/dist/{calendar-BVXW8DqC.cjs → calendar-7_ii5p5b.cjs} +1 -1
  18. package/dist/{calendar-CjC4qlh6.js → calendar-BCUnpa_x.js} +3 -3
  19. package/dist/{card-sydjK3Bb.cjs → card-Cf_jOdya.cjs} +1 -1
  20. package/dist/{card-sDOe54um.js → card-KB9C2f1R.js} +7 -7
  21. package/dist/{checkbox-DJDphhqz.js → checkbox-C2eVUk13.js} +4 -4
  22. package/dist/{checkbox-8uxbeU30.cjs → checkbox-Ch23npOC.cjs} +1 -1
  23. package/dist/{class-map-CAQCRB1W.js → class-map-BRV7-mcj.js} +1 -1
  24. package/dist/{class-map-CCZN5uS3.cjs → class-map-DzzW3MwB.cjs} +1 -1
  25. package/dist/combobox-Db1fB0K7.cjs +139 -0
  26. package/dist/{combobox-CzZIf5hd.js → combobox-dSWHF8Ii.js} +50 -14
  27. package/dist/components/fileupload/fileupload.d.ts +2 -0
  28. package/dist/components/input-wrapper/tri-state.test.d.ts +0 -0
  29. package/dist/components/input-wrapper/wrapper-parity.test.d.ts +1 -0
  30. package/dist/components/select/select.test.d.ts +2 -2
  31. package/dist/components/textarea/textarea.d.ts +2 -1
  32. package/dist/components/textarea/textarea.test.d.ts +2 -2
  33. package/dist/components/textinput/textinput.test.d.ts +2 -2
  34. package/dist/{consent--Nw43WFU.cjs → consent-C57oumuq.cjs} +1 -1
  35. package/dist/{consent-B2hSTCGC.js → consent-CibGBVvV.js} +2 -2
  36. package/dist/{datepicker-DrB54Rhc.cjs → datepicker-Ci6Oj0YL.cjs} +6 -7
  37. package/dist/{datepicker-BMuplkDE.js → datepicker-D9Qr98VO.js} +39 -16
  38. package/dist/{directive-helpers-DIF5DwN4.js → directive-helpers-BXtu2yHt.js} +1 -1
  39. package/dist/{directive-helpers-BshFvt7A.cjs → directive-helpers-CEtfS19L.cjs} +1 -1
  40. package/dist/docs/components/verifisering.d.ts +15 -0
  41. package/dist/{element-BoJrqauR.js → element-8RsANDBG.js} +2 -1
  42. package/dist/{element-DJzurxgt.cjs → element-DCm4GU7z.cjs} +1 -1
  43. package/dist/{element-with-slot-mXLg8gg8.js → element-with-slot-DFNWwYys.js} +2 -2
  44. package/dist/{element-with-slot-BY7Jc-q8.cjs → element-with-slot-N79a3tub.cjs} +1 -1
  45. package/dist/{fileupload-D4gQN64T.js → fileupload-D008usH2.js} +11 -8
  46. package/dist/{fileupload-CGKUJrlj.cjs → fileupload-DtAujY-k.cjs} +2 -2
  47. package/dist/{footer-BIrJeRuZ.js → footer-05rSMBWl.js} +5 -5
  48. package/dist/{footer-Ber-jqM1.cjs → footer-DmBOxk43.cjs} +1 -1
  49. package/dist/{header-BFk1an4z.cjs → header-Dd8_GxXA.cjs} +1 -1
  50. package/dist/{header-Deg_LGJo.js → header-iug8KwEF.js} +12 -12
  51. package/dist/{header-menu-GFtJKM4j.js → header-menu-CcXQsag0.js} +5 -5
  52. package/dist/{header-menu-BHqK4QA6.cjs → header-menu-DEMDRByb.cjs} +1 -1
  53. package/dist/{heading-DyJpBFT2.cjs → heading-DbLmrGyU.cjs} +1 -1
  54. package/dist/{heading-CD0PhL8g.js → heading-V-gyod7_.js} +2 -2
  55. package/dist/{helptext-C_brWCIT.js → helptext-BFGNgeqw.js} +8 -5
  56. package/dist/{helptext-Cq5rNaAC.cjs → helptext-CpaW9Zwl.cjs} +4 -1
  57. package/dist/{icon-DGFEIlco.js → icon-8ZKcRaGi.js} +1 -1
  58. package/dist/{icon-B8PVq2O6.cjs → icon-BlJZpl2L.cjs} +1 -1
  59. package/dist/{if-defined-D0bnetLJ.js → if-defined-DfH9X3nd.js} +1 -1
  60. package/dist/if-defined-h7sbeYue.cjs +1 -0
  61. package/dist/{input-element-B0qLbEdF.cjs → input-element-CmFzbxiZ.cjs} +1 -1
  62. package/dist/{input-element-C0RnXi-J.js → input-element-DPXwOJeT.js} +147 -123
  63. package/dist/input-wrapper-CohL8Onb.cjs +54 -0
  64. package/dist/input-wrapper-CrJMQj_L.js +122 -0
  65. package/dist/{input-wrapper-BweP6t3O.js → input-wrapper-D-fPaTbY.js} +2 -2
  66. package/dist/{input-wrapper-wh3fWIhk.cjs → input-wrapper-D5PR0rJn.cjs} +1 -1
  67. package/dist/{link-Geze7498.cjs → link-BQXhsaBM.cjs} +1 -1
  68. package/dist/{link-FmkQvSQL.js → link-C-0YV0XX.js} +4 -4
  69. package/dist/{linkcard-6LCXRTR-.cjs → linkcard-BN34yjcT.cjs} +1 -1
  70. package/dist/{linkcard-BKdT0zU_.js → linkcard-DGHxt0ef.js} +4 -4
  71. package/dist/{listbox-LdyWPu7W.js → listbox-CMLqAc5-.js} +4 -4
  72. package/dist/{listbox-89wMuQVK.cjs → listbox-CgjhUALC.cjs} +1 -1
  73. package/dist/{loader-BA-i8CaQ.js → loader-CxcWuAB7.js} +4 -4
  74. package/dist/{loader-CgW4Vst3.cjs → loader-DFWbjQV5.cjs} +1 -1
  75. package/dist/{messagebox-VD5oEZ19.cjs → messagebox-D0kz1ic4.cjs} +1 -1
  76. package/dist/{messagebox-fiaAyHLG.js → messagebox-DvlF8XbD.js} +4 -4
  77. package/dist/{modal-CPnFteYS.cjs → modal-BQcORqnQ.cjs} +1 -1
  78. package/dist/{modal-xNK_wpHk.js → modal-C9pamgry.js} +5 -5
  79. package/dist/pkt-accordion.cjs +1 -1
  80. package/dist/pkt-accordion.js +1 -1
  81. package/dist/pkt-alert.cjs +1 -1
  82. package/dist/pkt-alert.js +1 -1
  83. package/dist/pkt-backlink.cjs +1 -1
  84. package/dist/pkt-backlink.js +1 -1
  85. package/dist/pkt-breadcrumbs.cjs +1 -1
  86. package/dist/pkt-breadcrumbs.js +1 -1
  87. package/dist/pkt-button.cjs +1 -1
  88. package/dist/pkt-button.js +1 -1
  89. package/dist/pkt-calendar.cjs +1 -1
  90. package/dist/pkt-calendar.js +1 -1
  91. package/dist/pkt-card.cjs +1 -1
  92. package/dist/pkt-card.js +1 -1
  93. package/dist/pkt-checkbox.cjs +1 -1
  94. package/dist/pkt-checkbox.js +1 -1
  95. package/dist/pkt-combobox.cjs +1 -1
  96. package/dist/pkt-combobox.js +1 -1
  97. package/dist/pkt-consent.cjs +1 -1
  98. package/dist/pkt-consent.js +1 -1
  99. package/dist/pkt-datepicker.cjs +1 -1
  100. package/dist/pkt-datepicker.js +1 -1
  101. package/dist/pkt-fileupload.cjs +1 -1
  102. package/dist/pkt-fileupload.js +1 -1
  103. package/dist/pkt-footer.cjs +1 -1
  104. package/dist/pkt-footer.js +1 -1
  105. package/dist/pkt-header-menu.cjs +1 -1
  106. package/dist/pkt-header-menu.js +1 -1
  107. package/dist/pkt-header.cjs +1 -1
  108. package/dist/pkt-header.js +1 -1
  109. package/dist/pkt-heading.cjs +1 -1
  110. package/dist/pkt-heading.js +1 -1
  111. package/dist/pkt-helptext.cjs +1 -1
  112. package/dist/pkt-helptext.js +1 -1
  113. package/dist/pkt-icon.cjs +1 -1
  114. package/dist/pkt-icon.js +1 -1
  115. package/dist/pkt-index.cjs +1 -1
  116. package/dist/pkt-index.js +34 -34
  117. package/dist/pkt-input-wrapper.cjs +1 -1
  118. package/dist/pkt-input-wrapper.js +1 -1
  119. package/dist/pkt-link.cjs +1 -1
  120. package/dist/pkt-link.js +1 -1
  121. package/dist/pkt-linkcard.cjs +1 -1
  122. package/dist/pkt-linkcard.js +1 -1
  123. package/dist/pkt-listbox.cjs +1 -1
  124. package/dist/pkt-listbox.js +1 -1
  125. package/dist/pkt-loader.cjs +1 -1
  126. package/dist/pkt-loader.js +1 -1
  127. package/dist/pkt-messagebox.cjs +1 -1
  128. package/dist/pkt-messagebox.js +1 -1
  129. package/dist/pkt-modal.cjs +1 -1
  130. package/dist/pkt-modal.js +1 -1
  131. package/dist/{pkt-options-controller-CPrfqepu.cjs → pkt-options-controller-DeyC9FDP.cjs} +1 -1
  132. package/dist/{pkt-options-controller-DMxYwnnj.js → pkt-options-controller-sblvLBnq.js} +1 -1
  133. package/dist/pkt-progressbar.cjs +1 -1
  134. package/dist/pkt-progressbar.js +1 -1
  135. package/dist/pkt-radiobutton.cjs +1 -1
  136. package/dist/pkt-radiobutton.js +1 -1
  137. package/dist/pkt-searchinput.cjs +1 -1
  138. package/dist/pkt-searchinput.js +1 -1
  139. package/dist/pkt-select.cjs +1 -1
  140. package/dist/pkt-select.js +1 -1
  141. package/dist/pkt-tabs.cjs +1 -1
  142. package/dist/pkt-tabs.js +1 -1
  143. package/dist/pkt-tag.cjs +1 -1
  144. package/dist/pkt-tag.js +1 -1
  145. package/dist/pkt-textarea.cjs +1 -1
  146. package/dist/pkt-textarea.js +1 -1
  147. package/dist/pkt-textinput.cjs +1 -1
  148. package/dist/pkt-textinput.js +1 -1
  149. package/dist/pkt-timepicker.cjs +1 -1
  150. package/dist/pkt-timepicker.js +1 -1
  151. package/dist/{progressbar-mtjtbpCO.js → progressbar-DtmVoP1j.js} +4 -4
  152. package/dist/{progressbar-BN5y20CC.cjs → progressbar-fnJJsQkh.cjs} +1 -1
  153. package/dist/{radiobutton-DjWBoSHv.cjs → radiobutton-CGT4kJSf.cjs} +1 -1
  154. package/dist/{radiobutton-C6htwDrT.js → radiobutton-DZHVyjoe.js} +4 -4
  155. package/dist/{ref-DO7TzO4O.cjs → ref-CYXJQuCZ.cjs} +1 -1
  156. package/dist/{ref-WGS0u7i2.js → ref-CkGzlsAX.js} +2 -2
  157. package/dist/{repeat-DRnLAg0O.cjs → repeat-C_7HwdFQ.cjs} +1 -1
  158. package/dist/{repeat-BH0cL82q.js → repeat-CwZAKBjZ.js} +2 -2
  159. package/dist/{searchinput-DGjeN1YI.cjs → searchinput-1CESdZO9.cjs} +1 -1
  160. package/dist/{searchinput-CfUVfpqL.js → searchinput-Dw1LAHgD.js} +4 -4
  161. package/dist/{select-CsBXXbLP.js → select-Cb8RH7lk.js} +24 -20
  162. package/dist/select-Duav6lVT.cjs +49 -0
  163. package/dist/shared-types/booleanish.d.ts +11 -0
  164. package/dist/shared-types/index.d.ts +1 -1
  165. package/dist/shared-utils/forms/described-by.d.ts +24 -0
  166. package/dist/{tabs-q62MK_-r.cjs → tabs-A665GUTo.cjs} +1 -1
  167. package/dist/{tabs-BqrKLCjd.js → tabs-BV4w4QfM.js} +4 -4
  168. package/dist/{tag-XE-VRxT2.cjs → tag-DaRo48qb.cjs} +1 -1
  169. package/dist/{tag-BTiTXyf5.js → tag-rb23IBFT.js} +5 -5
  170. package/dist/textarea-bLTy-JGg.cjs +49 -0
  171. package/dist/{textarea-Cm392CEI.js → textarea-nyH0_IkD.js} +35 -28
  172. package/dist/{textinput-TSPcBcih.js → textinput-B5Gk13OB.js} +30 -25
  173. package/dist/{textinput-DqJbTglM.cjs → textinput-D3bL7BTc.cjs} +10 -10
  174. package/dist/{timepicker-DWbgTDKq.js → timepicker-BH9yjtLA.js} +18 -11
  175. package/dist/{timepicker-CqFYMV8q.cjs → timepicker-lv-CQ8of.cjs} +5 -2
  176. package/dist/translations/no.json.d.ts +2 -1
  177. package/package.json +2 -2
  178. package/src/components/combobox/combobox.core.test.ts +2 -2
  179. package/src/components/combobox/combobox.ts +7 -1
  180. package/src/components/datepicker/datepicker-single.ts +1 -1
  181. package/src/components/datepicker/datepicker.ts +5 -3
  182. package/src/components/fileupload/fileupload.ts +6 -1
  183. package/src/components/helptext/helptext.ts +3 -0
  184. package/src/components/input-wrapper/input-wrapper.test.ts +3 -3
  185. package/src/components/input-wrapper/input-wrapper.ts +31 -29
  186. package/src/components/input-wrapper/tri-state.test.ts +91 -0
  187. package/src/components/input-wrapper/wrapper-parity.test.ts +141 -0
  188. package/src/components/select/select.test.ts +8 -7
  189. package/src/components/select/select.ts +13 -5
  190. package/src/components/textarea/textarea.test.ts +6 -6
  191. package/src/components/textarea/textarea.ts +20 -11
  192. package/src/components/textinput/textinput.test.ts +6 -6
  193. package/src/components/textinput/textinput.ts +17 -11
  194. package/src/components/timepicker/timepicker.ts +12 -5
  195. package/dist/booleanish-B9Kf0nES.cjs +0 -1
  196. package/dist/combobox-BH9lym0-.cjs +0 -136
  197. package/dist/if-defined-DrwbxQOj.cjs +0 -1
  198. package/dist/input-wrapper-DPkkiG7S.cjs +0 -57
  199. package/dist/input-wrapper-fWV5P4MN.js +0 -126
  200. package/dist/select-CZvAW7W2.cjs +0 -50
  201. package/dist/textarea-sTcZxOIB.cjs +0 -47
@@ -214,6 +214,9 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
214
214
  }
215
215
  }
216
216
 
217
+ this.classList.add('pkt-combobox-component')
218
+ this.classList.toggle('pkt-combobox-component--fullwidth', !!this.fullwidth)
219
+
217
220
  super.updated(changedProperties)
218
221
  }
219
222
 
@@ -284,6 +287,9 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
284
287
  .helptextDropdown=${ifDefined(this.helptextDropdown)}
285
288
  .helptextDropdownButton=${ifDefined(this.helptextDropdownButton)}
286
289
  ?fullwidth=${this.fullwidth}
290
+ ?counter=${this.effectiveCounter(!!this.multiple && this.maxlength != null)}
291
+ .counterCurrent=${this._value.length}
292
+ .counterMaxLength=${this.maxlength ?? 0}
287
293
  ?hasError=${this.hasError}
288
294
  ?inline=${this.inline}
289
295
  ?disabled=${this.disabled}
@@ -295,7 +301,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
295
301
  .tagText=${this.tagText}
296
302
  useWrapper=${this.useWrapper}
297
303
  .forId=${this._hasTextInput ? this.id + '-input' : this.id + '-combobox'}
298
- ?hasFieldset=${!this._hasTextInput}
304
+ ?hasFieldset=${this.effectiveHasFieldset(!this._hasTextInput)}
299
305
  class="pkt-combobox__wrapper"
300
306
  @labelClick=${this.handleInputClick}
301
307
  >
@@ -37,7 +37,7 @@ export class PktDatepickerSingle extends PktDatepickerBase {
37
37
  max=${ifDefined(this.max)}
38
38
  placeholder=${ifDefined(this.placeholder)}
39
39
  ?readonly=${this.isInputReadonly}
40
- aria-describedby="${this.id}-helptext"
40
+ aria-describedby="${this.id}-input-helptext"
41
41
  @click=${(e: MouseEvent) => {
42
42
  e.preventDefault()
43
43
  this.dispatchToggleCalendar(e)
@@ -226,6 +226,9 @@ export class PktDatepicker extends PktInputElement<Props> {
226
226
  this._value = [this._value[0] ?? '']
227
227
  }
228
228
  }
229
+ // Ytre boks, samme klasser og rolle som Reacts ytre div
230
+ this.classList.add('pkt-datepicker')
231
+ this.classList.toggle('pkt-datepicker--fullwidth', this.fullwidth)
229
232
  this.classList.toggle('pkt-datepicker--medium', this.inputSize === 'medium')
230
233
  this.classList.toggle('pkt-datepicker--small', this.inputSize === 'small')
231
234
  this.classList.toggle('pkt-datepicker--xsmall', this.inputSize === 'xsmall')
@@ -501,7 +504,7 @@ export class PktDatepicker extends PktInputElement<Props> {
501
504
  .counterMaxLength=${this.maxlength}
502
505
  ?disabled=${this.disabled}
503
506
  ?hasError=${this.hasError}
504
- ?hasFieldset=${this.hasFieldset}
507
+ ?hasFieldset=${this.effectiveHasFieldset(false)}
505
508
  ?inline=${this.inline}
506
509
  ?required=${this.required}
507
510
  ?optionalTag=${this.optionalTag}
@@ -514,9 +517,8 @@ export class PktDatepicker extends PktInputElement<Props> {
514
517
  .helptext=${this.helptext}
515
518
  .helptextDropdown=${this.helptextDropdown}
516
519
  .helptextDropdownButton=${this.helptextDropdownButton}
517
- .ariaDescribedBy=${this.ariaDescribedBy}
520
+ .ariaDescribedby=${this.ariaDescribedby}
518
521
  .size=${this.inputSize}
519
- class="pkt-datepicker"
520
522
  >
521
523
  <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
522
524
  ${this.multiple
@@ -1,4 +1,4 @@
1
- import { html } from 'lit'
1
+ import { html, PropertyValues } from 'lit'
2
2
  import { classMap } from 'lit/directives/class-map.js'
3
3
  import { ifDefined } from 'lit/directives/if-defined.js'
4
4
  import { customElement, state } from 'lit/decorators.js'
@@ -58,6 +58,11 @@ export class PktFileUpload extends PktFileUploadBase implements IPktFileUpload {
58
58
  @state() private previewCurrentIndex = 0
59
59
  @state() private failedImageFileIds: Record<string, FileImageFailureFlags> = {}
60
60
 
61
+ protected updated(changedProperties: PropertyValues): void {
62
+ super.updated(changedProperties)
63
+ this.classList.add('pkt-fileupload-component')
64
+ }
65
+
61
66
  render() {
62
67
  const content = this.renderContent()
63
68
 
@@ -47,6 +47,8 @@ export class PktHelptext extends PktElementWithSlot {
47
47
  <button
48
48
  class="pkt-link pkt-link--icon-right pkt-btn pkt-btn--small pkt-btn--tertiary pkt-btn--icon-right"
49
49
  type="button"
50
+ aria-expanded=${this.isHelpTextOpen ? 'true' : 'false'}
51
+ aria-controls="${this.forId}-helptext-expandable"
50
52
  @click=${toggleDropdown}
51
53
  >
52
54
  <pkt-icon
@@ -56,6 +58,7 @@ export class PktHelptext extends PktElementWithSlot {
56
58
  <span class="pkt-btn__text">${unsafeHTML(this.helptextDropdownButton)}</span>
57
59
  </button>
58
60
  <div
61
+ id="${this.forId}-helptext-expandable"
59
62
  class="${classMap({
60
63
  'pkt-inputwrapper__helptext': true,
61
64
  'pkt-inputwrapper__helptext-expandable-open': this.isHelpTextOpen,
@@ -322,7 +322,7 @@ describe('PktInputWrapper', () => {
322
322
 
323
323
  expect(wrapper.hasError).toBe(true)
324
324
  // Check for error-related classes based on actual component implementation
325
- const inputWrapper = wrapper.querySelector('.pkt-inputwrapper')
325
+ const inputWrapper = wrapper
326
326
  expect(inputWrapper?.classList.contains('pkt-inputwrapper--error')).toBe(true)
327
327
  })
328
328
 
@@ -333,7 +333,7 @@ describe('PktInputWrapper', () => {
333
333
  wrapper.size = 'small'
334
334
  await wrapper.updateComplete
335
335
 
336
- const inputWrapper = wrapper.querySelector('.pkt-inputwrapper')
336
+ const inputWrapper = wrapper
337
337
  expect(inputWrapper?.classList.contains('pkt-inputwrapper--small')).toBe(true)
338
338
  })
339
339
 
@@ -344,7 +344,7 @@ describe('PktInputWrapper', () => {
344
344
  wrapper.size = 'xsmall'
345
345
  await wrapper.updateComplete
346
346
 
347
- const inputWrapper = wrapper.querySelector('.pkt-inputwrapper')
347
+ const inputWrapper = wrapper
348
348
  expect(inputWrapper?.classList.contains('pkt-inputwrapper--xsmall')).toBe(true)
349
349
  expect(inputWrapper?.classList.contains('pkt-inputwrapper--small')).toBe(false)
350
350
  })
@@ -1,9 +1,10 @@
1
1
  import { PktElementWithSlot } from '@/base-elements/element-with-slot'
2
2
  import { slotContent } from '@/directives/slot-content'
3
+ import { counterStatusId, isCounterExceeded } from 'shared-utils/forms/described-by'
4
+ import strings from '@/translations/no.json'
3
5
  import { ifDefined } from 'lit/directives/if-defined.js'
4
6
  import { html, nothing, PropertyValues } from 'lit'
5
7
  import { unsafeHTML } from 'lit/directives/unsafe-html.js'
6
- import { classMap } from 'lit/directives/class-map.js'
7
8
  import { customElement, property } from 'lit/decorators.js'
8
9
  import { ElementProps } from '@/types/typeUtils'
9
10
  import { uuidish } from 'shared-utils/utils'
@@ -67,7 +68,7 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
67
68
  @property({ type: Boolean }) inline: boolean = specs.props.inline.default
68
69
  @property({ type: String }) ariaDescribedby: string | undefined = undefined
69
70
  @property({ type: Boolean }) hasFieldset: boolean = specs.props.hasFieldset.default
70
- @property({ type: String, reflect: true }) role: string | null = 'group'
71
+ @property({ type: String, reflect: true }) role: string | null = null
71
72
  @property({ type: Boolean, reflect: true, converter: booleanishConverter })
72
73
  useWrapper: Booleanish = specs.props.useWrapper.default
73
74
  @property({ type: String, attribute: 'input-wrapper-size' })
@@ -75,24 +76,24 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
75
76
 
76
77
  protected updated(changedProperties: PropertyValues): void {
77
78
  super.updated(changedProperties)
79
+ this.classList.add('pkt-inputwrapper')
80
+ this.classList.toggle('pkt-inputwrapper--error', this.hasError)
81
+ this.classList.toggle('pkt-inputwrapper--disabled', this.disabled)
82
+ this.classList.toggle('pkt-inputwrapper--inline', this.inline)
83
+ this.classList.toggle('pkt-inputwrapper--medium', this.size === 'medium')
84
+ this.classList.toggle('pkt-inputwrapper--small', this.size === 'small')
85
+ this.classList.toggle('pkt-inputwrapper--xsmall', this.size === 'xsmall')
86
+ if (this.role) this.setAttribute('aria-labelledby', `${this.forId}-label`)
87
+ else this.removeAttribute('aria-labelledby')
78
88
  }
79
89
 
80
- // Speiler navigeringsmodus til den interne pkt-inputwrapper-div-en som
81
- // data-navtype="pointer" (eller fjerner attributtet) så CSS kan skille
82
- // dempet fokus (klikk) fra kraftig fokus (tab). Controlleren registrerer
83
- // seg selv på hosten i konstruktøren, så vi trenger ikke beholde referansen.
84
- focusModality = new FocusModalityController(this, '.pkt-inputwrapper')
90
+ // Speiler navigeringsmodus til verten som data-navtype="pointer" (eller
91
+ // fjerner attributtet) så CSS kan skille dempet fokus (klikk) fra kraftig
92
+ // fokus (tab). Controlleren registrerer seg selv hosten i konstruktøren,
93
+ // så vi trenger ikke beholde referansen.
94
+ focusModality = new FocusModalityController(this)
85
95
 
86
96
  render() {
87
- const classes = {
88
- 'pkt-inputwrapper': true,
89
- 'pkt-inputwrapper--error': this.hasError,
90
- 'pkt-inputwrapper--disabled': this.disabled,
91
- 'pkt-inputwrapper--inline': this.inline,
92
- 'pkt-inputwrapper--small': this.size === 'small',
93
- 'pkt-inputwrapper--xsmall': this.size === 'xsmall',
94
- }
95
-
96
97
  const tagClasses = 'pkt-tag pkt-tag--small pkt-tag--thin-text'
97
98
 
98
99
  const describedBy = this.ariaDescribedby
@@ -129,19 +130,13 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
129
130
  return html`<label
130
131
  class="pkt-inputwrapper__label"
131
132
  for="${this.forId}"
132
- aria-describedby="${describedBy}"
133
133
  id="${this.forId}-label"
134
134
  @click=${this.handleLabelClick}
135
135
  >${this.label}${tagElement()}</label
136
136
  >`
137
137
  }
138
138
  } else {
139
- return html`<label
140
- for="${this.forId}"
141
- class="pkt-sr-only"
142
- aria-describedby="${describedBy}"
143
- id="${this.forId}-label"
144
- >
139
+ return html`<label for="${this.forId}" class="pkt-sr-only" id="${this.forId}-label">
145
140
  ${this.label}
146
141
  </label>`
147
142
  }
@@ -165,14 +160,21 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
165
160
  }
166
161
 
167
162
  const counterElement = () => {
168
- if (this.counter) {
169
- return html`<div class="pkt-input__counter" aria-live="polite" aria-atomic="true">
163
+ if (!this.counter) return nothing
164
+ // Den synlige telleren er aria-hidden. Statusregionen under holdes tom
165
+ // til grensa er brutt, så skjermlesere bare varsler når det faktisk er noe å si.
166
+ const exceeded = isCounterExceeded(this.counterCurrent, this.counterMaxLength)
167
+ return html`<div class="pkt-input__counter" aria-hidden="true">
170
168
  ${this.counterError ? this.counterError : nothing} ${this.counterCurrent || 0}
171
169
  ${this.counterMaxLength ? `/${this.counterMaxLength}` : nothing}
170
+ </div>
171
+ <div class="pkt-sr-only" id=${counterStatusId(this.forId)} aria-live="polite">
172
+ ${exceeded
173
+ ? strings.forms.messages.counterExceeded
174
+ .replace('{current}', String(this.counterCurrent ?? 0))
175
+ .replace('{max}', String(this.counterMaxLength))
176
+ : ''}
172
177
  </div>`
173
- } else {
174
- return nothing
175
- }
176
178
  }
177
179
 
178
180
  const errorElement = () => {
@@ -214,7 +216,7 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
214
216
  : html`<div class="pkt-inputwrapper__fieldset">${inputContent()}</div>`
215
217
  }
216
218
 
217
- return html`<div class=${classMap(classes)}>${wrapperElement()}</div> `
219
+ return wrapperElement()
218
220
  }
219
221
 
220
222
  private toggleDropdown(e: CustomEvent) {
@@ -0,0 +1,91 @@
1
+ import '@testing-library/jest-dom'
2
+ import { describe, expect, test } from 'vitest'
3
+ import { createElementTest } from '../../tests/test-framework'
4
+ import { CustomElementFor } from '../../tests/component-registry'
5
+ import '../textinput'
6
+ import '../combobox'
7
+ import '../timepicker'
8
+
9
+ const make = async (tag: any, config: Record<string, unknown>) => {
10
+ const { element } = await createElementTest<CustomElementFor<'pkt-textinput'>, never>(
11
+ tag,
12
+
13
+ config as any,
14
+ )
15
+ return element
16
+ }
17
+
18
+ const counterOf = (el: Element) => el.querySelector('.pkt-input__counter')
19
+ const legendOf = (el: Element) => el.querySelector('.pkt-inputwrapper__legend')
20
+ const labelOf = (el: Element) => el.querySelector('.pkt-inputwrapper__label')
21
+
22
+ describe('counter: tri-state', () => {
23
+ test('vises av seg selv når det finnes en grense å telle mot', async () => {
24
+ const el = await make('pkt-textinput', { label: 'Etikett', counterMaxLength: 20 })
25
+ expect(counterOf(el)).not.toBeNull()
26
+ })
27
+
28
+ test('vises ikke uten grense', async () => {
29
+ const el = await make('pkt-textinput', { label: 'Etikett' })
30
+ expect(counterOf(el)).toBeNull()
31
+ })
32
+
33
+ test('kan slås av eksplisitt selv om det finnes en grense', async () => {
34
+ const el = await make('pkt-textinput', {
35
+ label: 'Etikett',
36
+ counterMaxLength: 20,
37
+ counter: 'false',
38
+ })
39
+ expect(counterOf(el)).toBeNull()
40
+ })
41
+
42
+ test('kan slås på eksplisitt', async () => {
43
+ const el = await make('pkt-textinput', { label: 'Etikett', counter: '' })
44
+ expect(counterOf(el)).not.toBeNull()
45
+ })
46
+
47
+ test('combobox: utledes fra multiple + maxlength', async () => {
48
+ const auto = await make('pkt-combobox', { label: 'Etikett', multiple: '', maxlength: 3 })
49
+ expect(counterOf(auto)).not.toBeNull()
50
+
51
+ const off = await make('pkt-combobox', {
52
+ label: 'Etikett',
53
+ multiple: '',
54
+ maxlength: 3,
55
+ counter: 'false',
56
+ })
57
+ expect(counterOf(off)).toBeNull()
58
+ })
59
+ })
60
+
61
+ describe('hasFieldset: tri-state', () => {
62
+ test('label som standard', async () => {
63
+ const el = await make('pkt-textinput', { label: 'Etikett' })
64
+ expect(labelOf(el)).not.toBeNull()
65
+ expect(legendOf(el)).toBeNull()
66
+ })
67
+
68
+ test('legend når den settes eksplisitt', async () => {
69
+ const el = await make('pkt-textinput', { label: 'Etikett', hasFieldset: '' })
70
+ expect(legendOf(el)).not.toBeNull()
71
+ })
72
+
73
+ test('combobox uten tekstfelt utleder fieldset', async () => {
74
+ const el = await make('pkt-combobox', { label: 'Etikett' })
75
+ expect(legendOf(el)).not.toBeNull()
76
+ })
77
+
78
+ test('timepicker: label som standard, legend når satt', async () => {
79
+ const standard = await make('pkt-timepicker', { label: 'Etikett' })
80
+ expect(legendOf(standard)).toBeNull()
81
+
82
+ const medFieldset = await make('pkt-timepicker', { label: 'Etikett', hasFieldset: '' })
83
+ expect(legendOf(medFieldset)).not.toBeNull()
84
+ })
85
+
86
+ test('combobox kan overstyre utledningen til label', async () => {
87
+ const el = await make('pkt-combobox', { label: 'Etikett', hasFieldset: 'false' })
88
+ expect(legendOf(el)).toBeNull()
89
+ expect(labelOf(el)).not.toBeNull()
90
+ })
91
+ })
@@ -0,0 +1,141 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { describe, expect, test } from 'vitest'
4
+ import wrapperSpec from 'componentSpecs/input-wrapper.json'
5
+
6
+ const ROOT = join(__dirname, '../../../../..')
7
+
8
+ // OBS! Denne testen sjekker også React-komponentene.
9
+
10
+ const COMPONENTS = {
11
+ textinput: {
12
+ elements: 'packages/elements/src/components/textinput/textinput.ts',
13
+ react: 'packages/react/src/components/textinput/Textinput.tsx',
14
+ },
15
+ textarea: {
16
+ elements: 'packages/elements/src/components/textarea/textarea.ts',
17
+ react: 'packages/react/src/components/textarea/Textarea.tsx',
18
+ },
19
+ select: {
20
+ elements: 'packages/elements/src/components/select/select.ts',
21
+ react: 'packages/react/src/components/select/Select.tsx',
22
+ },
23
+ combobox: {
24
+ elements: 'packages/elements/src/components/combobox/combobox.ts',
25
+ react: 'packages/react/src/components/combobox/Combobox.tsx',
26
+ },
27
+ datepicker: {
28
+ elements: 'packages/elements/src/components/datepicker/datepicker.ts',
29
+ react: 'packages/react/src/components/datepicker/Datepicker.tsx',
30
+ },
31
+ timepicker: {
32
+ elements: 'packages/elements/src/components/timepicker/timepicker.ts',
33
+ react: 'packages/react/src/components/timepicker/Timepicker.tsx',
34
+ },
35
+ fileupload: {
36
+ elements: 'packages/elements/src/components/fileupload/fileupload.ts',
37
+ react: 'packages/react/src/components/fileupload/FileUpload.tsx',
38
+ },
39
+ } as const
40
+
41
+ // Props som med vilje ikke videresendes, med begrunnelse.
42
+ const OMITTED: Record<string, Partial<Record<keyof typeof COMPONENTS, string>>> = {
43
+ counter: {
44
+ select: 'ingenting å telle i en select',
45
+ timepicker: 'ingenting å telle',
46
+ fileupload: 'ingenting å telle',
47
+ },
48
+ counterCurrent: {
49
+ select: 'se counter',
50
+ timepicker: 'se counter',
51
+ fileupload: 'se counter',
52
+ },
53
+ counterMaxLength: {
54
+ select: 'se counter',
55
+ timepicker: 'se counter',
56
+ fileupload: 'se counter',
57
+ },
58
+ counterPosition: {
59
+ textinput: 'teller over feltet er aldri støttet',
60
+ textarea: 'teller over feltet er aldri støttet',
61
+ select: 'teller over feltet er aldri støttet',
62
+ combobox: 'teller over feltet er aldri støttet',
63
+ datepicker: 'teller over feltet er aldri støttet',
64
+ timepicker: 'teller over feltet er aldri støttet',
65
+ fileupload: 'teller over feltet er aldri støttet',
66
+ },
67
+ hasFieldset: {
68
+ fileupload: 'ikke støttet',
69
+ },
70
+ inline: { fileupload: 'ikke et enkelt felt på en linje' },
71
+ useWrapper: { fileupload: 'wrapperen rendres bare når label er satt' },
72
+ ariaDescribedby: { combobox: 'ikke implementert ennå', fileupload: 'ikke implementert ennå' },
73
+ errorMessage: { fileupload: 'egen feilvisning' },
74
+ size: { fileupload: 'har ikke størrelsesvarianter' },
75
+ role: {},
76
+ forId: {},
77
+ }
78
+
79
+ // Navn som ikke er egne props på wrapper-kallet, men beregnes eller heter noe annet.
80
+ const NOT_A_FORWARDED_PROP = new Set(['forId', 'role'])
81
+
82
+ const read = (rel: string) => readFileSync(join(ROOT, rel), 'utf8')
83
+
84
+ /** Henter ut wrapper-kallet fra kildekoden, der propsene videresendes. */
85
+ const wrapperCall = (source: string, framework: 'elements' | 'react') => {
86
+ const open = framework === 'elements' ? '<pkt-input-wrapper' : '<PktInputWrapper'
87
+ const start = source.indexOf(open)
88
+ if (start < 0) return ''
89
+ // Kallet slutter ved første '>' som avslutter åpningstaggen
90
+ const rest = source.slice(start)
91
+ const end = rest.search(/\n\s*>/)
92
+ return end < 0 ? rest.slice(0, 4000) : rest.slice(0, end)
93
+ }
94
+
95
+ const forwards = (block: string, prop: string) =>
96
+ new RegExp(`[.?]?${prop}\\s*=`).test(block) || new RegExp(`\\b${prop}\\b`).test(block)
97
+
98
+ const wrapperProps = Object.keys(wrapperSpec.props).filter((p) => !NOT_A_FORWARDED_PROP.has(p))
99
+
100
+ describe('input-wrapper: paritet mellom rammeverk', () => {
101
+ for (const [name, paths] of Object.entries(COMPONENTS)) {
102
+ describe(name, () => {
103
+ const elements = wrapperCall(read(paths.elements), 'elements')
104
+ const react = wrapperCall(read(paths.react), 'react')
105
+
106
+ test('begge rammeverk kaller input-wrapper', () => {
107
+ expect(elements, `${paths.elements} mangler <pkt-input-wrapper`).not.toBe('')
108
+ expect(react, `${paths.react} mangler <PktInputWrapper`).not.toBe('')
109
+ })
110
+
111
+ for (const prop of wrapperProps) {
112
+ const omitted = OMITTED[prop]?.[name as keyof typeof COMPONENTS]
113
+
114
+ test(`${prop}${omitted ? ' (utelatt med vilje)' : ''}`, () => {
115
+ const inElements = forwards(elements, prop)
116
+ const inReact = forwards(react, prop)
117
+
118
+ if (omitted) {
119
+ expect(
120
+ inElements || inReact,
121
+ `${prop} er listet som utelatt for ${name} (${omitted}), men videresendes likevel i ` +
122
+ `${inElements ? 'Elements' : 'React'}. Fjern den fra OMITTED hvis den skal støttes.`,
123
+ ).toBe(false)
124
+ return
125
+ }
126
+
127
+ expect(
128
+ inElements,
129
+ `Elements' ${name} videresender ikke ${prop} til input-wrapper. Legg den til, eller ` +
130
+ `dokumentér utelatelsen i OMITTED.`,
131
+ ).toBe(true)
132
+ expect(
133
+ inReact,
134
+ `React-versjonen av ${name} videresender ikke ${prop} til input-wrapper. Legg den til, ` +
135
+ `eller dokumentér utelatelsen i OMITTED.`,
136
+ ).toBe(true)
137
+ })
138
+ }
139
+ })
140
+ }
141
+ })
@@ -5,6 +5,7 @@ import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
5
5
  import { CustomElementFor } from '../../tests/component-registry'
6
6
  import { type IPktSelect, type TSelectOption } from './select'
7
7
  import './select'
8
+ import { PktInputWrapper } from '@/components/input-wrapper/input-wrapper'
8
9
 
9
10
  export interface SelectTestConfig extends Partial<IPktSelect>, BaseTestConfig {
10
11
  id?: string
@@ -15,8 +16,8 @@ export interface SelectTestConfig extends Partial<IPktSelect>, BaseTestConfig {
15
16
  errorMessage?: string
16
17
  helptext?: string
17
18
  fullwidth?: boolean
18
- ariaLabelledby?: string | null
19
- ariaDescribedBy?: string | null
19
+ 'aria-labelledby'?: string | null
20
+ 'aria-describedby'?: string | null
20
21
  counter?: boolean
21
22
  inline?: boolean
22
23
  useWrapper?: boolean | string
@@ -202,15 +203,15 @@ describe('pkt-select', () => {
202
203
 
203
204
  test('passes through accessibility attributes', async () => {
204
205
  const { select } = await createSelectTest({
205
- ariaLabelledby: 'external-label',
206
- ariaDescribedBy: 'external-description',
206
+ 'aria-labelledby': 'external-label',
207
+ 'aria-describedby': 'external-description',
207
208
  })
208
209
 
209
210
  const selectElement = select.querySelector('select')
210
211
  expect(selectElement?.getAttribute('aria-labelledby')).toBe('external-label')
211
212
 
212
- const inputWrapper = select.querySelector('pkt-input-wrapper')
213
- expect(inputWrapper?.getAttribute('ariaDescribedBy')).toBe('external-description')
213
+ const inputWrapper = select.querySelector('pkt-input-wrapper') as PktInputWrapper | null
214
+ expect(inputWrapper?.ariaDescribedby).toBe('external-description')
214
215
  })
215
216
 
216
217
  test('handles input wrapper properties', async () => {
@@ -220,8 +221,8 @@ describe('pkt-select', () => {
220
221
  })
221
222
 
222
223
  const inputWrapper = select.querySelector('pkt-input-wrapper')
223
- expect(inputWrapper?.hasAttribute('counter')).toBe(true)
224
224
  expect(inputWrapper?.hasAttribute('inline')).toBe(true)
225
+ expect(inputWrapper?.hasAttribute('counter')).toBe(false)
225
226
  })
226
227
 
227
228
  test('handles useWrapper false', async () => {
@@ -7,6 +7,7 @@ import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
7
7
  import { slotContent } from '@/directives/slot-content'
8
8
  import type { TSelectOption } from 'shared-types/select'
9
9
  import '@/components/input-wrapper'
10
+ import { describedByIds } from 'shared-utils/forms/described-by'
10
11
 
11
12
  export type { TSelectOption } from 'shared-types/select'
12
13
 
@@ -162,6 +163,7 @@ export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> impleme
162
163
 
163
164
  protected firstUpdated(_changedProperties: PropertyValues): void {
164
165
  super.firstUpdated(_changedProperties)
166
+ this.classList.add('pkt-select')
165
167
  if (this._optionsProp.length) {
166
168
  this._options = this._optionsProp
167
169
  }
@@ -181,16 +183,14 @@ export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> impleme
181
183
  return html`
182
184
  <pkt-input-wrapper
183
185
  .size=${this.inputSize}
184
- ?counter=${this.counter}
185
186
  ?disabled=${this.disabled}
186
187
  ?hasError=${this.hasError}
187
- ?hasFieldset=${this.hasFieldset}
188
+ ?hasFieldset=${this.effectiveHasFieldset(false)}
188
189
  ?inline=${this.inline}
189
190
  ?optionalTag=${this.optionalTag}
190
191
  ?requiredTag=${this.requiredTag}
191
192
  useWrapper=${this.useWrapper}
192
- ariaDescribedBy=${ifDefined(this.ariaDescribedBy)}
193
- class="pkt-select"
193
+ .ariaDescribedby=${this.ariaDescribedby}
194
194
  errorMessage=${ifDefined(this.errorMessage)}
195
195
  forId=${this.id + '-input'}
196
196
  helptext=${ifDefined(this.helptext)}
@@ -204,7 +204,15 @@ export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> impleme
204
204
  <select
205
205
  class=${inputClass}
206
206
  aria-invalid=${this.hasError}
207
- aria-errormessage=${`${this.id}-error`}
207
+ aria-errormessage=${ifDefined(this.hasError ? `${this.id}-input-error` : undefined)}
208
+ aria-describedby=${ifDefined(
209
+ describedByIds({
210
+ id: this.id + '-input',
211
+ hasHelptext: !!this.helptext,
212
+ hasCounter: false,
213
+ ariaDescribedby: this.ariaDescribedby,
214
+ }),
215
+ )}
208
216
  aria-labelledby=${ifDefined(this.ariaLabelledby)}
209
217
  ?disabled=${this.disabled}
210
218
  id=${this.id + '-input'}
@@ -28,8 +28,8 @@ export interface TextareaTestConfig extends BaseTestConfig {
28
28
  fullwidth?: boolean
29
29
  counter?: boolean
30
30
  inline?: boolean
31
- ariaLabelledby?: string | null
32
- ariaDescribedBy?: string | null
31
+ 'aria-labelledby'?: string | null
32
+ 'aria-describedby'?: string | null
33
33
  inputSize?: TPktInputSize
34
34
  }
35
35
 
@@ -246,15 +246,15 @@ describe('PktTextarea', () => {
246
246
  describe('Accessibility', () => {
247
247
  test('passes through accessibility attributes', async () => {
248
248
  const { textarea } = await createTextareaTest({
249
- ariaLabelledby: 'external-label',
250
- ariaDescribedBy: 'external-description',
249
+ 'aria-labelledby': 'external-label',
250
+ 'aria-describedby': 'external-description',
251
251
  })
252
252
 
253
253
  const textareaElement = textarea.querySelector('textarea')
254
254
  expect(textareaElement?.getAttribute('aria-labelledby')).toBe('external-label')
255
255
 
256
- // ariaDescribedBy is passed as property to input-wrapper
257
- expect(textarea.ariaDescribedBy).toBe('external-description')
256
+ // ariaDescribedby videresendes som property til input-wrapper
257
+ expect(textarea.ariaDescribedby).toBe('external-description')
258
258
  })
259
259
 
260
260
  test('textarea is accessible', async () => {