@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
249 lines (210 loc) • 8.16 kB
text/typescript
import type { IPktComboboxOption } from './combobox-types'
import { findOptionByValue, isMaxSelectionReached } from 'shared-utils/combobox/option-utils'
import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
import { selectionMutators } from './combobox-utils'
import { ComboboxBase } from './combobox-base'
/**
* Value management layer for PktCombobox.
* Handles selection, deselection, user-added values, and input reset.
*/
export class ComboboxValue extends ComboboxBase {
public toggleValue(value: string | null): void {
if (this.disabled) return
this.touched = true
this._userInfoMessage = ''
this._addValueText = null
const valueFromOptions: string | null = findOptionByValue(this.options, 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 = isMaxSelectionReached(this._value.length, this.maxlength)
let shouldOptionsBeOpen: boolean = false
let shouldResetInput: boolean = true
let userInfoMessage: string | null = ''
let searchValue: string | null = ''
if (isDisabled) return
// Not in option list and allowUserInput is true
if (!isInOption && this.allowUserInput && !isEmpty) {
this.addNewUserValue(value)
userInfoMessage = 'Ny verdi lagt til'
shouldOptionsBeOpen = isMultiple
}
// Not in option list and allowUserInput is 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'
}
// Value is already selected — deselect it
else if (isSelected) {
this.removeValue(valueFromOptions)
shouldOptionsBeOpen = true
// For single+typeahead: clear the input immediately so tab-out doesn't re-select
if (
isSingle &&
this._hasTextInput &&
this.inputRef.value &&
this.inputRef.value.type !== 'hidden'
) {
this.inputRef.value.value = ''
}
}
// Empty value in single-select mode — clear selection
else if (isEmpty && isSingle) {
this.removeAllSelected()
shouldOptionsBeOpen = true
}
// Single-select — replace current selection
else if (isSingle) {
this._value[0] && this.removeSelected(this._value[0])
this.setSelected(valueFromOptions)
shouldOptionsBeOpen = false
}
// Multi-select with room for more selections
else if (isMultiple && !isMaxItemsReached) {
this.setSelected(valueFromOptions)
shouldOptionsBeOpen = true
}
// Multi-select with max selections reached
else if (isMultiple && isMaxItemsReached) {
userInfoMessage = 'Maks antall valg nådd'
shouldResetInput = false
searchValue = value
}
// No matching condition — fallback
else {
isSingle && this.removeAllSelected()
userInfoMessage = 'Ingen gyldig verdi valgt'
shouldResetInput = false
shouldOptionsBeOpen = true
searchValue = value
}
this._isOptionsOpen = shouldOptionsBeOpen
if (!shouldOptionsBeOpen) {
if (isSingle && this._hasTextInput) {
// Suppress the next handleFocus from reopening the dropdown,
// then move focus back to the text input so screen readers
// announce the selected value instead of the browser window.
this._suppressNextOpen = true
}
window.setTimeout(() => {
this.focusTrigger()
}, 0)
}
this._userInfoMessage = userInfoMessage
this._search = searchValue || ''
this.resetComboboxInput(shouldResetInput)
isMultiple && this.updateMaxReached()
}
protected setSelected(value: string | null): void {
if (this._value.includes(value as string)) return
if (this.multiple && isMaxSelectionReached(this._value.length, this.maxlength)) {
this._userInfoMessage = 'Maks antall valg nådd'
return
}
!this.multiple && this.removeAllSelected()
this._value = value ? [...this._value, value] : this._value
selectionMutators.markOptionSelected(this._options, value)
this.resetComboboxInput(true)
}
protected removeSelected(value: string | null): void {
if (!value) return
this._value = this._value.filter((v) => v !== value)
const _opt = findOptionByValue(this.options, value)
if (_opt) {
selectionMutators.markOptionDeselected(this.options, value)
if (_opt.userAdded) {
this._options = [...this._options.filter((o) => o.value !== value)]
this.options = [...this.options.filter((o) => o.value !== value)]
} else if (!this._options.some((o) => o.value === value)) {
// Re-add only if option was filtered out (e.g. by typeahead)
this._options = [...this._options, _opt]
}
}
}
protected 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)
selectionMutators.markAllSelected(this._options)
this.requestUpdate()
}
protected removeAllSelected(): void {
this._value = []
selectionMutators.markAllDeselected(this._options)
this._options = selectionMutators.removeUserAddedOptions(this._options)
this.requestUpdate()
}
protected addValue(): void {
const input = this.inputRef.value?.value.trim() || ''
this._search = input
// If the typed value is already selected, don't toggle (which would deselect).
// Just reset the input and keep the "Verdien er allerede valgt" message.
if (input && this._value.includes(input)) {
this.resetComboboxInput(true)
this._userInfoMessage = 'Verdien er allerede valgt'
return
}
this.toggleValue(input)
}
protected removeValue(value: string | null): void {
this._value = this.multiple ? this._value.filter((v) => v !== value) : []
this.removeSelected(value)
}
protected addNewUserValue(value: string | null): void {
if (!value || value.trim() === '') return
if (!this.multiple) {
this._value[0] && this.removeSelected(this._value[0])
this._value = [value]
} else if (!findOptionByValue(this.options, value)) {
if (isMaxSelectionReached(this._value.length, this.maxlength)) return
this._value = [...this._value, value]
}
const newOption: IPktComboboxOption = { value, label: value, userAdded: true, selected: true }
this.options = [newOption, ...this.options]
this._options = [newOption, ...this._options]
this.resetComboboxInput(true)
if (!this.multiple) {
this._isOptionsOpen = false
if (this._hasTextInput) {
this._suppressNextOpen = true
}
window.setTimeout(() => {
this.focusTrigger()
}, 0)
}
this.requestUpdate()
}
protected resetComboboxInput(shouldResetInput: boolean = true): void {
this._addValueText = null
if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
this._search = ''
if (!this.multiple) {
// Single+typeahead: show the selected value's display text in the input
this.inputRef.value.value = this._value[0]
? getSingleValueForInput(this._value[0], this.options, this.displayValueAs)
: ''
this._userInfoMessage = ''
} else {
this.inputRef.value.value = ''
}
}
this._options = [...this.options]
}
protected 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.updateMaxReached()
}
}