@oslokommune/punkt-elements 19.5.0 → 19.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/{accordion-CZ0O4c0_.cjs → accordion-5lSFQToJ.cjs} +6 -6
  3. package/dist/{accordion-DFcdonl-.js → accordion-BJh6X1Ek.js} +6 -6
  4. package/dist/{alert-CUOWdvit.js → alert-C0PiYnot.js} +28 -35
  5. package/dist/alert-IaZzEgyI.cjs +39 -0
  6. package/dist/{backlink-vLhK1hsj.cjs → backlink-BRqhwUr2.cjs} +1 -1
  7. package/dist/base-elements/checkable-native-parity.test.d.ts +0 -0
  8. package/dist/base-elements/form-disabled.test.d.ts +0 -0
  9. package/dist/base-elements/input-element.d.ts +10 -1
  10. package/dist/{button-B0uSFQVn.js → button-B78im4wB.js} +15 -15
  11. package/dist/button-BNVTj7nB.cjs +32 -0
  12. package/dist/{card-CfFW7_-K.js → card-CYKhPUNI.js} +11 -11
  13. package/dist/{card-C15acwk4.cjs → card-CaLEuumN.cjs} +10 -10
  14. package/dist/{checkbox-B2O0fkh9.js → checkbox-Bbxt-tVW.js} +8 -8
  15. package/dist/checkbox-_NnoYkue.cjs +40 -0
  16. package/dist/{combobox-DvsnfA0c.cjs → combobox-BBx87DCG.cjs} +27 -27
  17. package/dist/{combobox-Bhfh04Su.js → combobox-CjLX_Gfd.js} +41 -41
  18. package/dist/components/alert/alert.d.ts +0 -2
  19. package/dist/{consent-_DBvIRT2.js → consent-CleJMbVH.js} +1 -1
  20. package/dist/{consent-BfLkJbOk.cjs → consent-Dl-R0iC3.cjs} +1 -1
  21. package/dist/{datepicker-Cg-iueRC.cjs → datepicker-Bk-m5Zaa.cjs} +31 -31
  22. package/dist/{datepicker-BY2IKrPk.js → datepicker-CFCRxFQL.js} +13 -13
  23. package/dist/directives/slot-content.d.ts +36 -1
  24. package/dist/element-with-slot-D7sTqOr3.cjs +1 -0
  25. package/dist/{element-with-slot-FD826ooP.js → element-with-slot-DQv0cbbD.js} +80 -21
  26. package/dist/{fileupload-iuXrgrog.js → fileupload-B2606SvG.js} +31 -31
  27. package/dist/{fileupload-Djp6VJYY.cjs → fileupload-COAxc3sX.cjs} +21 -21
  28. package/dist/{footer-BCdb0wlF.js → footer-DdLls3F-.js} +13 -13
  29. package/dist/{footer-bTpLxigB.cjs → footer-vy9NqT65.cjs} +9 -9
  30. package/dist/{header-2fF-Zu5T.js → header-BQdc29IO.js} +55 -55
  31. package/dist/{header-DbSSOX-h.cjs → header-BhM7FRoC.cjs} +50 -50
  32. package/dist/{header-menu-DVYLZyez.js → header-menu-ANj5nL2M.js} +1 -1
  33. package/dist/{header-menu-ChDMnWhW.cjs → header-menu-BfkCp8jw.cjs} +1 -1
  34. package/dist/{heading-BRHCV_5J.js → heading-CUrZvkrM.js} +1 -1
  35. package/dist/{heading-u9Arof4p.cjs → heading-zoz-QZJ9.cjs} +1 -1
  36. package/dist/{helptext-CmsuAXSE.cjs → helptext-BCkwSz1m.cjs} +2 -2
  37. package/dist/{helptext-4TAHKV5_.js → helptext-DCT_9dw1.js} +1 -1
  38. package/dist/input-element-CSfDGslx.cjs +1 -0
  39. package/dist/{input-element-B1BntJeJ.js → input-element-lLzfwdim.js} +198 -173
  40. package/dist/{input-wrapper-B5rqKWSB.cjs → input-wrapper-BBG8EXQ8.cjs} +28 -28
  41. package/dist/{input-wrapper-CaMP5-Vm.js → input-wrapper-Bz9GwYrn.js} +46 -46
  42. package/dist/{link-CCFAmhL5.js → link-CmZwAw5u.js} +1 -1
  43. package/dist/{link-Bo2o0Akv.cjs → link-LO_5_La8.cjs} +2 -2
  44. package/dist/{linkcard-9L6QF7US.js → linkcard-CQluSyJJ.js} +7 -7
  45. package/dist/{linkcard-D7NbrluR.cjs → linkcard-UKsxADrP.cjs} +3 -3
  46. package/dist/{listbox-DEzd-dxF.js → listbox-Ct9jIV_5.js} +8 -8
  47. package/dist/{listbox-BoBwzL4p.cjs → listbox-g-vHHLkD.cjs} +20 -20
  48. package/dist/{loader-CQaGvyF9.cjs → loader-C_PH05aw.cjs} +2 -2
  49. package/dist/{loader-RQQ_o2aR.js → loader-DzctJ9jW.js} +1 -1
  50. package/dist/{messagebox-C-BQREWz.js → messagebox-DPFBuisE.js} +1 -1
  51. package/dist/{messagebox-BRqI3DW2.cjs → messagebox-JWKMJEFc.cjs} +2 -2
  52. package/dist/{modal-BGTTVHeW.cjs → modal-BymjMIJi.cjs} +2 -2
  53. package/dist/{modal-DCwqqNQc.js → modal-CELmA0-8.js} +1 -1
  54. package/dist/pkt-accordion.cjs +1 -1
  55. package/dist/pkt-accordion.js +1 -1
  56. package/dist/pkt-alert.cjs +1 -1
  57. package/dist/pkt-alert.js +1 -1
  58. package/dist/pkt-backlink.cjs +1 -1
  59. package/dist/pkt-backlink.js +1 -1
  60. package/dist/pkt-button.cjs +1 -1
  61. package/dist/pkt-button.js +1 -1
  62. package/dist/pkt-card.cjs +1 -1
  63. package/dist/pkt-card.js +1 -1
  64. package/dist/pkt-checkbox.cjs +1 -1
  65. package/dist/pkt-checkbox.js +1 -1
  66. package/dist/pkt-combobox.cjs +1 -1
  67. package/dist/pkt-combobox.js +1 -1
  68. package/dist/pkt-consent.cjs +1 -1
  69. package/dist/pkt-consent.js +1 -1
  70. package/dist/pkt-datepicker.cjs +1 -1
  71. package/dist/pkt-datepicker.js +1 -1
  72. package/dist/pkt-fileupload.cjs +1 -1
  73. package/dist/pkt-fileupload.js +1 -1
  74. package/dist/pkt-footer.cjs +1 -1
  75. package/dist/pkt-footer.js +1 -1
  76. package/dist/pkt-header-menu.cjs +1 -1
  77. package/dist/pkt-header-menu.js +1 -1
  78. package/dist/pkt-header.cjs +1 -1
  79. package/dist/pkt-header.js +1 -1
  80. package/dist/pkt-heading.cjs +1 -1
  81. package/dist/pkt-heading.js +1 -1
  82. package/dist/pkt-helptext.cjs +1 -1
  83. package/dist/pkt-helptext.js +1 -1
  84. package/dist/pkt-index.cjs +1 -1
  85. package/dist/pkt-index.js +30 -30
  86. package/dist/pkt-input-wrapper.cjs +1 -1
  87. package/dist/pkt-input-wrapper.js +1 -1
  88. package/dist/pkt-link.cjs +1 -1
  89. package/dist/pkt-link.js +1 -1
  90. package/dist/pkt-linkcard.cjs +1 -1
  91. package/dist/pkt-linkcard.js +1 -1
  92. package/dist/pkt-listbox.cjs +1 -1
  93. package/dist/pkt-listbox.js +1 -1
  94. package/dist/pkt-loader.cjs +1 -1
  95. package/dist/pkt-loader.js +1 -1
  96. package/dist/pkt-messagebox.cjs +1 -1
  97. package/dist/pkt-messagebox.js +1 -1
  98. package/dist/pkt-modal.cjs +1 -1
  99. package/dist/pkt-modal.js +1 -1
  100. package/dist/{pkt-options-controller-Dz-rYCG1.cjs → pkt-options-controller-CpEZcomb.cjs} +1 -1
  101. package/dist/{pkt-options-controller-sUzxHa5A.js → pkt-options-controller-D7l18Iw0.js} +1 -1
  102. package/dist/pkt-progressbar.cjs +1 -1
  103. package/dist/pkt-progressbar.js +1 -1
  104. package/dist/pkt-radiobutton.cjs +1 -1
  105. package/dist/pkt-radiobutton.js +1 -1
  106. package/dist/pkt-searchinput.cjs +1 -1
  107. package/dist/pkt-searchinput.js +1 -1
  108. package/dist/pkt-select.cjs +1 -1
  109. package/dist/pkt-select.js +1 -1
  110. package/dist/pkt-tabs.cjs +1 -1
  111. package/dist/pkt-tabs.js +1 -1
  112. package/dist/pkt-tag.cjs +1 -1
  113. package/dist/pkt-tag.js +1 -1
  114. package/dist/pkt-textarea.cjs +1 -1
  115. package/dist/pkt-textarea.js +1 -1
  116. package/dist/pkt-textinput.cjs +1 -1
  117. package/dist/pkt-textinput.js +1 -1
  118. package/dist/pkt-timepicker.cjs +1 -1
  119. package/dist/pkt-timepicker.js +1 -1
  120. package/dist/{progressbar-CAEQkFpP.cjs → progressbar-CIXNSwTy.cjs} +2 -3
  121. package/dist/{progressbar-DzX0p2vn.js → progressbar-DH5lIBpx.js} +6 -4
  122. package/dist/{radiobutton-Cv1BlwLV.cjs → radiobutton-C3kEjvHQ.cjs} +2 -2
  123. package/dist/{radiobutton-B0V-jSEX.js → radiobutton-D9htviZG.js} +5 -5
  124. package/dist/{searchinput-D6vtMUHw.cjs → searchinput-CbClDE-d.cjs} +1 -1
  125. package/dist/{select-C0GafKtu.js → select-BizHes0n.js} +20 -20
  126. package/dist/{select-Bd6Gg4Ar.cjs → select-BuQxosyi.cjs} +15 -15
  127. package/dist/{tabs-B134M9k2.js → tabs-B8blHQrL.js} +18 -18
  128. package/dist/{tabs-Dw9E06fL.cjs → tabs-CjX-GWvM.cjs} +15 -15
  129. package/dist/tag-B5dUg7AD.cjs +27 -0
  130. package/dist/{tag-CvFakm1k.js → tag-Bns7Xj1O.js} +10 -10
  131. package/dist/{textarea-CeqScqQa.cjs → textarea-BXadsEdL.cjs} +12 -12
  132. package/dist/{textarea-BZ2ufamc.js → textarea-CL1yUDVL.js} +18 -18
  133. package/dist/{textinput-DI8Tgmep.js → textinput-Csil0Zda.js} +16 -16
  134. package/dist/{textinput-L218fT2-.cjs → textinput-qi0f5NZH.cjs} +12 -12
  135. package/dist/{timepicker-ZSHZt0aW.cjs → timepicker-CvL2-Siz.cjs} +17 -17
  136. package/dist/{timepicker-CgXpmG3h.js → timepicker-DNoYMBAL.js} +22 -22
  137. package/package.json +2 -2
  138. package/src/components/alert/alert.test.ts +15 -0
  139. package/src/components/alert/alert.ts +0 -12
  140. package/src/components/checkbox/checkbox.ts +6 -9
  141. package/src/components/combobox/combobox-handlers.ts +6 -6
  142. package/src/components/combobox/combobox-value.ts +1 -1
  143. package/src/components/combobox/combobox.ts +8 -8
  144. package/src/components/datepicker/datepicker.ts +9 -9
  145. package/src/components/helptext/helptext-association.test.ts +79 -0
  146. package/src/components/input-wrapper/input-wrapper.test.ts +85 -0
  147. package/src/components/input-wrapper/input-wrapper.ts +17 -8
  148. package/src/components/progressbar/progressbar.test.ts +19 -0
  149. package/src/components/progressbar/progressbar.ts +2 -6
  150. package/src/components/radiobutton/radiobutton.ts +3 -6
  151. package/src/components/select/select.ts +5 -5
  152. package/src/components/textarea/textarea.ts +5 -5
  153. package/src/components/textinput/textinput.ts +5 -5
  154. package/src/components/timepicker/timepicker.ts +10 -10
  155. package/dist/alert-DcyO0WfW.cjs +0 -40
  156. package/dist/button-PcKtiNGS.cjs +0 -32
  157. package/dist/checkbox-C0zLtnCI.cjs +0 -40
  158. package/dist/element-with-slot-2PHu2w7O.cjs +0 -1
  159. package/dist/input-element-W6Gklt2E.cjs +0 -1
  160. package/dist/tag--kFPXNaj.cjs +0 -27
  161. package/dist/{backlink-DdyQq62f.js → backlink-BslFewn-.js} +1 -1
  162. package/dist/{searchinput-DyjUl08X.js → searchinput-Co8tcq-l.js} +1 -1
@@ -13,7 +13,7 @@ import type {
13
13
  import { findOptionByValue, findOptionIndex } from 'shared-utils/combobox/option-utils'
14
14
  import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
15
15
  import { slotUtils, optionStateUtils } from './combobox-utils'
16
- import { slotContent } from '@/directives/slot-content'
16
+ import { forwardSlots } from '@/directives/slot-content'
17
17
  import { ComboboxHandlers } from './combobox-handlers'
18
18
  import { formatString } from 'shared-strings'
19
19
  import type { IPktStringsProps } from 'shared-strings'
@@ -338,6 +338,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
338
338
  render() {
339
339
  return html`
340
340
  <pkt-input-wrapper
341
+ ${forwardSlots(this, ['helptext'])}
341
342
  .strings=${this.strings}
342
343
  .size=${this.inputSize}
343
344
  .label=${this.label}
@@ -350,7 +351,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
350
351
  .counterMaxLength=${this.maxlength ?? 0}
351
352
  ?hasError=${this.hasError}
352
353
  ?inline=${this.inline}
353
- ?disabled=${this.disabled}
354
+ ?disabled=${this.isDisabled}
354
355
  .errorMessage=${this.errorMessage}
355
356
  ?optionalTag=${this.optionalTag}
356
357
  .optionalText=${this.optionalText}
@@ -363,7 +364,6 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
363
364
  class="pkt-combobox__wrapper"
364
365
  @labelClick=${this.handleInputClick}
365
366
  >
366
- <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
367
367
  <div class="pkt-combobox" @focusout=${this.handleFocusOut}>
368
368
  <div
369
369
  class=${classMap({
@@ -372,7 +372,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
372
372
  'pkt-combobox__input--fullwidth': this.fullwidth,
373
373
  'pkt-combobox__input--open': this._isOptionsOpen,
374
374
  'pkt-combobox__input--error': this.hasError,
375
- 'pkt-combobox__input--disabled': this.disabled,
375
+ 'pkt-combobox__input--disabled': this.isDisabled,
376
376
  })}
377
377
  id=${ifDefined(!this._hasTextInput ? `${this.id}-combobox` : undefined)}
378
378
  role=${ifDefined(!this._hasTextInput ? 'combobox' : undefined)}
@@ -394,7 +394,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
394
394
  : undefined,
395
395
  )}
396
396
  aria-description=${ifDefined(this._selectionDescription || undefined)}
397
- tabindex=${!this._hasTextInput ? (this.disabled ? '-1' : '0') : '-1'}
397
+ tabindex=${!this._hasTextInput ? (this.isDisabled ? '-1' : '0') : '-1'}
398
398
  @click=${this.handleInputClick}
399
399
  @keydown=${!this._hasTextInput ? this.handleSelectOnlyKeydown : nothing}
400
400
  @blur=${this.restoreInputTab}
@@ -429,7 +429,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
429
429
  .isOpen=${this._isOptionsOpen}
430
430
  .searchPlaceholder=${this.searchPlaceholder}
431
431
  .label="Liste: ${this.label || ''}"
432
- ?disabled=${this.disabled}
432
+ ?disabled=${this.isDisabled}
433
433
  ?include-search=${this.includeSearch}
434
434
  ?is-multi-select=${this.multiple}
435
435
  ?allow-user-input=${this.allowUserInput && !this._maxIsReached}
@@ -467,7 +467,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
467
467
  private _describedBy(): string | undefined {
468
468
  return describedByIds({
469
469
  id: this._hasTextInput ? `${this.id}-input` : `${this.id}-combobox`,
470
- hasHelptext: !!this.helptext,
470
+ hasHelptext: !!this.helptext || this.hasSlotContent('helptext'),
471
471
  ariaDescribedby: this.ariaDescribedby,
472
472
  })
473
473
  }
@@ -551,7 +551,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
551
551
  >
552
552
  <pkt-tag
553
553
  skin=${tagSkinColor || 'blue-dark'}
554
- ?closeTag=${!this.disabled}
554
+ ?closeTag=${!this.isDisabled}
555
555
  .buttonTabindex=${isOutside ? undefined : -1}
556
556
  @close=${() => this.handleTagRemove(value)}
557
557
  @keydown=${
@@ -14,7 +14,7 @@ import './datepicker-popup'
14
14
  import './datepicker-single'
15
15
  import './datepicker-range'
16
16
  import './datepicker-multiple'
17
- import { slotContent } from '@/directives/slot-content'
17
+ import { forwardSlots } from '@/directives/slot-content'
18
18
  import {
19
19
  valueUtils,
20
20
  inputTypeUtils,
@@ -301,12 +301,12 @@ export class PktDatepicker extends PktInputElement<Props> {
301
301
  .max=${this.max}
302
302
  .placeholder=${this.placeholder}
303
303
  .readonly=${this.readonly}
304
- .disabled=${this.disabled}
304
+ .disabled=${this.isDisabled}
305
305
  .inputClasses=${this.inputClasses}
306
306
  .inputSize=${this.inputSize}
307
307
  .internals=${this.internals}
308
308
  .hasError=${this.hasError}
309
- .hasHelptext=${Boolean(this.helptext || this.helptextDropdown)}
309
+ .hasHelptext=${Boolean(this.helptext || this.helptextDropdown || this.hasSlotContent('helptext'))}
310
310
  .strings=${this.strings}
311
311
  @toggle-calendar=${(e: CustomEvent) => this.toggleCalendar(e.detail)}
312
312
  @input-change=${() => this.onInput()}
@@ -339,13 +339,13 @@ export class PktDatepicker extends PktInputElement<Props> {
339
339
  .max=${this.max}
340
340
  .placeholder=${this.placeholder}
341
341
  .readonly=${this.readonly}
342
- .disabled=${this.disabled}
342
+ .disabled=${this.isDisabled}
343
343
  .showRangeLabels=${this.showRangeLabels}
344
344
  .inputClasses=${this.inputClasses}
345
345
  .inputSize=${this.inputSize}
346
346
  .internals=${this.internals}
347
347
  .hasError=${this.hasError}
348
- .hasHelptext=${Boolean(this.helptext || this.helptextDropdown)}
348
+ .hasHelptext=${Boolean(this.helptext || this.helptextDropdown || this.hasSlotContent('helptext'))}
349
349
  .strings=${this.strings}
350
350
  @toggle-calendar=${(e: CustomEvent) => this.toggleCalendar(e.detail)}
351
351
  @input-change=${() => this.onInput()}
@@ -427,13 +427,13 @@ export class PktDatepicker extends PktInputElement<Props> {
427
427
  .max=${this.max}
428
428
  .placeholder=${this.placeholder}
429
429
  .readonly=${this.readonly}
430
- .disabled=${this.disabled}
430
+ .disabled=${this.isDisabled}
431
431
  .maxlength=${this.maxlength}
432
432
  .inputClasses=${this.inputClasses}
433
433
  .inputSize=${this.inputSize}
434
434
  .internals=${this.internals}
435
435
  .hasError=${this.hasError}
436
- .hasHelptext=${Boolean(this.helptext || this.helptextDropdown)}
436
+ .hasHelptext=${Boolean(this.helptext || this.helptextDropdown || this.hasSlotContent('helptext'))}
437
437
  .strings=${this.strings}
438
438
  @toggle-calendar=${(e: CustomEvent) => this.toggleCalendar(e.detail)}
439
439
  @input-change=${() => this.onInput()}
@@ -505,13 +505,14 @@ export class PktDatepicker extends PktInputElement<Props> {
505
505
 
506
506
  return html`
507
507
  <pkt-input-wrapper
508
+ ${forwardSlots(this, ['helptext'])}
508
509
  .strings=${this.strings}
509
510
  label="${this.label}"
510
511
  forId="${this.id}-input"
511
512
  ?counter=${this.multiple && !!this.maxlength}
512
513
  .counterCurrent=${this.value ? this._value.length : 0}
513
514
  .counterMaxLength=${this.maxlength}
514
- ?disabled=${this.disabled}
515
+ ?disabled=${this.isDisabled}
515
516
  ?hasError=${this.hasError}
516
517
  ?hasFieldset=${this.effectiveHasFieldset(false)}
517
518
  ?inline=${this.inline}
@@ -529,7 +530,6 @@ export class PktDatepicker extends PktInputElement<Props> {
529
530
  .ariaDescribedby=${this.ariaDescribedby}
530
531
  .size=${this.inputSize}
531
532
  >
532
- <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
533
533
  ${this.multiple
534
534
  ? html`<pkt-date-tags
535
535
  .dates=${this._value}
@@ -82,6 +82,85 @@ describe('helptext association', () => {
82
82
  expect(input?.getAttribute('aria-label')).toBe('Velg fil')
83
83
  })
84
84
 
85
+ describe('helptext from the slot', () => {
86
+ const slotCases: Array<[string, string]> = [
87
+ ['pkt-textinput', ''],
88
+ ['pkt-textarea', ''],
89
+ ['pkt-select', ''],
90
+ ['pkt-datepicker', ''],
91
+ ['pkt-timepicker', ''],
92
+ ['pkt-combobox', ''],
93
+ ['pkt-combobox', 'allow-user-input'],
94
+ ]
95
+
96
+ const settle = async (host: HTMLElement & { updateComplete: Promise<unknown> }) => {
97
+ for (let i = 0; i < 3; i++) {
98
+ await new Promise((resolve) => setTimeout(resolve, 0))
99
+ await host.updateComplete
100
+ const nested = Array.from(
101
+ host.querySelectorAll<HTMLElement & { updateComplete: Promise<unknown> }>(
102
+ 'pkt-input-wrapper, pkt-helptext',
103
+ ),
104
+ )
105
+ await Promise.all(nested.map((el) => el.updateComplete))
106
+ }
107
+ }
108
+
109
+ const mountWithSlot = async (tag: string, attrs: string, slot: string) => {
110
+ document.body.innerHTML = `<${tag} id="felt" name="felt" label="Etikett" ${attrs}>${slot}</${tag}>`
111
+ await customElements.whenDefined(tag)
112
+ const host = document.querySelector(tag) as HTMLElement & { updateComplete: Promise<unknown> }
113
+ await settle(host)
114
+ return host
115
+ }
116
+
117
+ const controls = (host: HTMLElement) =>
118
+ host.localName === 'pkt-timepicker'
119
+ ? Array.from(host.querySelectorAll('[role="spinbutton"]'))
120
+ : [host.querySelector(CONTROL)]
121
+
122
+ const expectDescribedBySlot = (host: HTMLElement) => {
123
+ const helptext = host.querySelector('.pkt-inputwrapper__helptext')
124
+ expect(helptext?.textContent).toContain('Hjelp fra slot')
125
+ controls(host).forEach((control) => {
126
+ expect(control?.getAttribute('aria-describedby')?.split(' ')).toContain(helptext?.id)
127
+ })
128
+ }
129
+
130
+ test.each(slotCases)('%s %s renders slotted helptext inside the helptext element', async (tag, attrs) => {
131
+ const host = await mountWithSlot(tag, attrs, '<div slot="helptext">Hjelp fra slot</div>')
132
+
133
+ expectDescribedBySlot(host)
134
+ })
135
+
136
+ test.each(slotCases)('%s %s picks up slotted helptext added after the first render', async (tag, attrs) => {
137
+ const host = await mountWithSlot(tag, attrs, '')
138
+ controls(host).forEach((control) => {
139
+ expect(control?.getAttribute('aria-describedby') ?? '').not.toContain('helptext')
140
+ })
141
+
142
+ const slotted = document.createElement('div')
143
+ slotted.setAttribute('slot', 'helptext')
144
+ slotted.textContent = 'Hjelp fra slot'
145
+ host.appendChild(slotted)
146
+ await settle(host)
147
+
148
+ expectDescribedBySlot(host)
149
+ })
150
+
151
+ test.each(slotCases)('%s %s drops the link when slotted helptext is removed', async (tag, attrs) => {
152
+ const host = await mountWithSlot(tag, attrs, '<div slot="helptext" class="slotted">Hjelp fra slot</div>')
153
+ expectDescribedBySlot(host)
154
+
155
+ host.querySelector('.slotted')?.remove()
156
+ await settle(host)
157
+
158
+ controls(host).forEach((control) => {
159
+ expect(control?.getAttribute('aria-describedby') ?? '').not.toContain('helptext')
160
+ })
161
+ })
162
+ })
163
+
85
164
  test('no label carries the description', async () => {
86
165
  for (const [tag, attrs] of cases) {
87
166
  const host = await mount(tag, attrs)
@@ -373,6 +373,91 @@ describe('PktInputWrapper', () => {
373
373
  })
374
374
  })
375
375
 
376
+ describe('Fieldset name and description', () => {
377
+ const mountFieldset = async (attributes: string, content = '') => {
378
+ document.body.innerHTML = `<pkt-input-wrapper forId="g" label="Gruppe" hasFieldset ${attributes}>${content}</pkt-input-wrapper>`
379
+ await customElements.whenDefined('pkt-input-wrapper')
380
+ const wrapper = document.querySelector('pkt-input-wrapper') as CustomElementFor<'pkt-input-wrapper'>
381
+ await wrapper.updateComplete
382
+ return wrapper.querySelector('fieldset') as HTMLFieldSetElement
383
+ }
384
+
385
+ const describedByIds = (fieldset: HTMLFieldSetElement) =>
386
+ (fieldset.getAttribute('aria-describedby') ?? '').split(' ').filter(Boolean)
387
+
388
+ afterEach(() => {
389
+ document.body.innerHTML = ''
390
+ })
391
+
392
+ test('links the error message to the fieldset', async () => {
393
+ const fieldset = await mountFieldset('hasError errorMessage="Velg ett alternativ"')
394
+
395
+ expect(describedByIds(fieldset)).toContain('g-error')
396
+ expect(document.getElementById('g-error')).toBeInTheDocument()
397
+ })
398
+
399
+ test('links both helptext and error message to the fieldset', async () => {
400
+ const fieldset = await mountFieldset('helptext="Velg ett" hasError errorMessage="Mangler"')
401
+
402
+ expect(describedByIds(fieldset)).toEqual(['g-helptext', 'g-error'])
403
+ })
404
+
405
+ test('does not link an error message when hasError is off', async () => {
406
+ const fieldset = await mountFieldset('errorMessage="Mangler"')
407
+
408
+ expect(fieldset.hasAttribute('aria-describedby')).toBe(false)
409
+ })
410
+
411
+ test('links slotted helptext to the fieldset', async () => {
412
+ const fieldset = await mountFieldset('', '<div class="pkt-contents" slot="helptext">Velg ett</div>')
413
+
414
+ expect(describedByIds(fieldset)).toEqual(['g-helptext'])
415
+ })
416
+
417
+ test('renders slotted helptext inside the helptext element', async () => {
418
+ await mountFieldset('', '<div slot="helptext">Velg ett</div>')
419
+
420
+ expect(document.getElementById('g-helptext')?.textContent).toContain('Velg ett')
421
+ })
422
+
423
+ test('links slotted helptext added after the first render', async () => {
424
+ const fieldset = await mountFieldset('')
425
+ expect(fieldset.hasAttribute('aria-describedby')).toBe(false)
426
+ await new Promise((resolve) => setTimeout(resolve, 0))
427
+
428
+ const wrapper = document.querySelector('pkt-input-wrapper') as CustomElementFor<'pkt-input-wrapper'>
429
+ const slotted = document.createElement('div')
430
+ slotted.setAttribute('slot', 'helptext')
431
+ slotted.textContent = 'Velg ett'
432
+ wrapper.appendChild(slotted)
433
+ for (let i = 0; i < 3; i++) {
434
+ await new Promise((resolve) => setTimeout(resolve, 0))
435
+ await wrapper.updateComplete
436
+ await (wrapper.querySelector('pkt-helptext') as CustomElementFor<'pkt-helptext'>).updateComplete
437
+ }
438
+
439
+ expect(describedByIds(fieldset)).toEqual(['g-helptext'])
440
+ expect(document.getElementById('g-helptext')?.textContent).toContain('Velg ett')
441
+ })
442
+
443
+ test('only references ids that are rendered', async () => {
444
+ const fieldset = await mountFieldset('helptext="Velg ett" hasError errorMessage="Mangler"')
445
+
446
+ describedByIds(fieldset).forEach((id) => {
447
+ expect(document.getElementById(id)).toBeInTheDocument()
448
+ })
449
+ })
450
+
451
+ test('names the fieldset with a hidden legend when useWrapper is off', async () => {
452
+ const fieldset = await mountFieldset('useWrapper="false"')
453
+
454
+ const legend = fieldset.querySelector(':scope > legend')
455
+ expect(legend).toHaveClass('pkt-sr-only')
456
+ expect(legend?.textContent?.trim()).toBe('Gruppe')
457
+ expect(fieldset.querySelector('label')).toBeNull()
458
+ })
459
+ })
460
+
376
461
  describe('Wrapper functionality', () => {
377
462
  test('handles useWrapper property values', async () => {
378
463
  // Test true value
@@ -1,5 +1,5 @@
1
1
  import { PktElementWithSlot } from '@/base-elements/element-with-slot'
2
- import { slotContent } from '@/directives/slot-content'
2
+ import { forwardSlots, slotContent } from '@/directives/slot-content'
3
3
  import { counterStatusId, isCounterExceeded } from 'shared-utils/forms/described-by'
4
4
  import { formatString, type IPktStringsProps } from 'shared-strings'
5
5
  import { ifDefined } from 'lit/directives/if-defined.js'
@@ -99,11 +99,17 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
99
99
  render() {
100
100
  const tagClasses = 'pkt-tag pkt-tag--small pkt-tag--thin-text'
101
101
 
102
- const describedBy = this.ariaDescribedby
103
- ? this.ariaDescribedby
104
- : this.helptext
105
- ? `${this.forId}-helptext`
106
- : nothing
102
+ const hasHelptext = !!this.helptext || this.hasSlotContent('helptext')
103
+ const describedByIds = new Set(
104
+ [
105
+ this.ariaDescribedby || (hasHelptext ? `${this.forId}-helptext` : ''),
106
+ this.hasError && this.errorMessage ? `${this.forId}-error` : '',
107
+ ]
108
+ .join(' ')
109
+ .split(' ')
110
+ .filter(Boolean),
111
+ )
112
+ const describedBy = describedByIds.size ? [...describedByIds].join(' ') : nothing
107
113
 
108
114
  const formStrings = this.pktStrings.get('forms')
109
115
 
@@ -144,6 +150,8 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
144
150
  >${this.label}${tagElement()}</label
145
151
  >`
146
152
  }
153
+ } else if (this.hasFieldset) {
154
+ return html`<legend class="pkt-sr-only" id="${this.forId}-label">${this.label}</legend>`
147
155
  } else {
148
156
  return html`<label for="${this.forId}" class="pkt-sr-only" id="${this.forId}-label">
149
157
  ${this.label}
@@ -152,8 +160,10 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
152
160
  }
153
161
 
154
162
  const helptextElement = () => {
163
+ if (!hasHelptext && !this.helptextDropdown) return nothing
155
164
  return html`
156
165
  <pkt-helptext
166
+ ${forwardSlots(this, { helptext: null })}
157
167
  class="${ifDefined(!this.useWrapper ? 'pkt-hide' : undefined)}"
158
168
  .forId=${this.forId}
159
169
  .helptext=${this.helptext}
@@ -162,8 +172,7 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
162
172
  @toggleHelpText=${(e: CustomEvent) => {
163
173
  this.toggleDropdown(e)
164
174
  }}
165
- >${slotContent(this, 'helptext')}</pkt-helptext
166
- >
175
+ ></pkt-helptext>
167
176
  `
168
177
  }
169
178
 
@@ -26,6 +26,25 @@ afterEach(() => {
26
26
  })
27
27
 
28
28
  describe('PktProgressbar', () => {
29
+ describe('Host id', () => {
30
+ afterEach(() => {
31
+ document.body.innerHTML = ''
32
+ })
33
+
34
+ test('keeps the id on the host element', async () => {
35
+ document.body.innerHTML = '<pkt-progressbar id="min-framdrift" title="Framdrift"></pkt-progressbar>'
36
+ const progressbar = document.querySelector('pkt-progressbar') as CustomElementFor<'pkt-progressbar'>
37
+ await progressbar.updateComplete
38
+
39
+ expect(document.getElementById('min-framdrift')).toBe(progressbar)
40
+ expect(document.querySelectorAll('[id="min-framdrift"]')).toHaveLength(1)
41
+ expect(progressbar.querySelector('[role="progressbar"]')).toHaveAttribute(
42
+ 'aria-labelledby',
43
+ 'min-framdrift-title',
44
+ )
45
+ })
46
+ })
47
+
29
48
  describe('Rendering and basic functionality', () => {
30
49
  test('renders without errors', async () => {
31
50
  const { progressbar } = await createProgressbarTest()
@@ -45,7 +45,7 @@ export class PktProgressbar extends PktElement implements IPktProgressbar {
45
45
  @property({ type: String, reflect: true }) ariaLabelledby: string | null = null
46
46
  @property({ type: String, reflect: true }) ariaValueText: string | null = null
47
47
  @property({ type: String }) ariaLive: TAriaLive = 'polite'
48
- @property({ type: String }) id: string = uuidish()
48
+ @property({ type: String, reflect: true }) id: string = uuidish()
49
49
  @property({ type: String }) role: TProgressbarRole = 'progressbar'
50
50
  @property({ type: String }) skin: TProgressbarSkin = 'dark-blue'
51
51
  @property({ type: String }) statusPlacement: TProgressbarStatusPlacement = 'following'
@@ -67,10 +67,7 @@ export class PktProgressbar extends PktElement implements IPktProgressbar {
67
67
 
68
68
  protected willUpdate(changedProperties: Map<string | number | symbol, unknown>) {
69
69
  super.willUpdate(changedProperties)
70
- if (this.hasAttribute('id')) {
71
- this.progressbarId = this.getAttribute('id') || this.progressbarId
72
- this.removeAttribute('id')
73
- } else if (this.id) {
70
+ if (this.id) {
74
71
  this.progressbarId = this.id
75
72
  }
76
73
  // Verdien må fanges FØR removeAttribute: den utløser attributeChangedCallback,
@@ -131,7 +128,6 @@ export class PktProgressbar extends PktElement implements IPktProgressbar {
131
128
 
132
129
  return html`<div
133
130
  ${ref(this.progressBarRef)}
134
- id=${this.progressbarId}
135
131
  role=${this.resolvedRole}
136
132
  aria-live=${ifDefined(this.ariaLive || undefined)}
137
133
  aria-atomic="true"
@@ -42,9 +42,6 @@ export class PktRadioButton extends PktInputElement<Props> {
42
42
  }
43
43
 
44
44
  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
45
- if (name === 'defaultChecked') {
46
- this._checked = this.defaultChecked
47
- }
48
45
  if (name === 'checked') {
49
46
  this._checked = this.checked === '' || this.checked === 'true' || this.checked === true
50
47
  }
@@ -55,7 +52,7 @@ export class PktRadioButton extends PktInputElement<Props> {
55
52
  const inputTileClasses = classMap({
56
53
  'pkt-input-check__input': true,
57
54
  'pkt-input-check__input--tile': this.hasTile,
58
- 'pkt-input-check__input--tile-disabled': this.disabled && this.hasTile,
55
+ 'pkt-input-check__input--tile-disabled': this.isDisabled && this.hasTile,
59
56
  })
60
57
 
61
58
  const inputCheckBoxClasses = classMap({
@@ -65,7 +62,7 @@ export class PktRadioButton extends PktInputElement<Props> {
65
62
 
66
63
  const labelClasses = classMap({
67
64
  'pkt-input-check__input-label': true,
68
- 'pkt-input-check__input-label--disabled': this.disabled,
65
+ 'pkt-input-check__input-label--disabled': this.isDisabled,
69
66
  'pkt-sr-only': this.hideLabel,
70
67
  })
71
68
 
@@ -122,7 +119,7 @@ export class PktRadioButton extends PktInputElement<Props> {
122
119
  id=${this.id + '-internal'}
123
120
  class=${inputCheckBoxClasses}
124
121
  type="radio"
125
- ?disabled=${this.disabled}
122
+ ?disabled=${this.isDisabled}
126
123
  form=""
127
124
  name=${this.name + '-internal'}
128
125
  ${ref(this.inputRef)}
@@ -4,7 +4,7 @@ import { Ref, createRef, ref } from 'lit/directives/ref.js'
4
4
  import { ifDefined } from 'lit/directives/if-defined.js'
5
5
  import { PktOptionsInputElement } from '@/base-elements/options-input-element'
6
6
  import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
7
- import { slotContent } from '@/directives/slot-content'
7
+ import { forwardSlots } from '@/directives/slot-content'
8
8
  import type { TSelectOption } from 'shared-types/select'
9
9
  import '@/components/input-wrapper'
10
10
  import { describedByIds } from 'shared-utils/forms/described-by'
@@ -182,9 +182,10 @@ export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> impleme
182
182
 
183
183
  return html`
184
184
  <pkt-input-wrapper
185
+ ${forwardSlots(this, ['helptext'])}
185
186
  .strings=${this.strings}
186
187
  .size=${this.inputSize}
187
- ?disabled=${this.disabled}
188
+ ?disabled=${this.isDisabled}
188
189
  ?hasError=${this.hasError}
189
190
  ?hasFieldset=${this.effectiveHasFieldset(false)}
190
191
  ?inline=${this.inline}
@@ -209,13 +210,13 @@ export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> impleme
209
210
  aria-describedby=${ifDefined(
210
211
  describedByIds({
211
212
  id: this.id + '-input',
212
- hasHelptext: !!this.helptext,
213
+ hasHelptext: !!this.helptext || this.hasSlotContent('helptext'),
213
214
  hasCounter: false,
214
215
  ariaDescribedby: this.ariaDescribedby,
215
216
  }),
216
217
  )}
217
218
  aria-labelledby=${ifDefined(this.ariaLabelledby)}
218
- ?disabled=${this.disabled}
219
+ ?disabled=${this.isDisabled}
219
220
  id=${this.id + '-input'}
220
221
  name=${(this.name || this.id) + '-input'}
221
222
  @change=${(e: Event) => {
@@ -251,7 +252,6 @@ export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> impleme
251
252
  )
252
253
  : ''}
253
254
  </select>
254
- <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
255
255
  </pkt-input-wrapper>
256
256
  `
257
257
  }
@@ -5,7 +5,7 @@ import { Ref, createRef, ref } from 'lit/directives/ref.js'
5
5
  import { classMap } from 'lit/directives/class-map.js'
6
6
  import { live } from 'lit/directives/live.js'
7
7
  import { PktInputElement } from '@/base-elements/input-element'
8
- import { slotContent } from '@/directives/slot-content'
8
+ import { forwardSlots } from '@/directives/slot-content'
9
9
 
10
10
  import '@/components/input-wrapper'
11
11
  import '@/components/icon'
@@ -61,11 +61,12 @@ export class PktTextarea extends PktInputElement<Props> {
61
61
  const labelledBy = this.ariaLabelledby || `${this.id}-input-label`
62
62
 
63
63
  return html`<pkt-input-wrapper
64
+ ${forwardSlots(this, ['helptext'])}
64
65
  .strings=${this.strings}
65
66
  label=${this.label}
66
67
  ?counter=${showCounter}
67
68
  ?hasFieldset=${this.effectiveHasFieldset(false)}
68
- ?disabled=${this.disabled}
69
+ ?disabled=${this.isDisabled}
69
70
  ?hasError=${this.hasError}
70
71
  ?inline=${this.inline}
71
72
  ?optionalTag=${this.optionalTag}
@@ -85,7 +86,6 @@ export class PktTextarea extends PktInputElement<Props> {
85
86
  .tagText=${this.tagText}
86
87
  .size=${this.inputSize}
87
88
  >
88
- <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
89
89
  <textarea
90
90
  ${ref(this.inputRef)}
91
91
  class=${inputClasses}
@@ -103,14 +103,14 @@ export class PktTextarea extends PktInputElement<Props> {
103
103
  aria-describedby=${ifDefined(
104
104
  describedByIds({
105
105
  id: this.id + '-input',
106
- hasHelptext: !!this.helptext,
106
+ hasHelptext: !!this.helptext || this.hasSlotContent('helptext'),
107
107
  hasCounter: showCounter,
108
108
  ariaDescribedby: this.ariaDescribedby,
109
109
  }),
110
110
  )}
111
111
  rows=${ifDefined(this.rows ?? undefined)}
112
112
  cols=${ifDefined(this.cols ?? undefined)}
113
- ?disabled=${this.disabled}
113
+ ?disabled=${this.isDisabled}
114
114
  @input=${(e: InputEvent) => {
115
115
  this.value = (e.target as HTMLInputElement).value
116
116
  this.onInput()
@@ -5,7 +5,7 @@ import { Ref, createRef, ref } from 'lit/directives/ref.js'
5
5
  import { ElementProps } from '@/types/typeUtils'
6
6
  import { classMap } from 'lit/directives/class-map.js'
7
7
  import { PktInputElement } from '@/base-elements/input-element'
8
- import { slotContent } from '@/directives/slot-content'
8
+ import { forwardSlots } from '@/directives/slot-content'
9
9
 
10
10
  import '@/components/input-wrapper'
11
11
  import '@/components/icon'
@@ -77,11 +77,12 @@ export class PktTextinput extends PktInputElement<Props> {
77
77
 
78
78
  return html`
79
79
  <pkt-input-wrapper
80
+ ${forwardSlots(this, ['helptext'])}
80
81
  .strings=${this.strings}
81
82
  label="${this.label}"
82
83
  ?counter=${showCounter}
83
84
  ?hasFieldset=${this.effectiveHasFieldset(false)}
84
- ?disabled=${this.disabled}
85
+ ?disabled=${this.isDisabled}
85
86
  ?hasError=${this.hasError}
86
87
  ?inline=${this.inline}
87
88
  ?optionalTag=${this.optionalTag}
@@ -101,7 +102,6 @@ export class PktTextinput extends PktInputElement<Props> {
101
102
  .tagText=${this.tagText}
102
103
  .size=${this.inputSize}
103
104
  >
104
- <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
105
105
  <div class=${`pkt-input__container pkt-input__container--${this.inputSize}`}>
106
106
  ${this.prefix ? html`<div class="pkt-input-prefix">${this.prefix}</div>` : nothing}
107
107
  <input
@@ -121,13 +121,13 @@ export class PktTextinput extends PktInputElement<Props> {
121
121
  ?readonly=${this.readonly}
122
122
  size=${this.size || nothing}
123
123
  .value=${this.value}
124
- ?disabled=${this.disabled}
124
+ ?disabled=${this.isDisabled}
125
125
  aria-invalid=${this.hasError}
126
126
  aria-errormessage=${ifDefined(this.hasError ? `${this.id}-input-error` : undefined)}
127
127
  aria-describedby=${ifDefined(
128
128
  describedByIds({
129
129
  id: this.id + '-input',
130
- hasHelptext: !!this.helptext,
130
+ hasHelptext: !!this.helptext || this.hasSlotContent('helptext'),
131
131
  hasCounter: showCounter,
132
132
  ariaDescribedby: this.ariaDescribedby,
133
133
  }),