UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

249 lines (210 loc) 8.16 kB
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() } }