@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,580 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { fireEvent } from '@testing-library/dom'
|
|
3
|
+
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
|
|
4
|
+
import { CustomElementFor } from '../../tests/component-registry'
|
|
5
|
+
import { type IPktListbox } from './listbox'
|
|
6
|
+
import './listbox'
|
|
7
|
+
|
|
8
|
+
// jsdom does not implement scrollIntoView
|
|
9
|
+
HTMLElement.prototype.scrollIntoView = function () {}
|
|
10
|
+
|
|
11
|
+
// focusAndScrollIntoView uses setTimeout(0) for focus, so we need to flush
|
|
12
|
+
const flushFocusTimers = () => new Promise((resolve) => setTimeout(resolve, 10))
|
|
13
|
+
|
|
14
|
+
export interface ListboxTestConfig extends Partial<IPktListbox>, BaseTestConfig {
|
|
15
|
+
label?: string
|
|
16
|
+
id?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Properties that must be set via JS because their attribute names are kebab-case
|
|
20
|
+
const jsOnlyProps = [
|
|
21
|
+
'options',
|
|
22
|
+
'isOpen',
|
|
23
|
+
'includeSearch',
|
|
24
|
+
'isMultiSelect',
|
|
25
|
+
'allowUserInput',
|
|
26
|
+
'maxIsReached',
|
|
27
|
+
'customUserInput',
|
|
28
|
+
'searchPlaceholder',
|
|
29
|
+
'searchValue',
|
|
30
|
+
'maxLength',
|
|
31
|
+
'userMessage',
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
export const createListboxTest = async (config: ListboxTestConfig = {}) => {
|
|
35
|
+
// Separate JS-only props from HTML-safe attributes (id, label, disabled)
|
|
36
|
+
const htmlConfig: Record<string, unknown> = {}
|
|
37
|
+
const jsConfig: Record<string, unknown> = {}
|
|
38
|
+
|
|
39
|
+
for (const [key, value] of Object.entries(config)) {
|
|
40
|
+
if ((jsOnlyProps as readonly string[]).includes(key)) {
|
|
41
|
+
jsConfig[key] = value
|
|
42
|
+
} else {
|
|
43
|
+
htmlConfig[key] = value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { container, element } = await createElementTest<
|
|
48
|
+
CustomElementFor<'pkt-listbox'>,
|
|
49
|
+
BaseTestConfig & Record<string, unknown>
|
|
50
|
+
>('pkt-listbox', htmlConfig)
|
|
51
|
+
|
|
52
|
+
// Set JS-only properties directly
|
|
53
|
+
for (const [key, value] of Object.entries(jsConfig)) {
|
|
54
|
+
;(element as any)[key] = value
|
|
55
|
+
}
|
|
56
|
+
await element.updateComplete
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
container,
|
|
60
|
+
listbox: element,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
document.body.innerHTML = ''
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('PktListbox', () => {
|
|
69
|
+
describe('Option click handling', () => {
|
|
70
|
+
test('dispatches option-toggle event with correct value', async () => {
|
|
71
|
+
const options = [
|
|
72
|
+
{ value: 'option1', label: 'Option 1' },
|
|
73
|
+
{ value: 'option2', label: 'Option 2' },
|
|
74
|
+
]
|
|
75
|
+
const { listbox } = await createListboxTest({ options })
|
|
76
|
+
|
|
77
|
+
let toggledValue: string | null = null
|
|
78
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
79
|
+
toggledValue = e.detail
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option')
|
|
83
|
+
fireEvent.click(optionElement!)
|
|
84
|
+
await listbox.updateComplete
|
|
85
|
+
|
|
86
|
+
expect(toggledValue).toBe('option1')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('does not dispatch event for disabled options', async () => {
|
|
90
|
+
const options = [
|
|
91
|
+
{ value: 'disabled-option', label: 'Disabled Option', disabled: true },
|
|
92
|
+
]
|
|
93
|
+
const { listbox } = await createListboxTest({ options })
|
|
94
|
+
|
|
95
|
+
let toggledValue: string | null = null
|
|
96
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
97
|
+
toggledValue = e.detail
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option')
|
|
101
|
+
fireEvent.click(optionElement!)
|
|
102
|
+
await listbox.updateComplete
|
|
103
|
+
|
|
104
|
+
expect(toggledValue).toBeNull()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('does not dispatch event for globally disabled listbox', async () => {
|
|
108
|
+
const options = [
|
|
109
|
+
{ value: 'option1', label: 'Option 1' },
|
|
110
|
+
]
|
|
111
|
+
const { listbox } = await createListboxTest({ disabled: true, options })
|
|
112
|
+
|
|
113
|
+
let toggledValue: string | null = null
|
|
114
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
115
|
+
toggledValue = e.detail
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option')
|
|
119
|
+
fireEvent.click(optionElement!)
|
|
120
|
+
await listbox.updateComplete
|
|
121
|
+
|
|
122
|
+
expect(toggledValue).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('allows deselecting when maxIsReached and option is selected', async () => {
|
|
126
|
+
const options = [
|
|
127
|
+
{ value: 'selected', label: 'Selected', selected: true },
|
|
128
|
+
{ value: 'unselected', label: 'Unselected' },
|
|
129
|
+
]
|
|
130
|
+
const { listbox } = await createListboxTest({
|
|
131
|
+
isMultiSelect: true,
|
|
132
|
+
maxIsReached: true,
|
|
133
|
+
options,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
let toggledValue: string | null = null
|
|
137
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
138
|
+
toggledValue = e.detail
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
|
|
142
|
+
fireEvent.click(optionElements[0]) // selected
|
|
143
|
+
await listbox.updateComplete
|
|
144
|
+
|
|
145
|
+
expect(toggledValue).toBe('selected')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('Keyboard navigation', () => {
|
|
150
|
+
test('navigates down with ArrowDown key', async () => {
|
|
151
|
+
const options = [
|
|
152
|
+
{ value: 'option1', label: 'Option 1' },
|
|
153
|
+
{ value: 'option2', label: 'Option 2' },
|
|
154
|
+
{ value: 'option3', label: 'Option 3' },
|
|
155
|
+
]
|
|
156
|
+
const { listbox } = await createListboxTest({ options })
|
|
157
|
+
|
|
158
|
+
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
|
|
159
|
+
;(optionElements[0] as HTMLElement).focus()
|
|
160
|
+
|
|
161
|
+
fireEvent.keyDown(optionElements[0], { key: 'ArrowDown' })
|
|
162
|
+
await flushFocusTimers()
|
|
163
|
+
|
|
164
|
+
expect(document.activeElement).toBe(optionElements[1])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('navigates up with ArrowUp key', async () => {
|
|
168
|
+
const options = [
|
|
169
|
+
{ value: 'option1', label: 'Option 1' },
|
|
170
|
+
{ value: 'option2', label: 'Option 2' },
|
|
171
|
+
{ value: 'option3', label: 'Option 3' },
|
|
172
|
+
]
|
|
173
|
+
const { listbox } = await createListboxTest({ options })
|
|
174
|
+
|
|
175
|
+
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
|
|
176
|
+
;(optionElements[2] as HTMLElement).focus()
|
|
177
|
+
|
|
178
|
+
fireEvent.keyDown(optionElements[2], { key: 'ArrowUp' })
|
|
179
|
+
await flushFocusTimers()
|
|
180
|
+
|
|
181
|
+
expect(document.activeElement).toBe(optionElements[1])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('navigates to first option with Home key', async () => {
|
|
185
|
+
const options = [
|
|
186
|
+
{ value: 'option1', label: 'Option 1' },
|
|
187
|
+
{ value: 'option2', label: 'Option 2' },
|
|
188
|
+
{ value: 'option3', label: 'Option 3' },
|
|
189
|
+
]
|
|
190
|
+
const { listbox } = await createListboxTest({ options })
|
|
191
|
+
|
|
192
|
+
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
|
|
193
|
+
;(optionElements[2] as HTMLElement).focus()
|
|
194
|
+
|
|
195
|
+
fireEvent.keyDown(optionElements[2], { key: 'Home' })
|
|
196
|
+
await flushFocusTimers()
|
|
197
|
+
|
|
198
|
+
expect(document.activeElement).toBe(optionElements[0])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('navigates to last option with End key', async () => {
|
|
202
|
+
const options = [
|
|
203
|
+
{ value: 'option1', label: 'Option 1' },
|
|
204
|
+
{ value: 'option2', label: 'Option 2' },
|
|
205
|
+
{ value: 'option3', label: 'Option 3' },
|
|
206
|
+
]
|
|
207
|
+
const { listbox } = await createListboxTest({ options })
|
|
208
|
+
|
|
209
|
+
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
|
|
210
|
+
;(optionElements[0] as HTMLElement).focus()
|
|
211
|
+
|
|
212
|
+
fireEvent.keyDown(optionElements[0], { key: 'End' })
|
|
213
|
+
await flushFocusTimers()
|
|
214
|
+
|
|
215
|
+
expect(document.activeElement).toBe(optionElements[2])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('selects option with Enter key', async () => {
|
|
219
|
+
const options = [
|
|
220
|
+
{ value: 'option1', label: 'Option 1' },
|
|
221
|
+
{ value: 'option2', label: 'Option 2' },
|
|
222
|
+
]
|
|
223
|
+
const { listbox } = await createListboxTest({ options })
|
|
224
|
+
|
|
225
|
+
let toggledValue: string | null = null
|
|
226
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
227
|
+
toggledValue = e.detail
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
|
|
231
|
+
optionElement.focus()
|
|
232
|
+
|
|
233
|
+
fireEvent.keyDown(optionElement, { key: 'Enter' })
|
|
234
|
+
await listbox.updateComplete
|
|
235
|
+
|
|
236
|
+
expect(toggledValue).toBe('option1')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('selects option with Space key', async () => {
|
|
240
|
+
const options = [
|
|
241
|
+
{ value: 'option1', label: 'Option 1' },
|
|
242
|
+
{ value: 'option2', label: 'Option 2' },
|
|
243
|
+
]
|
|
244
|
+
const { listbox } = await createListboxTest({ options })
|
|
245
|
+
|
|
246
|
+
let toggledValue: string | null = null
|
|
247
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
248
|
+
toggledValue = e.detail
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
|
|
252
|
+
optionElement.focus()
|
|
253
|
+
|
|
254
|
+
fireEvent.keyDown(optionElement, { key: ' ' })
|
|
255
|
+
await listbox.updateComplete
|
|
256
|
+
|
|
257
|
+
expect(toggledValue).toBe('option1')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('closes options with Escape key', async () => {
|
|
261
|
+
const options = [
|
|
262
|
+
{ value: 'option1', label: 'Option 1' },
|
|
263
|
+
]
|
|
264
|
+
const { listbox } = await createListboxTest({ isOpen: true, options })
|
|
265
|
+
|
|
266
|
+
let closedFired = false
|
|
267
|
+
listbox.addEventListener('close-options', () => {
|
|
268
|
+
closedFired = true
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
|
|
272
|
+
optionElement.focus()
|
|
273
|
+
|
|
274
|
+
fireEvent.keyDown(optionElement, { key: 'Escape' })
|
|
275
|
+
await listbox.updateComplete
|
|
276
|
+
|
|
277
|
+
expect(closedFired).toBe(true)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('dispatches select-all event with Ctrl+A', async () => {
|
|
281
|
+
const options = [
|
|
282
|
+
{ value: 'option1', label: 'Option 1' },
|
|
283
|
+
{ value: 'option2', label: 'Option 2' },
|
|
284
|
+
]
|
|
285
|
+
const { listbox } = await createListboxTest({
|
|
286
|
+
isMultiSelect: true,
|
|
287
|
+
options,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
let selectAllFired = false
|
|
291
|
+
listbox.addEventListener('select-all', () => {
|
|
292
|
+
selectAllFired = true
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
|
|
296
|
+
optionElement.focus()
|
|
297
|
+
|
|
298
|
+
fireEvent.keyDown(optionElement, { key: 'a', ctrlKey: true })
|
|
299
|
+
await listbox.updateComplete
|
|
300
|
+
|
|
301
|
+
expect(selectAllFired).toBe(true)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe('Search functionality', () => {
|
|
306
|
+
test('renders search input when includeSearch is true', async () => {
|
|
307
|
+
const { listbox } = await createListboxTest({ includeSearch: true })
|
|
308
|
+
|
|
309
|
+
const searchInput = listbox.querySelector('[role="searchbox"]')
|
|
310
|
+
expect(searchInput).toBeInTheDocument()
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('does not render search input when includeSearch is false', async () => {
|
|
314
|
+
const { listbox } = await createListboxTest({ includeSearch: false })
|
|
315
|
+
|
|
316
|
+
const searchInput = listbox.querySelector('[role="searchbox"]')
|
|
317
|
+
expect(searchInput).not.toBeInTheDocument()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('filters options by search value', async () => {
|
|
321
|
+
const options = [
|
|
322
|
+
{ value: 'apple', label: 'Apple' },
|
|
323
|
+
{ value: 'banana', label: 'Banana' },
|
|
324
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
325
|
+
]
|
|
326
|
+
const { listbox } = await createListboxTest({
|
|
327
|
+
includeSearch: true,
|
|
328
|
+
options,
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
listbox.searchValue = 'app'
|
|
332
|
+
listbox.filterOptions()
|
|
333
|
+
await listbox.updateComplete
|
|
334
|
+
|
|
335
|
+
const visibleOptions = listbox.querySelectorAll('.pkt-listbox__option')
|
|
336
|
+
expect(visibleOptions).toHaveLength(1)
|
|
337
|
+
expect(visibleOptions[0].textContent?.trim()).toContain('Apple')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('shows all options when search is cleared', async () => {
|
|
341
|
+
const options = [
|
|
342
|
+
{ value: 'apple', label: 'Apple' },
|
|
343
|
+
{ value: 'banana', label: 'Banana' },
|
|
344
|
+
]
|
|
345
|
+
const { listbox } = await createListboxTest({
|
|
346
|
+
includeSearch: true,
|
|
347
|
+
options,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Filter
|
|
351
|
+
listbox.searchValue = 'app'
|
|
352
|
+
listbox.filterOptions()
|
|
353
|
+
await listbox.updateComplete
|
|
354
|
+
expect(listbox.querySelectorAll('.pkt-listbox__option')).toHaveLength(1)
|
|
355
|
+
|
|
356
|
+
// Clear
|
|
357
|
+
listbox.searchValue = ''
|
|
358
|
+
listbox.filterOptions()
|
|
359
|
+
await listbox.updateComplete
|
|
360
|
+
expect(listbox.querySelectorAll('.pkt-listbox__option')).toHaveLength(2)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
test('dispatches search event when typing in search input', async () => {
|
|
364
|
+
const { listbox } = await createListboxTest({ includeSearch: true })
|
|
365
|
+
|
|
366
|
+
let searchDetail: string | null = null
|
|
367
|
+
listbox.addEventListener('search', (e: any) => {
|
|
368
|
+
searchDetail = e.detail
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
const searchInput = listbox.querySelector('[role="searchbox"]') as HTMLInputElement
|
|
372
|
+
fireEvent.input(searchInput, { target: { value: 'test' } })
|
|
373
|
+
await listbox.updateComplete
|
|
374
|
+
|
|
375
|
+
expect(searchDetail).toBe('test')
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('applies search placeholder', async () => {
|
|
379
|
+
const { listbox } = await createListboxTest({
|
|
380
|
+
includeSearch: true,
|
|
381
|
+
searchPlaceholder: 'Search here...',
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const searchInput = listbox.querySelector('[role="searchbox"]') as HTMLInputElement
|
|
385
|
+
expect(searchInput?.placeholder).toBe('Search here...')
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('Multi-select features', () => {
|
|
390
|
+
test('renders checkboxes in multi-select mode', async () => {
|
|
391
|
+
const options = [
|
|
392
|
+
{ value: 'option1', label: 'Option 1' },
|
|
393
|
+
{ value: 'option2', label: 'Option 2' },
|
|
394
|
+
]
|
|
395
|
+
const { listbox } = await createListboxTest({
|
|
396
|
+
isMultiSelect: true,
|
|
397
|
+
options,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
|
|
401
|
+
expect(checkboxes).toHaveLength(2)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test('renders check icon for selected option in single-select mode', async () => {
|
|
405
|
+
const options = [
|
|
406
|
+
{ value: 'option1', label: 'Option 1', selected: true },
|
|
407
|
+
{ value: 'option2', label: 'Option 2' },
|
|
408
|
+
]
|
|
409
|
+
const { listbox } = await createListboxTest({ options })
|
|
410
|
+
|
|
411
|
+
const checkIcon = listbox.querySelector('pkt-icon[name="check-big"]')
|
|
412
|
+
expect(checkIcon).toBeInTheDocument()
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
test('shows maximum reached banner', async () => {
|
|
416
|
+
const options = [
|
|
417
|
+
{ value: 'option1', label: 'Option 1', selected: true },
|
|
418
|
+
{ value: 'option2', label: 'Option 2', selected: true },
|
|
419
|
+
]
|
|
420
|
+
const { listbox } = await createListboxTest({
|
|
421
|
+
isMultiSelect: true,
|
|
422
|
+
maxLength: 3,
|
|
423
|
+
options,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const banner = listbox.querySelector('.pkt-listbox__banner--maximum-reached')
|
|
427
|
+
expect(banner).toBeInTheDocument()
|
|
428
|
+
expect(banner?.textContent).toContain('2 av maks 3')
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('disables unselected checkboxes when maxIsReached', async () => {
|
|
432
|
+
const options = [
|
|
433
|
+
{ value: 'selected', label: 'Selected', selected: true },
|
|
434
|
+
{ value: 'unselected', label: 'Unselected' },
|
|
435
|
+
]
|
|
436
|
+
const { listbox } = await createListboxTest({
|
|
437
|
+
isMultiSelect: true,
|
|
438
|
+
maxIsReached: true,
|
|
439
|
+
options,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
|
|
443
|
+
expect(checkboxes[0]).not.toBeDisabled() // selected can still deselect
|
|
444
|
+
expect(checkboxes[1]).toBeDisabled() // unselected is disabled
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
describe('User input banner', () => {
|
|
449
|
+
test('shows new option banner when customUserInput is set', async () => {
|
|
450
|
+
const { listbox } = await createListboxTest({
|
|
451
|
+
allowUserInput: true,
|
|
452
|
+
customUserInput: 'New Value',
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
|
|
456
|
+
expect(newOptionBanner).toBeInTheDocument()
|
|
457
|
+
expect(newOptionBanner?.getAttribute('data-value')).toBe('New Value')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('does not show new option banner when allowUserInput is false', async () => {
|
|
461
|
+
const { listbox } = await createListboxTest({
|
|
462
|
+
allowUserInput: false,
|
|
463
|
+
customUserInput: 'New Value',
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
|
|
467
|
+
expect(newOptionBanner).not.toBeInTheDocument()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
test('dispatches option-toggle when clicking new option banner', async () => {
|
|
471
|
+
const { listbox } = await createListboxTest({
|
|
472
|
+
allowUserInput: true,
|
|
473
|
+
customUserInput: 'New Value',
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
let toggledValue: string | null = null
|
|
477
|
+
listbox.addEventListener('option-toggle', (e: any) => {
|
|
478
|
+
toggledValue = e.detail
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
|
|
482
|
+
fireEvent.click(newOptionBanner!)
|
|
483
|
+
await listbox.updateComplete
|
|
484
|
+
|
|
485
|
+
expect(toggledValue).toBe('New Value')
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
describe('User message display', () => {
|
|
490
|
+
test('shows user message when set', async () => {
|
|
491
|
+
const { listbox } = await createListboxTest({
|
|
492
|
+
userMessage: 'Ingen treff i søket',
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
const messageEl = listbox.querySelector('.pkt-listbox__banner--user-message')
|
|
496
|
+
expect(messageEl).toBeInTheDocument()
|
|
497
|
+
expect(messageEl?.textContent).toContain('Ingen treff i søket')
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('does not show user message when null', async () => {
|
|
501
|
+
const { listbox } = await createListboxTest({
|
|
502
|
+
userMessage: null,
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const messageEl = listbox.querySelector('.pkt-listbox__banner--user-message')
|
|
506
|
+
expect(messageEl).not.toBeInTheDocument()
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
describe('Option rendering', () => {
|
|
511
|
+
test('renders option prefix when present', async () => {
|
|
512
|
+
const options = [
|
|
513
|
+
{ value: 'no', label: 'Norway', prefix: 'NO' },
|
|
514
|
+
]
|
|
515
|
+
const { listbox } = await createListboxTest({ options })
|
|
516
|
+
|
|
517
|
+
const prefix = listbox.querySelector('.pkt-listbox__option-prefix')
|
|
518
|
+
expect(prefix).toBeInTheDocument()
|
|
519
|
+
expect(prefix?.textContent).toBe('NO')
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('renders option description when present', async () => {
|
|
523
|
+
const options = [
|
|
524
|
+
{ value: 'option1', label: 'Option 1', description: 'A description' },
|
|
525
|
+
]
|
|
526
|
+
const { listbox } = await createListboxTest({ options })
|
|
527
|
+
|
|
528
|
+
const description = listbox.querySelector('.pkt-listbox__option-description')
|
|
529
|
+
expect(description).toBeInTheDocument()
|
|
530
|
+
expect(description?.textContent).toBe('A description')
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test('uses value as label when label is not provided', async () => {
|
|
534
|
+
const options = [
|
|
535
|
+
{ value: 'my-value' },
|
|
536
|
+
]
|
|
537
|
+
const { listbox } = await createListboxTest({ options })
|
|
538
|
+
|
|
539
|
+
const label = listbox.querySelector('.pkt-listbox__option-label')
|
|
540
|
+
expect(label?.textContent?.trim()).toBe('my-value')
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
test('sets correct data attributes on options', async () => {
|
|
544
|
+
const options = [
|
|
545
|
+
{ value: 'option1', label: 'Option 1', selected: true },
|
|
546
|
+
]
|
|
547
|
+
const { listbox } = await createListboxTest({ options })
|
|
548
|
+
|
|
549
|
+
const optionEl = listbox.querySelector('.pkt-listbox__option')
|
|
550
|
+
expect(optionEl?.getAttribute('data-value')).toBe('option1')
|
|
551
|
+
expect(optionEl?.getAttribute('data-selected')).toBe('true')
|
|
552
|
+
expect(optionEl?.getAttribute('aria-selected')).toBe('true')
|
|
553
|
+
expect(optionEl?.getAttribute('role')).toBe('option')
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test('renders selected class on selected option in single mode', async () => {
|
|
557
|
+
const options = [
|
|
558
|
+
{ value: 'option1', label: 'Option 1', selected: true },
|
|
559
|
+
{ value: 'option2', label: 'Option 2' },
|
|
560
|
+
]
|
|
561
|
+
const { listbox } = await createListboxTest({ options })
|
|
562
|
+
|
|
563
|
+
const selectedOption = listbox.querySelector('.pkt-listbox__option--selected')
|
|
564
|
+
expect(selectedOption).toBeInTheDocument()
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
test('renders checkbox class on options in multi-select mode', async () => {
|
|
568
|
+
const options = [
|
|
569
|
+
{ value: 'option1', label: 'Option 1' },
|
|
570
|
+
]
|
|
571
|
+
const { listbox } = await createListboxTest({
|
|
572
|
+
isMultiSelect: true,
|
|
573
|
+
options,
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const checkboxOption = listbox.querySelector('.pkt-listbox__option--checkBox')
|
|
577
|
+
expect(checkboxOption).toBeInTheDocument()
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
})
|
|
@@ -11,18 +11,44 @@ export interface ListboxTestConfig extends Partial<IPktListbox>, BaseTestConfig
|
|
|
11
11
|
id?: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Properties that must be set via JS because their attribute names are kebab-case
|
|
15
|
+
const jsOnlyProps = [
|
|
16
|
+
'options',
|
|
17
|
+
'isOpen',
|
|
18
|
+
'includeSearch',
|
|
19
|
+
'isMultiSelect',
|
|
20
|
+
'allowUserInput',
|
|
21
|
+
'maxIsReached',
|
|
22
|
+
'customUserInput',
|
|
23
|
+
'searchPlaceholder',
|
|
24
|
+
'searchValue',
|
|
25
|
+
'maxLength',
|
|
26
|
+
'userMessage',
|
|
27
|
+
] as const
|
|
28
|
+
|
|
14
29
|
// Use shared framework
|
|
15
30
|
export const createListboxTest = async (config: ListboxTestConfig = {}) => {
|
|
31
|
+
const htmlConfig: Record<string, unknown> = {}
|
|
32
|
+
const jsConfig: Record<string, unknown> = {}
|
|
33
|
+
|
|
34
|
+
for (const [key, value] of Object.entries(config)) {
|
|
35
|
+
if ((jsOnlyProps as readonly string[]).includes(key)) {
|
|
36
|
+
jsConfig[key] = value
|
|
37
|
+
} else {
|
|
38
|
+
htmlConfig[key] = value
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
const { container, element } = await createElementTest<
|
|
17
43
|
CustomElementFor<'pkt-listbox'>,
|
|
18
|
-
|
|
19
|
-
>('pkt-listbox',
|
|
44
|
+
BaseTestConfig & Record<string, unknown>
|
|
45
|
+
>('pkt-listbox', htmlConfig)
|
|
20
46
|
|
|
21
|
-
// Set
|
|
22
|
-
|
|
23
|
-
element
|
|
24
|
-
await element.updateComplete
|
|
47
|
+
// Set JS-only properties directly
|
|
48
|
+
for (const [key, value] of Object.entries(jsConfig)) {
|
|
49
|
+
;(element as any)[key] = value
|
|
25
50
|
}
|
|
51
|
+
await element.updateComplete
|
|
26
52
|
|
|
27
53
|
return {
|
|
28
54
|
container,
|