@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
@@ -1,32 +1,27 @@
1
1
  import { html, nothing, PropertyValues } from 'lit'
2
2
  import { ifDefined } from 'lit/directives/if-defined.js'
3
- import { customElement, property, state } from 'lit/decorators.js'
4
- import { Ref, createRef, ref } from 'lit/directives/ref.js'
3
+ import { customElement } from 'lit/decorators.js'
4
+ import { ref } from 'lit/directives/ref.js'
5
5
  import { classMap } from 'lit/directives/class-map.js'
6
6
  import { repeat } from 'lit/directives/repeat.js'
7
- import { PktInputElement } from '@/base-elements/input-element'
8
- import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
9
- import { PktSlotController } from '@/controllers/pkt-slot-controller'
10
- import specs from 'componentSpecs/combobox.json'
7
+
8
+ import type {
9
+ IPktComboboxOption,
10
+ TPktComboboxTagPlacement,
11
+ TPktComboboxDisplayValue,
12
+ } from './combobox-types'
13
+ import { findOptionByValue, findOptionIndex } from 'shared-utils/combobox/option-utils'
14
+ import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
15
+ import { slotUtils, optionStateUtils } from './combobox-utils'
16
+ import { ComboboxHandlers } from './combobox-handlers'
11
17
 
12
18
  import '../input-wrapper'
13
19
  import '../icon'
14
20
  import '../tag'
15
21
  import '../listbox'
16
- import PktListbox from '../listbox'
17
- import { TTagSkin } from '../tag'
18
22
 
19
- export interface IPktComboboxOption {
20
- description?: string
21
- disabled?: boolean
22
- fulltext?: string
23
- label?: string
24
- prefix?: string
25
- selected?: boolean
26
- tagSkinColor?: TTagSkin
27
- userAdded?: boolean
28
- value: string
29
- }
23
+ // Re-export types for backward compatibility
24
+ export type { IPktComboboxOption, TPktComboboxTagPlacement } from './combobox-types'
30
25
 
31
26
  export interface IPktCombobox {
32
27
  allowUserInput?: boolean
@@ -57,10 +52,9 @@ export interface IPktCombobox {
57
52
  tagPlacement?: TPktComboboxTagPlacement | null
58
53
  tagText?: string | null
59
54
  value?: string | string[]
55
+ isOpen?: boolean
60
56
  }
61
57
 
62
- export type TPktComboboxTagPlacement = 'inside' | 'outside'
63
-
64
58
  declare global {
65
59
  interface HTMLElementTagNameMap {
66
60
  'pkt-combobox': PktCombobox & HTMLSelectElement
@@ -68,57 +62,20 @@ declare global {
68
62
  }
69
63
 
70
64
  @customElement('pkt-combobox')
71
- export class PktCombobox extends PktInputElement implements IPktCombobox {
72
- private helptextSlot: Ref<HTMLElement> = createRef()
73
-
74
- constructor() {
75
- super()
76
- this.optionsController = new PktOptionsSlotController(this)
77
- this.slotController = new PktSlotController(this, this.helptextSlot)
78
- this.slotController.skipOptions = true
65
+ export class PktCombobox extends ComboboxHandlers implements IPktCombobox {
66
+ // Bound handler for body click — stored for cleanup in disconnectedCallback
67
+ private handleBodyClick = (e: MouseEvent) => {
68
+ if (this._isOptionsOpen && !this.contains(e.target as Node)) {
69
+ this.closeAndProcessInput()
70
+ }
79
71
  }
80
72
 
81
- // Props / Attributes
82
- @property({ type: String, reflect: true }) value: string | string[] = ''
83
- @property({ type: Array }) options: IPktComboboxOption[] = []
84
- @property({ type: Array }) defaultOptions: IPktComboboxOption[] = []
85
- @property({ type: Boolean }) allowUserInput: boolean = false
86
- @property({ type: Boolean }) typeahead: boolean = false
87
- @property({ type: Boolean }) includeSearch: boolean = false
88
- @property({ type: String }) searchPlaceholder: string = ''
89
- @property({ type: Boolean }) multiple: boolean = false
90
- @property({ type: Number }) maxlength: number | null = null
91
- @property({ type: String }) displayValueAs: string = specs.props.displayValueAs.default
92
- @property({ type: String }) tagPlacement: TPktComboboxTagPlacement | null = null
93
-
94
- // State
95
- @state() _options: IPktComboboxOption[] = []
96
- @state() _value: string[] = [] // Internal value representation
97
- @state() private _isOptionsOpen = false
98
- @state() private _userInfoMessage: string = ''
99
- @state() private _addValueText: string | null = null
100
- @state() private _maxIsReached: boolean = false
101
- @state() private _search: string = ''
102
- @state() private _inputFocus: boolean = false
103
- @state() private _editingSingleValue: boolean = false
104
-
105
- // Refs
106
- inputRef: Ref<HTMLInputElement> = createRef()
107
- arrowRef: Ref<HTMLButtonElement> = createRef()
108
- listboxRef: Ref<PktListbox> = createRef()
109
- focusRef: Ref<HTMLElement> = createRef()
110
- optionTagRef: Ref<HTMLElement> = createRef()
111
-
112
73
  // Lifecycle methods
74
+
113
75
  connectedCallback(): void {
114
76
  super.connectedCallback()
115
77
 
116
- document &&
117
- document.body.addEventListener('click', (e: MouseEvent) => {
118
- if (this._isOptionsOpen && !this.contains(e.target as Node)) {
119
- this.handleFocusOut(e)
120
- }
121
- })
78
+ document?.body.addEventListener('click', this.handleBodyClick)
122
79
 
123
80
  this._options = []
124
81
 
@@ -131,50 +88,77 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
131
88
 
132
89
  // If options are provided via the options slot, we need to extract them
133
90
  if (this.optionsController?.nodes && this.optionsController.nodes.length) {
134
- const options: IPktComboboxOption[] = []
135
- this.optionsController.nodes.forEach((node: Element) => {
136
- if (!node.textContent && !node.getAttribute('value')) return null
137
- const option: IPktComboboxOption = {
138
- value: node.getAttribute('value') || node.textContent || '',
139
- label: node.textContent || node.getAttribute('value') || '',
140
- }
141
- if (node.getAttribute('data-prefix')) {
142
- option.prefix = node.getAttribute('data-prefix') || undefined
143
- }
144
- if (node.getAttribute('tagskincolor')) {
145
- option.tagSkinColor = node.getAttribute('tagskincolor') as TTagSkin
146
- }
147
- if (node.getAttribute('description')) {
148
- option.description = node.getAttribute('description') || undefined
149
- }
150
- option.fulltext = option.value + option.label + (option.prefix || '')
151
- options.push(option)
152
- })
153
- if (options.length) {
154
- this.options = [...options]
155
- this._options = [...options]
91
+ const parsedOptions = slotUtils.parseOptionsFromSlot(this.optionsController.nodes)
92
+ if (parsedOptions.length) {
93
+ this.options = [...parsedOptions]
94
+ this._options = [...parsedOptions]
95
+ this._optionsFromSlot = true
96
+ this._lastSlotGeneration = this.optionsController.generation
156
97
  }
157
98
  }
158
99
  }
159
100
 
160
- updated(changedProperties: PropertyValues): void {
161
- if (changedProperties.has('_value')) {
162
- this.valueChanged(this._value, changedProperties.get('_value') as string[])
101
+ protected willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
102
+ // Re-parse slot options when the controller detects mutations.
103
+ // The controller increments its generation counter on each mutation, but
104
+ // doesn't set any reactive properties — so we detect the change here.
105
+ if (this._optionsFromSlot && this.optionsController) {
106
+ const currentGen = this.optionsController.generation
107
+ if (currentGen !== this._lastSlotGeneration) {
108
+ this._lastSlotGeneration = currentGen
109
+ const parsedOptions = slotUtils.parseOptionsFromSlot(this.optionsController.nodes)
110
+ const userAdded = this._options.filter((o) => o.userAdded)
111
+ this.options = [...userAdded, ...parsedOptions]
112
+ }
163
113
  }
164
- if (changedProperties.has('value')) {
165
- if (Array.isArray(this.value)) {
166
- this._value = this.value
167
- } else if (this.value && this.multiple) {
168
- this._value = this.value.split(',')
169
- } else if (this.value) {
170
- this._value = [this.value]
171
- } else {
172
- this._value = []
114
+ super.willUpdate(changedProperties)
115
+ }
116
+
117
+ disconnectedCallback(): void {
118
+ super.disconnectedCallback()
119
+ document?.body.removeEventListener('click', this.handleBodyClick)
120
+ }
121
+
122
+ firstUpdated(changedProperties: PropertyValues): void {
123
+ // Apply defaultValue before the base class firstUpdated, which calls
124
+ // valueChanged(defaultValue) — a no-op in combobox. Setting this.value
125
+ // here lets updated() handle the sync via the normal value-change path.
126
+ if (this.defaultValue !== null && !this.value) {
127
+ this.value = this.defaultValue
128
+ }
129
+ super.firstUpdated(changedProperties)
130
+ }
131
+
132
+ updated(changedProperties: PropertyValues): void {
133
+ if (changedProperties.has('isOpen')) {
134
+ this._isOptionsOpen = this.isOpen
135
+ }
136
+
137
+ // Handle value and _value changes.
138
+ // Three cases:
139
+ // 1. value changed from our own syncValueAndDispatch (internal sync) — skip value handler,
140
+ // but still process concurrent _value changes
141
+ // 2. value changed externally — sync _value from value, dispatch events
142
+ // 3. Only _value changed — sync value from _value, dispatch events
143
+ const valueChanged = changedProperties.has('value')
144
+ const internalChanged = changedProperties.has('_value')
145
+ const isInternalSync = valueChanged && this._internalValueSync
146
+
147
+ if (isInternalSync) {
148
+ this._internalValueSync = false
149
+ if (internalChanged) {
150
+ this.syncValueAndDispatch(changedProperties.get('_value') as string[])
173
151
  }
174
- if (!this.multiple && this._value.length > 1) {
175
- this._value = [this._value[0]]
152
+ } else if (valueChanged) {
153
+ const oldInternal = [...this._value]
154
+ const newInternal = this.parseValue()
155
+ if (newInternal.join(',') !== this._value.join(',')) {
156
+ this._value = newInternal
176
157
  }
177
- this.isMaxItemsReached()
158
+ this.updateMaxReached()
159
+ this.syncValueAndDispatch(oldInternal)
160
+ } else if (internalChanged) {
161
+ this.syncValueAndDispatch(changedProperties.get('_value') as string[])
178
162
  }
179
163
 
180
164
  // If defaultOptions changed, update options (preserving userAdded)
@@ -186,35 +170,22 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
186
170
  }
187
171
 
188
172
  if (changedProperties.has('options')) {
189
- // If options change, we need to update _options, but we need to preserve userAdded values
190
- // Bug fix 2025-09-19: Fetch userAdded from _old_ options to prevent accidental overwrite
191
173
  const prevOptions =
192
174
  (changedProperties.get('options') as IPktComboboxOption[]) || this._options || []
193
- const userAddedValues = prevOptions.filter((option) => option && option.userAdded)
194
- const filteredUserAdded = userAddedValues.filter(
195
- (userOpt) =>
196
- !(Array.isArray(this.options) ? this.options : []).some(
197
- (opt) => opt.value === userOpt.value,
198
- ),
199
- )
200
- const mergedOptions = [...filteredUserAdded, ...this.options]
175
+ const mergedOptions = optionStateUtils.mergeWithUserAdded(this.options, prevOptions)
201
176
  this._options = mergedOptions
202
- if (filteredUserAdded.length > 0) {
177
+ if (mergedOptions.length > this.options.length) {
203
178
  this.options = mergedOptions
204
179
  }
205
180
 
206
- this._options.forEach((option) => {
207
- if (option.value && !option.label) {
208
- option.label = option.value
209
- }
210
- if (option.selected && !this._value.includes(option.value)) {
211
- const oldValue = [...this._value]
212
- this._value = [...this._value, option.value]
213
- this.valueChanged(this._value, oldValue)
214
- }
215
- option.fulltext = option.value + option.label + (option.prefix || '')
216
- option.selected = option.selected || this._value.includes(option.value)
217
- })
181
+ const syncResult = optionStateUtils.syncOptionsWithValues(this._options, this._value)
182
+ this._options = syncResult.options
183
+
184
+ if (syncResult.newValues.length > this._value.length) {
185
+ const oldValue = [...this._value]
186
+ this._value = syncResult.newValues
187
+ this.syncValueAndDispatch(oldValue)
188
+ }
218
189
  }
219
190
  if (changedProperties.has('_search')) {
220
191
  this.dispatchEvent(
@@ -224,41 +195,83 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
224
195
  }),
225
196
  )
226
197
  }
198
+ // Sync text input display value for single+typeahead when dropdown is closed
199
+ if (
200
+ !this._isOptionsOpen &&
201
+ !this.multiple &&
202
+ this._hasTextInput &&
203
+ this.inputRef.value &&
204
+ this.inputRef.value.type !== 'hidden'
205
+ ) {
206
+ const displayValue = this._value[0]
207
+ ? getSingleValueForInput(this._value[0], this.options, this.displayValueAs)
208
+ : ''
209
+ if (this.inputRef.value.value !== displayValue) {
210
+ this.inputRef.value.value = displayValue
211
+ }
212
+ }
213
+
227
214
  super.updated(changedProperties)
228
215
  }
229
216
 
230
- attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
231
- if (name === 'value') {
232
- if (Array.isArray(this.value)) {
233
- this._value = this.value
234
- } else if (this.value && this.multiple) {
235
- this._value = this.value.split(',')
236
- } else if (this.value) {
237
- this._value = [this.value]
238
- } else {
239
- this._value = []
240
- }
241
- if (!this.multiple && this._value.length > 1) {
242
- this._value = [this._value[0]]
243
- }
217
+ /**
218
+ * Override form reset to properly restore combobox state.
219
+ * The base class deselects all options and sets value/defaultValue, but
220
+ * combobox needs to re-sync _options with the restored values and clean up
221
+ * user-added options and UI state.
222
+ */
223
+ protected override formResetCallback(): void {
224
+ this.touched = false
225
+
226
+ // Restore value from defaultValue (set by base class firstUpdated from
227
+ // the initial value attribute, per MDN HTMLInputElement.defaultValue)
228
+ const resetValue = this.defaultValue || (this.multiple ? '' : '')
229
+ this.value = resetValue
230
+ this._value = this.parseValue()
231
+
232
+ // Remove user-added options, then re-sync selection state with restored _value.
233
+ // We must create new arrays because the base class mutates option objects in place.
234
+ this._options = this._options
235
+ .filter((o) => !o.userAdded)
236
+ .map((o) => ({ ...o, selected: this._value.includes(o.value) }))
237
+ this.options = this.options
238
+ .filter((o) => !o.userAdded)
239
+ .map((o) => ({ ...o, selected: this._value.includes(o.value) }))
240
+
241
+ // Reset UI state
242
+ this._search = ''
243
+ this._isOptionsOpen = false
244
+ this._userInfoMessage = ''
245
+ this._addValueText = null
246
+ this._inputFocus = false
247
+ this.updateMaxReached()
248
+
249
+ if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
250
+ this.inputRef.value.value = ''
244
251
  }
252
+
253
+ this.internals.setFormValue('')
254
+ this.internals.ariaInvalid = 'false'
255
+ this.requestUpdate()
256
+ }
257
+
258
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
259
+ // Don't set _value here for 'value' changes — this.value hasn't been updated yet
260
+ // (super.attributeChangedCallback does that). Let updated() handle the sync.
245
261
  if (name === 'options') {
246
262
  this._options = Array.isArray(this.options) ? [...this.options] : []
247
- this._options.forEach((option) => {
248
- if (option.value && !option.label) {
249
- option.label = option.value
250
- }
251
- if (option.selected && !this._value.includes(option.value)) {
252
- this._value = [...this._value, option.value]
253
- }
254
- option.fulltext = option.value + option.label + (option.prefix || '')
255
- })
263
+ const syncResult = optionStateUtils.syncOptionsWithValues(this._options, this._value)
264
+ this._options = syncResult.options
265
+ if (syncResult.newValues.length > this._value.length) {
266
+ this._value = syncResult.newValues
267
+ }
256
268
  this._search = ''
257
269
  }
258
270
  super.attributeChangedCallback(name, _old, value)
259
271
  }
260
272
 
261
273
  // Render methods
274
+
262
275
  render() {
263
276
  return html`
264
277
  <pkt-input-wrapper
@@ -277,7 +290,8 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
277
290
  .requiredText=${this.requiredText}
278
291
  .tagText=${this.tagText}
279
292
  useWrapper=${this.useWrapper}
280
- .forId=${this.allowUserInput || this.typeahead ? this.id + '-input' : this.id + '-arrow'}
293
+ .forId=${this._hasTextInput ? this.id + '-input' : this.id + '-combobox'}
294
+ ?hasFieldset=${!this._hasTextInput}
281
295
  class="pkt-combobox__wrapper"
282
296
  @labelClick=${this.handleInputClick}
283
297
  >
@@ -291,10 +305,31 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
291
305
  'pkt-combobox__input--error': this.hasError,
292
306
  'pkt-combobox__input--disabled': this.disabled,
293
307
  })}
294
- tabindex="-1"
308
+ id=${ifDefined(!this._hasTextInput ? `${this.id}-combobox` : undefined)}
309
+ role=${ifDefined(!this._hasTextInput ? 'combobox' : undefined)}
310
+ aria-expanded=${ifDefined(
311
+ !this._hasTextInput ? (this._isOptionsOpen ? 'true' : 'false') : undefined,
312
+ )}
313
+ aria-controls=${ifDefined(!this._hasTextInput ? `${this.id}-listbox` : undefined)}
314
+ aria-haspopup=${ifDefined(!this._hasTextInput ? 'listbox' : undefined)}
315
+ aria-labelledby=${ifDefined(
316
+ !this._hasTextInput ? `${this.id}-combobox-label` : undefined,
317
+ )}
318
+ aria-activedescendant=${ifDefined(
319
+ !this._hasTextInput &&
320
+ this._value[0] &&
321
+ !!findOptionByValue(this.options, this._value[0])
322
+ ? `${this.id}-listbox-${findOptionIndex(this._options, this._value[0])}`
323
+ : undefined,
324
+ )}
325
+ aria-description=${ifDefined(this._selectionDescription || undefined)}
326
+ tabindex=${!this._hasTextInput ? (this.disabled ? '-1' : '0') : '-1'}
295
327
  @click=${this.handleInputClick}
328
+ @keydown=${!this._hasTextInput ? this.handleSelectOnlyKeydown : nothing}
329
+ ${ref(this.triggerRef)}
296
330
  >
297
- ${this.placeholder &&
331
+ ${!this._hasTextInput &&
332
+ this.placeholder &&
298
333
  (!this._value.length || (this.multiple && this.tagPlacement == 'outside')) &&
299
334
  !this._inputFocus
300
335
  ? html`<span class="pkt-combobox__placeholder" @click=${this.handlePlaceholderClick}
@@ -304,35 +339,14 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
304
339
  ? this.renderSingleOrMultipleValues()
305
340
  : nothing}
306
341
  ${this.renderInputField()}
307
- <div
308
- class="pkt-btn pkt-btn--tertiary pkt-combobox__arrow"
309
- @click=${this.handleArrowClick}
310
- @keydown=${this.handleArrowClick}
311
- id="${this.id}-arrow"
312
- ${ref(this.arrowRef)}
313
- aria-expanded=${this._isOptionsOpen}
314
- aria-controls="${this.id}-listbox"
315
- aria-haspopup="listbox"
316
- aria-label="Åpne liste"
317
- ?disabled=${this.disabled}
318
- ?data-disabled=${this.disabled}
319
- role="button"
320
- tabindex="${this.disabled ? '-1' : '0'}"
321
- >
322
- <pkt-icon
323
- class=${classMap({
324
- 'pkt-combobox__arrow-icon': true,
325
- 'pkt-combobox__arrow-icon--open': this._isOptionsOpen,
326
- })}
327
- name="chevron-thin-down"
328
- ></pkt-icon>
329
- </div>
330
- <div
331
- ${ref(this.focusRef)}
332
- tabindex="-1"
333
- @keydown=${this.handleArrowClick}
334
- class="pkt-contents"
335
- ></div>
342
+ <pkt-icon
343
+ class=${classMap({
344
+ 'pkt-combobox__arrow-icon': true,
345
+ 'pkt-combobox__arrow-icon--open': this._isOptionsOpen,
346
+ })}
347
+ name="chevron-thin-down"
348
+ aria-hidden="true"
349
+ ></pkt-icon>
336
350
  </div>
337
351
 
338
352
  <pkt-listbox
@@ -341,16 +355,17 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
341
355
  .isOpen=${this._isOptionsOpen}
342
356
  .searchPlaceholder=${this.searchPlaceholder}
343
357
  .label="Liste: ${this.label || ''}"
344
- ?includeSearch=${this.includeSearch}
345
- ?isMultiSelect=${this.multiple}
346
- ?allowUserInput=${this.allowUserInput && !this._maxIsReached}
347
- ?maxIsReached=${this._maxIsReached}
358
+ ?include-search=${this.includeSearch}
359
+ ?is-multi-select=${this.multiple}
360
+ ?allow-user-input=${this.allowUserInput && !this._maxIsReached}
361
+ ?max-is-reached=${this._maxIsReached}
348
362
  .customUserInput=${ifDefined(this._addValueText)}
349
363
  .userMessage=${this._userInfoMessage}
350
364
  @search=${this.handleSearch}
351
365
  @option-toggle=${this.handleOptionToggled}
352
366
  @select-all=${this.addAllOptions}
353
367
  @close-options=${() => (this._isOptionsOpen = false)}
368
+ @tab-close=${() => this.closeAndProcessInput()}
354
369
  .searchValue=${this._search || null}
355
370
  .maxLength=${this.maxlength || 0}
356
371
  ${ref(this.listboxRef)}
@@ -366,7 +381,7 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
366
381
  `
367
382
  }
368
383
 
369
- renderInputField() {
384
+ private renderInputField() {
370
385
  return this.typeahead || this.allowUserInput
371
386
  ? html`
372
387
  <div class="pkt-combobox__input-div combobox__input">
@@ -374,20 +389,31 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
374
389
  type="text"
375
390
  id="${this.id}-input"
376
391
  name=${(this.name || this.id) + '-input'}
392
+ placeholder=${ifDefined(
393
+ !this._value.length || (this.multiple && this.tagPlacement === 'outside')
394
+ ? this.placeholder
395
+ : undefined,
396
+ )}
377
397
  @input=${this.handleInput}
398
+ @change=${(e: Event) => {
399
+ e.stopPropagation()
400
+ e.stopImmediatePropagation()
401
+ }}
378
402
  @keydown=${this.handleInputKeydown}
379
403
  @focus=${this.handleFocus}
380
404
  @blur=${this.handleBlur}
381
405
  autocomplete="off"
382
406
  role="combobox"
407
+ aria-expanded=${this._isOptionsOpen ? 'true' : 'false'}
383
408
  aria-label=${ifDefined(this.label)}
384
- aria-autocomplete=${this.typeahead ? 'both' : 'list'}
409
+ aria-autocomplete=${this.typeahead ? 'both' : this.allowUserInput ? 'list' : 'none'}
385
410
  aria-controls="${this.id}-listbox"
386
411
  aria-activedescendant=${ifDefined(
387
- this._value[0] && !!this.findValueInOptions(this._value[0])
388
- ? `${this.id}-listbox-${this.findIndexInOptions(this._value[0])}`
412
+ this._value[0] && !!findOptionByValue(this.options, this._value[0])
413
+ ? `${this.id}-listbox-${findOptionIndex(this._options, this._value[0])}`
389
414
  : undefined,
390
415
  )}
416
+ aria-description=${ifDefined(this._selectionDescription || undefined)}
391
417
  ${ref(this.inputRef)}
392
418
  />
393
419
  </div>
@@ -403,39 +429,56 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
403
429
  `
404
430
  }
405
431
 
406
- renderSingleOrMultipleValues() {
432
+ private renderSingleOrMultipleValues() {
433
+ // Single select with text input: value is shown in the input field, not as a span
434
+ if (!this.multiple && this._hasTextInput) return nothing
435
+
407
436
  const isSingleValueDisplay = !this.multiple
408
437
 
409
- // enkeltverdi som tekst
410
- const singleValueContent = !this._editingSingleValue
411
- ? this.renderValueTag(this.findValueInOptions(this._value[0]))
412
- : null
413
-
414
- // Multiple vises som tags
415
- const multipleValuesContent = repeat(
416
- this._value,
417
- (value: string) => value,
418
- (value: string) => {
419
- const option = this.findValueInOptions(value)
420
- const tagSkinColor = this.options.find((o) => o.value === value)?.tagSkinColor
421
- return html`
422
- <pkt-tag
423
- skin=${tagSkinColor || 'blue-dark'}
424
- ?closeTag=${!this.disabled}
425
- @close=${() => this.handleTagRemove(value)}
426
- >
427
- ${this.renderValueTag(option)}
428
- </pkt-tag>
429
- `
430
- },
431
- )
438
+ // Single value displayed as text (select-only mode)
439
+ const singleValueContent = this.renderValueTag(findOptionByValue(this.options, this._value[0]))
440
+
441
+ // Multiple values displayed as tags, wrapped in a list for accessibility
442
+ const isOutside = this.tagPlacement === 'outside'
443
+ const multipleValuesContent = html`
444
+ <ul role="list" class="pkt-combobox__tag-list">
445
+ ${repeat(
446
+ this._value,
447
+ (value: string) => value,
448
+ (value: string, index: number) => {
449
+ const option = findOptionByValue(this.options, value)
450
+ const tagSkinColor = option?.tagSkinColor
451
+ return html`
452
+ <li
453
+ role="listitem"
454
+ @click=${isOutside ? nothing : (e: MouseEvent) => e.stopPropagation()}
455
+ @mousedown=${isOutside ? nothing : (e: MouseEvent) => e.preventDefault()}
456
+ >
457
+ <pkt-tag
458
+ skin=${tagSkinColor || 'blue-dark'}
459
+ ?closeTag=${!this.disabled}
460
+ .buttonTabindex=${isOutside ? undefined : -1}
461
+ @close=${() => this.handleTagRemove(value)}
462
+ @keydown=${isOutside
463
+ ? nothing
464
+ : (e: KeyboardEvent) => this.handleTagKeydown(e, index)}
465
+ >
466
+ ${this.renderValueTag(option)}
467
+ </pkt-tag>
468
+ </li>
469
+ `
470
+ },
471
+ )}
472
+ </ul>
473
+ `
432
474
 
433
475
  return isSingleValueDisplay ? singleValueContent : multipleValuesContent
434
476
  }
435
477
 
436
- renderValueTag(option: IPktComboboxOption | null) {
478
+ private renderValueTag(option: IPktComboboxOption | null) {
437
479
  if (!option) return ''
438
- switch (this.displayValueAs) {
480
+ const displayAs = this.displayValueAs as TPktComboboxDisplayValue
481
+ switch (displayAs) {
439
482
  case 'prefixAndValue':
440
483
  return html`<span class="pkt-combobox__value" data-focusfix=${this.id}
441
484
  >${option.prefix || ''} ${option.value}</span
@@ -451,523 +494,6 @@ export class PktCombobox extends PktInputElement implements IPktCombobox {
451
494
  >`
452
495
  }
453
496
  }
454
-
455
- // Event handlers
456
-
457
- handleInput(e: InputEvent): void {
458
- e.stopPropagation()
459
- e.stopImmediatePropagation()
460
-
461
- if (this.disabled) return
462
-
463
- this.touched = true
464
- const input = e.target as HTMLInputElement
465
- this._search = input.value
466
- this.checkForMatches()
467
-
468
- if (this.typeahead) {
469
- if (this._search) {
470
- this._options = this.options.filter((option) =>
471
- option.fulltext?.toLowerCase().includes(this._search.toLowerCase()),
472
- )
473
-
474
- if (e.inputType !== 'deleteContentBackward') {
475
- const matchingOptions = this._options.filter(
476
- (option) =>
477
- !option.selected &&
478
- option.label?.toLowerCase().startsWith(this._search.toLowerCase()),
479
- )
480
-
481
- if (
482
- matchingOptions.length > 0 &&
483
- this.inputRef.value &&
484
- this.inputRef.value.type !== 'hidden'
485
- ) {
486
- const match = matchingOptions[0]
487
-
488
- if (match?.label) {
489
- input.value = match.label
490
- window.setTimeout(
491
- () => input.setSelectionRange(this._search.length, input.value.length),
492
- 0,
493
- )
494
- input.selectionDirection = 'backward'
495
- }
496
- }
497
- }
498
- } else {
499
- this._options = [...this.options]
500
- }
501
- }
502
- }
503
-
504
- private handleFocus(): void {
505
- if (this.disabled) return
506
-
507
- if (
508
- !this.multiple &&
509
- this._value[0] &&
510
- this.inputRef.value &&
511
- this.inputRef.value.type !== 'hidden'
512
- ) {
513
- const option = this.findValueInOptions(this._value[0])
514
- this._editingSingleValue = true
515
- this.inputRef.value.value =
516
- this.displayValueAs === 'label' && option?.label ? option.label : this._value[0]
517
- }
518
- this._inputFocus = true
519
- this._search = ''
520
- this._options = [...this.options]
521
- this._isOptionsOpen = true
522
- this.onFocus()
523
-
524
- this.requestUpdate()
525
- }
526
-
527
- private handleFocusOut(e: FocusEvent): void {
528
- if (this.disabled) return
529
-
530
- // Triggered when focus completely leaves the combobox and its children
531
- if (
532
- (e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
533
- (e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
534
- (e.target as Element)?.getAttribute('data-focusfix') !== this.id &&
535
- e.relatedTarget !== this.focusRef.value &&
536
- e.relatedTarget !== this.inputRef.value &&
537
- e.relatedTarget !== this.arrowRef.value &&
538
- this._isOptionsOpen
539
- ) {
540
- this._inputFocus = false
541
- this._addValueText = null
542
- this._userInfoMessage = ''
543
- this._search = ''
544
-
545
- // If value in text input, check if it should be added
546
- if (
547
- this.inputRef.value &&
548
- this.inputRef.value.type !== 'hidden' &&
549
- this.inputRef.value.value !== ''
550
- ) {
551
- const val = this.inputRef.value.value
552
- const valInOptions = this.findValueInOptions(val)
553
- if (!this._value.includes(val) && !valInOptions) {
554
- if (this.allowUserInput) {
555
- this.addNewUserValue(val)
556
- } else if (!this.multiple) {
557
- this.removeValue(this._value[0])
558
- }
559
- } else if (valInOptions && !this._value.includes(valInOptions.value)) {
560
- this.setSelected(valInOptions.value)
561
- }
562
- this.inputRef.value.value = ''
563
- }
564
- this._isOptionsOpen = false
565
- this.onBlur()
566
- }
567
- }
568
-
569
- private handleBlur(): void {
570
- this._inputFocus = false
571
- this._editingSingleValue = false
572
- this.onBlur()
573
- }
574
-
575
- private handleInputClick(e: MouseEvent): void {
576
- if (this.disabled) {
577
- e.preventDefault()
578
- e.stopImmediatePropagation()
579
- return
580
- }
581
- if (
582
- e.currentTarget &&
583
- e.currentTarget !== this.arrowRef.value &&
584
- this.inputRef.value?.type !== 'hidden'
585
- ) {
586
- this.inputRef.value?.focus()
587
- this.requestUpdate()
588
- } else {
589
- this.handleArrowClick(e)
590
- }
591
- }
592
-
593
- private handlePlaceholderClick(e: MouseEvent): void {
594
- if (this.disabled) return
595
- e.stopPropagation()
596
- if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
597
- this.inputRef.value.focus()
598
- this._inputFocus = true
599
- this.requestUpdate()
600
- }
601
- }
602
-
603
- private handleArrowClick(e: MouseEvent | KeyboardEvent): void {
604
- if (this.disabled) return
605
-
606
- if (e instanceof KeyboardEvent && e.key) {
607
- if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'ArrowDown') return
608
- }
609
-
610
- e.stopImmediatePropagation()
611
- e.preventDefault()
612
-
613
- this._isOptionsOpen = !this._isOptionsOpen
614
-
615
- if (this._isOptionsOpen) {
616
- this.listboxRef.value?.focusFirstOrSelectedOption()
617
- } else {
618
- this.arrowRef.value?.focus()
619
- }
620
- }
621
-
622
- private handleOptionToggled(e: CustomEvent) {
623
- this.toggleValue(e.detail)
624
- }
625
-
626
- private handleSearch(e: CustomEvent) {
627
- e.stopPropagation()
628
- this._search = e.detail.toLowerCase()
629
- }
630
-
631
- private handleInputKeydown(e: KeyboardEvent): void {
632
- switch (e.key) {
633
- case ',':
634
- if (this.multiple) {
635
- e.preventDefault()
636
- this.addValue()
637
- }
638
- break
639
- case 'Enter':
640
- e.preventDefault()
641
- this.addValue()
642
- break
643
- case 'Backspace':
644
- if (!this._search && this.inputRef.value?.type === 'hidden') this.removeLastValue(e)
645
- break
646
- case 'Tab':
647
- case 'ArrowDown':
648
- if (!e.shiftKey) {
649
- this.listboxRef.value?.focusFirstOrSelectedOption()
650
- e.preventDefault()
651
- }
652
- break
653
- case 'Escape':
654
- this._isOptionsOpen = false
655
- this.arrowRef.value?.focus()
656
- e.preventDefault()
657
- break
658
- default:
659
- break
660
- }
661
- }
662
-
663
- private handleTagRemove(value: string | null): void {
664
- this.removeSelected(value)
665
- }
666
-
667
- private blurInput(): void {
668
- if (this.inputRef.value && this.inputRef.value.matches(':focus')) {
669
- this.inputRef.value.blur()
670
- }
671
- }
672
-
673
- private checkForMatches() {
674
- // sjekker om verdiene bruker skriver inn finnes, er valgt eller kan legges til
675
- //setter riktig infomelding til bruker
676
- const inputValue = this.inputRef.value?.value || this._search || ''
677
- const searchValue = inputValue.trim().toLowerCase() || ''
678
-
679
- if (!searchValue) {
680
- if (!this.multiple && this._value[0]) {
681
- this.removeValue(this._value[0])
682
- }
683
-
684
- this.resetComboboxInput(false)
685
- return
686
- }
687
-
688
- const matchedValues = this._value.find((value) => value.toLowerCase() === searchValue)
689
- const matchedOptions: IPktComboboxOption[] = this._options.filter(
690
- (option) => option.label?.toLowerCase().includes(searchValue) ?? false,
691
- )
692
- const matchedOption = matchedOptions.find(
693
- (option) =>
694
- option.label?.toLowerCase() === searchValue || option.value.toLowerCase() === searchValue,
695
- )
696
-
697
- // sett riktig infomelding til bruker
698
- switch (true) {
699
- case (matchedOptions.length === 0 || !matchedOption) && this.allowUserInput:
700
- this._addValueText = inputValue
701
- this._userInfoMessage = ''
702
- break
703
-
704
- case matchedOptions.length === 0 && !this._options.length && !this.allowUserInput:
705
- this._addValueText = null
706
- this._userInfoMessage = 'Ingen match i søket'
707
- break
708
-
709
- case !!matchedValues:
710
- this._addValueText = null
711
- this._userInfoMessage = 'Verdien er allerede valgt'
712
- break
713
-
714
- case matchedOptions.length > 1:
715
- this._addValueText = null
716
- this._userInfoMessage = ''
717
- break
718
-
719
- default:
720
- this._addValueText = null
721
- this._userInfoMessage = '' // Default for å fjerne melding
722
- }
723
- }
724
-
725
- private findValueInOptions(value: string | null): IPktComboboxOption | null {
726
- return (
727
- this.options.find((option) => {
728
- return option.value === value || option.label === value
729
- }) || null
730
- )
731
- }
732
-
733
- private findIndexInOptions(value: string | null): number {
734
- return this._options.findIndex((option) => {
735
- return option.value === value || option.label === value
736
- })
737
- }
738
-
739
- private isMaxItemsReached(): boolean {
740
- const isReached = this.maxlength !== null && this._value.length >= this.maxlength
741
- if (!isReached) {
742
- this._maxIsReached = false
743
- } else {
744
- this._maxIsReached = true
745
- }
746
- return isReached
747
- }
748
-
749
- public toggleValue(value: string | null): void {
750
- if (this.disabled) return
751
-
752
- this.touched = true
753
- this._userInfoMessage = ''
754
- this._addValueText = null
755
-
756
- const valueFromOptions: string | null = this.findValueInOptions(value)?.value || null
757
- const isSelected: boolean = this._value.includes(value || valueFromOptions || '')
758
- const isInOption: boolean = !!valueFromOptions
759
- const isDisabled: boolean = this._options.find((o) => o.value === value)?.disabled || false
760
- const isEmpty: boolean = !value?.trim()
761
- const isSingle: boolean = !this.multiple
762
- const isMultiple: boolean = this.multiple
763
- const isMaxItemsReached: boolean = this.isMaxItemsReached()
764
-
765
- let shouldOptionsBeOpen: boolean = false
766
- let shouldResetInput: boolean = true
767
- let userInfoMessage: string | null = ''
768
- let searchValue: string | null = ''
769
-
770
- if (isDisabled) return
771
-
772
- // Dersom ikke i listen og allowUserInput er true
773
- if (!isInOption && this.allowUserInput && !isEmpty) {
774
- this.addNewUserValue(value)
775
- userInfoMessage = 'Ny verdi lagt til'
776
- shouldOptionsBeOpen = !isMultiple
777
- }
778
-
779
- // Dersom ikke i listen men allowUserInput er false
780
- else if (!isInOption && !this.allowUserInput) {
781
- if (isSingle && this._value[0]) {
782
- this.removeValue(this._value[0])
783
- }
784
- shouldResetInput = false
785
- shouldOptionsBeOpen = true
786
- userInfoMessage = 'Ingen treff i søket'
787
- }
788
-
789
- // Dersom verdien er valgt allerede
790
- else if (isSelected) {
791
- this.removeValue(valueFromOptions)
792
- shouldOptionsBeOpen = true
793
- }
794
-
795
- // Dersom verdien er en tom streng, og det er enkeltvalg
796
- else if (isEmpty && isSingle) {
797
- this.removeAllSelected()
798
- shouldOptionsBeOpen = true
799
- }
800
-
801
- // Dersom det er enkeltvalg
802
- else if (isSingle) {
803
- this._value[0] && this.removeSelected(this._value[0])
804
- this.setSelected(valueFromOptions)
805
- shouldOptionsBeOpen = false
806
- if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
807
- this.inputRef.value.value = ''
808
- this.inputRef.value.blur()
809
- }
810
- }
811
-
812
- // Dersom det er flervalg og mulig å legge til fler
813
- else if (isMultiple && !isMaxItemsReached) {
814
- this.setSelected(valueFromOptions)
815
- shouldOptionsBeOpen = true
816
- }
817
-
818
- // Dersom det er flervalg og maks antall er nådd
819
- else if (isMultiple && isMaxItemsReached) {
820
- this._userInfoMessage = 'Maks antall valg nådd'
821
- shouldResetInput = false
822
- searchValue = value
823
- }
824
-
825
- // Dersom ingen av de over passer
826
- else {
827
- isSingle && this.removeAllSelected()
828
- this._userInfoMessage = 'Ingen gyldig verdi valgt'
829
- shouldResetInput = false
830
- shouldOptionsBeOpen = true
831
- searchValue = value
832
- }
833
-
834
- this._isOptionsOpen = shouldOptionsBeOpen
835
- if (!shouldOptionsBeOpen) {
836
- window.setTimeout(() => {
837
- this.focusRef.value?.focus()
838
- }, 0)
839
- }
840
- this._userInfoMessage = userInfoMessage
841
- this._search = searchValue || ''
842
- this.resetComboboxInput(shouldResetInput)
843
- isMultiple && this.isMaxItemsReached()
844
- }
845
-
846
- private setSelected(value: string | null): void {
847
- if (this._value.includes(value as string)) return
848
-
849
- if (this.multiple && this.isMaxItemsReached()) {
850
- this._userInfoMessage = 'Maks antall valg nådd'
851
- return
852
- }
853
-
854
- !this.multiple && this.removeAllSelected()
855
-
856
- this._value = value ? [...this._value, value] : this._value
857
- this._options = this._options.map((option) => {
858
- if (option.value === value) {
859
- option.selected = true
860
- }
861
- return option
862
- })
863
-
864
- this.resetComboboxInput(true)
865
- }
866
-
867
- private removeSelected(value: string | null): void {
868
- if (!value) return
869
- this._value = this._value.filter((v) => v !== value)
870
- const _opt = this.findValueInOptions(value)
871
- if (_opt) {
872
- _opt.selected = false
873
- if (_opt.userAdded) {
874
- this._options = [...this._options.filter((o) => o.value !== value)]
875
- this.options = [...this.options.filter((o) => o.value !== value)]
876
- } else {
877
- this._options = [...this._options, _opt]
878
- }
879
- } else if (!value && !this.multiple) {
880
- this._options = this._options.map((option) => {
881
- option.selected = false
882
- return option
883
- })
884
- }
885
- }
886
-
887
- private addAllOptions(): void {
888
- if (!this.multiple) return
889
- if (this.maxlength && this._options.length > this.maxlength) {
890
- this._userInfoMessage = 'For mange valgt'
891
- return
892
- }
893
-
894
- this._value = this._options.map((option) => option.value)
895
- this._options = this._options.map((option) => {
896
- option.selected = true
897
- return option
898
- })
899
- this.requestUpdate()
900
- }
901
-
902
- private removeAllSelected(): void {
903
- this._value = []
904
- this._options = this._options.map((option) => {
905
- option.selected = false
906
- return option
907
- })
908
- this._options = this._options.filter((option) => !option.userAdded)
909
- this.requestUpdate()
910
- }
911
-
912
- private addValue(): void {
913
- const input = this.inputRef.value?.value.trim() || ''
914
- this._search = input
915
- this.toggleValue(input)
916
- }
917
-
918
- private removeValue(value: string | null): void {
919
- this._value = this.multiple ? this._value.filter((v) => v !== value) : []
920
- this.removeSelected(value)
921
- }
922
-
923
- private addNewUserValue(value: string | null): void {
924
- if (!value || value.trim() === '') return
925
-
926
- if (!this.multiple) {
927
- this._value[0] && this.removeSelected(this._value[0])
928
- this._value = [value]
929
- this._isOptionsOpen = false
930
- this.blurInput()
931
- } else if (!this.findValueInOptions(value)) {
932
- if (this.isMaxItemsReached()) return
933
- this._value = [...this._value, value]
934
- }
935
-
936
- const newOption: IPktComboboxOption = { value, label: value, userAdded: true }
937
-
938
- this.options = [newOption, ...this.options]
939
- this._options = [newOption, ...this._options]
940
- this.setSelected(value)
941
- this.requestUpdate()
942
- }
943
-
944
- private resetComboboxInput(shouldResetInput: boolean = true): void {
945
- this._addValueText = null
946
- if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
947
- this._search = ''
948
- if (this.multiple) {
949
- this.inputRef.value.value = ''
950
- } else {
951
- const option = this.findValueInOptions(this._value[0])
952
- window.setTimeout(() => {
953
- if (!this.inputRef.value || this.inputRef.value.type === 'hidden') return
954
- this.inputRef.value.value =
955
- this.displayValueAs === 'label' && option?.label ? option.label : this._value[0] || ''
956
- }, 0)
957
- this._userInfoMessage = ''
958
- }
959
- }
960
- this._options = [...this.options]
961
- }
962
-
963
- private removeLastValue(e: Event): void {
964
- if (this._value.length === 0) return
965
-
966
- e.preventDefault()
967
-
968
- const val = this._value[this._value.length - 1]
969
- val && this.removeSelected(val)
970
-
971
- this.isMaxItemsReached()
972
- }
973
497
  }
498
+
499
+ export default PktCombobox