@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.
- package/CHANGELOG.md +34 -0
- package/dist/{card-CnPjrdre.js → card-CmfUyl_s.js} +1 -1
- package/dist/{card-5S2r9UD1.cjs → card-Db9QSEqh.cjs} +1 -1
- package/dist/{checkbox-D98_NjcU.cjs → checkbox-Cpyay9_l.cjs} +1 -1
- package/dist/{checkbox-BSz71IeT.js → checkbox-D6nltMuc.js} +1 -1
- package/dist/combobox-Bv37b6cI.cjs +135 -0
- package/dist/combobox-CoO8T-F-.js +818 -0
- package/dist/{datepicker-SEKblnRR.cjs → datepicker-CrvQ5Y5w.cjs} +1 -1
- package/dist/{datepicker-nnyTW0vf.js → datepicker-DbsIuC5Z.js} +2 -2
- package/dist/index.d.ts +157 -90
- package/dist/{input-element-Bkv6Yxld.js → input-element-BGNbdzy2.js} +1 -1
- package/dist/{input-element-DM0tY799.cjs → input-element-CSDVA3Y6.cjs} +1 -1
- package/dist/listbox-Dm2mKp6_.cjs +101 -0
- package/dist/listbox-OdkIn9_A.js +431 -0
- 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-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +2 -2
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +9 -9
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-options-controller-BogGk-6J.cjs +1 -0
- package/dist/{pkt-options-controller-BcGywCmf.js → pkt-options-controller-Z-bPox7n.js} +2 -2
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.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/{radiobutton-95wp024h.cjs → radiobutton-CNHCpKn0.cjs} +1 -1
- package/dist/{radiobutton-CTFAV5GU.js → radiobutton-DgC27mb0.js} +1 -1
- package/dist/{select-YLvYAQX6.js → select-7VuYtPZv.js} +2 -2
- package/dist/{select-CZ_Lx5W6.cjs → select-PWPy5gTB.cjs} +1 -1
- package/dist/{tag-68q0_Sn0.js → tag-DZPqFiem.js} +37 -33
- package/dist/tag-DmbgBCKu.cjs +27 -0
- package/dist/{textarea-CuTsE1WX.cjs → textarea-CO7Ikug5.cjs} +1 -1
- package/dist/{textarea-DhWH99qN.js → textarea-VpCEjVFx.js} +1 -1
- package/dist/{textinput-BCi9p0Du.js → textinput-C2AZ9ss2.js} +1 -1
- package/dist/{textinput-st4Vml5J.cjs → textinput-DRFZU3dA.cjs} +1 -1
- package/package.json +4 -4
- package/src/components/card/card.ts +1 -0
- package/src/components/combobox/combobox-base.ts +158 -0
- package/src/components/combobox/combobox-handlers.ts +419 -0
- package/src/components/combobox/combobox-types.ts +10 -0
- package/src/components/combobox/combobox-utils.ts +135 -0
- package/src/components/combobox/combobox-value.ts +248 -0
- package/src/components/combobox/combobox.accessibility.test.ts +243 -0
- package/src/components/combobox/{combobox.test.ts → combobox.core.test.ts} +104 -46
- package/src/components/combobox/combobox.interaction.test.ts +436 -0
- package/src/components/combobox/combobox.selection.test.ts +543 -0
- package/src/components/combobox/combobox.ts +260 -734
- package/src/components/listbox/index.ts +2 -0
- package/src/components/listbox/listbox.interaction.test.ts +580 -0
- package/src/components/listbox/listbox.test.ts +32 -6
- package/src/components/listbox/listbox.ts +109 -126
- package/src/components/tag/tag.ts +3 -0
- package/dist/combobox-C5YcNVSZ.cjs +0 -128
- package/dist/combobox-cer7PLSE.js +0 -533
- package/dist/listbox-C7NEa9SU.cjs +0 -96
- package/dist/listbox-Cykec1bj.js +0 -361
- package/dist/pkt-options-controller-BnTmkl3g.cjs +0 -1
- 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
|
+
})
|