@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
917 lines (815 loc) • 30.2 kB
text/typescript
import { html, nothing, PropertyValues } from 'lit'
import { ifDefined } from 'lit/directives/if-defined.js'
import { customElement, property, state } from 'lit/decorators.js'
import { Ref, createRef, ref } from 'lit/directives/ref.js'
import { classMap } from 'lit/directives/class-map.js'
import { repeat } from 'lit/directives/repeat.js'
import { PktInputElement } from '@/base-elements/input-element'
import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
import { PktSlotController } from '@/controllers/pkt-slot-controller'
import specs from 'componentSpecs/combobox.json'
import '../input-wrapper'
import '../icon'
import '../tag'
import '../listbox'
import PktListbox from '../listbox'
import { TTagSkin } from '../tag'
export interface IPktComboboxOption {
description?: string
disabled?: boolean
fulltext?: string
label?: string
prefix?: string
selected?: boolean
tagSkinColor?: TTagSkin
userAdded?: boolean
value: string
}
export interface IPktCombobox {
allowUserInput?: boolean
typeahead?: boolean
disabled?: boolean
displayValueAs?: string
errorMessage?: string
fullwidth?: boolean
hasError?: boolean
helptext?: string | null
helptextDropdown?: string | null
helptextDropdownButton?: string | null
id?: string
includeSearch?: boolean
label?: string | null
maxlength?: number | null
minlength?: number | null
multiple?: boolean
name?: string
optionalTag?: boolean
optionalText?: string
options?: IPktComboboxOption[]
defaultOptions?: IPktComboboxOption[]
placeholder?: string | null
requiredTag?: boolean
requiredText?: string
searchPlaceholder?: string
tagPlacement?: TPktComboboxTagPlacement | null
tagText?: string | null
value?: string | string[]
}
export type TPktComboboxTagPlacement = 'inside' | 'outside'
declare global {
interface HTMLElementTagNameMap {
'pkt-combobox': PktCombobox & HTMLSelectElement
}
}
export class PktCombobox extends PktInputElement implements IPktCombobox {
private helptextSlot: Ref<HTMLElement> = createRef()
constructor() {
super()
this.optionsController = new PktOptionsSlotController(this)
this.slotController = new PktSlotController(this, this.helptextSlot)
this.slotController.skipOptions = true
}
// Props / Attributes
value: string | string[] = ''
options: IPktComboboxOption[] = []
defaultOptions: IPktComboboxOption[] = []
allowUserInput: boolean = false
typeahead: boolean = false
includeSearch: boolean = false
searchPlaceholder: string = ''
multiple: boolean = false
maxlength: number | null = null
displayValueAs: string = specs.props.displayValueAs.default
tagPlacement: TPktComboboxTagPlacement | null = null
// State
private _options: IPktComboboxOption[] = []
private _isOptionsOpen = false
private _value: string[] = []
private _userInfoMessage: string = ''
private _addValueText: string | null = null
private _maxIsReached: boolean = false
private _search: string = ''
private _inputFocus: boolean = false
private _editingSingleValue: boolean = false
// Refs
inputRef: Ref<HTMLInputElement> = createRef()
arrowRef: Ref<HTMLButtonElement> = createRef()
listboxRef: Ref<PktListbox> = createRef()
focusRef: Ref<HTMLElement> = createRef()
optionTagRef: Ref<HTMLElement> = createRef()
// Lifecycle methods
connectedCallback(): void {
super.connectedCallback()
document &&
document.body.addEventListener('click', (e: MouseEvent) => {
if (this._isOptionsOpen && !this.contains(e.target as Node)) {
this.handleFocusOut(e)
}
})
this._options = []
// Deep clone defaultOptions into options, preserving userAdded options
if (this.defaultOptions && this.defaultOptions.length) {
const userAdded = this.options?.filter((opt) => opt.userAdded) || []
this.options = [...userAdded, ...JSON.parse(JSON.stringify(this.defaultOptions))]
this._options = [...this.options]
}
// If options are provided via the options slot, we need to extract them
if (this.optionsController.nodes.length) {
const options: IPktComboboxOption[] = []
this.optionsController.nodes.forEach((node: Element) => {
if (!node.textContent && !node.getAttribute('value')) return null
const option: IPktComboboxOption = {
value: node.getAttribute('value') || node.textContent || '',
label: node.textContent || node.getAttribute('value') || '',
}
if (node.getAttribute('data-prefix')) {
option.prefix = node.getAttribute('data-prefix') || undefined
}
if (node.getAttribute('tagskincolor')) {
option.tagSkinColor = node.getAttribute('tagskincolor') as TTagSkin
}
if (node.getAttribute('description')) {
option.description = node.getAttribute('description') || undefined
}
option.fulltext = option.value + option.label + (option.prefix || '')
options.push(option)
})
if (options.length) {
this.options = [...options]
this._options = [...options]
}
}
}
updated(changedProperties: PropertyValues): void {
if (changedProperties.has('_value')) {
this.valueChanged(this._value, changedProperties.get('_value') as string[])
}
if (changedProperties.has('value')) {
this._value = Array.isArray(this.value) ? this.value : this.value ? this.value.split(',') : []
if (!this.multiple && this._value.length > 1) {
this._value = [this._value[0]]
}
this.isMaxItemsReached()
}
// If defaultOptions changed, update options (preserving userAdded)
if (changedProperties.has('defaultOptions') && this.defaultOptions.length) {
const userAdded = this.options?.filter((opt) => opt.userAdded) || []
this.options = [...userAdded, ...JSON.parse(JSON.stringify(this.defaultOptions))]
this._options = [...this.options]
}
if (changedProperties.has('options') && this.options.length) {
// If options change, we need to update _options, but we need to preserve userAdded values
const userAddedValues = this._options.filter((option) => option.userAdded)
// Filter out userAddedValues that are overridden by this.options
const filteredUserAdded = userAddedValues.filter(
(userOpt) => !this.options.some((opt) => opt.value === userOpt.value),
)
// Merge, giving precedence to this.options
this._options = [...filteredUserAdded, ...this.options]
this._options.forEach((option) => {
if (option.value && !option.label) {
option.label = option.value
}
if (option.selected && !this._value.includes(option.value)) {
const oldValue = [...this._value]
this._value = [...this._value, option.value]
this.valueChanged(this._value, oldValue)
}
option.fulltext = option.value + option.label + (option.prefix || '')
option.selected = option.selected || this._value.includes(option.value)
})
}
if (changedProperties.has('_search')) {
this.dispatchEvent(
new CustomEvent('search', {
detail: this._search,
bubbles: false,
}),
)
}
super.updated(changedProperties)
}
attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
if (name === 'value') {
this._value = Array.isArray(this.value) ? this.value : this.value ? this.value.split(',') : []
if (!this.multiple && this._value.length > 1) {
this._value = [this._value[0]]
}
}
if (name === 'options') {
this._options = this.options
this._options.forEach((option) => {
if (option.value && !option.label) {
option.label = option.value
}
if (option.selected && !this._value.includes(option.value)) {
this._value = [...this._value, option.value]
}
option.fulltext = option.value + option.label + (option.prefix || '')
})
this._search = ''
}
super.attributeChangedCallback(name, _old, value)
}
// Render methods
render() {
return html`
<pkt-input-wrapper
.label=${this.label}
.helptext=${this.helptext}
.helptextDropdown=${ifDefined(this.helptextDropdown)}
.helptextDropdownButton=${ifDefined(this.helptextDropdownButton)}
?fullwidth=${this.fullwidth}
?hasError=${this.hasError}
?inline=${this.inline}
?disabled=${this.disabled}
.errorMessage=${this.errorMessage}
?optionalTag=${this.optionalTag}
.optionalText=${this.optionalText}
?requiredTag=${this.requiredTag}
.requiredText=${this.requiredText}
.tagText=${this.tagText}
?useWrapper=${this.useWrapper}
.forId=${this.allowUserInput || this.typeahead ? this.id + '-input' : this.id + '-arrow'}
class="pkt-combobox__wrapper"
=${this.handleInputClick}
>
<div class="pkt-contents" ${ref(this.helptextSlot)} name="helptext" slot="helptext"></div>
<div class="pkt-combobox" =${this.handleFocusOut}>
<div
class=${classMap({
'pkt-combobox__input': true,
'pkt-combobox__input--fullwidth': this.fullwidth,
'pkt-combobox__input--open': this._isOptionsOpen,
'pkt-combobox__input--error': this.hasError,
'pkt-combobox__input--disabled': this.disabled,
})}
tabindex="-1"
=${this.handleInputClick}
>
${this.placeholder &&
(!this._value.length || (this.multiple && this.tagPlacement == 'outside')) &&
!this._inputFocus
? html`<span class="pkt-combobox__placeholder">${this.placeholder}</span>`
: this.tagPlacement !== 'outside'
? this.renderSingleOrMultipleValues()
: nothing}
${this.renderInputField()}
<div
class="pkt-btn pkt-btn--tertiary pkt-combobox__arrow"
=${this.handleArrowClick}
=${this.handleArrowClick}
id="${this.id}-arrow"
${ref(this.arrowRef)}
aria-expanded=${this._isOptionsOpen}
aria-controls="${this.id}-listbox"
aria-haspopup="listbox"
aria-label="Åpne liste"
?disabled=${this.disabled}
?data-disabled=${this.disabled}
role="button"
tabindex="${this.disabled ? '-1' : '0'}"
>
<pkt-icon
class=${classMap({
'pkt-combobox__arrow-icon': true,
'pkt-combobox__arrow-icon--open': this._isOptionsOpen,
})}
name="chevron-thin-down"
></pkt-icon>
</div>
<div ${ref(this.focusRef)} tabindex="-1" =${this.handleArrowClick}></div>
</div>
<pkt-listbox
id="${this.id}-listbox"
.options=${this._options}
.isOpen=${this._isOptionsOpen}
.searchPlaceholder=${this.searchPlaceholder}
.label="Liste: ${this.label || ''}"
?includeSearch=${this.includeSearch}
?isMultiSelect=${this.multiple}
?allowUserInput=${this.allowUserInput && !this._maxIsReached}
?maxIsReached=${this._maxIsReached}
.customUserInput=${ifDefined(this._addValueText)}
.userMessage=${this._userInfoMessage}
=${this.handleSearch}
-toggle=${this.handleOptionToggled}
-all=${this.addAllOptions}
-options=${() => (this._isOptionsOpen = false)}
.searchValue=${this._search || null}
.maxLength=${this.maxlength || 0}
${ref(this.listboxRef)}
></pkt-listbox>
</div>
${this.tagPlacement === 'outside' && this.multiple
? html`<div class="pkt-combobox__tags-outside">
${this.renderSingleOrMultipleValues()}
</div>`
: nothing}
</pkt-input-wrapper>
`
}
renderInputField() {
return this.typeahead || this.allowUserInput
? html`
<div class="pkt-combobox__input-div combobox__input">
<input
type="text"
id="${this.id}-input"
name=${(this.name || this.id) + '-input'}
=${this.handleInput}
=${this.handleInputKeydown}
=${this.handleFocus}
=${this.handleBlur}
autocomplete="off"
role="combobox"
aria-label=${ifDefined(this.label)}
aria-autocomplete=${this.typeahead ? 'both' : 'list'}
aria-controls="${this.id}-listbox"
aria-multiselectable=${ifDefined(this.multiple ? 'true' : undefined)}
aria-activedescendant=${ifDefined(
this._value[0] && !!this.findValueInOptions(this._value[0])
? `${this.id}-listbox-${this.findIndexInOptions(this._value[0])}`
: undefined,
)}
${ref(this.inputRef)}
/>
</div>
`
: html`
<input
type="hidden"
id="${this.id}-input"
name=${(this.name || this.id) + '-input'}
.value=${this._value.join(',')}
${ref(this.inputRef)}
/>
`
}
renderSingleOrMultipleValues() {
const isSingleValueDisplay = !this.multiple
// enkeltverdi som tekst
const singleValueContent = !this._editingSingleValue
? this.renderValueTag(this.findValueInOptions(this._value[0]))
: null
// Multiple vises som tags
const multipleValuesContent = repeat(
this._value,
(value: string) => value,
(value: string) => {
const option = this.findValueInOptions(value)
const tagSkinColor = this.options.find((o) => o.value === value)?.tagSkinColor
return html`
<pkt-tag
skin=${tagSkinColor || 'blue-dark'}
?closeTag=${!this.disabled}
=${() => this.handleTagRemove(value)}
>
${this.renderValueTag(option)}
</pkt-tag>
`
},
)
return isSingleValueDisplay ? singleValueContent : multipleValuesContent
}
renderValueTag(option: IPktComboboxOption | null) {
if (!option) return ''
switch (this.displayValueAs) {
case 'prefixAndValue':
return html`<span data-focusfix=${this.id}>${option.prefix || ''} ${option.value}</span>`
case 'value':
return html`<span data-focusfix=${this.id}>${option.value}</span>`
case 'label':
default:
return html`<span data-focusfix=${this.id}>${option.label || option.value}</span>`
}
}
// Event handlers
handleInput(e: InputEvent): void {
e.stopPropagation()
e.stopImmediatePropagation()
if (this.disabled) return
this.touched = true
const input = e.target as HTMLInputElement
this._search = input.value
this.checkForMatches()
if (this.typeahead) {
if (this._search) {
this._options = this.options.filter((option) =>
option.fulltext?.toLowerCase().includes(this._search.toLowerCase()),
)
if (e.inputType !== 'deleteContentBackward') {
const matchingOptions = this._options.filter(
(option) =>
!option.selected &&
option.label?.toLowerCase().startsWith(this._search.toLowerCase()),
)
if (
matchingOptions.length > 0 &&
this.inputRef.value &&
this.inputRef.value.type !== 'hidden'
) {
const match = matchingOptions[0]
if (match?.label) {
input.value = match.label
window.setTimeout(
() => input.setSelectionRange(this._search.length, input.value.length),
0,
)
input.selectionDirection = 'backward'
}
}
}
} else {
this._options = [...this.options]
}
}
}
private handleFocus(): void {
if (this.disabled) return
if (
!this.multiple &&
this._value[0] &&
this.inputRef.value &&
this.inputRef.value.type !== 'hidden'
) {
const option = this.findValueInOptions(this._value[0])
this._editingSingleValue = true
this.inputRef.value.value =
this.displayValueAs === 'label' && option?.label ? option.label : this._value[0]
}
this._inputFocus = true
this._search = ''
this._options = [...this.options]
this._isOptionsOpen = true
this.onFocus()
this.requestUpdate() // Ensure the UI updates
}
private handleFocusOut(e: FocusEvent): void {
if (this.disabled) return
// Triggered when focus completely leaves the combobox and its children
if (
(e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
(e.relatedTarget as Element)?.closest('pkt-combobox')?.id !== this.id &&
(e.target as Element)?.getAttribute('data-focusfix') !== this.id &&
e.relatedTarget !== this.focusRef.value &&
e.relatedTarget !== this.inputRef.value &&
e.relatedTarget !== this.arrowRef.value &&
this._isOptionsOpen
) {
this._inputFocus = false
this._addValueText = null
this._userInfoMessage = ''
this._search = ''
// If value in text input, check if it should be added
if (
this.inputRef.value &&
this.inputRef.value.type !== 'hidden' &&
this.inputRef.value.value !== ''
) {
const val = this.inputRef.value.value
const valInOptions = this.findValueInOptions(val)
if (!this._value.includes(val) && !valInOptions) {
if (this.allowUserInput) {
this.addNewUserValue(val)
} else if (!this.multiple) {
this.removeValue(this._value[0])
}
} else if (valInOptions && !this._value.includes(valInOptions.value)) {
this.setSelected(valInOptions.value)
}
this.inputRef.value.value = ''
}
this._isOptionsOpen = false
this.onBlur()
}
}
private handleBlur(): void {
this._inputFocus = false
this._editingSingleValue = false
this.onBlur()
}
private handleInputClick(e: MouseEvent): void {
if (this.disabled) return
if (
e.currentTarget &&
e.currentTarget !== this.arrowRef.value &&
this.inputRef.value?.type !== 'hidden'
) {
this.inputRef.value?.focus()
} else {
this.handleArrowClick(e)
}
}
private handleArrowClick(e: MouseEvent | KeyboardEvent): void {
if (this.disabled) return
if (e instanceof KeyboardEvent && e.key) {
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'ArrowDown') return
}
e.stopImmediatePropagation()
e.preventDefault()
this._isOptionsOpen = !this._isOptionsOpen
if (this._isOptionsOpen) {
this.listboxRef.value?.focusFirstOrSelectedOption()
} else {
this.arrowRef.value?.focus()
}
}
private handleOptionToggled(e: CustomEvent) {
this.toggleValue(e.detail)
}
private handleSearch(e: CustomEvent) {
e.stopPropagation()
this._search = e.detail.toLowerCase()
}
private handleInputKeydown(e: KeyboardEvent): void {
switch (e.key) {
case ',':
case 'Enter':
e.preventDefault()
this.addValue()
break
case 'Backspace':
if (!this._search && this.inputRef.value?.type === 'hidden') this.removeLastValue(e)
break
case 'Tab':
case 'ArrowDown':
if (!e.shiftKey) {
this.listboxRef.value?.focusFirstOrSelectedOption()
e.preventDefault()
}
break
case 'Escape':
this._isOptionsOpen = false
this.arrowRef.value?.focus() // Return focus to the button
e.preventDefault()
break
default:
break
}
}
private handleTagRemove(value: string | null): void {
this.removeSelected(value)
}
private blurInput(): void {
if (this.inputRef.value && this.inputRef.value.matches(':focus')) {
this.inputRef.value.blur()
}
}
private checkForMatches() {
// sjekker om verdiene bruker skriver inn finnes, er valgt eller kan legges til
//setter riktig infomelding til bruker
const inputValue = this.inputRef.value?.value || this._search || ''
const searchValue = inputValue.trim().toLowerCase() || ''
if (!searchValue) {
if (!this.multiple && this._value[0]) {
this.removeValue(this._value[0])
}
this.resetComboboxInput(false)
return
}
const matchedValues = this._value.find((value) => value.toLowerCase() === searchValue)
const matchedOptions: IPktComboboxOption[] = this._options.filter(
(option) => option.label?.toLowerCase().includes(searchValue) ?? false,
)
const matchedOption = matchedOptions.find(
(option) =>
option.label?.toLowerCase() === searchValue || option.value.toLowerCase() === searchValue,
)
// sett riktig infomelding til bruker
switch (true) {
case (matchedOptions.length === 0 || !matchedOption) && this.allowUserInput:
this._addValueText = inputValue
this._userInfoMessage = ''
break
case matchedOptions.length === 0 && !this.allowUserInput:
this._addValueText = null
this._userInfoMessage = 'Ingen match i søket'
break
case !!matchedValues:
this._addValueText = null
this._userInfoMessage = 'Verdien er allerede valgt'
break
case matchedOptions.length > 1:
this._addValueText = null
this._userInfoMessage = ''
break
default:
this._addValueText = null
this._userInfoMessage = '' // Default for å fjerne melding
}
}
private findValueInOptions(value: string | null): IPktComboboxOption | null {
return (
this.options.find((option) => {
return option.value === value || option.label === value
}) || null
)
}
private findIndexInOptions(value: string | null): number {
return this._options.findIndex((option) => {
return option.value === value || option.label === value
})
}
private isMaxItemsReached(): boolean {
const isReached = this.maxlength !== null && this._value.length >= this.maxlength
if (!isReached) {
this._maxIsReached = false
} else {
this._maxIsReached = true
}
return isReached
}
public toggleValue(value: string | null): void {
if (this.disabled) return
this.touched = true
this._userInfoMessage = ''
this._addValueText = null
const valueFromOptions: string | null = this.findValueInOptions(value)?.value || null
const isSelected: boolean = this._value.includes(value || valueFromOptions || '')
const isInOption: boolean = !!valueFromOptions
const isDisabled: boolean = this._options.find((o) => o.value === value)?.disabled || false
const isEmpty: boolean = !value?.trim()
const isSingle: boolean = !this.multiple
const isMultiple: boolean = this.multiple
const isMaxItemsReached: boolean = this.isMaxItemsReached()
let shouldOptionsBeOpen: boolean = false
let shouldResetInput: boolean = true
let userInfoMessage: string | null = ''
let searchValue: string | null = ''
if (isDisabled) return
// Dersom ikke i listen og allowUserInput er true
if (!isInOption && this.allowUserInput && !isEmpty) {
this.addNewUserValue(value)
userInfoMessage = 'Ny verdi lagt til'
shouldOptionsBeOpen = !isMultiple
}
// Dersom ikke i listen men allowUserInput er false
else if (!isInOption && !this.allowUserInput) {
if (isSingle && this._value[0]) {
this.removeValue(this._value[0])
}
shouldResetInput = false
shouldOptionsBeOpen = true
userInfoMessage = 'Ingen treff i søket'
}
// Dersom verdien er valgt allerede
else if (isSelected) {
this.removeValue(valueFromOptions)
shouldOptionsBeOpen = true
}
// Dersom verdien er en tom streng, og det er enkeltvalg
else if (isEmpty && isSingle) {
this.removeAllSelected()
shouldOptionsBeOpen = true
}
// Dersom det er enkeltvalg
else if (isSingle) {
this._value[0] && this.removeSelected(this._value[0])
this.setSelected(valueFromOptions)
shouldOptionsBeOpen = false
if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
this.inputRef.value.value = ''
this.inputRef.value.blur()
}
}
// Dersom det er flervalg og mulig å legge til fler
else if (isMultiple && !isMaxItemsReached) {
this.setSelected(valueFromOptions)
shouldOptionsBeOpen = true
}
// Dersom det er flervalg og maks antall er nådd
else if (isMultiple && isMaxItemsReached) {
this._userInfoMessage = 'Maks antall valg nådd'
shouldResetInput = false
searchValue = value
}
// Dersom ingen av de over passer
else {
isSingle && this.removeAllSelected()
this._userInfoMessage = 'Ingen gyldig verdi valgt'
shouldResetInput = false
shouldOptionsBeOpen = true
searchValue = value
}
this._isOptionsOpen = shouldOptionsBeOpen
if (!shouldOptionsBeOpen) {
window.setTimeout(() => {
this.focusRef.value?.focus()
}, 0)
}
this._userInfoMessage = userInfoMessage
this._search = searchValue || ''
this.resetComboboxInput(shouldResetInput)
isMultiple && this.isMaxItemsReached()
}
private setSelected(value: string | null): void {
if (this._value.includes(value as string)) return
if (this.multiple && this.isMaxItemsReached()) {
this._userInfoMessage = 'Maks antall valg nådd'
return
}
!this.multiple && this.removeAllSelected()
this._value = value ? [...this._value, value] : this._value
this._options = this._options.map((option) => {
if (option.value === value) {
option.selected = true
}
return option
})
this.resetComboboxInput(true)
}
private removeSelected(value: string | null): void {
if (!value) return
this._value = this._value.filter((v) => v !== value)
const _opt = this.findValueInOptions(value)
if (_opt) {
_opt.selected = false
if (_opt.userAdded) {
this._options = [...this._options.filter((o) => o.value !== value)]
this.options = [...this.options.filter((o) => o.value !== value)]
} else {
this._options = [...this._options, _opt]
}
} else if (!value && !this.multiple) {
this._options = this._options.map((option) => {
option.selected = false
return option
})
}
}
private addAllOptions(): void {
if (!this.multiple) return
if (this.maxlength && this._options.length > this.maxlength) {
this._userInfoMessage = 'For mange valgt'
return
}
this._value = this._options.map((option) => option.value)
this._options = this._options.map((option) => {
option.selected = true
return option
})
this.requestUpdate()
}
private removeAllSelected(): void {
this._value = []
this._options = this._options.map((option) => {
option.selected = false
return option
})
this._options = this._options.filter((option) => !option.userAdded)
this.requestUpdate()
}
private addValue(): void {
const input = this.inputRef.value?.value.trim() || ''
this._search = input
this.toggleValue(input)
}
private removeValue(value: string | null): void {
this._value = this.multiple ? this._value.filter((v) => v !== value) : []
this.removeSelected(value)
}
private addNewUserValue(value: string | null): void {
if (!value || value.trim() === '') return
if (!this.multiple) {
this._value[0] && this.removeSelected(this._value[0])
this._value = [value]
this._isOptionsOpen = false
this.blurInput()
} else if (!this.findValueInOptions(value)) {
if (this.isMaxItemsReached()) return
this._value = [...this._value, value]
}
const newOption: IPktComboboxOption = { value, label: value, userAdded: true }
this.options = [newOption, ...this.options]
this._options = [newOption, ...this._options]
this.setSelected(value)
this.requestUpdate()
}
private resetComboboxInput(shouldResetInput: boolean = true): void {
this._addValueText = null
if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
this._search = ''
if (this.multiple) {
this.inputRef.value.value = ''
} else {
const option = this.findValueInOptions(this._value[0])
window.setTimeout(() => {
if (!this.inputRef.value || this.inputRef.value.type === 'hidden') return
this.inputRef.value.value =
this.displayValueAs === 'label' && option?.label ? option.label : this._value[0] || ''
}, 0)
this._userInfoMessage = ''
}
}
this._options = [...this.options]
}
private removeLastValue(e: Event): void {
if (this._value.length === 0) return
e.preventDefault()
const val = this._value[this._value.length - 1]
val && this.removeSelected(val)
this.isMaxItemsReached()
}
}