@oslokommune/punkt-elements 15.4.5 → 16.0.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 (73) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{card-CnPjrdre.js → card-CmfUyl_s.js} +1 -1
  3. package/dist/{card-5S2r9UD1.cjs → card-Db9QSEqh.cjs} +1 -1
  4. package/dist/{checkbox-D98_NjcU.cjs → checkbox-Cpyay9_l.cjs} +1 -1
  5. package/dist/{checkbox-BSz71IeT.js → checkbox-D6nltMuc.js} +1 -1
  6. package/dist/combobox-Bv37b6cI.cjs +135 -0
  7. package/dist/combobox-CoO8T-F-.js +818 -0
  8. package/dist/{datepicker-SEKblnRR.cjs → datepicker-CrvQ5Y5w.cjs} +1 -1
  9. package/dist/{datepicker-nnyTW0vf.js → datepicker-DbsIuC5Z.js} +2 -2
  10. package/dist/index.d.ts +157 -90
  11. package/dist/{input-element-Bkv6Yxld.js → input-element-BGNbdzy2.js} +1 -1
  12. package/dist/{input-element-DM0tY799.cjs → input-element-CSDVA3Y6.cjs} +1 -1
  13. package/dist/listbox-Dm2mKp6_.cjs +101 -0
  14. package/dist/listbox-OdkIn9_A.js +431 -0
  15. package/dist/pkt-card.cjs +1 -1
  16. package/dist/pkt-card.js +1 -1
  17. package/dist/pkt-checkbox.cjs +1 -1
  18. package/dist/pkt-checkbox.js +1 -1
  19. package/dist/pkt-combobox.cjs +1 -1
  20. package/dist/pkt-combobox.js +1 -1
  21. package/dist/pkt-datepicker.cjs +1 -1
  22. package/dist/pkt-datepicker.js +2 -2
  23. package/dist/pkt-header.cjs +1 -1
  24. package/dist/pkt-header.js +1 -1
  25. package/dist/pkt-index.cjs +1 -1
  26. package/dist/pkt-index.js +9 -9
  27. package/dist/pkt-listbox.cjs +1 -1
  28. package/dist/pkt-listbox.js +1 -1
  29. package/dist/pkt-options-controller-BogGk-6J.cjs +1 -0
  30. package/dist/{pkt-options-controller-BcGywCmf.js → pkt-options-controller-Z-bPox7n.js} +2 -2
  31. package/dist/pkt-radiobutton.cjs +1 -1
  32. package/dist/pkt-radiobutton.js +1 -1
  33. package/dist/pkt-select.cjs +1 -1
  34. package/dist/pkt-select.js +1 -1
  35. package/dist/pkt-tag.cjs +1 -1
  36. package/dist/pkt-tag.js +1 -1
  37. package/dist/pkt-textarea.cjs +1 -1
  38. package/dist/pkt-textarea.js +1 -1
  39. package/dist/pkt-textinput.cjs +1 -1
  40. package/dist/pkt-textinput.js +1 -1
  41. package/dist/{radiobutton-95wp024h.cjs → radiobutton-CNHCpKn0.cjs} +1 -1
  42. package/dist/{radiobutton-CTFAV5GU.js → radiobutton-DgC27mb0.js} +1 -1
  43. package/dist/{select-YLvYAQX6.js → select-7VuYtPZv.js} +2 -2
  44. package/dist/{select-CZ_Lx5W6.cjs → select-PWPy5gTB.cjs} +1 -1
  45. package/dist/{tag-68q0_Sn0.js → tag-DZPqFiem.js} +37 -33
  46. package/dist/tag-DmbgBCKu.cjs +27 -0
  47. package/dist/{textarea-CuTsE1WX.cjs → textarea-CO7Ikug5.cjs} +1 -1
  48. package/dist/{textarea-DhWH99qN.js → textarea-VpCEjVFx.js} +1 -1
  49. package/dist/{textinput-BCi9p0Du.js → textinput-C2AZ9ss2.js} +1 -1
  50. package/dist/{textinput-st4Vml5J.cjs → textinput-DRFZU3dA.cjs} +1 -1
  51. package/package.json +4 -4
  52. package/src/components/card/card.ts +1 -0
  53. package/src/components/combobox/combobox-base.ts +158 -0
  54. package/src/components/combobox/combobox-handlers.ts +419 -0
  55. package/src/components/combobox/combobox-types.ts +10 -0
  56. package/src/components/combobox/combobox-utils.ts +135 -0
  57. package/src/components/combobox/combobox-value.ts +248 -0
  58. package/src/components/combobox/combobox.accessibility.test.ts +243 -0
  59. package/src/components/combobox/{combobox.test.ts → combobox.core.test.ts} +104 -46
  60. package/src/components/combobox/combobox.interaction.test.ts +436 -0
  61. package/src/components/combobox/combobox.selection.test.ts +543 -0
  62. package/src/components/combobox/combobox.ts +260 -734
  63. package/src/components/listbox/index.ts +2 -0
  64. package/src/components/listbox/listbox.interaction.test.ts +580 -0
  65. package/src/components/listbox/listbox.test.ts +32 -6
  66. package/src/components/listbox/listbox.ts +109 -126
  67. package/src/components/tag/tag.ts +3 -0
  68. package/dist/combobox-C5YcNVSZ.cjs +0 -128
  69. package/dist/combobox-cer7PLSE.js +0 -533
  70. package/dist/listbox-C7NEa9SU.cjs +0 -96
  71. package/dist/listbox-Cykec1bj.js +0 -361
  72. package/dist/pkt-options-controller-BnTmkl3g.cjs +0 -1
  73. package/dist/tag-BnT5onW2.cjs +0 -26
@@ -0,0 +1,419 @@
1
+ import { findOptionByValue, findTypeaheadMatches } from 'shared-utils/combobox/option-utils'
2
+ import {
3
+ getInputKeyAction,
4
+ getInputValueAction,
5
+ checkForMatches,
6
+ getSingleValueForInput,
7
+ } from 'shared-utils/combobox/input-utils'
8
+ import { ComboboxValue } from './combobox-value'
9
+ import type { PktTag } from '../tag'
10
+
11
+ /**
12
+ * Event handler layer for PktCombobox.
13
+ * Handles user interactions: input, focus, keyboard, clicks, tags.
14
+ */
15
+ export class ComboboxHandlers extends ComboboxValue {
16
+ protected handleInput(e: InputEvent): void {
17
+ e.stopPropagation()
18
+ e.stopImmediatePropagation()
19
+
20
+ if (this.disabled) return
21
+
22
+ this.touched = true
23
+ const input = e.target as HTMLInputElement
24
+ this._search = input.value
25
+ this.checkForMatches()
26
+
27
+ if (this.typeahead) {
28
+ if (this._search) {
29
+ const { filtered, suggestion } = findTypeaheadMatches(this.options, this._search)
30
+ this._options = filtered
31
+
32
+ if (
33
+ e.inputType !== 'deleteContentBackward' &&
34
+ suggestion?.label &&
35
+ this.inputRef.value &&
36
+ this.inputRef.value.type !== 'hidden'
37
+ ) {
38
+ input.value = suggestion.label
39
+ window.setTimeout(
40
+ () => input.setSelectionRange(this._search.length, input.value.length),
41
+ 0,
42
+ )
43
+ input.selectionDirection = 'backward'
44
+ }
45
+ } else {
46
+ this._options = [...this.options]
47
+ }
48
+ }
49
+ }
50
+
51
+ protected handleFocus(): void {
52
+ if (this.disabled) return
53
+
54
+ // After selecting a value in single+typeahead, focus returns to the input.
55
+ // Skip reopening the dropdown so screen readers announce the selected value.
56
+ if (this._suppressNextOpen) {
57
+ this._suppressNextOpen = false
58
+ this._inputFocus = true
59
+ this.requestUpdate()
60
+ return
61
+ }
62
+
63
+ if (
64
+ !this.multiple &&
65
+ this._value[0] &&
66
+ this.inputRef.value &&
67
+ this.inputRef.value.type !== 'hidden'
68
+ ) {
69
+ this.inputRef.value.value = getSingleValueForInput(
70
+ this._value[0],
71
+ this.options,
72
+ this.displayValueAs,
73
+ )
74
+ }
75
+ this._inputFocus = true
76
+ this._search = ''
77
+ this._options = [...this.options]
78
+ this._isOptionsOpen = true
79
+ this.onFocus()
80
+
81
+ this.requestUpdate()
82
+ }
83
+
84
+ protected handleFocusOut(e: FocusEvent): void {
85
+ if (this.disabled || !this._isOptionsOpen) return
86
+
87
+ const related = e.relatedTarget as Element | null
88
+ const isFocusInsideCombobox =
89
+ related?.closest('pkt-combobox')?.id === this.id ||
90
+ (e.target as Element)?.getAttribute('data-focusfix') === this.id ||
91
+ related === this.inputRef.value ||
92
+ related === this.triggerRef.value
93
+
94
+ if (!isFocusInsideCombobox) {
95
+ this.closeAndProcessInput()
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Shared close logic used by both focusout and outside-click handlers.
101
+ * Processes any pending input value, then closes the dropdown.
102
+ */
103
+ protected closeAndProcessInput(): void {
104
+ this._inputFocus = false
105
+ this._addValueText = null
106
+ this._userInfoMessage = ''
107
+ this._search = ''
108
+
109
+ if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
110
+ const inputText = this.inputRef.value.value
111
+
112
+ if (!this.multiple) {
113
+ if (!inputText) {
114
+ // Empty input — clear the selection
115
+ if (this._value[0]) this.removeSelected(this._value[0])
116
+ } else {
117
+ // Try to match input text to an option (by value or label)
118
+ const match = findOptionByValue(this.options, inputText)
119
+ if (match && match.value !== this._value[0]) {
120
+ // Input matches a different option — select it
121
+ this._value[0] && this.removeSelected(this._value[0])
122
+ this.setSelected(match.value)
123
+ } else if (!match && this.allowUserInput) {
124
+ // No match + allowUserInput — set as user value
125
+ this._value[0] && this.removeSelected(this._value[0])
126
+ this.addNewUserValue(inputText)
127
+ }
128
+ // No match, no allowUserInput — discard, keep previous selection
129
+ }
130
+ } else if (inputText !== '') {
131
+ // Multi: process typed text (add/select/remove)
132
+ const { action, value } = getInputValueAction(
133
+ inputText,
134
+ this._value,
135
+ this.options,
136
+ this.allowUserInput,
137
+ this.multiple,
138
+ )
139
+ switch (action) {
140
+ case 'addUserValue':
141
+ this.addNewUserValue(value)
142
+ break
143
+ case 'selectOption':
144
+ this.setSelected(value)
145
+ break
146
+ case 'removeValue':
147
+ this.removeValue(value)
148
+ break
149
+ }
150
+ }
151
+
152
+ // Restore input to display text of current selection (or clear)
153
+ if (!this.multiple && this._value[0]) {
154
+ this.inputRef.value.value = getSingleValueForInput(
155
+ this._value[0],
156
+ this.options,
157
+ this.displayValueAs,
158
+ )
159
+ } else {
160
+ this.inputRef.value.value = ''
161
+ }
162
+ }
163
+ this._isOptionsOpen = false
164
+ this.onBlur()
165
+ }
166
+
167
+ protected handleBlur(): void {
168
+ this._inputFocus = false
169
+ this.onBlur()
170
+ }
171
+
172
+ protected handleInputClick(e: MouseEvent): void {
173
+ if (this.disabled) {
174
+ e.preventDefault()
175
+ e.stopImmediatePropagation()
176
+ return
177
+ }
178
+ if (this._hasTextInput) {
179
+ this.inputRef.value?.focus()
180
+ this.requestUpdate()
181
+ } else {
182
+ // Select-only: toggle the listbox
183
+ e.stopImmediatePropagation()
184
+ e.preventDefault()
185
+ this._isOptionsOpen = !this._isOptionsOpen
186
+ if (this._isOptionsOpen) {
187
+ this.listboxRef.value?.focusFirstOrSelectedOption()
188
+ }
189
+ }
190
+ }
191
+
192
+ protected handlePlaceholderClick(e: MouseEvent): void {
193
+ if (this.disabled) return
194
+ e.stopPropagation()
195
+ if (this._hasTextInput && this.inputRef.value) {
196
+ this.inputRef.value.focus()
197
+ this._inputFocus = true
198
+ this.requestUpdate()
199
+ } else {
200
+ this._isOptionsOpen = !this._isOptionsOpen
201
+ this.requestUpdate()
202
+ }
203
+ }
204
+
205
+ protected handleSelectOnlyKeydown(e: KeyboardEvent): void {
206
+ if (this.disabled) return
207
+
208
+ switch (e.key) {
209
+ case 'Enter':
210
+ case ' ':
211
+ case 'ArrowDown':
212
+ case 'ArrowUp':
213
+ e.preventDefault()
214
+ if (!this._isOptionsOpen) {
215
+ this._isOptionsOpen = true
216
+ this.listboxRef.value?.focusFirstOrSelectedOption()
217
+ } else {
218
+ this._isOptionsOpen = false
219
+ }
220
+ break
221
+ case 'Escape':
222
+ if (this._isOptionsOpen) {
223
+ e.preventDefault()
224
+ this._isOptionsOpen = false
225
+ }
226
+ break
227
+ case 'Home':
228
+ case 'End':
229
+ e.preventDefault()
230
+ if (!this._isOptionsOpen) {
231
+ this._isOptionsOpen = true
232
+ }
233
+ this.listboxRef.value?.focusFirstOrSelectedOption()
234
+ break
235
+ case 'ArrowLeft':
236
+ if (this.multiple && this._value.length > 0) {
237
+ e.preventDefault()
238
+ this.focusTag(this._value.length - 1)
239
+ }
240
+ break
241
+ case 'Backspace':
242
+ case 'Delete':
243
+ if (this.multiple && this._value.length > 0) {
244
+ e.preventDefault()
245
+ this.removeSelected(this._value[this._value.length - 1])
246
+ }
247
+ break
248
+ }
249
+ }
250
+
251
+ protected handleOptionToggled(e: CustomEvent) {
252
+ this.toggleValue(e.detail)
253
+ }
254
+
255
+ protected handleSearch(e: CustomEvent) {
256
+ e.stopPropagation()
257
+ this._search = e.detail.toLowerCase()
258
+ this.checkForMatches()
259
+ }
260
+
261
+ protected handleInputKeydown(e: KeyboardEvent): void {
262
+ // Backspace has special DOM-dependent conditions
263
+ if (e.key === 'Backspace') {
264
+ const inputEmpty = !this.inputRef.value?.value
265
+ if (!this._search && inputEmpty && this.multiple && this._value.length > 0)
266
+ this.removeLastValue(e)
267
+ return
268
+ }
269
+
270
+ if (e.key === 'ArrowLeft' && this.multiple && this._value.length > 0) {
271
+ const input = this.inputRef.value
272
+ if (input && input.selectionStart === 0 && !input.value) {
273
+ e.preventDefault()
274
+ this.focusTag(this._value.length - 1)
275
+ return
276
+ }
277
+ }
278
+
279
+ const action = getInputKeyAction(e.key, e.shiftKey, this.multiple)
280
+ if (!action) return
281
+
282
+ // When the dropdown is closed, let Tab move focus naturally.
283
+ if (e.key === 'Tab' && !this._isOptionsOpen) return
284
+
285
+ // For Tab/'focusListbox': only focus the listbox if it has focusable options.
286
+ // If the listbox is empty (no matches), close and let Tab move focus naturally.
287
+ if (action === 'focusListbox' && e.key === 'Tab') {
288
+ const hasFocusable = this.listboxRef.value?.querySelector(
289
+ '[role="option"]:not([data-disabled]), [data-type="new-option"]',
290
+ )
291
+ if (!hasFocusable) {
292
+ this.closeAndProcessInput()
293
+ return
294
+ }
295
+ }
296
+
297
+ e.preventDefault()
298
+ switch (action) {
299
+ case 'addValue':
300
+ this.addValue()
301
+ break
302
+ case 'focusListbox':
303
+ this.listboxRef.value?.focusFirstOrSelectedOption()
304
+ break
305
+ case 'closeOptions':
306
+ this._isOptionsOpen = false
307
+ // Don't refocus — the text input already has focus, and focusing
308
+ // it again would trigger handleFocus which reopens the dropdown
309
+ break
310
+ }
311
+ }
312
+
313
+ protected handleTagRemove(value: string | null): void {
314
+ this.removeSelected(value)
315
+ if (this._hasTextInput && this.inputRef.value) {
316
+ this._inputFocus = true
317
+ this.inputRef.value.focus()
318
+ this.requestUpdate()
319
+ }
320
+ }
321
+
322
+ protected getInsideTags(): HTMLElement[] {
323
+ return Array.from(
324
+ this.querySelectorAll<HTMLElement>('.pkt-combobox__input .pkt-combobox__tag-list pkt-tag'),
325
+ )
326
+ }
327
+
328
+ protected focusTag(index: number): void {
329
+ const tags = this.getInsideTags()
330
+ tags.forEach((tag, i) => {
331
+ ;(tag as PktTag).buttonTabindex = i === index ? 0 : -1
332
+ })
333
+ // Focus the button inside the target pkt-tag
334
+ const btn = tags[index]?.querySelector('button')
335
+ btn?.focus()
336
+ }
337
+
338
+ protected resetTagTabindices(): void {
339
+ const tags = this.getInsideTags()
340
+ tags.forEach((tag) => {
341
+ ;(tag as PktTag).buttonTabindex = -1
342
+ })
343
+ }
344
+
345
+ protected handleTagKeydown(e: KeyboardEvent, index: number): void {
346
+ e.stopPropagation()
347
+ const returnFocusToTrigger = () => {
348
+ this.resetTagTabindices()
349
+ if (this._hasTextInput && this.inputRef.value) {
350
+ this.inputRef.value.focus()
351
+ } else {
352
+ this.triggerRef.value?.focus()
353
+ }
354
+ }
355
+
356
+ switch (e.key) {
357
+ case 'ArrowLeft':
358
+ e.preventDefault()
359
+ if (index > 0) {
360
+ this.focusTag(index - 1)
361
+ }
362
+ break
363
+ case 'ArrowRight':
364
+ e.preventDefault()
365
+ if (index < this._value.length - 1) {
366
+ this.focusTag(index + 1)
367
+ } else {
368
+ returnFocusToTrigger()
369
+ }
370
+ break
371
+ case 'Backspace':
372
+ case 'Delete':
373
+ e.preventDefault()
374
+ {
375
+ const val = this._value[index]
376
+ const nextIndex = index >= this._value.length - 1 ? index - 1 : index
377
+ this.removeSelected(val)
378
+ if (nextIndex >= 0) {
379
+ this.requestUpdate()
380
+ this.updateComplete.then(() => this.focusTag(nextIndex))
381
+ } else {
382
+ returnFocusToTrigger()
383
+ }
384
+ }
385
+ break
386
+ case 'Tab':
387
+ // Let Tab move focus naturally, but reset roving tabindex
388
+ // so the next Tab into the combobox lands on the trigger, not a tag
389
+ this.resetTagTabindices()
390
+ break
391
+ case 'Escape':
392
+ e.preventDefault()
393
+ returnFocusToTrigger()
394
+ break
395
+ }
396
+ }
397
+
398
+ protected checkForMatches() {
399
+ const inputValue = this.inputRef.value?.value || this._search || ''
400
+ const result = checkForMatches(
401
+ inputValue,
402
+ this._value,
403
+ this.options,
404
+ this.allowUserInput,
405
+ this.multiple,
406
+ )
407
+
408
+ if (result.shouldRemoveValue) {
409
+ this.removeValue(this._value[0])
410
+ }
411
+ if (result.shouldResetInput) {
412
+ this.resetComboboxInput(false)
413
+ return
414
+ }
415
+
416
+ this._addValueText = result.addValueText
417
+ this._userInfoMessage = result.userInfoMessage
418
+ }
419
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type definitions for the combobox component family.
3
+ * Re-exports from shared-types for backward compatibility.
4
+ */
5
+ export type {
6
+ IPktComboboxOption,
7
+ TPktComboboxTagSkin,
8
+ TPktComboboxDisplayValue,
9
+ TPktComboboxTagPlacement,
10
+ } from 'shared-types/combobox'
@@ -0,0 +1,135 @@
1
+ import type { IPktComboboxOption } from 'shared-types/combobox'
2
+ import type { TTagSkin } from '../tag'
3
+ import { buildFulltext } from 'shared-utils/combobox/option-utils'
4
+
5
+ /**
6
+ * Lit-specific utility functions for PktCombobox component.
7
+ *
8
+ * Framework-agnostic functions live in shared-utils/combobox/.
9
+ * Only Lit-specific functions (using Ref, in-place mutation, PktSlotController) stay here.
10
+ */
11
+
12
+ // Selection state helpers (mutating, Lit-specific)
13
+
14
+ /**
15
+ * In-place option mutation helpers that preserve referential identity
16
+ * between this._options and this.options in the Lit component.
17
+ */
18
+ export const selectionMutators = {
19
+ markOptionSelected(options: IPktComboboxOption[], value: string | null): void {
20
+ if (!value) return
21
+ for (const option of options) {
22
+ if (option.value === value) {
23
+ option.selected = true
24
+ break
25
+ }
26
+ }
27
+ },
28
+
29
+ markOptionDeselected(options: IPktComboboxOption[], value: string | null): void {
30
+ if (!value) return
31
+ for (const option of options) {
32
+ if (option.value === value) {
33
+ option.selected = false
34
+ break
35
+ }
36
+ }
37
+ },
38
+
39
+ markAllSelected(options: IPktComboboxOption[]): void {
40
+ for (const option of options) {
41
+ option.selected = true
42
+ }
43
+ },
44
+
45
+ markAllDeselected(options: IPktComboboxOption[]): void {
46
+ for (const option of options) {
47
+ option.selected = false
48
+ }
49
+ },
50
+
51
+ removeUserAddedOptions(options: IPktComboboxOption[]): IPktComboboxOption[] {
52
+ return options.filter((option) => !option.userAdded)
53
+ },
54
+ }
55
+
56
+ // Slot parsing (Lit-specific)
57
+
58
+ /**
59
+ * Parses option elements from slot controller nodes into IPktComboboxOption[].
60
+ */
61
+ export const slotUtils = {
62
+ parseOptionsFromSlot(nodes: Element[]): IPktComboboxOption[] {
63
+ const options: IPktComboboxOption[] = []
64
+
65
+ nodes.forEach((node: Element) => {
66
+ if (!node.textContent && !node.getAttribute('value')) return
67
+
68
+ const option: IPktComboboxOption = {
69
+ value: node.getAttribute('value') || node.textContent || '',
70
+ label: node.textContent || node.getAttribute('value') || '',
71
+ }
72
+
73
+ if (node.getAttribute('data-prefix')) {
74
+ option.prefix = node.getAttribute('data-prefix') || undefined
75
+ }
76
+ if (node.getAttribute('tagskincolor')) {
77
+ option.tagSkinColor = node.getAttribute('tagskincolor') as TTagSkin
78
+ }
79
+ if (node.getAttribute('description')) {
80
+ option.description = node.getAttribute('description') || undefined
81
+ }
82
+
83
+ option.fulltext = buildFulltext(option)
84
+ options.push(option)
85
+ })
86
+
87
+ return options
88
+ },
89
+ }
90
+
91
+ // Options state synchronization (Lit-specific, in-place mutation)
92
+
93
+ export const optionStateUtils = {
94
+ /**
95
+ * Ensures options have labels and fulltext set.
96
+ * Also syncs selected state with current values.
97
+ *
98
+ * IMPORTANT: Mutates options in place to preserve referential identity
99
+ * between this._options and this.options in the Lit component.
100
+ */
101
+ syncOptionsWithValues(
102
+ options: IPktComboboxOption[],
103
+ values: string[],
104
+ ): { options: IPktComboboxOption[]; newValues: string[] } {
105
+ const newValues = [...values]
106
+
107
+ options.forEach((option) => {
108
+ if (option.value && !option.label) {
109
+ option.label = option.value
110
+ }
111
+ if (option.selected && !newValues.includes(option.value)) {
112
+ newValues.push(option.value)
113
+ }
114
+ option.fulltext = buildFulltext(option)
115
+ option.selected = option.selected || newValues.includes(option.value)
116
+ })
117
+
118
+ return { options, newValues }
119
+ },
120
+
121
+ /**
122
+ * Merges user-added options with new options, preserving user additions.
123
+ */
124
+ mergeWithUserAdded(
125
+ newOptions: IPktComboboxOption[],
126
+ previousOptions: IPktComboboxOption[],
127
+ ): IPktComboboxOption[] {
128
+ const userAddedValues = previousOptions.filter((option) => option?.userAdded && option.selected)
129
+ const filteredUserAdded = userAddedValues.filter(
130
+ (userOpt) =>
131
+ !(Array.isArray(newOptions) ? newOptions : []).some((opt) => opt.value === userOpt.value),
132
+ )
133
+ return [...filteredUserAdded, ...newOptions]
134
+ },
135
+ }