@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
@@ -0,0 +1,248 @@
1
+ import type { IPktComboboxOption } from './combobox-types'
2
+ import { findOptionByValue, isMaxSelectionReached } from 'shared-utils/combobox/option-utils'
3
+ import { getSingleValueForInput } from 'shared-utils/combobox/input-utils'
4
+ import { selectionMutators } from './combobox-utils'
5
+ import { ComboboxBase } from './combobox-base'
6
+
7
+ /**
8
+ * Value management layer for PktCombobox.
9
+ * Handles selection, deselection, user-added values, and input reset.
10
+ */
11
+ export class ComboboxValue extends ComboboxBase {
12
+ public toggleValue(value: string | null): void {
13
+ if (this.disabled) return
14
+
15
+ this.touched = true
16
+ this._userInfoMessage = ''
17
+ this._addValueText = null
18
+
19
+ const valueFromOptions: string | null = findOptionByValue(this.options, value)?.value || null
20
+ const isSelected: boolean = this._value.includes(value || valueFromOptions || '')
21
+ const isInOption: boolean = !!valueFromOptions
22
+ const isDisabled: boolean = this._options.find((o) => o.value === value)?.disabled || false
23
+ const isEmpty: boolean = !value?.trim()
24
+ const isSingle: boolean = !this.multiple
25
+ const isMultiple: boolean = this.multiple
26
+ const isMaxItemsReached: boolean = isMaxSelectionReached(this._value.length, this.maxlength)
27
+
28
+ let shouldOptionsBeOpen: boolean = false
29
+ let shouldResetInput: boolean = true
30
+ let userInfoMessage: string | null = ''
31
+ let searchValue: string | null = ''
32
+
33
+ if (isDisabled) return
34
+
35
+ // Not in option list and allowUserInput is true
36
+ if (!isInOption && this.allowUserInput && !isEmpty) {
37
+ this.addNewUserValue(value)
38
+ userInfoMessage = 'Ny verdi lagt til'
39
+ shouldOptionsBeOpen = isMultiple
40
+ }
41
+
42
+ // Not in option list and allowUserInput is false
43
+ else if (!isInOption && !this.allowUserInput) {
44
+ if (isSingle && this._value[0]) {
45
+ this.removeValue(this._value[0])
46
+ }
47
+ shouldResetInput = false
48
+ shouldOptionsBeOpen = true
49
+ userInfoMessage = 'Ingen treff i søket'
50
+ }
51
+
52
+ // Value is already selected — deselect it
53
+ else if (isSelected) {
54
+ this.removeValue(valueFromOptions)
55
+ shouldOptionsBeOpen = true
56
+ // For single+typeahead: clear the input immediately so tab-out doesn't re-select
57
+ if (
58
+ isSingle &&
59
+ this._hasTextInput &&
60
+ this.inputRef.value &&
61
+ this.inputRef.value.type !== 'hidden'
62
+ ) {
63
+ this.inputRef.value.value = ''
64
+ }
65
+ }
66
+
67
+ // Empty value in single-select mode — clear selection
68
+ else if (isEmpty && isSingle) {
69
+ this.removeAllSelected()
70
+ shouldOptionsBeOpen = true
71
+ }
72
+
73
+ // Single-select — replace current selection
74
+ else if (isSingle) {
75
+ this._value[0] && this.removeSelected(this._value[0])
76
+ this.setSelected(valueFromOptions)
77
+ shouldOptionsBeOpen = false
78
+ }
79
+
80
+ // Multi-select with room for more selections
81
+ else if (isMultiple && !isMaxItemsReached) {
82
+ this.setSelected(valueFromOptions)
83
+ shouldOptionsBeOpen = true
84
+ }
85
+
86
+ // Multi-select with max selections reached
87
+ else if (isMultiple && isMaxItemsReached) {
88
+ userInfoMessage = 'Maks antall valg nådd'
89
+ shouldResetInput = false
90
+ searchValue = value
91
+ }
92
+
93
+ // No matching condition — fallback
94
+ else {
95
+ isSingle && this.removeAllSelected()
96
+ userInfoMessage = 'Ingen gyldig verdi valgt'
97
+ shouldResetInput = false
98
+ shouldOptionsBeOpen = true
99
+ searchValue = value
100
+ }
101
+
102
+ this._isOptionsOpen = shouldOptionsBeOpen
103
+ if (!shouldOptionsBeOpen) {
104
+ if (isSingle && this._hasTextInput) {
105
+ // Suppress the next handleFocus from reopening the dropdown,
106
+ // then move focus back to the text input so screen readers
107
+ // announce the selected value instead of the browser window.
108
+ this._suppressNextOpen = true
109
+ }
110
+ window.setTimeout(() => {
111
+ this.focusTrigger()
112
+ }, 0)
113
+ }
114
+ this._userInfoMessage = userInfoMessage
115
+ this._search = searchValue || ''
116
+ this.resetComboboxInput(shouldResetInput)
117
+ isMultiple && this.updateMaxReached()
118
+ }
119
+
120
+ protected setSelected(value: string | null): void {
121
+ if (this._value.includes(value as string)) return
122
+
123
+ if (this.multiple && isMaxSelectionReached(this._value.length, this.maxlength)) {
124
+ this._userInfoMessage = 'Maks antall valg nådd'
125
+ return
126
+ }
127
+
128
+ !this.multiple && this.removeAllSelected()
129
+
130
+ this._value = value ? [...this._value, value] : this._value
131
+ selectionMutators.markOptionSelected(this._options, value)
132
+
133
+ this.resetComboboxInput(true)
134
+ }
135
+
136
+ protected removeSelected(value: string | null): void {
137
+ if (!value) return
138
+ this._value = this._value.filter((v) => v !== value)
139
+ const _opt = findOptionByValue(this.options, value)
140
+ if (_opt) {
141
+ selectionMutators.markOptionDeselected(this.options, value)
142
+ if (_opt.userAdded) {
143
+ this._options = [...this._options.filter((o) => o.value !== value)]
144
+ this.options = [...this.options.filter((o) => o.value !== value)]
145
+ } else if (!this._options.some((o) => o.value === value)) {
146
+ // Re-add only if option was filtered out (e.g. by typeahead)
147
+ this._options = [...this._options, _opt]
148
+ }
149
+ }
150
+ }
151
+
152
+ protected addAllOptions(): void {
153
+ if (!this.multiple) return
154
+ if (this.maxlength && this._options.length > this.maxlength) {
155
+ this._userInfoMessage = 'For mange valgt'
156
+ return
157
+ }
158
+
159
+ this._value = this._options.map((option) => option.value)
160
+ selectionMutators.markAllSelected(this._options)
161
+ this.requestUpdate()
162
+ }
163
+
164
+ protected removeAllSelected(): void {
165
+ this._value = []
166
+ selectionMutators.markAllDeselected(this._options)
167
+ this._options = selectionMutators.removeUserAddedOptions(this._options)
168
+ this.requestUpdate()
169
+ }
170
+
171
+ protected addValue(): void {
172
+ const input = this.inputRef.value?.value.trim() || ''
173
+ this._search = input
174
+
175
+ // If the typed value is already selected, don't toggle (which would deselect).
176
+ // Just reset the input and keep the "Verdien er allerede valgt" message.
177
+ if (input && this._value.includes(input)) {
178
+ this.resetComboboxInput(true)
179
+ this._userInfoMessage = 'Verdien er allerede valgt'
180
+ return
181
+ }
182
+
183
+ this.toggleValue(input)
184
+ }
185
+
186
+ protected removeValue(value: string | null): void {
187
+ this._value = this.multiple ? this._value.filter((v) => v !== value) : []
188
+ this.removeSelected(value)
189
+ }
190
+
191
+ protected addNewUserValue(value: string | null): void {
192
+ if (!value || value.trim() === '') return
193
+
194
+ if (!this.multiple) {
195
+ this._value[0] && this.removeSelected(this._value[0])
196
+ this._value = [value]
197
+ } else if (!findOptionByValue(this.options, value)) {
198
+ if (isMaxSelectionReached(this._value.length, this.maxlength)) return
199
+ this._value = [...this._value, value]
200
+ }
201
+
202
+ const newOption: IPktComboboxOption = { value, label: value, userAdded: true, selected: true }
203
+
204
+ this.options = [newOption, ...this.options]
205
+ this._options = [newOption, ...this._options]
206
+ this.resetComboboxInput(true)
207
+
208
+ if (!this.multiple) {
209
+ this._isOptionsOpen = false
210
+ if (this._hasTextInput) {
211
+ this._suppressNextOpen = true
212
+ }
213
+ window.setTimeout(() => {
214
+ this.focusTrigger()
215
+ }, 0)
216
+ }
217
+
218
+ this.requestUpdate()
219
+ }
220
+
221
+ protected resetComboboxInput(shouldResetInput: boolean = true): void {
222
+ this._addValueText = null
223
+ if (this.inputRef.value && this.inputRef.value.type !== 'hidden' && shouldResetInput) {
224
+ this._search = ''
225
+ if (!this.multiple) {
226
+ // Single+typeahead: show the selected value's display text in the input
227
+ this.inputRef.value.value = this._value[0]
228
+ ? getSingleValueForInput(this._value[0], this.options, this.displayValueAs)
229
+ : ''
230
+ this._userInfoMessage = ''
231
+ } else {
232
+ this.inputRef.value.value = ''
233
+ }
234
+ }
235
+ this._options = [...this.options]
236
+ }
237
+
238
+ protected removeLastValue(e: Event): void {
239
+ if (this._value.length === 0) return
240
+
241
+ e.preventDefault()
242
+
243
+ const val = this._value[this._value.length - 1]
244
+ val && this.removeSelected(val)
245
+
246
+ this.updateMaxReached()
247
+ }
248
+ }
@@ -0,0 +1,243 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { fireEvent } from '@testing-library/dom'
4
+
5
+ expect.extend(toHaveNoViolations)
6
+
7
+ import './combobox'
8
+ import { PktCombobox } from './combobox'
9
+ import type { IPktComboboxOption } from './combobox'
10
+
11
+ const waitForCustomElements = async () => {
12
+ await customElements.whenDefined('pkt-combobox')
13
+ }
14
+
15
+ const createCombobox = async (comboboxProps = '') => {
16
+ const container = document.createElement('div')
17
+ container.innerHTML = `
18
+ <pkt-combobox ${comboboxProps}></pkt-combobox>
19
+ `
20
+ document.body.appendChild(container)
21
+ await waitForCustomElements()
22
+ return container
23
+ }
24
+
25
+ const defaultOptions: IPktComboboxOption[] = [
26
+ { value: 'apple', label: 'Apple' },
27
+ { value: 'banana', label: 'Banana' },
28
+ { value: 'cherry', label: 'Cherry' },
29
+ ]
30
+
31
+ afterEach(() => {
32
+ document.body.innerHTML = ''
33
+ })
34
+
35
+ describe('PktCombobox', () => {
36
+ describe('Accessibility (axe)', () => {
37
+ test('basic combobox has no accessibility violations', async () => {
38
+ const container = await createCombobox('id="accessible" name="test" label="Choose a fruit"')
39
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
40
+ await combobox.updateComplete
41
+
42
+ const results = await axe(combobox)
43
+ expect(results).toHaveNoViolations()
44
+ })
45
+
46
+ test('combobox with options has no accessibility violations', async () => {
47
+ const container = await createCombobox('id="accessible" name="test" label="Choose a fruit"')
48
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
49
+ combobox.options = [...defaultOptions]
50
+ await combobox.updateComplete
51
+
52
+ const results = await axe(combobox)
53
+ expect(results).toHaveNoViolations()
54
+ })
55
+
56
+ test('combobox with text input has no accessibility violations', async () => {
57
+ const container = await createCombobox(
58
+ 'id="accessible" name="test" label="Choose a fruit" allow-user-input',
59
+ )
60
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
61
+ combobox.options = [...defaultOptions]
62
+ await combobox.updateComplete
63
+
64
+ const results = await axe(combobox)
65
+ expect(results).toHaveNoViolations()
66
+ })
67
+
68
+ test('combobox with typeahead has no accessibility violations', async () => {
69
+ const container = await createCombobox(
70
+ 'id="accessible" name="test" label="Choose a fruit" typeahead',
71
+ )
72
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
73
+ combobox.options = [...defaultOptions]
74
+ await combobox.updateComplete
75
+
76
+ const results = await axe(combobox)
77
+ expect(results).toHaveNoViolations()
78
+ })
79
+
80
+ test('multiple combobox has no accessibility violations', async () => {
81
+ const container = await createCombobox(
82
+ 'id="accessible" name="test" label="Choose fruits" multiple',
83
+ )
84
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
85
+ combobox.options = [...defaultOptions]
86
+ await combobox.updateComplete
87
+
88
+ const results = await axe(combobox)
89
+ expect(results).toHaveNoViolations()
90
+ })
91
+
92
+ test('disabled combobox has no accessibility violations', async () => {
93
+ const container = await createCombobox(
94
+ 'id="accessible" name="test" label="Choose a fruit" disabled',
95
+ )
96
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
97
+ await combobox.updateComplete
98
+
99
+ const results = await axe(combobox)
100
+ expect(results).toHaveNoViolations()
101
+ })
102
+
103
+ test('combobox with error state has no accessibility violations', async () => {
104
+ const container = await createCombobox(
105
+ 'id="accessible" name="test" label="Choose a fruit" error-message="Required field"',
106
+ )
107
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
108
+ combobox.hasError = true
109
+ await combobox.updateComplete
110
+
111
+ const results = await axe(combobox)
112
+ expect(results).toHaveNoViolations()
113
+ })
114
+ })
115
+
116
+ describe('ARIA attributes', () => {
117
+ test('select-only combobox has correct ARIA attributes', async () => {
118
+ const container = await createCombobox('id="test-aria" name="test" label="Test Label"')
119
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
120
+ await combobox.updateComplete
121
+
122
+ const comboboxInput = combobox.querySelector('.pkt-combobox__input')
123
+ expect(comboboxInput?.getAttribute('role')).toBe('combobox')
124
+ expect(comboboxInput?.getAttribute('aria-controls')).toBe('test-aria-listbox')
125
+ expect(comboboxInput?.getAttribute('aria-haspopup')).toBe('listbox')
126
+ expect(comboboxInput?.getAttribute('aria-expanded')).toBe('false')
127
+ expect(comboboxInput?.getAttribute('aria-labelledby')).toBe('test-aria-combobox-label')
128
+ })
129
+
130
+ test('arrow button aria-expanded updates when dropdown opens', async () => {
131
+ const container = await createCombobox('id="test" name="test" label="Test"')
132
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
133
+ await combobox.updateComplete
134
+
135
+ const arrowButton = combobox.querySelector('.pkt-combobox__input')
136
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('false')
137
+
138
+ fireEvent.click(arrowButton!)
139
+ await combobox.updateComplete
140
+
141
+ expect(arrowButton?.getAttribute('aria-expanded')).toBe('true')
142
+ })
143
+
144
+ test('text input has correct ARIA attributes for allowUserInput', async () => {
145
+ const container = await createCombobox(
146
+ 'id="test-input" name="test" label="Test Label" allow-user-input',
147
+ )
148
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
149
+ await combobox.updateComplete
150
+
151
+ const textInput = combobox.querySelector('input[type="text"]')
152
+ expect(textInput?.getAttribute('role')).toBe('combobox')
153
+ expect(textInput?.getAttribute('aria-controls')).toBe('test-input-listbox')
154
+ expect(textInput?.getAttribute('aria-label')).toBe('Test Label')
155
+ expect(textInput?.getAttribute('aria-autocomplete')).toBe('list')
156
+ })
157
+
158
+ test('text input has correct ARIA attributes for typeahead', async () => {
159
+ const container = await createCombobox(
160
+ 'id="test-input" name="test" label="Test Label" typeahead',
161
+ )
162
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
163
+ await combobox.updateComplete
164
+
165
+ const textInput = combobox.querySelector('input[type="text"]')
166
+ expect(textInput?.getAttribute('aria-autocomplete')).toBe('both')
167
+ })
168
+
169
+ test('text input sets aria-activedescendant when value is selected', async () => {
170
+ const container = await createCombobox(
171
+ 'id="test" name="test" label="Test" allow-user-input value="apple"',
172
+ )
173
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
174
+ combobox.options = [...defaultOptions]
175
+ await combobox.updateComplete
176
+
177
+ const textInput = combobox.querySelector('input[type="text"]')
178
+ expect(textInput?.getAttribute('aria-activedescendant')).toBeTruthy()
179
+ })
180
+
181
+ test('listbox has correct id for aria-controls reference', async () => {
182
+ const container = await createCombobox('id="test-combo" name="test" label="Test"')
183
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
184
+ await combobox.updateComplete
185
+
186
+ const listbox = combobox.querySelector('pkt-listbox')
187
+ expect(listbox?.getAttribute('id')).toBe('test-combo-listbox')
188
+ })
189
+ })
190
+
191
+ describe('Keyboard accessibility', () => {
192
+ test('arrow button is focusable when not disabled', async () => {
193
+ const container = await createCombobox('id="test" name="test" label="Test"')
194
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
195
+ await combobox.updateComplete
196
+
197
+ const arrowButton = combobox.querySelector('.pkt-combobox__input') as HTMLElement
198
+ expect(arrowButton.getAttribute('tabindex')).toBe('0')
199
+ })
200
+
201
+ test('arrow button is not focusable when disabled', async () => {
202
+ const container = await createCombobox('id="test" name="test" label="Test" disabled')
203
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
204
+ await combobox.updateComplete
205
+
206
+ const arrowButton = combobox.querySelector('.pkt-combobox__input') as HTMLElement
207
+ expect(arrowButton.getAttribute('tabindex')).toBe('-1')
208
+ })
209
+
210
+ test('text input is part of tab order', async () => {
211
+ const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
212
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
213
+ await combobox.updateComplete
214
+
215
+ const textInput = combobox.querySelector('input[type="text"]') as HTMLElement
216
+ expect(textInput).toBeInTheDocument()
217
+ // Text inputs are naturally tabbable (no tabindex needed)
218
+ expect(textInput.hasAttribute('tabindex')).toBe(false)
219
+ })
220
+ })
221
+
222
+ describe('Label association', () => {
223
+ test('input wrapper has correct forId for text input', async () => {
224
+ const container = await createCombobox(
225
+ 'id="test-id" name="test" label="Test Label" allow-user-input',
226
+ )
227
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
228
+ await combobox.updateComplete
229
+
230
+ const wrapper = combobox.querySelector('pkt-input-wrapper') as any
231
+ expect(wrapper?.forId).toBe('test-id-input')
232
+ })
233
+
234
+ test('input wrapper has correct forId for combobox (no text input)', async () => {
235
+ const container = await createCombobox('id="test-id" name="test" label="Test Label"')
236
+ const combobox = container.querySelector('pkt-combobox') as PktCombobox
237
+ await combobox.updateComplete
238
+
239
+ const wrapper = combobox.querySelector('pkt-input-wrapper') as any
240
+ expect(wrapper?.forId).toBe('test-id-combobox')
241
+ })
242
+ })
243
+ })