@oslokommune/punkt-elements 18.7.1 → 18.7.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.
- package/CHANGELOG.md +17 -0
- package/dist/{accordion-NO9LYXaN.cjs → accordion-CsrLQXEA.cjs} +1 -1
- package/dist/{accordion-BI-Qiznz.js → accordion-DoxhiwK-.js} +5 -5
- package/dist/{alert-HFqn-cvb.cjs → alert-DH3VqNqh.cjs} +1 -1
- package/dist/{alert-CayKowTW.js → alert-DxAGxSX_.js} +6 -6
- package/dist/{async-directive-CAZoZMez.js → async-directive-BSwJ8-GL.js} +2 -2
- package/dist/{async-directive-DlB0Sf4j.cjs → async-directive-CfPXShXu.cjs} +1 -1
- package/dist/{backlink-DYO8qA9C.cjs → backlink-CDH824db.cjs} +1 -1
- package/dist/{backlink-BHtxOlTw.js → backlink-CGc7r9CS.js} +3 -3
- package/dist/{breadcrumbs-HG9wzGtr.cjs → breadcrumbs-B37tsIoh.cjs} +1 -1
- package/dist/{breadcrumbs-BlRDuZbZ.js → breadcrumbs-DRGhIXmN.js} +2 -2
- package/dist/{button-CIb0IxTg.cjs → button-Cx6Xdp6t.cjs} +1 -1
- package/dist/{button-DWMHtdWk.js → button-D6QhHPWH.js} +5 -5
- package/dist/{calendar-CBghTpkZ.js → calendar-42o_q-jI.js} +3 -3
- package/dist/{calendar-CA1mz04B.cjs → calendar-CAwIY7x_.cjs} +1 -1
- package/dist/{card-CsSNvjZV.cjs → card-CB72gwBC.cjs} +1 -1
- package/dist/{card-PXDm9JXv.js → card-cyZE94EO.js} +6 -6
- package/dist/{checkbox-DReZOEy2.js → checkbox-C6JdANel.js} +4 -4
- package/dist/{checkbox-CMu4pF8-.cjs → checkbox-CddODhna.cjs} +1 -1
- package/dist/{class-map-BL_YL_Ed.js → class-map-DSoj1J68.js} +1 -1
- package/dist/{class-map-BEib48YL.cjs → class-map-DupQbnhE.cjs} +1 -1
- package/dist/{combobox-DcBFiuwP.js → combobox-Cqqokx73.js} +172 -128
- package/dist/combobox-N3NP77le.cjs +137 -0
- package/dist/components/combobox/combobox-base.d.ts +14 -12
- package/dist/components/combobox/combobox-handlers.d.ts +1 -0
- package/dist/components/combobox/combobox-utils.d.ts +3 -1
- package/dist/components/combobox/combobox-value.d.ts +5 -0
- package/dist/components/combobox/combobox.attributes.test.d.ts +0 -0
- package/dist/components/combobox/combobox.d.ts +31 -2
- package/dist/components/combobox/combobox.preselection.test.d.ts +0 -0
- package/dist/components/combobox/index.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/listbox/listbox.d.ts +3 -1
- package/dist/{consent-Yt1MpkUy.js → consent-CsgKWDsR.js} +2 -2
- package/dist/{consent-D1SMUJLs.cjs → consent-DnE6IPEj.cjs} +1 -1
- package/dist/{datepicker-NSWMVoGi.cjs → datepicker-BzRhOIkG.cjs} +1 -1
- package/dist/{datepicker-DboWQcrh.js → datepicker-CtSYgxMs.js} +11 -11
- package/dist/{directive-helpers-Ddr8wUxL.cjs → directive-helpers-2mcpDDv5.cjs} +1 -1
- package/dist/{directive-helpers-CohhRNQz.js → directive-helpers-ChEduoup.js} +1 -1
- package/dist/{element-DRc10X2R.cjs → element-D_mCyFeX.cjs} +1 -1
- package/dist/{element-e2MAMNEP.js → element-J8nynuVk.js} +1 -0
- package/dist/{element-with-slot-GwTT3WRD.js → element-with-slot-BOuXlWSY.js} +2 -2
- package/dist/{element-with-slot-DgtT8lCH.cjs → element-with-slot-C66Jck5e.cjs} +1 -1
- package/dist/{fileupload-BYVtnSaI.cjs → fileupload-3OtW4Xlo.cjs} +1 -1
- package/dist/{fileupload-CqxWUH0y.js → fileupload-PJWEBD88.js} +8 -8
- package/dist/{footer-YrlXRq7e.js → footer-ByAxlj9_.js} +4 -4
- package/dist/{footer-CLSihvYS.cjs → footer-Ca3DM544.cjs} +1 -1
- package/dist/{header-DsaPCLjJ.js → header-CxfDtS3Y.js} +11 -11
- package/dist/{header-DzVHM7gZ.cjs → header-D2SD2JLt.cjs} +1 -1
- package/dist/{header-menu-wcnhBSzs.cjs → header-menu-B9ABa0WW.cjs} +1 -1
- package/dist/{header-menu-DKAcUFzh.js → header-menu-CIuu8_UC.js} +4 -4
- package/dist/{heading-OfJKjUi2.cjs → heading-4ZHACOeL.cjs} +1 -1
- package/dist/{heading-C23xQS4l.js → heading-BHWdVbvN.js} +2 -2
- package/dist/{helptext-DyHzv2ZG.cjs → helptext-6HsH-I6a.cjs} +1 -1
- package/dist/{helptext-DqdOdYRj.js → helptext-BESB9Vlq.js} +4 -4
- package/dist/{icon-DBFTFd-J.cjs → icon-BzxBkRyE.cjs} +1 -1
- package/dist/{icon-jEFbKTB_.js → icon-DkVqRWbC.js} +1 -1
- package/dist/{if-defined-r8tKY4tG.js → if-defined-BILLSM7i.js} +1 -1
- package/dist/if-defined-BbUZeoaQ.cjs +1 -0
- package/dist/{input-element-kvI_5er5.cjs → input-element-DhIEFeuP.cjs} +1 -1
- package/dist/{input-element-BiUfK_gU.js → input-element-Qb0w3ChW.js} +2 -2
- package/dist/{input-wrapper-Bwh720ke.js → input-wrapper-Bg06k-cj.js} +6 -6
- package/dist/{input-wrapper-wyvCnWPp.cjs → input-wrapper-D64Qn-QH.cjs} +1 -1
- package/dist/{link-BWAhHUku.cjs → link-Bk7aDZCC.cjs} +1 -1
- package/dist/{link-BvjWWWlc.js → link-C7fOEv66.js} +4 -4
- package/dist/{linkcard-6CbGf4I_.js → linkcard-Bguo9oCv.js} +4 -4
- package/dist/{linkcard-D6fRaZSc.cjs → linkcard-rZpQuO1Z.cjs} +1 -1
- package/dist/{listbox-CsP0qbU1.js → listbox-CrQboPG6.js} +80 -63
- package/dist/listbox-kV4OO1Qa.cjs +111 -0
- package/dist/{loader-B5t78Yk7.cjs → loader-BAn68gBb.cjs} +1 -1
- package/dist/{loader-BS7EJqxD.js → loader-C3gwO0i3.js} +4 -4
- package/dist/{messagebox-Bt0IvUlD.js → messagebox-DAE59dMX.js} +4 -4
- package/dist/{messagebox-DBVUz-DN.cjs → messagebox-WH2HjT5I.cjs} +1 -1
- package/dist/{modal-C1g2fiUq.cjs → modal-DZm_fn3K.cjs} +1 -1
- package/dist/{modal-z5RzZ-fr.js → modal-DdC1_qT3.js} +5 -5
- package/dist/pkt-accordion.cjs +1 -1
- package/dist/pkt-accordion.js +1 -1
- package/dist/pkt-alert.cjs +1 -1
- package/dist/pkt-alert.js +1 -1
- package/dist/pkt-backlink.cjs +1 -1
- package/dist/pkt-backlink.js +1 -1
- package/dist/pkt-breadcrumbs.cjs +1 -1
- package/dist/pkt-breadcrumbs.js +1 -1
- package/dist/pkt-button.cjs +1 -1
- package/dist/pkt-button.js +1 -1
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-consent.cjs +1 -1
- package/dist/pkt-consent.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-fileupload.cjs +1 -1
- package/dist/pkt-fileupload.js +1 -1
- package/dist/pkt-footer.cjs +1 -1
- package/dist/pkt-footer.js +1 -1
- package/dist/pkt-header-menu.cjs +1 -1
- package/dist/pkt-header-menu.js +1 -1
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-heading.cjs +1 -1
- package/dist/pkt-heading.js +1 -1
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-icon.cjs +1 -1
- package/dist/pkt-icon.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +34 -34
- package/dist/pkt-input-wrapper.cjs +1 -1
- package/dist/pkt-input-wrapper.js +1 -1
- package/dist/pkt-link.cjs +1 -1
- package/dist/pkt-link.js +1 -1
- package/dist/pkt-linkcard.cjs +1 -1
- package/dist/pkt-linkcard.js +1 -1
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-loader.cjs +1 -1
- package/dist/pkt-loader.js +1 -1
- package/dist/pkt-messagebox.cjs +1 -1
- package/dist/pkt-messagebox.js +1 -1
- package/dist/pkt-modal.cjs +1 -1
- package/dist/pkt-modal.js +1 -1
- package/dist/{pkt-options-controller-AFYyv3-b.cjs → pkt-options-controller-DXAAVcBJ.cjs} +1 -1
- package/dist/{pkt-options-controller-DKkmDDzT.js → pkt-options-controller-XcIgx_v1.js} +1 -1
- package/dist/pkt-progressbar.cjs +1 -1
- package/dist/pkt-progressbar.js +1 -1
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-searchinput.cjs +1 -1
- package/dist/pkt-searchinput.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-tabs.cjs +1 -1
- package/dist/pkt-tabs.js +1 -1
- package/dist/pkt-tag.cjs +1 -1
- package/dist/pkt-tag.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/pkt-timepicker.cjs +1 -1
- package/dist/pkt-timepicker.js +1 -1
- package/dist/{progressbar-BPHLC7ce.js → progressbar-D1x6MwdY.js} +5 -5
- package/dist/{progressbar-BpcsDCnG.cjs → progressbar-DsE4-qaW.cjs} +1 -1
- package/dist/{radiobutton-bGdAEz26.js → radiobutton-B2P48MMf.js} +4 -4
- package/dist/{radiobutton-B6pzZQZV.cjs → radiobutton-CdwYbHnx.cjs} +1 -1
- package/dist/{ref-BkV4Cd8U.cjs → ref-AuNEkS5a.cjs} +1 -1
- package/dist/{ref-lE1mGUOL.js → ref-DNowxdYH.js} +2 -2
- package/dist/{repeat-uBgVG8Ch.cjs → repeat-B3ksuY2F.cjs} +1 -1
- package/dist/{repeat-CCnTfedv.js → repeat-CZ1oMXnW.js} +2 -2
- package/dist/{searchinput-DrUcPajl.js → searchinput-CmyWnNr6.js} +4 -4
- package/dist/{searchinput-CiPB8xvh.cjs → searchinput-vi7twt5b.cjs} +1 -1
- package/dist/{select-ay8WHI7p.cjs → select-TAwBNhhf.cjs} +1 -1
- package/dist/{select-D7r2s7Cw.js → select-c15YAQbr.js} +7 -7
- package/dist/shared-strings/nb.d.ts +1 -0
- package/dist/{tabs-DqzPOdi9.cjs → tabs-hiTrWsXq.cjs} +1 -1
- package/dist/{tabs-DHvCjUi4.js → tabs-kxsX1ZEf.js} +4 -4
- package/dist/{tag-Cojf4jgA.cjs → tag-CGlxtBOH.cjs} +1 -1
- package/dist/{tag-D2Xvsy1i.js → tag-bztxjcF0.js} +5 -5
- package/dist/{textarea-s4IDLsBi.cjs → textarea-BHGZhMN4.cjs} +1 -1
- package/dist/{textarea-CzAL8e6L.js → textarea-BPQVrOp9.js} +9 -9
- package/dist/{textinput-B9w4jhIP.cjs → textinput-Cv4tal5W.cjs} +1 -1
- package/dist/{textinput-DT3HR9eU.js → textinput-DHGUOsIA.js} +8 -8
- package/dist/{timepicker-C-Ip5Jgl.cjs → timepicker-BBUYnYzM.cjs} +1 -1
- package/dist/{timepicker-BE0UouVr.js → timepicker-C8DHbOpM.js} +8 -8
- package/package.json +2 -2
- package/src/components/combobox/combobox-base.ts +42 -28
- package/src/components/combobox/combobox-handlers.ts +11 -5
- package/src/components/combobox/combobox-utils.ts +9 -1
- package/src/components/combobox/combobox-value.ts +32 -16
- package/src/components/combobox/combobox.accessibility.test.ts +29 -3
- package/src/components/combobox/combobox.attributes.test.ts +82 -0
- package/src/components/combobox/combobox.core.test.ts +46 -1
- package/src/components/combobox/combobox.interaction.test.ts +167 -0
- package/src/components/combobox/combobox.preselection.test.ts +271 -0
- package/src/components/combobox/combobox.ts +143 -79
- package/src/components/combobox/index.ts +6 -1
- package/src/components/index.ts +1 -0
- package/src/components/listbox/listbox.ts +56 -29
- package/dist/combobox-BZm_12qR.cjs +0 -140
- package/dist/if-defined-BeVQnl3C.cjs +0 -1
- package/dist/listbox-D95b5zAd.cjs +0 -106
|
@@ -27,14 +27,15 @@ export class ComboboxBase extends PktInputElement<IPktStringsProps<'combobox' |
|
|
|
27
27
|
@property({ type: Boolean, attribute: 'allow-user-input' }) allowUserInput: boolean = false
|
|
28
28
|
@property({ type: Boolean }) typeahead: boolean = false
|
|
29
29
|
@property({ type: Boolean, attribute: 'include-search' }) includeSearch: boolean = false
|
|
30
|
-
@property({ type: String, attribute: 'search-placeholder' }) searchPlaceholder: string =
|
|
30
|
+
@property({ type: String, attribute: 'search-placeholder' }) searchPlaceholder: string | null =
|
|
31
|
+
null
|
|
31
32
|
@property({ type: Boolean }) multiple: boolean = false
|
|
32
33
|
@property({ type: Number }) maxlength: number | null = null
|
|
33
34
|
@property({ type: String, attribute: 'display-value-as' }) displayValueAs: string =
|
|
34
35
|
specs.props.displayValueAs.default
|
|
35
36
|
@property({ type: String, attribute: 'tag-placement' })
|
|
36
37
|
tagPlacement: TPktComboboxTagPlacement | null = null
|
|
37
|
-
// Internal use only — syncs to _isOptionsOpen in
|
|
38
|
+
// Internal use only — syncs to _isOptionsOpen in willUpdate(), but does not
|
|
38
39
|
// reliably open the listbox as a declarative attribute (requires focus state
|
|
39
40
|
// and populated _options). Not part of the public API / component spec.
|
|
40
41
|
// Used in tests to set initial open state without relying on focus or options population.
|
|
@@ -45,14 +46,15 @@ export class ComboboxBase extends PktInputElement<IPktStringsProps<'combobox' |
|
|
|
45
46
|
@state() protected _value: string[] = []
|
|
46
47
|
@state() protected _isOptionsOpen = false
|
|
47
48
|
@state() protected _userInfoMessage: TComboboxMessageKey = ''
|
|
49
|
+
@state() protected _messageValue: string | null = null
|
|
48
50
|
@state() protected _addValueText: string | null = null
|
|
49
51
|
@state() protected _maxIsReached: boolean = false
|
|
50
52
|
@state() protected _search: string = ''
|
|
51
53
|
@state() protected _inputFocus: boolean = false
|
|
52
|
-
protected
|
|
54
|
+
@state() protected _skipInputTab: boolean = false
|
|
53
55
|
protected _optionsFromSlot = false
|
|
54
56
|
protected _lastSlotGeneration = 0
|
|
55
|
-
/**
|
|
57
|
+
/** Set when focus returns to the input on close, so handleFocus does not reopen the list. */
|
|
56
58
|
protected _suppressNextOpen = false
|
|
57
59
|
|
|
58
60
|
// Refs
|
|
@@ -69,10 +71,7 @@ export class ComboboxBase extends PktInputElement<IPktStringsProps<'combobox' |
|
|
|
69
71
|
return `${this._value.length} valgt`
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
/**
|
|
73
|
-
* Focuses the appropriate trigger element after closing the listbox.
|
|
74
|
-
* Select-only: the combobox input div. Editable: the text input.
|
|
75
|
-
*/
|
|
74
|
+
/** Returns focus to the combobox after the listbox closes without a selection. */
|
|
76
75
|
protected focusTrigger(): void {
|
|
77
76
|
if (this._hasTextInput) {
|
|
78
77
|
this.inputRef.value?.focus()
|
|
@@ -81,18 +80,35 @@ export class ComboboxBase extends PktInputElement<IPktStringsProps<'combobox' |
|
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
|
|
83
|
+
protected focusAfterCommit(): void {
|
|
84
|
+
// The input sits inside the wrapper, so Tab would step back into it.
|
|
85
|
+
// Written to the DOM directly so it lands before focus moves.
|
|
86
|
+
this._skipInputTab = true
|
|
87
|
+
const input = this.inputRef.value
|
|
88
|
+
if (input && input.type !== 'hidden') input.setAttribute('tabindex', '-1')
|
|
89
|
+
this.triggerRef.value?.focus()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected restoreInputTab(): void {
|
|
93
|
+
this._skipInputTab = false
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
/**
|
|
85
97
|
* Parses the value prop into an internal string array.
|
|
86
98
|
*/
|
|
87
99
|
protected parseValue(): string[] {
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
return this.parseToValues(this.value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected parseToValues(raw: string | string[] | null | undefined): string[] {
|
|
104
|
+
if (Array.isArray(raw)) {
|
|
105
|
+
return this.multiple ? raw : raw.length > 0 ? [raw[0]] : []
|
|
90
106
|
}
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
107
|
+
if (raw && this.multiple) {
|
|
108
|
+
return raw.split(',')
|
|
93
109
|
}
|
|
94
|
-
if (
|
|
95
|
-
return [
|
|
110
|
+
if (raw) {
|
|
111
|
+
return [raw]
|
|
96
112
|
}
|
|
97
113
|
return []
|
|
98
114
|
}
|
|
@@ -104,26 +120,26 @@ export class ComboboxBase extends PktInputElement<IPktStringsProps<'combobox' |
|
|
|
104
120
|
this._maxIsReached = isMaxSelectionReached(this._value.length, this.maxlength)
|
|
105
121
|
}
|
|
106
122
|
|
|
123
|
+
protected _pendingDispatchFrom: string[] | null = null
|
|
124
|
+
|
|
107
125
|
/**
|
|
108
|
-
* Syncs the public value property from internal _value state
|
|
109
|
-
*
|
|
110
|
-
* to prevent array→string reflect cascades.
|
|
126
|
+
* Syncs the public value property from internal _value state. Always assigns
|
|
127
|
+
* a string to prevent array→string reflect cascades.
|
|
111
128
|
*/
|
|
112
|
-
protected
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
// Sync public value as a string (avoids array→string attribute reflect cascade)
|
|
116
|
-
const newPublicStr = this.multiple ? newInternal.join(',') : newInternal[0] || ''
|
|
129
|
+
protected syncPublicValue(): void {
|
|
130
|
+
const newPublicStr = this.multiple ? this._value.join(',') : this._value[0] || ''
|
|
117
131
|
const currentPublicStr = Array.isArray(this.value)
|
|
118
132
|
? this.value.join(',')
|
|
119
133
|
: String(this.value || '')
|
|
120
134
|
|
|
121
135
|
if (newPublicStr !== currentPublicStr) {
|
|
122
|
-
this._internalValueSync = true
|
|
123
136
|
this.value = newPublicStr
|
|
124
137
|
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected dispatchValueChange(oldInternal: string[]): void {
|
|
141
|
+
const newInternal = this._value
|
|
125
142
|
|
|
126
|
-
// Dispatch events if value content changed
|
|
127
143
|
if (oldInternal?.join(',') !== newInternal.join(',')) {
|
|
128
144
|
const eventValue = this.multiple ? [...newInternal] : newInternal[0] || ''
|
|
129
145
|
this.onChange(eventValue)
|
|
@@ -147,9 +163,7 @@ export class ComboboxBase extends PktInputElement<IPktStringsProps<'combobox' |
|
|
|
147
163
|
* No-op override of the base class valueChanged.
|
|
148
164
|
* The base class version sets both this.value AND this._value, which creates
|
|
149
165
|
* an infinite _value → valueChanged → value → parseValue → _value loop.
|
|
150
|
-
* Combobox
|
|
166
|
+
* Combobox derives value in willUpdate() and dispatches in updated() instead.
|
|
151
167
|
*/
|
|
152
|
-
protected override valueChanged(): void {
|
|
153
|
-
// Intentionally empty — combobox manages value sync in updated()
|
|
154
|
-
}
|
|
168
|
+
protected override valueChanged(): void {}
|
|
155
169
|
}
|
|
@@ -43,7 +43,7 @@ export class ComboboxHandlers extends ComboboxValue {
|
|
|
43
43
|
input.selectionDirection = 'backward'
|
|
44
44
|
}
|
|
45
45
|
} else {
|
|
46
|
-
this.
|
|
46
|
+
this.refreshOptionsFromValue()
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -51,8 +51,8 @@ export class ComboboxHandlers extends ComboboxValue {
|
|
|
51
51
|
protected handleFocus(): void {
|
|
52
52
|
if (this.disabled) return
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
this._skipInputTab = false
|
|
55
|
+
|
|
56
56
|
if (this._suppressNextOpen) {
|
|
57
57
|
this._suppressNextOpen = false
|
|
58
58
|
this._inputFocus = true
|
|
@@ -74,20 +74,26 @@ export class ComboboxHandlers extends ComboboxValue {
|
|
|
74
74
|
}
|
|
75
75
|
this._inputFocus = true
|
|
76
76
|
this._search = ''
|
|
77
|
-
this.
|
|
77
|
+
this.refreshOptionsFromValue()
|
|
78
78
|
this._isOptionsOpen = true
|
|
79
79
|
this.onFocus()
|
|
80
80
|
|
|
81
81
|
this.requestUpdate()
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
protected handleCloseOptions(): void {
|
|
85
|
+
this._isOptionsOpen = false
|
|
86
|
+
if (this._hasTextInput) this._suppressNextOpen = true
|
|
87
|
+
this.focusTrigger()
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
protected handleFocusOut(e: FocusEvent): void {
|
|
85
91
|
if (this.disabled || !this._isOptionsOpen) return
|
|
86
92
|
|
|
87
93
|
const related = e.relatedTarget as Element | null
|
|
88
94
|
const isFocusInsideCombobox =
|
|
89
95
|
related?.closest('pkt-combobox')?.id === this.id ||
|
|
90
|
-
(e.target as Element)?.
|
|
96
|
+
(e.target as Element)?.classList?.contains('pkt-combobox__value') ||
|
|
91
97
|
related === this.inputRef.value ||
|
|
92
98
|
related === this.triggerRef.value
|
|
93
99
|
|
|
@@ -91,6 +91,11 @@ export const slotUtils = {
|
|
|
91
91
|
// Options state synchronization (Lit-specific, in-place mutation)
|
|
92
92
|
|
|
93
93
|
export const optionStateUtils = {
|
|
94
|
+
/** Copies options before the component writes to them, so selection state does not leak into the consumer's array. */
|
|
95
|
+
cloneOptions(options: IPktComboboxOption[]): IPktComboboxOption[] {
|
|
96
|
+
return Array.isArray(options) ? options.map((option) => ({ ...option })) : []
|
|
97
|
+
},
|
|
98
|
+
|
|
94
99
|
/**
|
|
95
100
|
* Ensures options have labels and fulltext set.
|
|
96
101
|
* Also syncs selected state with current values.
|
|
@@ -124,8 +129,11 @@ export const optionStateUtils = {
|
|
|
124
129
|
mergeWithUserAdded(
|
|
125
130
|
newOptions: IPktComboboxOption[],
|
|
126
131
|
previousOptions: IPktComboboxOption[],
|
|
132
|
+
values: string[],
|
|
127
133
|
): IPktComboboxOption[] {
|
|
128
|
-
const userAddedValues = previousOptions.filter(
|
|
134
|
+
const userAddedValues = previousOptions.filter(
|
|
135
|
+
(option) => option?.userAdded && values.includes(option.value),
|
|
136
|
+
)
|
|
129
137
|
const filteredUserAdded = userAddedValues.filter(
|
|
130
138
|
(userOpt) =>
|
|
131
139
|
!(Array.isArray(newOptions) ? newOptions : []).some((opt) => opt.value === userOpt.value),
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type { IPktComboboxOption } from './combobox-types'
|
|
2
|
-
import {
|
|
1
|
+
import type { IPktComboboxOption, TPktComboboxDisplayValue } from './combobox-types'
|
|
2
|
+
import {
|
|
3
|
+
findOptionByValue,
|
|
4
|
+
getOptionDisplayText,
|
|
5
|
+
isMaxSelectionReached,
|
|
6
|
+
} from 'shared-utils/combobox/option-utils'
|
|
3
7
|
import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
|
|
4
|
-
import { selectionMutators } from './combobox-utils'
|
|
8
|
+
import { optionStateUtils, selectionMutators } from './combobox-utils'
|
|
5
9
|
import { ComboboxBase } from './combobox-base'
|
|
6
10
|
import type { TComboboxMessageKey } from 'shared-utils/combobox'
|
|
7
11
|
|
|
@@ -29,6 +33,7 @@ export class ComboboxValue extends ComboboxBase {
|
|
|
29
33
|
let shouldOptionsBeOpen: boolean = false
|
|
30
34
|
let shouldResetInput: boolean = true
|
|
31
35
|
let userInfoMessage: TComboboxMessageKey = ''
|
|
36
|
+
let messageValue: string | null = null
|
|
32
37
|
let searchValue: string | null = ''
|
|
33
38
|
|
|
34
39
|
if (isDisabled) return
|
|
@@ -76,6 +81,14 @@ export class ComboboxValue extends ComboboxBase {
|
|
|
76
81
|
this._value[0] && this.removeSelected(this._value[0])
|
|
77
82
|
this.setSelected(valueFromOptions)
|
|
78
83
|
shouldOptionsBeOpen = false
|
|
84
|
+
userInfoMessage = 'valueSelected'
|
|
85
|
+
// The two modes resolve displayValueAs through different helpers.
|
|
86
|
+
messageValue = this._hasTextInput
|
|
87
|
+
? getSingleValueForInput(valueFromOptions || '', this.options, this.displayValueAs)
|
|
88
|
+
: getOptionDisplayText(
|
|
89
|
+
findOptionByValue(this.options, valueFromOptions),
|
|
90
|
+
this.displayValueAs as TPktComboboxDisplayValue,
|
|
91
|
+
)
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
// Multi-select with room for more selections
|
|
@@ -102,19 +115,15 @@ export class ComboboxValue extends ComboboxBase {
|
|
|
102
115
|
|
|
103
116
|
this._isOptionsOpen = shouldOptionsBeOpen
|
|
104
117
|
if (!shouldOptionsBeOpen) {
|
|
105
|
-
if (isSingle && this._hasTextInput) {
|
|
106
|
-
// Suppress the next handleFocus from reopening the dropdown,
|
|
107
|
-
// then move focus back to the text input so screen readers
|
|
108
|
-
// announce the selected value instead of the browser window.
|
|
109
|
-
this._suppressNextOpen = true
|
|
110
|
-
}
|
|
111
118
|
window.setTimeout(() => {
|
|
112
|
-
this.
|
|
119
|
+
this.focusAfterCommit()
|
|
113
120
|
}, 0)
|
|
114
121
|
}
|
|
115
|
-
this._userInfoMessage = userInfoMessage
|
|
116
122
|
this._search = searchValue || ''
|
|
117
123
|
this.resetComboboxInput(shouldResetInput)
|
|
124
|
+
// After resetComboboxInput, which clears any stale message in single mode.
|
|
125
|
+
this._userInfoMessage = userInfoMessage
|
|
126
|
+
this._messageValue = messageValue
|
|
118
127
|
isMultiple && this.updateMaxReached()
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -208,17 +217,24 @@ export class ComboboxValue extends ComboboxBase {
|
|
|
208
217
|
|
|
209
218
|
if (!this.multiple) {
|
|
210
219
|
this._isOptionsOpen = false
|
|
211
|
-
if (this._hasTextInput) {
|
|
212
|
-
this._suppressNextOpen = true
|
|
213
|
-
}
|
|
214
220
|
window.setTimeout(() => {
|
|
215
|
-
this.
|
|
221
|
+
this.focusAfterCommit()
|
|
216
222
|
}, 0)
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
this.requestUpdate()
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Rebuilds _options from the consumer's options array. Selection lives in
|
|
230
|
+
* _value, not in this.options, so it has to be reapplied here.
|
|
231
|
+
*/
|
|
232
|
+
protected refreshOptionsFromValue(): void {
|
|
233
|
+
const cloned = optionStateUtils.cloneOptions(this.options)
|
|
234
|
+
cloned.forEach((option) => (option.selected = this._value.includes(option.value)))
|
|
235
|
+
this._options = optionStateUtils.syncOptionsWithValues(cloned, this._value).options
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
protected resetComboboxInput(shouldResetInput: boolean = true): void {
|
|
223
239
|
this._addValueText = null
|
|
224
240
|
if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
|
|
@@ -233,7 +249,7 @@ export class ComboboxValue extends ComboboxBase {
|
|
|
233
249
|
this.inputRef.value.value = ''
|
|
234
250
|
}
|
|
235
251
|
}
|
|
236
|
-
this.
|
|
252
|
+
this.refreshOptionsFromValue()
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
protected removeLastValue(e: Event): void {
|
|
@@ -6,6 +6,7 @@ expect.extend(toHaveNoViolations)
|
|
|
6
6
|
|
|
7
7
|
import './combobox'
|
|
8
8
|
import { PktCombobox } from './combobox'
|
|
9
|
+
import type { PktListbox } from '../listbox/listbox'
|
|
9
10
|
import type { IPktComboboxOption } from './combobox'
|
|
10
11
|
|
|
11
12
|
const waitForCustomElements = async () => {
|
|
@@ -175,7 +176,14 @@ describe('PktCombobox', () => {
|
|
|
175
176
|
await combobox.updateComplete
|
|
176
177
|
|
|
177
178
|
const textInput = combobox.querySelector('input[type="text"]')
|
|
178
|
-
expect(textInput
|
|
179
|
+
expect(textInput).not.toHaveAttribute('aria-activedescendant')
|
|
180
|
+
|
|
181
|
+
combobox.isOpen = true
|
|
182
|
+
await combobox.updateComplete
|
|
183
|
+
|
|
184
|
+
const active = textInput?.getAttribute('aria-activedescendant')
|
|
185
|
+
expect(active).toBeTruthy()
|
|
186
|
+
expect(combobox.querySelector(`[id="${active}"]`)).not.toBeNull()
|
|
179
187
|
})
|
|
180
188
|
|
|
181
189
|
test('listbox has correct id for aria-controls reference', async () => {
|
|
@@ -183,8 +191,26 @@ describe('PktCombobox', () => {
|
|
|
183
191
|
const combobox = container.querySelector('pkt-combobox') as PktCombobox
|
|
184
192
|
await combobox.updateComplete
|
|
185
193
|
|
|
186
|
-
const
|
|
187
|
-
|
|
194
|
+
const controller = combobox.querySelector('[aria-controls]')
|
|
195
|
+
const target = controller?.getAttribute('aria-controls')
|
|
196
|
+
expect(target).toBe('test-combo-listbox')
|
|
197
|
+
expect(combobox.querySelectorAll(`[id="${target}"]`).length).toBe(1)
|
|
198
|
+
expect(combobox.querySelector('pkt-listbox')?.hasAttribute('id')).toBe(false)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('aria-controls still resolves after the combobox id changes', async () => {
|
|
202
|
+
const container = await createCombobox('id="test-combo" name="test" label="Test"')
|
|
203
|
+
const combobox = container.querySelector('pkt-combobox') as PktCombobox
|
|
204
|
+
await combobox.updateComplete
|
|
205
|
+
|
|
206
|
+
combobox.id = 'renamed-combo'
|
|
207
|
+
await combobox.updateComplete
|
|
208
|
+
await (combobox.querySelector('pkt-listbox') as PktListbox).updateComplete
|
|
209
|
+
|
|
210
|
+
const controller = combobox.querySelector('[aria-controls]')
|
|
211
|
+
const target = controller?.getAttribute('aria-controls')
|
|
212
|
+
expect(target).toBe('renamed-combo-listbox')
|
|
213
|
+
expect(combobox.querySelectorAll(`[id="${target}"]`).length).toBe(1)
|
|
188
214
|
})
|
|
189
215
|
})
|
|
190
216
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
|
|
3
|
+
import spec from 'componentSpecs/combobox.json'
|
|
4
|
+
import './combobox'
|
|
5
|
+
import { PktCombobox } from './combobox'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Attribute spelling -> the property it drives, with a value to set and the
|
|
9
|
+
* value the property should end up with. Mirrors IPktComboboxAttributes, and is
|
|
10
|
+
* checked against a live element so the spellings cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
const cases: { attribute: string; property: keyof PktCombobox; set: string; expect: unknown }[] = [
|
|
13
|
+
{ attribute: 'allow-user-input', property: 'allowUserInput', set: '', expect: true },
|
|
14
|
+
{ attribute: 'include-search', property: 'includeSearch', set: '', expect: true },
|
|
15
|
+
{ attribute: 'haserror', property: 'hasError', set: '', expect: true },
|
|
16
|
+
{ attribute: 'optionaltag', property: 'optionalTag', set: '', expect: true },
|
|
17
|
+
{ attribute: 'requiredtag', property: 'requiredTag', set: '', expect: true },
|
|
18
|
+
{ attribute: 'display-value-as', property: 'displayValueAs', set: 'value', expect: 'value' },
|
|
19
|
+
{ attribute: 'search-placeholder', property: 'searchPlaceholder', set: 'x', expect: 'x' },
|
|
20
|
+
{ attribute: 'tag-placement', property: 'tagPlacement', set: 'outside', expect: 'outside' },
|
|
21
|
+
{ attribute: 'defaultvalue', property: 'defaultValue', set: 'x', expect: 'x' },
|
|
22
|
+
{ attribute: 'errormessage', property: 'errorMessage', set: 'x', expect: 'x' },
|
|
23
|
+
{ attribute: 'helptextdropdown', property: 'helptextDropdown', set: 'x', expect: 'x' },
|
|
24
|
+
{
|
|
25
|
+
attribute: 'helptextdropdownbutton',
|
|
26
|
+
property: 'helptextDropdownButton',
|
|
27
|
+
set: 'x',
|
|
28
|
+
expect: 'x',
|
|
29
|
+
},
|
|
30
|
+
{ attribute: 'optionaltext', property: 'optionalText', set: 'x', expect: 'x' },
|
|
31
|
+
{ attribute: 'requiredtext', property: 'requiredText', set: 'x', expect: 'x' },
|
|
32
|
+
{ attribute: 'tagtext', property: 'tagText', set: 'x', expect: 'x' },
|
|
33
|
+
{ attribute: 'hasfieldset', property: 'hasFieldset', set: 'true', expect: true },
|
|
34
|
+
{ attribute: 'usewrapper', property: 'useWrapper', set: 'false', expect: false },
|
|
35
|
+
{
|
|
36
|
+
attribute: 'default-options',
|
|
37
|
+
property: 'defaultOptions',
|
|
38
|
+
set: '[{"value":"a"}]',
|
|
39
|
+
expect: [{ value: 'a' }],
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const mount = async (attribute: string, value: string) => {
|
|
44
|
+
const container = document.createElement('div')
|
|
45
|
+
container.innerHTML = `<pkt-combobox id="a" name="a" label="L" ${attribute}="${value}"></pkt-combobox>`
|
|
46
|
+
document.body.appendChild(container)
|
|
47
|
+
await customElements.whenDefined('pkt-combobox')
|
|
48
|
+
const combobox = container.querySelector('pkt-combobox') as PktCombobox
|
|
49
|
+
await combobox.updateComplete
|
|
50
|
+
return combobox
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
document.body.innerHTML = ''
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('PktCombobox attributes', () => {
|
|
58
|
+
test.each(cases)(
|
|
59
|
+
'$attribute sets the $property property',
|
|
60
|
+
async ({ attribute, property, set, expect: expected }) => {
|
|
61
|
+
const combobox = await mount(attribute, set)
|
|
62
|
+
|
|
63
|
+
expect(combobox[property]).toEqual(expected)
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
test('kebab-case spellings of lowercased attributes do not work', async () => {
|
|
68
|
+
const combobox = await mount('has-error', '')
|
|
69
|
+
|
|
70
|
+
expect(combobox.hasError).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('every renamed attribute in the component spec is declared', () => {
|
|
74
|
+
const renamed = Object.entries(spec.props)
|
|
75
|
+
.filter(([name, prop]) => 'attribute' in prop && prop.attribute !== name)
|
|
76
|
+
.map(([, prop]) => (prop as { attribute: string }).attribute)
|
|
77
|
+
// input-size and data-testid come from the shared input wrapper, not combobox
|
|
78
|
+
.filter((attribute) => !['input-size', 'data-testid'].includes(attribute))
|
|
79
|
+
|
|
80
|
+
expect(cases.map((c) => c.attribute).sort()).toEqual(renamed.sort())
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -730,7 +730,14 @@ describe('PktCombobox', () => {
|
|
|
730
730
|
await combobox.updateComplete
|
|
731
731
|
|
|
732
732
|
const textInput = combobox.querySelector('input[type="text"]')
|
|
733
|
-
expect(textInput
|
|
733
|
+
expect(textInput).not.toHaveAttribute('aria-activedescendant')
|
|
734
|
+
|
|
735
|
+
combobox.isOpen = true
|
|
736
|
+
await combobox.updateComplete
|
|
737
|
+
|
|
738
|
+
const active = textInput?.getAttribute('aria-activedescendant')
|
|
739
|
+
expect(active).toBe('test-listbox-0')
|
|
740
|
+
expect(combobox.querySelector(`[id="${active}"]`)).not.toBeNull()
|
|
734
741
|
})
|
|
735
742
|
})
|
|
736
743
|
|
|
@@ -825,4 +832,42 @@ describe('PktCombobox', () => {
|
|
|
825
832
|
expect(combobox['_options'].some((o: IPktComboboxOption) => o.value === 'c')).toBe(false)
|
|
826
833
|
})
|
|
827
834
|
})
|
|
835
|
+
|
|
836
|
+
describe('Consumer-owned options array', () => {
|
|
837
|
+
const mount = async (options: unknown[], value?: string) => {
|
|
838
|
+
const el = document.createElement('pkt-combobox') as PktCombobox
|
|
839
|
+
el.label = 'Velg'
|
|
840
|
+
;(el as unknown as { options: unknown[] }).options = options
|
|
841
|
+
if (value) el.value = value
|
|
842
|
+
document.body.appendChild(el)
|
|
843
|
+
await waitForCustomElements()
|
|
844
|
+
await el.updateComplete
|
|
845
|
+
return el
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
test('does not mutate the array passed in', async () => {
|
|
849
|
+
const shared = [
|
|
850
|
+
{ value: 'a', text: 'Alfa' },
|
|
851
|
+
{ value: 'b', text: 'Bravo' },
|
|
852
|
+
]
|
|
853
|
+
const before = JSON.stringify(shared)
|
|
854
|
+
|
|
855
|
+
await mount(shared, 'b')
|
|
856
|
+
|
|
857
|
+
expect(JSON.stringify(shared)).toBe(before)
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
test('two comboboxes do not share selection via the same options array', async () => {
|
|
861
|
+
const shared = [
|
|
862
|
+
{ value: 'a', text: 'Alfa' },
|
|
863
|
+
{ value: 'b', text: 'Bravo' },
|
|
864
|
+
]
|
|
865
|
+
|
|
866
|
+
await mount(shared, 'b')
|
|
867
|
+
const second = await mount(shared)
|
|
868
|
+
|
|
869
|
+
expect(second.value).toBe('')
|
|
870
|
+
})
|
|
871
|
+
})
|
|
872
|
+
|
|
828
873
|
})
|