@api-client/ui 0.5.24 → 0.5.25

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 (67) hide show
  1. package/.cursor/rules/lit-best-practices.mdc +12 -1
  2. package/.github/instructions/lit-best-practices.instructions.md +2 -0
  3. package/build/src/md/dropdown-list/internals/UiDropdownList.d.ts.map +1 -1
  4. package/build/src/md/dropdown-list/internals/UiDropdownList.js +4 -3
  5. package/build/src/md/dropdown-list/internals/UiDropdownList.js.map +1 -1
  6. package/build/src/md/input/Input.d.ts +8 -4
  7. package/build/src/md/input/Input.d.ts.map +1 -1
  8. package/build/src/md/input/Input.js +8 -36
  9. package/build/src/md/input/Input.js.map +1 -1
  10. package/build/src/md/list/internals/List.d.ts +3 -1
  11. package/build/src/md/list/internals/List.d.ts.map +1 -1
  12. package/build/src/md/list/internals/List.js +9 -4
  13. package/build/src/md/list/internals/List.js.map +1 -1
  14. package/build/src/md/menu/internal/Menu.d.ts +8 -7
  15. package/build/src/md/menu/internal/Menu.d.ts.map +1 -1
  16. package/build/src/md/menu/internal/Menu.js +26 -29
  17. package/build/src/md/menu/internal/Menu.js.map +1 -1
  18. package/build/src/md/select/index.d.ts +4 -0
  19. package/build/src/md/select/index.d.ts.map +1 -0
  20. package/build/src/md/select/index.js +3 -0
  21. package/build/src/md/select/index.js.map +1 -0
  22. package/build/src/md/select/internals/Option.d.ts +125 -0
  23. package/build/src/md/select/internals/Option.d.ts.map +1 -0
  24. package/build/src/md/select/internals/Option.js +242 -0
  25. package/build/src/md/select/internals/Option.js.map +1 -0
  26. package/build/src/md/select/internals/Option.styles.d.ts +3 -0
  27. package/build/src/md/select/internals/Option.styles.d.ts.map +1 -0
  28. package/build/src/md/select/internals/Option.styles.js +139 -0
  29. package/build/src/md/select/internals/Option.styles.js.map +1 -0
  30. package/build/src/md/select/internals/Select.d.ts +250 -0
  31. package/build/src/md/select/internals/Select.d.ts.map +1 -0
  32. package/build/src/md/select/internals/Select.js +606 -0
  33. package/build/src/md/select/internals/Select.js.map +1 -0
  34. package/build/src/md/select/internals/Select.styles.d.ts +3 -0
  35. package/build/src/md/select/internals/Select.styles.d.ts.map +1 -0
  36. package/build/src/md/select/internals/Select.styles.js +22 -0
  37. package/build/src/md/select/internals/Select.styles.js.map +1 -0
  38. package/build/src/md/select/ui-option.d.ts +12 -0
  39. package/build/src/md/select/ui-option.d.ts.map +1 -0
  40. package/build/src/md/select/ui-option.js +29 -0
  41. package/build/src/md/select/ui-option.js.map +1 -0
  42. package/build/src/md/select/ui-select.d.ts +12 -0
  43. package/build/src/md/select/ui-select.d.ts.map +1 -0
  44. package/build/src/md/select/ui-select.js +27 -0
  45. package/build/src/md/select/ui-select.js.map +1 -0
  46. package/build/src/md/text-field/internals/TextField.d.ts.map +1 -1
  47. package/build/src/md/text-field/internals/TextField.js +1 -0
  48. package/build/src/md/text-field/internals/TextField.js.map +1 -1
  49. package/demo/md/index.html +2 -0
  50. package/demo/md/inputs/input.ts +4 -0
  51. package/demo/md/select/index.html +16 -0
  52. package/demo/md/select/index.ts +202 -0
  53. package/package.json +1 -1
  54. package/src/md/dropdown-list/internals/UiDropdownList.ts +4 -3
  55. package/src/md/input/Input.ts +8 -37
  56. package/src/md/list/internals/List.ts +12 -5
  57. package/src/md/menu/internal/Menu.ts +27 -18
  58. package/src/md/select/index.ts +3 -0
  59. package/src/md/select/internals/Option.styles.ts +139 -0
  60. package/src/md/select/internals/Option.ts +210 -0
  61. package/src/md/select/internals/Select.styles.ts +22 -0
  62. package/src/md/select/internals/Select.ts +534 -0
  63. package/src/md/select/ui-option.ts +18 -0
  64. package/src/md/select/ui-select.ts +17 -0
  65. package/src/md/text-field/internals/TextField.ts +1 -0
  66. package/test/md/menu/SubMenu.test.ts +2 -3
  67. package/test/md/select/Select.test.ts +667 -0
@@ -0,0 +1,667 @@
1
+ import { assert, fixture, html, nextFrame, oneEvent } from '@open-wc/testing'
2
+ import sinon from 'sinon'
3
+ import UiSelect from '../../../src/md/select/internals/Select.js'
4
+ import UiOption from '../../../src/md/select/internals/Option.js'
5
+ import type { UiSelectChangeEvent } from '../../../src/md/select/internals/Select.js'
6
+
7
+ import '../../../src/md/select/ui-select.js'
8
+ import '../../../src/md/select/ui-option.js'
9
+ import '../../../src/md/icons/ui-icon.js'
10
+
11
+ describe('md', () => {
12
+ describe('UiSelect', () => {
13
+ async function basicFixture(): Promise<UiSelect> {
14
+ return fixture(html`
15
+ <ui-select label="Select an option">
16
+ <ui-option value="apple">Apple</ui-option>
17
+ <ui-option value="banana">Banana</ui-option>
18
+ <ui-option value="cherry">Cherry</ui-option>
19
+ </ui-select>
20
+ `)
21
+ }
22
+
23
+ async function withValueFixture(): Promise<UiSelect> {
24
+ return fixture(html`
25
+ <ui-select label="Select an option" value="banana">
26
+ <ui-option value="apple">Apple</ui-option>
27
+ <ui-option value="banana">Banana</ui-option>
28
+ <ui-option value="cherry">Cherry</ui-option>
29
+ </ui-select>
30
+ `)
31
+ }
32
+
33
+ async function requiredFixture(): Promise<UiSelect> {
34
+ return fixture(html`
35
+ <ui-select label="Required field" required>
36
+ <ui-option value="apple">Apple</ui-option>
37
+ <ui-option value="banana">Banana</ui-option>
38
+ </ui-select>
39
+ `)
40
+ }
41
+
42
+ async function disabledFixture(): Promise<UiSelect> {
43
+ return fixture(html`
44
+ <ui-select label="Disabled select" disabled>
45
+ <ui-option value="apple">Apple</ui-option>
46
+ <ui-option value="banana">Banana</ui-option>
47
+ </ui-select>
48
+ `)
49
+ }
50
+
51
+ async function formFixture(): Promise<HTMLFormElement> {
52
+ return fixture(html`
53
+ <form>
54
+ <input name="text" value="test" />
55
+ <ui-select name="fruit" label="Select fruit">
56
+ <ui-option value="apple">Apple</ui-option>
57
+ <ui-option value="banana">Banana</ui-option>
58
+ </ui-select>
59
+ </form>
60
+ `)
61
+ }
62
+
63
+ async function invalidFixture(): Promise<UiSelect> {
64
+ return fixture(html`
65
+ <ui-select label="Invalid select" invalid invalidText="This field is required">
66
+ <ui-option value="apple">Apple</ui-option>
67
+ <ui-option value="banana">Banana</ui-option>
68
+ </ui-select>
69
+ `)
70
+ }
71
+
72
+ async function emptyFixture(): Promise<UiSelect> {
73
+ return fixture(html`<ui-select label="Empty select"></ui-select>`)
74
+ }
75
+
76
+ describe('Basic functionality', () => {
77
+ it('should create select element', async () => {
78
+ const element = await basicFixture()
79
+ assert.instanceOf(element, UiSelect)
80
+ assert.equal(element.tagName.toLowerCase(), 'ui-select')
81
+ })
82
+
83
+ it('has formAssociated set', async () => {
84
+ assert.isTrue(UiSelect.formAssociated)
85
+ })
86
+
87
+ it('should have correct default properties', async () => {
88
+ const element = await basicFixture()
89
+ assert.isUndefined(element.value)
90
+ assert.isUndefined(element.name)
91
+ assert.equal(element.label, 'Select an option')
92
+ assert.isFalse(element.required)
93
+ assert.isUndefined(element.invalid)
94
+ assert.isUndefined(element.invalidText)
95
+ assert.isFalse(element.disabled)
96
+ assert.isFalse(element.open)
97
+ assert.isNull(element.selectedItem)
98
+ assert.equal(element.renderValue, '')
99
+ })
100
+
101
+ it('should set correct ARIA attributes', async () => {
102
+ const element = await basicFixture()
103
+ assert.equal(element.getAttribute('role'), 'combobox')
104
+ assert.equal(element.getAttribute('aria-haspopup'), 'listbox')
105
+ assert.equal(element.getAttribute('aria-controls'), 'menu')
106
+ assert.equal(element.getAttribute('aria-label'), 'Select an option')
107
+ assert.equal(element.tabIndex, 0)
108
+ })
109
+
110
+ it('should not have tabindex when disabled', async () => {
111
+ const element = await disabledFixture()
112
+ assert.equal(element.tabIndex, -1)
113
+ })
114
+ })
115
+
116
+ describe('Value and selection', () => {
117
+ it('should set value and select corresponding option', async () => {
118
+ const element = await basicFixture()
119
+ element.value = 'banana'
120
+ await element.updateComplete
121
+ // We need it here because the observer also awaits updateComplete
122
+ await nextFrame()
123
+
124
+ assert.equal(element.value, 'banana')
125
+ assert.isNotNull(element.selectedItem)
126
+ assert.equal(element.selectedItem!.value, 'banana')
127
+ assert.equal(element.renderValue, 'Banana')
128
+ })
129
+
130
+ it('should initialize with preselected value', async () => {
131
+ const element = await withValueFixture()
132
+ await element.updateComplete
133
+
134
+ assert.equal(element.value, 'banana')
135
+ assert.isNotNull(element.selectedItem)
136
+ assert.equal(element.selectedItem!.value, 'banana')
137
+ assert.equal(element.renderValue, 'Banana')
138
+ })
139
+
140
+ it('should handle invalid value gracefully', async () => {
141
+ const element = await basicFixture()
142
+ element.value = 'nonexistent'
143
+ await element.updateComplete
144
+
145
+ assert.equal(element.value, 'nonexistent')
146
+ assert.isNull(element.selectedItem)
147
+ assert.equal(element.renderValue, '')
148
+ })
149
+
150
+ it('should clear selection when value is undefined', async () => {
151
+ const element = await withValueFixture()
152
+ await element.updateComplete
153
+
154
+ element.value = undefined
155
+ await element.updateComplete
156
+ // We need it here because the observer also awaits updateComplete
157
+ await nextFrame()
158
+
159
+ assert.isUndefined(element.value, 'the value should be undefined')
160
+ assert.isNull(element.selectedItem, 'selected item should be null')
161
+ assert.equal(element.renderValue, '')
162
+ })
163
+ })
164
+
165
+ describe('Form integration', () => {
166
+ it('should have no form when not in form', async () => {
167
+ const element = await basicFixture()
168
+ assert.isNull(element.form)
169
+ })
170
+
171
+ it('should have form when in a form', async () => {
172
+ const form = await formFixture()
173
+ const select = form.querySelector('ui-select')!
174
+ assert.ok(select.form, 'has a form')
175
+ assert.isTrue(select.form === form, 'has the parent form')
176
+ })
177
+
178
+ it('should participate in form submission', async () => {
179
+ const form = await formFixture()
180
+ const select = form.querySelector('ui-select')!
181
+ select.value = 'apple'
182
+ await select.updateComplete
183
+
184
+ const formData = new FormData(form)
185
+ assert.equal(formData.get('fruit'), 'apple')
186
+ })
187
+
188
+ it('should not submit when no value selected', async () => {
189
+ const form = await formFixture()
190
+ const formData = new FormData(form)
191
+ assert.isNull(formData.get('fruit'))
192
+ })
193
+
194
+ it('should reset value on form reset', async () => {
195
+ const form = await formFixture()
196
+ const select = form.querySelector('ui-select')!
197
+ select.value = 'apple'
198
+ await select.updateComplete
199
+
200
+ select.formResetCallback()
201
+ assert.isUndefined(select.value)
202
+ })
203
+
204
+ it('should restore state', async () => {
205
+ const element = await basicFixture()
206
+ element.formStateRestoreCallback('cherry')
207
+ assert.equal(element.value, 'cherry')
208
+
209
+ element.formStateRestoreCallback(null)
210
+ assert.isUndefined(element.value)
211
+ })
212
+ })
213
+
214
+ describe('Validation', () => {
215
+ it('should be valid by default', async () => {
216
+ const element = await basicFixture()
217
+ assert.isTrue(element.checkValidity())
218
+ assert.isTrue(element.validity.valid)
219
+ assert.equal(element.validationMessage, '')
220
+ })
221
+
222
+ it('should be invalid when required and no value', async () => {
223
+ const element = await requiredFixture()
224
+ element.validate()
225
+ await element.updateComplete
226
+
227
+ assert.isFalse(element.checkValidity())
228
+ assert.isFalse(element.validity.valid)
229
+ assert.isTrue(element.validity.valueMissing)
230
+ assert.equal(element.validationMessage, 'Please select an item.')
231
+ assert.isTrue(element.invalid)
232
+ assert.equal(element.invalidText, 'Please select an item.')
233
+ })
234
+
235
+ it('should be valid when required and has value', async () => {
236
+ const element = await requiredFixture()
237
+ element.value = 'apple'
238
+ await element.updateComplete
239
+
240
+ assert.isTrue(element.checkValidity())
241
+ assert.isTrue(element.validity.valid)
242
+ assert.equal(element.validationMessage, '')
243
+ assert.isFalse(element.invalid)
244
+ })
245
+
246
+ it('should display invalid state', async () => {
247
+ const element = await invalidFixture()
248
+ assert.isTrue(element.invalid)
249
+ assert.equal(element.invalidText, 'This field is required')
250
+ })
251
+ })
252
+
253
+ describe('Keyboard interaction', () => {
254
+ it('should open dropdown on Enter key', async () => {
255
+ const element = await basicFixture()
256
+ assert.isFalse(element.open)
257
+
258
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
259
+ await nextFrame()
260
+
261
+ assert.isTrue(element.open)
262
+ })
263
+
264
+ it('should open dropdown on Space key', async () => {
265
+ const element = await basicFixture()
266
+ assert.isFalse(element.open)
267
+
268
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
269
+ await nextFrame()
270
+
271
+ assert.isTrue(element.open)
272
+ })
273
+
274
+ it('should open dropdown on ArrowDown key', async () => {
275
+ const element = await basicFixture()
276
+ assert.isFalse(element.open)
277
+
278
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
279
+ await nextFrame()
280
+
281
+ assert.isTrue(element.open)
282
+ })
283
+
284
+ it('should open dropdown on ArrowUp key', async () => {
285
+ const element = await basicFixture()
286
+ assert.isFalse(element.open)
287
+
288
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
289
+ await nextFrame()
290
+
291
+ assert.isTrue(element.open)
292
+ })
293
+
294
+ it('should close dropdown on Escape key', async () => {
295
+ const element = await basicFixture()
296
+ element.open = true
297
+ await element.updateComplete
298
+
299
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
300
+ await nextFrame()
301
+
302
+ assert.isFalse(element.open)
303
+ })
304
+
305
+ it('should not respond to keyboard when disabled', async () => {
306
+ const element = await disabledFixture()
307
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
308
+ await nextFrame()
309
+
310
+ assert.isFalse(element.open)
311
+ })
312
+
313
+ it('should handle Tab key when menu is open', async () => {
314
+ const element = await basicFixture()
315
+ element.open = true
316
+ await element.updateComplete
317
+
318
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
319
+ await element.updateComplete
320
+
321
+ assert.isFalse(element.open)
322
+ })
323
+ })
324
+
325
+ describe('Mouse interaction', () => {
326
+ it('should open dropdown on click', async () => {
327
+ const element = await basicFixture()
328
+ assert.isFalse(element.open)
329
+
330
+ element.click()
331
+ await nextFrame()
332
+
333
+ assert.isTrue(element.open)
334
+ })
335
+
336
+ it('should not respond to click when disabled', async () => {
337
+ const element = await disabledFixture()
338
+ element.click()
339
+ await nextFrame()
340
+
341
+ assert.isFalse(element.open)
342
+ })
343
+
344
+ it('should close on blur when focus leaves component', async () => {
345
+ const element = await basicFixture()
346
+ element.open = true
347
+ await element.updateComplete
348
+
349
+ // Simulate blur event with no related target (focus leaving completely)
350
+ element.dispatchEvent(new FocusEvent('blur', { relatedTarget: null }))
351
+ await nextFrame()
352
+
353
+ assert.isFalse(element.open)
354
+ })
355
+
356
+ it('should not close on blur when focus moves to menu', async () => {
357
+ const element = await basicFixture()
358
+ element.open = true
359
+ await element.updateComplete
360
+
361
+ const menu = element.shadowRoot!.querySelector('.menu')!
362
+ // Simulate blur event with menu as related target
363
+ element.dispatchEvent(new FocusEvent('blur', { relatedTarget: menu as HTMLElement }))
364
+ await nextFrame()
365
+
366
+ assert.isTrue(element.open)
367
+ })
368
+ })
369
+
370
+ describe('Selection events', () => {
371
+ it('should dispatch change event when selection changes', async () => {
372
+ const element = await basicFixture()
373
+ element.open = true
374
+ await element.updateComplete
375
+
376
+ const changePromise = oneEvent(element, 'change')
377
+
378
+ // Simulate option selection
379
+ const option = element.querySelector('ui-option[value="apple"]') as UiOption
380
+ element.handleSelect(new CustomEvent('select', { detail: { item: option } }))
381
+
382
+ const changeEvent = (await changePromise) as CustomEvent<UiSelectChangeEvent>
383
+ assert.equal(changeEvent.detail.value, 'apple')
384
+ assert.equal(changeEvent.detail.item, option)
385
+ assert.isFalse(changeEvent.bubbles)
386
+ assert.isTrue(changeEvent.composed)
387
+ assert.isFalse(element.open) // Should close after selection
388
+ })
389
+
390
+ it('should dispatch open event when opening', async () => {
391
+ const element = await basicFixture()
392
+ await element.updateComplete
393
+
394
+ const openPromise = oneEvent(element, 'open')
395
+ element.open = true
396
+ await element.updateComplete
397
+
398
+ const openEvent = await openPromise
399
+ assert.isFalse(openEvent.bubbles)
400
+ assert.isTrue(openEvent.composed)
401
+ })
402
+
403
+ it('should dispatch close event when closing', async () => {
404
+ const element = await basicFixture()
405
+ element.open = true
406
+ await element.updateComplete
407
+
408
+ const closePromise = oneEvent(element, 'close')
409
+ element.open = false
410
+ await element.updateComplete
411
+
412
+ const closeEvent = await closePromise
413
+ assert.isFalse(closeEvent.bubbles)
414
+ assert.isTrue(closeEvent.composed)
415
+ })
416
+ })
417
+
418
+ describe('Menu integration', () => {
419
+ it('should highlight selected item when menu opens', async () => {
420
+ const element = await withValueFixture()
421
+ await element.updateComplete
422
+
423
+ // Check that menu shows popover when opened
424
+ element.open = true
425
+ await element.updateComplete
426
+
427
+ const menu = element.shadowRoot!.querySelector('ui-menu')!
428
+ assert.isTrue(menu.matches(':popover-open'))
429
+ })
430
+
431
+ it('should highlight first item when no selection and menu opens', async () => {
432
+ const element = await basicFixture()
433
+ await element.updateComplete
434
+
435
+ // Check that menu opens correctly
436
+ element.open = true
437
+ await element.updateComplete
438
+
439
+ const menu = element.shadowRoot!.querySelector('ui-menu')!
440
+ assert.isTrue(menu.matches(':popover-open'))
441
+ })
442
+
443
+ it('should handle menu close event', async () => {
444
+ const element = await basicFixture()
445
+ element.open = true
446
+ await element.updateComplete
447
+
448
+ element.handleMenuClose()
449
+ assert.isFalse(element.open)
450
+ })
451
+
452
+ it('should handle highlight change events', async () => {
453
+ const element = await basicFixture()
454
+ const option = element.querySelector('ui-option') as UiOption
455
+ option.id = 'test-option'
456
+
457
+ element.handleHighlightChange(
458
+ new CustomEvent('highlightchange', {
459
+ detail: { item: option },
460
+ })
461
+ )
462
+
463
+ await element.updateComplete
464
+
465
+ // Check that the highlight change was processed
466
+ // by verifying the DOM reflects the active descendant
467
+ const container = element.shadowRoot!.querySelector('.ui-select')!
468
+ assert.equal(container.getAttribute('aria-activedescendant'), 'test-option')
469
+ })
470
+
471
+ it('should clear aria-activedescendant when no item highlighted', async () => {
472
+ const element = await basicFixture()
473
+ element.handleHighlightChange(
474
+ new CustomEvent('highlightchange', {
475
+ detail: { item: null },
476
+ })
477
+ )
478
+
479
+ // Check that aria-activedescendant is cleared
480
+ const container = element.shadowRoot!.querySelector('.ui-select')!
481
+ assert.equal(container.getAttribute('aria-activedescendant'), '')
482
+ })
483
+ })
484
+
485
+ describe('Accessibility', () => {
486
+ it('should update aria-expanded when opening/closing', async () => {
487
+ const element = await basicFixture()
488
+ await element.updateComplete
489
+
490
+ element.open = true
491
+ await element.updateComplete
492
+ assert.equal(element.getAttribute('aria-expanded'), 'true')
493
+
494
+ element.open = false
495
+ await element.updateComplete
496
+ assert.equal(element.getAttribute('aria-expanded'), 'false')
497
+ })
498
+
499
+ it('should set aria-label from label property', async () => {
500
+ const element = await basicFixture()
501
+ element.label = 'Test label'
502
+ await element.updateComplete
503
+
504
+ assert.equal(element.getAttribute('aria-label'), 'Test label')
505
+ })
506
+
507
+ it('should remove aria-label when label is cleared', async () => {
508
+ const element = await basicFixture()
509
+ element.label = undefined
510
+ await element.updateComplete
511
+
512
+ assert.isFalse(element.hasAttribute('aria-label'))
513
+ })
514
+
515
+ it('should support aria-activedescendant', async () => {
516
+ const element = await basicFixture()
517
+ const option = element.querySelector('ui-option') as UiOption
518
+ option.id = 'test-id'
519
+
520
+ // Simulate highlight change to set aria-activedescendant
521
+ element.handleHighlightChange(
522
+ new CustomEvent('highlightchange', {
523
+ detail: { item: option },
524
+ })
525
+ )
526
+ await element.updateComplete
527
+
528
+ const container = element.shadowRoot!.querySelector('.ui-select')!
529
+ assert.equal(container.getAttribute('aria-activedescendant'), 'test-id')
530
+ })
531
+ })
532
+
533
+ describe('Edge cases', () => {
534
+ it('should handle empty option list', async () => {
535
+ const element = await emptyFixture()
536
+ element.value = 'nonexistent'
537
+ await element.updateComplete
538
+
539
+ assert.equal(element.value, 'nonexistent')
540
+ assert.isNull(element.selectedItem)
541
+ assert.equal(element.renderValue, '')
542
+ })
543
+
544
+ it('should handle setting open before menu is rendered', async () => {
545
+ const element = await basicFixture()
546
+ await element.updateComplete
547
+
548
+ // This should not throw an error even with timing issues
549
+ element.open = true
550
+ await element.updateComplete
551
+
552
+ assert.isTrue(element.open)
553
+
554
+ element.open = false
555
+ await element.updateComplete
556
+
557
+ assert.isFalse(element.open)
558
+ })
559
+
560
+ it('should maintain focus during interaction', async () => {
561
+ const element = await basicFixture()
562
+ const focusSpy = sinon.spy(element, 'focus')
563
+
564
+ // Simulate selection
565
+ const option = element.querySelector('ui-option[value="apple"]') as UiOption
566
+ element.handleSelect(new CustomEvent('select', { detail: { item: option } }))
567
+
568
+ assert.isTrue(focusSpy.called)
569
+ })
570
+
571
+ it('should handle selection with prevented event', async () => {
572
+ const element = await basicFixture()
573
+ element.open = true
574
+ await element.updateComplete
575
+
576
+ const event = new CustomEvent('select', {
577
+ detail: { item: element.querySelector('ui-option') as UiOption },
578
+ })
579
+ const stopPropagationSpy = sinon.spy(event, 'stopPropagation')
580
+
581
+ element.handleSelect(event)
582
+ assert.isTrue(stopPropagationSpy.called)
583
+ })
584
+ })
585
+
586
+ describe('Rendering', () => {
587
+ it('should render with correct classes', async () => {
588
+ const element = await basicFixture()
589
+ await element.updateComplete
590
+
591
+ const container = element.shadowRoot!.querySelector('.ui-select')!
592
+ assert.isTrue(container.classList.contains('ui-select'))
593
+ assert.isFalse(container.classList.contains('open'))
594
+ assert.isFalse(container.classList.contains('disabled'))
595
+ })
596
+
597
+ it('should render with open class when open', async () => {
598
+ const element = await basicFixture()
599
+ element.open = true
600
+ await element.updateComplete
601
+
602
+ const container = element.shadowRoot!.querySelector('.ui-select')!
603
+ assert.isTrue(container.classList.contains('open'))
604
+ })
605
+
606
+ it('should render with disabled class when disabled', async () => {
607
+ const element = await disabledFixture()
608
+ await element.updateComplete
609
+
610
+ const container = element.shadowRoot!.querySelector('.ui-select')!
611
+ assert.isTrue(container.classList.contains('disabled'))
612
+ })
613
+
614
+ it('should render focus ring', async () => {
615
+ const element = await basicFixture()
616
+ await element.updateComplete
617
+
618
+ const focusRing = element.shadowRoot!.querySelector('md-focus-ring')!
619
+ assert.isNotNull(focusRing)
620
+ assert.equal(focusRing.getAttribute('part'), 'focus-ring')
621
+ })
622
+
623
+ it('should render text field with correct properties', async () => {
624
+ const element = await withValueFixture()
625
+ await element.updateComplete
626
+
627
+ const textField = element.shadowRoot!.querySelector('ui-outlined-text-field')!
628
+ assert.equal(textField.label, 'Select an option')
629
+ // The text field should show the render value of the selected option
630
+ assert.equal(textField.value, element.renderValue)
631
+ assert.isTrue(textField.hasAttribute('readonly'))
632
+ assert.equal(textField.tabIndex, -1)
633
+ assert.isTrue(textField.hasAttribute('inert'))
634
+ assert.equal(textField.getAttribute('aria-hidden'), 'true')
635
+ })
636
+
637
+ it('should render dropdown icon', async () => {
638
+ const element = await basicFixture()
639
+ await element.updateComplete
640
+
641
+ const icon = element.shadowRoot!.querySelector('ui-icon[slot="suffix"]')!
642
+ assert.equal(icon.textContent!.trim(), 'arrow_drop_down')
643
+ })
644
+
645
+ it('should render menu with correct attributes', async () => {
646
+ const element = await basicFixture()
647
+ await element.updateComplete
648
+
649
+ const menu = element.shadowRoot!.querySelector('ui-menu')!
650
+ assert.equal(menu.id, 'menu')
651
+ assert.equal(menu.getAttribute('popover'), 'auto')
652
+ assert.equal(menu.getAttribute('selector'), 'ui-option')
653
+ })
654
+
655
+ it('should slot options correctly', async () => {
656
+ const element = await basicFixture()
657
+ await element.updateComplete
658
+
659
+ const options = element.querySelectorAll('ui-option')
660
+ assert.equal(options.length, 3)
661
+ assert.equal(options[0].value, 'apple')
662
+ assert.equal(options[1].value, 'banana')
663
+ assert.equal(options[2].value, 'cherry')
664
+ })
665
+ })
666
+ })
667
+ })