@oslokommune/punkt-elements 18.7.1 → 18.8.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 (192) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/{accordion-NO9LYXaN.cjs → accordion-CsrLQXEA.cjs} +1 -1
  3. package/dist/{accordion-BI-Qiznz.js → accordion-DoxhiwK-.js} +5 -5
  4. package/dist/{alert-HFqn-cvb.cjs → alert-DH3VqNqh.cjs} +1 -1
  5. package/dist/{alert-CayKowTW.js → alert-DxAGxSX_.js} +6 -6
  6. package/dist/{async-directive-CAZoZMez.js → async-directive-BSwJ8-GL.js} +2 -2
  7. package/dist/{async-directive-DlB0Sf4j.cjs → async-directive-CfPXShXu.cjs} +1 -1
  8. package/dist/{backlink-DYO8qA9C.cjs → backlink-CDH824db.cjs} +1 -1
  9. package/dist/{backlink-BHtxOlTw.js → backlink-CGc7r9CS.js} +3 -3
  10. package/dist/{breadcrumbs-HG9wzGtr.cjs → breadcrumbs-B37tsIoh.cjs} +1 -1
  11. package/dist/{breadcrumbs-BlRDuZbZ.js → breadcrumbs-DRGhIXmN.js} +2 -2
  12. package/dist/{button-CIb0IxTg.cjs → button-Cx6Xdp6t.cjs} +1 -1
  13. package/dist/{button-DWMHtdWk.js → button-D6QhHPWH.js} +5 -5
  14. package/dist/{calendar-CBghTpkZ.js → calendar-42o_q-jI.js} +3 -3
  15. package/dist/{calendar-CA1mz04B.cjs → calendar-CAwIY7x_.cjs} +1 -1
  16. package/dist/{card-CsSNvjZV.cjs → card-CB72gwBC.cjs} +1 -1
  17. package/dist/{card-PXDm9JXv.js → card-cyZE94EO.js} +6 -6
  18. package/dist/{checkbox-CMu4pF8-.cjs → checkbox-CBZEGmqa.cjs} +16 -8
  19. package/dist/{checkbox-DReZOEy2.js → checkbox-COw1Uwnm.js} +19 -11
  20. package/dist/{class-map-BL_YL_Ed.js → class-map-DSoj1J68.js} +1 -1
  21. package/dist/{class-map-BEib48YL.cjs → class-map-DupQbnhE.cjs} +1 -1
  22. package/dist/{combobox-DcBFiuwP.js → combobox-Cqqokx73.js} +172 -128
  23. package/dist/combobox-N3NP77le.cjs +137 -0
  24. package/dist/components/combobox/combobox-base.d.ts +14 -12
  25. package/dist/components/combobox/combobox-handlers.d.ts +1 -0
  26. package/dist/components/combobox/combobox-utils.d.ts +3 -1
  27. package/dist/components/combobox/combobox-value.d.ts +5 -0
  28. package/dist/components/combobox/combobox.attributes.test.d.ts +0 -0
  29. package/dist/components/combobox/combobox.d.ts +31 -2
  30. package/dist/components/combobox/combobox.preselection.test.d.ts +0 -0
  31. package/dist/components/combobox/index.d.ts +1 -1
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/listbox/listbox.d.ts +3 -1
  34. package/dist/{consent-Yt1MpkUy.js → consent-CsgKWDsR.js} +2 -2
  35. package/dist/{consent-D1SMUJLs.cjs → consent-DnE6IPEj.cjs} +1 -1
  36. package/dist/{datepicker-NSWMVoGi.cjs → datepicker-BzRhOIkG.cjs} +1 -1
  37. package/dist/{datepicker-DboWQcrh.js → datepicker-CtSYgxMs.js} +11 -11
  38. package/dist/{directive-helpers-Ddr8wUxL.cjs → directive-helpers-2mcpDDv5.cjs} +1 -1
  39. package/dist/{directive-helpers-CohhRNQz.js → directive-helpers-ChEduoup.js} +1 -1
  40. package/dist/docs/components/switch.d.ts +4 -0
  41. package/dist/{element-DRc10X2R.cjs → element-D_mCyFeX.cjs} +1 -1
  42. package/dist/{element-e2MAMNEP.js → element-J8nynuVk.js} +1 -0
  43. package/dist/{element-with-slot-GwTT3WRD.js → element-with-slot-BOuXlWSY.js} +2 -2
  44. package/dist/{element-with-slot-DgtT8lCH.cjs → element-with-slot-C66Jck5e.cjs} +1 -1
  45. package/dist/{fileupload-BYVtnSaI.cjs → fileupload-3OtW4Xlo.cjs} +1 -1
  46. package/dist/{fileupload-CqxWUH0y.js → fileupload-PJWEBD88.js} +8 -8
  47. package/dist/{footer-YrlXRq7e.js → footer-ByAxlj9_.js} +4 -4
  48. package/dist/{footer-CLSihvYS.cjs → footer-Ca3DM544.cjs} +1 -1
  49. package/dist/{header-DsaPCLjJ.js → header-CxfDtS3Y.js} +11 -11
  50. package/dist/{header-DzVHM7gZ.cjs → header-D2SD2JLt.cjs} +1 -1
  51. package/dist/{header-menu-wcnhBSzs.cjs → header-menu-B9ABa0WW.cjs} +1 -1
  52. package/dist/{header-menu-DKAcUFzh.js → header-menu-CIuu8_UC.js} +4 -4
  53. package/dist/{heading-OfJKjUi2.cjs → heading-4ZHACOeL.cjs} +1 -1
  54. package/dist/{heading-C23xQS4l.js → heading-BHWdVbvN.js} +2 -2
  55. package/dist/{helptext-DyHzv2ZG.cjs → helptext-6HsH-I6a.cjs} +1 -1
  56. package/dist/{helptext-DqdOdYRj.js → helptext-BESB9Vlq.js} +4 -4
  57. package/dist/{icon-DBFTFd-J.cjs → icon-BzxBkRyE.cjs} +1 -1
  58. package/dist/{icon-jEFbKTB_.js → icon-DkVqRWbC.js} +1 -1
  59. package/dist/{if-defined-r8tKY4tG.js → if-defined-BILLSM7i.js} +1 -1
  60. package/dist/if-defined-BbUZeoaQ.cjs +1 -0
  61. package/dist/{input-element-kvI_5er5.cjs → input-element-DhIEFeuP.cjs} +1 -1
  62. package/dist/{input-element-BiUfK_gU.js → input-element-Qb0w3ChW.js} +2 -2
  63. package/dist/{input-wrapper-Bwh720ke.js → input-wrapper-Bg06k-cj.js} +6 -6
  64. package/dist/{input-wrapper-wyvCnWPp.cjs → input-wrapper-D64Qn-QH.cjs} +1 -1
  65. package/dist/{link-BWAhHUku.cjs → link-Bk7aDZCC.cjs} +1 -1
  66. package/dist/{link-BvjWWWlc.js → link-C7fOEv66.js} +4 -4
  67. package/dist/{linkcard-6CbGf4I_.js → linkcard-Bguo9oCv.js} +4 -4
  68. package/dist/{linkcard-D6fRaZSc.cjs → linkcard-rZpQuO1Z.cjs} +1 -1
  69. package/dist/{listbox-CsP0qbU1.js → listbox-CrQboPG6.js} +80 -63
  70. package/dist/listbox-kV4OO1Qa.cjs +111 -0
  71. package/dist/{loader-B5t78Yk7.cjs → loader-BAn68gBb.cjs} +1 -1
  72. package/dist/{loader-BS7EJqxD.js → loader-C3gwO0i3.js} +4 -4
  73. package/dist/{messagebox-Bt0IvUlD.js → messagebox-DAE59dMX.js} +4 -4
  74. package/dist/{messagebox-DBVUz-DN.cjs → messagebox-WH2HjT5I.cjs} +1 -1
  75. package/dist/{modal-C1g2fiUq.cjs → modal-DZm_fn3K.cjs} +1 -1
  76. package/dist/{modal-z5RzZ-fr.js → modal-DdC1_qT3.js} +5 -5
  77. package/dist/pkt-accordion.cjs +1 -1
  78. package/dist/pkt-accordion.js +1 -1
  79. package/dist/pkt-alert.cjs +1 -1
  80. package/dist/pkt-alert.js +1 -1
  81. package/dist/pkt-backlink.cjs +1 -1
  82. package/dist/pkt-backlink.js +1 -1
  83. package/dist/pkt-breadcrumbs.cjs +1 -1
  84. package/dist/pkt-breadcrumbs.js +1 -1
  85. package/dist/pkt-button.cjs +1 -1
  86. package/dist/pkt-button.js +1 -1
  87. package/dist/pkt-calendar.cjs +1 -1
  88. package/dist/pkt-calendar.js +1 -1
  89. package/dist/pkt-card.cjs +1 -1
  90. package/dist/pkt-card.js +1 -1
  91. package/dist/pkt-checkbox.cjs +1 -1
  92. package/dist/pkt-checkbox.js +1 -1
  93. package/dist/pkt-combobox.cjs +1 -1
  94. package/dist/pkt-combobox.js +1 -1
  95. package/dist/pkt-consent.cjs +1 -1
  96. package/dist/pkt-consent.js +1 -1
  97. package/dist/pkt-datepicker.cjs +1 -1
  98. package/dist/pkt-datepicker.js +1 -1
  99. package/dist/pkt-fileupload.cjs +1 -1
  100. package/dist/pkt-fileupload.js +1 -1
  101. package/dist/pkt-footer.cjs +1 -1
  102. package/dist/pkt-footer.js +1 -1
  103. package/dist/pkt-header-menu.cjs +1 -1
  104. package/dist/pkt-header-menu.js +1 -1
  105. package/dist/pkt-header.cjs +1 -1
  106. package/dist/pkt-header.js +1 -1
  107. package/dist/pkt-heading.cjs +1 -1
  108. package/dist/pkt-heading.js +1 -1
  109. package/dist/pkt-helptext.cjs +1 -1
  110. package/dist/pkt-helptext.js +1 -1
  111. package/dist/pkt-icon.cjs +1 -1
  112. package/dist/pkt-icon.js +1 -1
  113. package/dist/pkt-index.cjs +1 -1
  114. package/dist/pkt-index.js +34 -34
  115. package/dist/pkt-input-wrapper.cjs +1 -1
  116. package/dist/pkt-input-wrapper.js +1 -1
  117. package/dist/pkt-link.cjs +1 -1
  118. package/dist/pkt-link.js +1 -1
  119. package/dist/pkt-linkcard.cjs +1 -1
  120. package/dist/pkt-linkcard.js +1 -1
  121. package/dist/pkt-listbox.cjs +1 -1
  122. package/dist/pkt-listbox.js +1 -1
  123. package/dist/pkt-loader.cjs +1 -1
  124. package/dist/pkt-loader.js +1 -1
  125. package/dist/pkt-messagebox.cjs +1 -1
  126. package/dist/pkt-messagebox.js +1 -1
  127. package/dist/pkt-modal.cjs +1 -1
  128. package/dist/pkt-modal.js +1 -1
  129. package/dist/{pkt-options-controller-AFYyv3-b.cjs → pkt-options-controller-DXAAVcBJ.cjs} +1 -1
  130. package/dist/{pkt-options-controller-DKkmDDzT.js → pkt-options-controller-XcIgx_v1.js} +1 -1
  131. package/dist/pkt-progressbar.cjs +1 -1
  132. package/dist/pkt-progressbar.js +1 -1
  133. package/dist/pkt-radiobutton.cjs +1 -1
  134. package/dist/pkt-radiobutton.js +1 -1
  135. package/dist/pkt-searchinput.cjs +1 -1
  136. package/dist/pkt-searchinput.js +1 -1
  137. package/dist/pkt-select.cjs +1 -1
  138. package/dist/pkt-select.js +1 -1
  139. package/dist/pkt-tabs.cjs +1 -1
  140. package/dist/pkt-tabs.js +1 -1
  141. package/dist/pkt-tag.cjs +1 -1
  142. package/dist/pkt-tag.js +1 -1
  143. package/dist/pkt-textarea.cjs +1 -1
  144. package/dist/pkt-textarea.js +1 -1
  145. package/dist/pkt-textinput.cjs +1 -1
  146. package/dist/pkt-textinput.js +1 -1
  147. package/dist/pkt-timepicker.cjs +1 -1
  148. package/dist/pkt-timepicker.js +1 -1
  149. package/dist/{progressbar-BPHLC7ce.js → progressbar-D1x6MwdY.js} +5 -5
  150. package/dist/{progressbar-BpcsDCnG.cjs → progressbar-DsE4-qaW.cjs} +1 -1
  151. package/dist/{radiobutton-B6pzZQZV.cjs → radiobutton-BEzjKx1J.cjs} +15 -7
  152. package/dist/{radiobutton-bGdAEz26.js → radiobutton-Cpwa5ezn.js} +19 -11
  153. package/dist/{ref-BkV4Cd8U.cjs → ref-AuNEkS5a.cjs} +1 -1
  154. package/dist/{ref-lE1mGUOL.js → ref-DNowxdYH.js} +2 -2
  155. package/dist/{repeat-uBgVG8Ch.cjs → repeat-B3ksuY2F.cjs} +1 -1
  156. package/dist/{repeat-CCnTfedv.js → repeat-CZ1oMXnW.js} +2 -2
  157. package/dist/{searchinput-DrUcPajl.js → searchinput-CmyWnNr6.js} +4 -4
  158. package/dist/{searchinput-CiPB8xvh.cjs → searchinput-vi7twt5b.cjs} +1 -1
  159. package/dist/{select-ay8WHI7p.cjs → select-TAwBNhhf.cjs} +1 -1
  160. package/dist/{select-D7r2s7Cw.js → select-c15YAQbr.js} +7 -7
  161. package/dist/shared-strings/nb.d.ts +1 -0
  162. package/dist/{tabs-DqzPOdi9.cjs → tabs-hiTrWsXq.cjs} +1 -1
  163. package/dist/{tabs-DHvCjUi4.js → tabs-kxsX1ZEf.js} +4 -4
  164. package/dist/{tag-Cojf4jgA.cjs → tag-CGlxtBOH.cjs} +1 -1
  165. package/dist/{tag-D2Xvsy1i.js → tag-bztxjcF0.js} +5 -5
  166. package/dist/{textarea-s4IDLsBi.cjs → textarea-BHGZhMN4.cjs} +1 -1
  167. package/dist/{textarea-CzAL8e6L.js → textarea-BPQVrOp9.js} +9 -9
  168. package/dist/{textinput-B9w4jhIP.cjs → textinput-Cv4tal5W.cjs} +1 -1
  169. package/dist/{textinput-DT3HR9eU.js → textinput-DHGUOsIA.js} +8 -8
  170. package/dist/{timepicker-C-Ip5Jgl.cjs → timepicker-BBUYnYzM.cjs} +1 -1
  171. package/dist/{timepicker-BE0UouVr.js → timepicker-C8DHbOpM.js} +8 -8
  172. package/package.json +2 -2
  173. package/src/components/checkbox/checkbox.test.ts +1 -1
  174. package/src/components/checkbox/checkbox.ts +32 -13
  175. package/src/components/combobox/combobox-base.ts +42 -28
  176. package/src/components/combobox/combobox-handlers.ts +11 -5
  177. package/src/components/combobox/combobox-utils.ts +9 -1
  178. package/src/components/combobox/combobox-value.ts +32 -16
  179. package/src/components/combobox/combobox.accessibility.test.ts +29 -3
  180. package/src/components/combobox/combobox.attributes.test.ts +82 -0
  181. package/src/components/combobox/combobox.core.test.ts +46 -1
  182. package/src/components/combobox/combobox.interaction.test.ts +167 -0
  183. package/src/components/combobox/combobox.preselection.test.ts +271 -0
  184. package/src/components/combobox/combobox.ts +143 -79
  185. package/src/components/combobox/index.ts +6 -1
  186. package/src/components/index.ts +1 -0
  187. package/src/components/listbox/listbox.ts +56 -29
  188. package/src/components/radiobutton/radiobutton.test.ts +1 -1
  189. package/src/components/radiobutton/radiobutton.ts +32 -13
  190. package/dist/combobox-BZm_12qR.cjs +0 -140
  191. package/dist/if-defined-BeVQnl3C.cjs +0 -1
  192. package/dist/listbox-D95b5zAd.cjs +0 -106
@@ -15,7 +15,9 @@ import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
15
15
  import { slotUtils, optionStateUtils } from './combobox-utils'
16
16
  import { slotContent } from '@/directives/slot-content'
17
17
  import { ComboboxHandlers } from './combobox-handlers'
18
+ import { formatString } from 'shared-strings'
18
19
  import type { IPktStringsProps } from 'shared-strings'
20
+ import type { Booleanish } from 'shared-types'
19
21
 
20
22
  import '../input-wrapper'
21
23
  import '../icon'
@@ -50,13 +52,42 @@ export interface IPktCombobox extends IPktStringsProps<'combobox' | 'listbox'> {
50
52
  placeholder?: string | null
51
53
  requiredTag?: boolean
52
54
  requiredText?: string
53
- searchPlaceholder?: string
55
+ searchPlaceholder?: string | null
54
56
  tagPlacement?: TPktComboboxTagPlacement | null
55
57
  tagText?: string | null
56
58
  value?: string | string[]
59
+ defaultValue?: string | string[] | null
60
+ hasFieldset?: boolean | null
61
+ useWrapper?: Booleanish
57
62
  isOpen?: boolean
58
63
  }
59
64
 
65
+ /**
66
+ * Attribute spellings for template use. TypeScript cannot see the `attribute`
67
+ * option on Lit's @property decorator, so markup like `allow-user-input` is
68
+ * otherwise reported as unknown.
69
+ */
70
+ export interface IPktComboboxAttributes {
71
+ 'allow-user-input'?: boolean
72
+ 'default-options'?: IPktComboboxOption[]
73
+ 'display-value-as'?: string
74
+ 'include-search'?: boolean
75
+ 'search-placeholder'?: string | null
76
+ 'tag-placement'?: TPktComboboxTagPlacement | null
77
+ defaultvalue?: string | string[] | null
78
+ errormessage?: string
79
+ haserror?: boolean
80
+ helptextdropdown?: string | null
81
+ helptextdropdownbutton?: string | null
82
+ optionaltag?: boolean
83
+ optionaltext?: string
84
+ requiredtag?: boolean
85
+ requiredtext?: string
86
+ tagtext?: string | null
87
+ hasfieldset?: boolean | null
88
+ usewrapper?: Booleanish
89
+ }
90
+
60
91
  declare global {
61
92
  interface HTMLElementTagNameMap {
62
93
  'pkt-combobox': PktCombobox & HTMLSelectElement
@@ -84,7 +115,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
84
115
  if (this.defaultOptions && this.defaultOptions.length) {
85
116
  const userAdded = this.options?.filter((opt) => opt.userAdded) || []
86
117
  this.options = [...userAdded, ...JSON.parse(JSON.stringify(this.defaultOptions))]
87
- this._options = Array.isArray(this.options) ? [...this.options] : []
118
+ this._options = optionStateUtils.cloneOptions(this.options)
88
119
  }
89
120
 
90
121
  // If options are provided via the options slot, we need to extract them
@@ -99,22 +130,6 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
99
130
  }
100
131
  }
101
132
 
102
- protected willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
103
- // Re-parse slot options when the controller detects mutations.
104
- // The controller increments its generation counter on each mutation, but
105
- // doesn't set any reactive properties — so we detect the change here.
106
- if (this._optionsFromSlot && this.optionsController) {
107
- const currentGen = this.optionsController.generation
108
- if (currentGen !== this._lastSlotGeneration) {
109
- this._lastSlotGeneration = currentGen
110
- const parsedOptions = slotUtils.parseOptionsFromSlot(this.optionsController.nodes)
111
- const userAdded = this._options.filter((o) => o.userAdded)
112
- this.options = [...userAdded, ...parsedOptions]
113
- }
114
- }
115
- super.willUpdate(changedProperties)
116
- }
117
-
118
133
  disconnectedCallback(): void {
119
134
  super.disconnectedCallback()
120
135
  document?.body.removeEventListener('click', this.handleBodyClick)
@@ -130,53 +145,71 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
130
145
  super.firstUpdated(changedProperties)
131
146
  }
132
147
 
133
- updated(changedProperties: PropertyValues): void {
148
+ protected willUpdate(changedProperties: PropertyValues): void {
149
+ // Re-parse slot options when the controller detects mutations.
150
+ // The controller increments its generation counter on each mutation, but
151
+ // doesn't set any reactive properties — so we detect the change here.
152
+ if (this._optionsFromSlot && this.optionsController) {
153
+ const currentGen = this.optionsController.generation
154
+ if (currentGen !== this._lastSlotGeneration) {
155
+ this._lastSlotGeneration = currentGen
156
+ const parsedOptions = slotUtils.parseOptionsFromSlot(this.optionsController.nodes)
157
+ const userAdded = this._options.filter((o) => o.userAdded)
158
+ this.options = [...userAdded, ...parsedOptions]
159
+ }
160
+ }
161
+
162
+ super.willUpdate(changedProperties)
163
+
134
164
  if (changedProperties.has('isOpen')) {
135
165
  this._isOptionsOpen = this.isOpen
136
166
  }
137
167
 
138
- // Handle value and _value changes.
139
- // Three cases:
140
- // 1. value changed from our own syncValueAndDispatch (internal sync) — skip value handler,
141
- // but still process concurrent _value changes
142
- // 2. value changed externally — sync _value from value, dispatch events
143
- // 3. Only _value changed — sync value from _value, dispatch events
168
+ if (changedProperties.has('defaultOptions') && this.defaultOptions.length) {
169
+ const userAdded =
170
+ (Array.isArray(this.options) ? this.options : []).filter((opt) => opt.userAdded) || []
171
+ this.options = [...userAdded, ...JSON.parse(JSON.stringify(this.defaultOptions))]
172
+ this._options = optionStateUtils.cloneOptions(this.options)
173
+ }
174
+
175
+ // `value` is derived before options, so that options.selected can add to
176
+ // the result rather than being overwritten by an empty value attribute.
144
177
  const valueChanged = changedProperties.has('value')
145
178
  const internalChanged = changedProperties.has('_value')
146
- const isInternalSync = valueChanged && this._internalValueSync
147
179
 
148
- if (isInternalSync) {
149
- this._internalValueSync = false
150
- if (internalChanged) {
151
- this.syncValueAndDispatch(changedProperties.get('_value') as string[])
152
- }
153
- } else if (valueChanged) {
180
+ if (valueChanged) {
154
181
  const oldInternal = [...this._value]
155
182
  const newInternal = this.parseValue()
156
183
  if (newInternal.join(',') !== this._value.join(',')) {
157
184
  this._value = newInternal
158
- this.options.forEach((o) => (o.selected = newInternal.includes(o.value)))
159
185
  this._options.forEach((o) => (o.selected = newInternal.includes(o.value)))
160
186
  this._options = [...this._options]
161
187
  }
162
- this.updateMaxReached()
163
- this.syncValueAndDispatch(oldInternal)
188
+ this._pendingDispatchFrom ??= oldInternal
164
189
  } else if (internalChanged) {
165
- this.syncValueAndDispatch(changedProperties.get('_value') as string[])
190
+ this._pendingDispatchFrom ??= (changedProperties.get('_value') as string[]) ?? []
166
191
  }
167
192
 
168
- // If defaultOptions changed, update options (preserving userAdded)
169
- if (changedProperties.has('defaultOptions') && this.defaultOptions.length) {
170
- const userAdded =
171
- (Array.isArray(this.options) ? this.options : []).filter((opt) => opt.userAdded) || []
172
- this.options = [...userAdded, ...JSON.parse(JSON.stringify(this.defaultOptions))]
173
- this._options = Array.isArray(this.options) ? [...this.options] : []
193
+ // The base class applies defaultValue through valueChanged(), which combobox
194
+ // overrides as a no-op, so it is seeded here instead.
195
+ if (changedProperties.has('defaultValue') && this.defaultValue != null && !this._value.length) {
196
+ const seeded = this.parseToValues(this.defaultValue)
197
+ if (seeded.length) {
198
+ this._pendingDispatchFrom ??= [...this._value]
199
+ this._value = seeded
200
+ this._options.forEach((o) => (o.selected = seeded.includes(o.value)))
201
+ this._options = [...this._options]
202
+ }
174
203
  }
175
204
 
176
205
  if (changedProperties.has('options')) {
177
206
  const prevOptions =
178
207
  (changedProperties.get('options') as IPktComboboxOption[]) || this._options || []
179
- const mergedOptions = optionStateUtils.mergeWithUserAdded(this.options, prevOptions)
208
+ const mergedOptions = optionStateUtils.mergeWithUserAdded(
209
+ optionStateUtils.cloneOptions(this.options),
210
+ prevOptions,
211
+ this._value,
212
+ )
180
213
  this._options = mergedOptions
181
214
  if (mergedOptions.length > this.options.length) {
182
215
  this.options = mergedOptions
@@ -186,11 +219,32 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
186
219
  this._options = syncResult.options
187
220
 
188
221
  if (syncResult.newValues.length > this._value.length) {
189
- const oldValue = [...this._value]
222
+ this._pendingDispatchFrom ??= [...this._value]
190
223
  this._value = syncResult.newValues
191
- this.syncValueAndDispatch(oldValue)
192
224
  }
193
225
  }
226
+
227
+ if (
228
+ valueChanged ||
229
+ internalChanged ||
230
+ changedProperties.has('options') ||
231
+ changedProperties.has('defaultValue')
232
+ ) {
233
+ this.updateMaxReached()
234
+ }
235
+
236
+ if (this._pendingDispatchFrom) {
237
+ this.syncPublicValue()
238
+ }
239
+ }
240
+
241
+ updated(changedProperties: PropertyValues): void {
242
+ if (this._pendingDispatchFrom) {
243
+ const from = this._pendingDispatchFrom
244
+ this._pendingDispatchFrom = null
245
+ this.dispatchValueChange(from)
246
+ }
247
+
194
248
  if (changedProperties.has('_search')) {
195
249
  this.dispatchEvent(
196
250
  new CustomEvent('search', {
@@ -264,9 +318,9 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
264
318
 
265
319
  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
266
320
  // Don't set _value here for 'value' changes — this.value hasn't been updated yet
267
- // (super.attributeChangedCallback does that). Let updated() handle the sync.
321
+ // (super.attributeChangedCallback does that). Let willUpdate() handle the sync.
268
322
  if (name === 'options') {
269
- this._options = Array.isArray(this.options) ? [...this.options] : []
323
+ this._options = optionStateUtils.cloneOptions(this.options)
270
324
  const syncResult = optionStateUtils.syncOptionsWithValues(this._options, this._value)
271
325
  this._options = syncResult.options
272
326
  if (syncResult.newValues.length > this._value.length) {
@@ -330,6 +384,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
330
384
  )}
331
385
  aria-activedescendant=${ifDefined(
332
386
  !this._hasTextInput &&
387
+ this._isOptionsOpen &&
333
388
  this._value[0] &&
334
389
  !!findOptionByValue(this.options, this._value[0])
335
390
  ? `${this.id}-listbox-${findOptionIndex(this._options, this._value[0])}`
@@ -339,18 +394,21 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
339
394
  tabindex=${!this._hasTextInput ? (this.disabled ? '-1' : '0') : '-1'}
340
395
  @click=${this.handleInputClick}
341
396
  @keydown=${!this._hasTextInput ? this.handleSelectOnlyKeydown : nothing}
397
+ @blur=${this.restoreInputTab}
342
398
  ${ref(this.triggerRef)}
343
399
  >
344
- ${!this._hasTextInput &&
345
- this.placeholder &&
346
- (!this._value.length || (this.multiple && this.tagPlacement == 'outside')) &&
347
- !this._inputFocus
348
- ? html`<span class="pkt-combobox__placeholder" @click=${this.handlePlaceholderClick}
349
- >${this.placeholder}</span
350
- >`
351
- : this.tagPlacement !== 'outside'
352
- ? this.renderSingleOrMultipleValues()
353
- : nothing}
400
+ ${
401
+ !this._hasTextInput &&
402
+ this.placeholder &&
403
+ (!this._value.length || (this.multiple && this.tagPlacement == 'outside')) &&
404
+ !this._inputFocus
405
+ ? html`<span class="pkt-combobox__placeholder" @click=${this.handlePlaceholderClick}
406
+ >${this.placeholder}</span
407
+ >`
408
+ : this.tagPlacement !== 'outside'
409
+ ? this.renderSingleOrMultipleValues()
410
+ : nothing
411
+ }
354
412
  ${this.renderInputField()}
355
413
  <pkt-icon
356
414
  class=${classMap({
@@ -368,18 +426,23 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
368
426
  .isOpen=${this._isOptionsOpen}
369
427
  .searchPlaceholder=${this.searchPlaceholder}
370
428
  .label="Liste: ${this.label || ''}"
429
+ ?disabled=${this.disabled}
371
430
  ?include-search=${this.includeSearch}
372
431
  ?is-multi-select=${this.multiple}
373
432
  ?allow-user-input=${this.allowUserInput && !this._maxIsReached}
374
433
  ?max-is-reached=${this._maxIsReached}
375
434
  .customUserInput=${ifDefined(this._addValueText)}
376
- .userMessage=${this._userInfoMessage
377
- ? this.pktStrings.get('combobox')[this._userInfoMessage]
378
- : ''}
435
+ .userMessage=${
436
+ this._userInfoMessage
437
+ ? formatString(this.pktStrings.get('combobox')[this._userInfoMessage], {
438
+ value: this._messageValue ?? '',
439
+ })
440
+ : ''
441
+ }
379
442
  @search=${this.handleSearch}
380
443
  @option-toggle=${this.handleOptionToggled}
381
444
  @select-all=${this.addAllOptions}
382
- @close-options=${() => (this._isOptionsOpen = false)}
445
+ @close-options=${this.handleCloseOptions}
383
446
  @tab-close=${() => this.closeAndProcessInput()}
384
447
  .searchValue=${this._search || null}
385
448
  .maxLength=${this.maxlength || 0}
@@ -387,11 +450,13 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
387
450
  ></pkt-listbox>
388
451
  </div>
389
452
 
390
- ${this.tagPlacement === 'outside' && this.multiple
391
- ? html`<div class="pkt-combobox__tags-outside">
392
- ${this.renderSingleOrMultipleValues()}
393
- </div>`
394
- : nothing}
453
+ ${
454
+ this.tagPlacement === 'outside' && this.multiple
455
+ ? html`<div class="pkt-combobox__tags-outside">
456
+ ${this.renderSingleOrMultipleValues()}
457
+ </div>`
458
+ : nothing
459
+ }
395
460
  </pkt-input-wrapper>
396
461
  `
397
462
  }
@@ -414,6 +479,7 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
414
479
  e.stopPropagation()
415
480
  e.stopImmediatePropagation()
416
481
  }}
482
+ tabindex=${ifDefined(this._skipInputTab ? '-1' : undefined)}
417
483
  @keydown=${this.handleInputKeydown}
418
484
  @focus=${this.handleFocus}
419
485
  @blur=${this.handleBlur}
@@ -424,7 +490,9 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
424
490
  aria-autocomplete=${this.typeahead ? 'both' : this.allowUserInput ? 'list' : 'none'}
425
491
  aria-controls="${this.id}-listbox"
426
492
  aria-activedescendant=${ifDefined(
427
- this._value[0] && !!findOptionByValue(this.options, this._value[0])
493
+ this._isOptionsOpen &&
494
+ this._value[0] &&
495
+ !!findOptionByValue(this.options, this._value[0])
428
496
  ? `${this.id}-listbox-${findOptionIndex(this._options, this._value[0])}`
429
497
  : undefined,
430
498
  )}
@@ -474,9 +542,9 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
474
542
  ?closeTag=${!this.disabled}
475
543
  .buttonTabindex=${isOutside ? undefined : -1}
476
544
  @close=${() => this.handleTagRemove(value)}
477
- @keydown=${isOutside
478
- ? nothing
479
- : (e: KeyboardEvent) => this.handleTagKeydown(e, index)}
545
+ @keydown=${
546
+ isOutside ? nothing : (e: KeyboardEvent) => this.handleTagKeydown(e, index)
547
+ }
480
548
  >
481
549
  ${this.renderValueTag(option)}
482
550
  </pkt-tag>
@@ -487,6 +555,8 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
487
555
  </ul>
488
556
  `
489
557
 
558
+ if (!isSingleValueDisplay && this._value.length === 0) return nothing
559
+
490
560
  return isSingleValueDisplay ? singleValueContent : multipleValuesContent
491
561
  }
492
562
 
@@ -495,18 +565,12 @@ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
495
565
  const displayAs = this.displayValueAs as TPktComboboxDisplayValue
496
566
  switch (displayAs) {
497
567
  case 'prefixAndValue':
498
- return html`<span class="pkt-combobox__value" data-focusfix=${this.id}
499
- >${option.prefix || ''} ${option.value}</span
500
- >`
568
+ return html`<span class="pkt-combobox__value">${option.prefix || ''} ${option.value}</span>`
501
569
  case 'value':
502
- return html`<span class="pkt-combobox__value" data-focusfix=${this.id}
503
- >${option.value}</span
504
- >`
570
+ return html`<span class="pkt-combobox__value">${option.value}</span>`
505
571
  case 'label':
506
572
  default:
507
- return html`<span class="pkt-combobox__value" data-focusfix=${this.id}
508
- >${option.label || option.value}</span
509
- >`
573
+ return html`<span class="pkt-combobox__value">${option.label || option.value}</span>`
510
574
  }
511
575
  }
512
576
  }
@@ -1,6 +1,11 @@
1
1
  import { PktCombobox } from './combobox'
2
2
 
3
- export type { IPktCombobox, IPktComboboxOption, TPktComboboxTagPlacement } from './combobox'
3
+ export type {
4
+ IPktCombobox,
5
+ IPktComboboxAttributes,
6
+ IPktComboboxOption,
7
+ TPktComboboxTagPlacement,
8
+ } from './combobox'
4
9
 
5
10
  export { PktCombobox }
6
11
  export default PktCombobox
@@ -81,6 +81,7 @@ export type {
81
81
 
82
82
  export type {
83
83
  IPktCombobox,
84
+ IPktComboboxAttributes,
84
85
  IPktComboboxOption,
85
86
  TPktComboboxTagPlacement,
86
87
  } from '@/components/combobox'
@@ -46,6 +46,8 @@ export interface IPktListbox extends IPktStringsProps<'listbox'> {
46
46
 
47
47
  export class PktListbox extends PktElement<IPktListbox> implements IPktListbox {
48
48
  @property({ type: String }) id: string = uuidish()
49
+
50
+ @state() private _listboxId: string = uuidish()
49
51
  @property({ type: String }) label: string | null = null
50
52
  @property({ type: Array }) options: IPktComboboxOption[] = []
51
53
  @property({ type: Boolean, reflect: true, attribute: 'is-open' }) isOpen: boolean = false
@@ -69,6 +71,12 @@ export class PktListbox extends PktElement<IPktListbox> implements IPktListbox {
69
71
  // Lifecycle methods
70
72
  connectedCallback(): void {
71
73
  super.connectedCallback()
74
+ const hostId = this.getAttribute('id')
75
+ if (hostId) {
76
+ this.adoptHostId(hostId)
77
+ } else if (this.id) {
78
+ this._listboxId = this.id
79
+ }
72
80
  if (this.includeSearch && !this.searchValue) {
73
81
  this.searchValue = ''
74
82
  }
@@ -84,18 +92,14 @@ export class PktListbox extends PktElement<IPktListbox> implements IPktListbox {
84
92
  this.typeahead.reset()
85
93
  }
86
94
 
87
- updated(changedProperties: PropertyValues) {
88
- if (changedProperties.has('options') || changedProperties.has('searchValue')) {
89
- this.filterOptions()
90
- }
91
- super.updated(changedProperties)
92
- }
93
-
94
95
  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
95
96
  if (name === 'options' || name === 'search-value') {
96
97
  this.filterOptions()
97
98
  }
98
99
  super.attributeChangedCallback(name, _old, value)
100
+ if (name === 'id' && value) {
101
+ this.adoptHostId(value)
102
+ }
99
103
  }
100
104
 
101
105
  // Render methods
@@ -103,9 +107,22 @@ export class PktListbox extends PktElement<IPktListbox> implements IPktListbox {
103
107
  return this._filteredOptions.length > 0 || this.options.length > 0
104
108
  }
105
109
 
110
+ private adoptHostId(hostId: string): void {
111
+ this._listboxId = hostId
112
+ this.removeAttribute('id')
113
+ }
114
+
115
+ protected willUpdate(changed: PropertyValues): void {
116
+ super.willUpdate(changed)
117
+ if (changed.has('options') || changed.has('searchValue')) {
118
+ this.filterOptions()
119
+ }
120
+ }
121
+
106
122
  render() {
107
123
  return html`
108
124
  <div
125
+ id=${this._listboxId}
109
126
  class=${classMap({
110
127
  'pkt-listbox': true,
111
128
  'pkt-listbox__open': this.isOpen,
@@ -129,20 +146,21 @@ export class PktListbox extends PktElement<IPktListbox> implements IPktListbox {
129
146
  `
130
147
  }
131
148
 
132
- private renderCheckboxOrCheckIcon(option: IPktComboboxOption, index: number) {
149
+ private renderCheckboxOrCheckIcon(option: IPktComboboxOption) {
133
150
  return this.isMultiSelect
134
151
  ? html`
135
152
  <input
136
153
  class="pkt-input-check__input-checkbox"
137
154
  type="checkbox"
138
- role="presentation"
155
+ inert
156
+ aria-hidden="true"
139
157
  tabindex="-1"
140
158
  value=${option.value}
141
159
  @change=${(e: Event) => {
142
160
  e.stopPropagation()
143
161
  }}
144
162
  .checked=${option.selected}
145
- aria-labelledby=${this.id + '-option-label-' + index}
163
+ ?checked=${option.selected}
146
164
  ?disabled=${this.disabled || option.disabled || (this.maxIsReached && !option.selected)}
147
165
  />
148
166
  `
@@ -186,29 +204,38 @@ export class PktListbox extends PktElement<IPktListbox> implements IPktListbox {
186
204
  data-index=${index}
187
205
  data-value=${option.value}
188
206
  data-selected=${option.selected ? 'true' : 'false'}
189
- ?data-disabled=${this.disabled ||
190
- option.disabled ||
191
- (this.maxIsReached && !option.selected)}
192
- aria-disabled=${this.disabled ||
193
- option.disabled ||
194
- (this.maxIsReached && !option.selected)
195
- ? 'true'
196
- : 'false'}
207
+ data-disabled=${ifDefined(
208
+ this.disabled || option.disabled || (this.maxIsReached && !option.selected)
209
+ ? 'true'
210
+ : undefined,
211
+ )}
212
+ aria-disabled=${
213
+ this.disabled || option.disabled || (this.maxIsReached && !option.selected)
214
+ ? 'true'
215
+ : 'false'
216
+ }
197
217
  role="option"
198
- id=${`${this.id}-${index}`}
218
+ id=${`${this._listboxId}-${index}`}
199
219
  >
200
- ${this.renderCheckboxOrCheckIcon(option, index)}
201
- <span class="pkt-listbox__option-label" id=${this.id + '-option-label-' + index}>
202
- ${option.prefix
203
- ? html`<span class="pkt-listbox__option-prefix">${option.prefix}</span>`
204
- : nothing}
220
+ ${this.renderCheckboxOrCheckIcon(option)}
221
+ <span
222
+ class="pkt-listbox__option-label"
223
+ id=${this._listboxId + '-option-label-' + index}
224
+ >
225
+ ${
226
+ option.prefix
227
+ ? html`<span class="pkt-listbox__option-prefix">${option.prefix}</span>`
228
+ : nothing
229
+ }
205
230
  ${option.label || option.value}
206
231
  </span>
207
- ${option.description
208
- ? html`<span class="pkt-listbox__option-description pkt-txt-14-light"
209
- >${option.description}</span
210
- >`
211
- : nothing}
232
+ ${
233
+ option.description
234
+ ? html`<span class="pkt-listbox__option-description pkt-txt-14-light"
235
+ >${option.description}</span
236
+ >`
237
+ : nothing
238
+ }
212
239
  </li>
213
240
  `,
214
241
  )}
@@ -159,7 +159,7 @@ describe('pkt-radiobutton', () => {
159
159
 
160
160
  const helptext = radiobutton.querySelector('.pkt-input-check__input-helptext')
161
161
  expect(helptext).toBeInTheDocument()
162
- expect(helptext?.textContent).toBe('This is helpful information')
162
+ expect(helptext?.textContent?.trim()).toBe('This is helpful information')
163
163
  })
164
164
 
165
165
  test('handles error state correctly', async () => {
@@ -75,25 +75,42 @@ export class PktRadioButton extends PktInputElement<Props> {
75
75
 
76
76
  const tags = () => {
77
77
  return html`
78
- ${this.tagText
79
- ? html`<span class=${tagClasses + ' pkt-tag--gray'}>${this.tagText}</span>`
80
- : nothing}
81
- ${this.optionalTag
82
- ? html`<span class=${tagClasses + ' pkt-tag--blue-light'}>${this.optionalText ?? formStrings.optional}</span>`
83
- : nothing}
84
- ${this.requiredTag
85
- ? html`<span class=${tagClasses + ' pkt-tag--beige'}>${this.requiredText ?? formStrings.required}</span>`
86
- : nothing}
78
+ ${
79
+ this.tagText
80
+ ? html`<span class=${tagClasses + ' pkt-tag--gray'}>${this.tagText}</span>`
81
+ : nothing
82
+ }
83
+ ${
84
+ this.optionalTag
85
+ ? html`<span class=${tagClasses + ' pkt-tag--blue-light'}
86
+ >${this.optionalText ?? formStrings.optional}</span
87
+ >`
88
+ : nothing
89
+ }
90
+ ${
91
+ this.requiredTag
92
+ ? html`<span class=${tagClasses + ' pkt-tag--beige'}
93
+ >${this.requiredText ?? formStrings.required}</span
94
+ >`
95
+ : nothing
96
+ }
87
97
  `
88
98
  }
89
99
 
100
+ const labelId = this.id + '-check-label'
101
+ const helptextId = this.id + '-check-helptext'
102
+
90
103
  const labelAndHelptext = () => {
91
104
  return html`
92
105
  <label class=${labelClasses} for=${this.id + '-internal'}>
93
- ${this.label} ${tags()}
94
- ${this.checkHelptext
95
- ? html`<div class="pkt-input-check__input-helptext">${this.checkHelptext}</div>`
96
- : nothing}
106
+ <span id=${labelId}>${this.label} ${tags()}</span>
107
+ ${
108
+ this.checkHelptext
109
+ ? html`<div id=${helptextId} class="pkt-input-check__input-helptext">
110
+ ${this.checkHelptext}
111
+ </div>`
112
+ : nothing
113
+ }
97
114
  </label>
98
115
  `
99
116
  }
@@ -114,6 +131,8 @@ export class PktRadioButton extends PktInputElement<Props> {
114
131
  @blur=${this.onBlur}
115
132
  @focus=${this.onFocus}
116
133
  ?checked=${this.checked}
134
+ aria-labelledby=${this.label ? labelId : nothing}
135
+ aria-describedby=${this.checkHelptext ? helptextId : nothing}
117
136
  />
118
137
  ${labelAndHelptext()}
119
138
  </div>