@api-client/ui 0.5.21 → 0.5.23

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 (83) hide show
  1. package/.github/instructions/lit-best-practices.instructions.md +1 -0
  2. package/build/src/md/button/ui-button-group.d.ts +1 -0
  3. package/build/src/md/button/ui-button-group.d.ts.map +1 -1
  4. package/build/src/md/button/ui-button-group.js.map +1 -1
  5. package/build/src/md/button/ui-button.d.ts +2 -0
  6. package/build/src/md/button/ui-button.d.ts.map +1 -1
  7. package/build/src/md/button/ui-button.js.map +1 -1
  8. package/build/src/md/chip/ui-chip.d.ts +1 -0
  9. package/build/src/md/chip/ui-chip.d.ts.map +1 -1
  10. package/build/src/md/chip/ui-chip.js +1 -0
  11. package/build/src/md/chip/ui-chip.js.map +1 -1
  12. package/build/src/md/dialog/internals/Dialog.d.ts +0 -1
  13. package/build/src/md/dialog/internals/Dialog.d.ts.map +1 -1
  14. package/build/src/md/dialog/internals/Dialog.js.map +1 -1
  15. package/build/src/md/dialog/ui-dialog.d.ts +1 -0
  16. package/build/src/md/dialog/ui-dialog.d.ts.map +1 -1
  17. package/build/src/md/dialog/ui-dialog.js.map +1 -1
  18. package/build/src/md/divider/ui-divider.d.ts +1 -0
  19. package/build/src/md/divider/ui-divider.d.ts.map +1 -1
  20. package/build/src/md/divider/ui-divider.js +1 -0
  21. package/build/src/md/divider/ui-divider.js.map +1 -1
  22. package/build/src/md/dropdown-list/ui-dropdown-list.d.ts +1 -0
  23. package/build/src/md/dropdown-list/ui-dropdown-list.d.ts.map +1 -1
  24. package/build/src/md/dropdown-list/ui-dropdown-list.js.map +1 -1
  25. package/build/src/md/icon-button/ui-icon-button.d.ts +2 -0
  26. package/build/src/md/icon-button/ui-icon-button.d.ts.map +1 -1
  27. package/build/src/md/icon-button/ui-icon-button.js.map +1 -1
  28. package/build/src/md/list/internals/ListItem.styles.d.ts.map +1 -1
  29. package/build/src/md/list/internals/ListItem.styles.js +7 -0
  30. package/build/src/md/list/internals/ListItem.styles.js.map +1 -1
  31. package/build/src/md/list/ui-list-item.d.ts +1 -0
  32. package/build/src/md/list/ui-list-item.d.ts.map +1 -1
  33. package/build/src/md/list/ui-list-item.js +1 -0
  34. package/build/src/md/list/ui-list-item.js.map +1 -1
  35. package/build/src/md/list/ui-list.d.ts +1 -0
  36. package/build/src/md/list/ui-list.d.ts.map +1 -1
  37. package/build/src/md/list/ui-list.js.map +1 -1
  38. package/build/src/md/menu/internal/Menu.d.ts +13 -0
  39. package/build/src/md/menu/internal/Menu.d.ts.map +1 -1
  40. package/build/src/md/menu/internal/Menu.js +47 -31
  41. package/build/src/md/menu/internal/Menu.js.map +1 -1
  42. package/build/src/md/menu/internal/Menu.styles.d.ts.map +1 -1
  43. package/build/src/md/menu/internal/Menu.styles.js +14 -2
  44. package/build/src/md/menu/internal/Menu.styles.js.map +1 -1
  45. package/build/src/md/menu/internal/MenuItem.d.ts +22 -0
  46. package/build/src/md/menu/internal/MenuItem.d.ts.map +1 -1
  47. package/build/src/md/menu/internal/MenuItem.js +74 -1
  48. package/build/src/md/menu/internal/MenuItem.js.map +1 -1
  49. package/build/src/md/menu/internal/MenuItem.styles.d.ts.map +1 -1
  50. package/build/src/md/menu/internal/MenuItem.styles.js +23 -0
  51. package/build/src/md/menu/internal/MenuItem.styles.js.map +1 -1
  52. package/build/src/md/menu/ui-menu-item.d.ts +1 -4
  53. package/build/src/md/menu/ui-menu-item.d.ts.map +1 -1
  54. package/build/src/md/menu/ui-menu-item.js +1 -4
  55. package/build/src/md/menu/ui-menu-item.js.map +1 -1
  56. package/build/src/md/menu/ui-menu.d.ts +1 -0
  57. package/build/src/md/menu/ui-menu.d.ts.map +1 -1
  58. package/build/src/md/menu/ui-menu.js.map +1 -1
  59. package/build/src/md/tabs/ui-tabs.d.ts +1 -0
  60. package/build/src/md/tabs/ui-tabs.d.ts.map +1 -1
  61. package/build/src/md/tabs/ui-tabs.js.map +1 -1
  62. package/demo/md/menu/index.ts +146 -1
  63. package/package.json +1 -1
  64. package/src/md/button/ui-button-group.ts +2 -0
  65. package/src/md/button/ui-button.ts +3 -0
  66. package/src/md/chip/ui-chip.ts +2 -0
  67. package/src/md/dialog/internals/Dialog.ts +0 -2
  68. package/src/md/dialog/ui-dialog.ts +2 -0
  69. package/src/md/divider/ui-divider.ts +2 -0
  70. package/src/md/dropdown-list/ui-dropdown-list.ts +2 -0
  71. package/src/md/icon-button/ui-icon-button.ts +3 -0
  72. package/src/md/list/internals/ListItem.styles.ts +7 -0
  73. package/src/md/list/ui-list-item.ts +2 -0
  74. package/src/md/list/ui-list.ts +2 -0
  75. package/src/md/menu/internal/Menu.styles.ts +14 -2
  76. package/src/md/menu/internal/Menu.ts +53 -32
  77. package/src/md/menu/internal/MenuItem.styles.ts +23 -0
  78. package/src/md/menu/internal/MenuItem.ts +49 -0
  79. package/src/md/menu/ui-menu-item.ts +2 -4
  80. package/src/md/menu/ui-menu.ts +2 -0
  81. package/src/md/tabs/ui-tabs.ts +2 -0
  82. package/test/md/menu/Menu.test.ts +346 -0
  83. package/test/md/menu/MenuItem.test.ts +292 -0
@@ -505,5 +505,351 @@ describe('md', () => {
505
505
  assert.isFalse(hideSpy.called)
506
506
  })
507
507
  })
508
+
509
+ describe('Menu item selection', () => {
510
+ async function selectionFixture(): Promise<Menu> {
511
+ return fixture(html`
512
+ <ui-menu id="selection-menu">
513
+ <ui-menu-item id="item1">Item 1</ui-menu-item>
514
+ <ui-menu-item id="item2" selected>Item 2</ui-menu-item>
515
+ <ui-menu-item id="item3">Item 3</ui-menu-item>
516
+ <ui-menu-item id="item4" disabled>Item 4 (Disabled)</ui-menu-item>
517
+ </ui-menu>
518
+ `)
519
+ }
520
+
521
+ describe('selectedItem getter', () => {
522
+ it('should return null when no item is selected', async () => {
523
+ const element = await basicFixture()
524
+ await nextFrame()
525
+
526
+ assert.isNull(element.selectedItem)
527
+ })
528
+
529
+ it('should return the selected menu item', async () => {
530
+ const element = await selectionFixture()
531
+ await nextFrame()
532
+
533
+ const selectedItem = element.selectedItem
534
+ const item2 = element.querySelector('#item2') as UiMenuItem
535
+
536
+ assert.isNotNull(selectedItem)
537
+ assert.equal(selectedItem, item2)
538
+ assert.isTrue(selectedItem!.selected)
539
+ })
540
+
541
+ it('should return the first selected item when multiple items are selected', async () => {
542
+ const element = await selectionFixture()
543
+ const item1 = element.querySelector('#item1') as UiMenuItem
544
+ const item3 = element.querySelector('#item3') as UiMenuItem
545
+ await nextFrame()
546
+
547
+ // Manually set multiple items as selected
548
+ item1.selected = true
549
+ item3.selected = true
550
+ await nextFrame()
551
+
552
+ const selectedItem = element.selectedItem
553
+ assert.equal(selectedItem, item1)
554
+ })
555
+ })
556
+
557
+ describe('setSelectedItem method', () => {
558
+ it('should select a menu item and clear previous selection', async () => {
559
+ const element = await selectionFixture()
560
+ const item1 = element.querySelector('#item1') as UiMenuItem
561
+ const item2 = element.querySelector('#item2') as UiMenuItem
562
+ await nextFrame()
563
+
564
+ // Initially item2 is selected
565
+ assert.isTrue(item2.selected)
566
+ assert.isFalse(item1.selected)
567
+
568
+ // Select item1
569
+ element.setSelectedItem(item1)
570
+ await nextFrame()
571
+
572
+ assert.isTrue(item1.selected)
573
+ assert.isFalse(item2.selected)
574
+ assert.equal(element.selectedItem, item1)
575
+ })
576
+
577
+ it('should clear all selections when passed null', async () => {
578
+ const element = await selectionFixture()
579
+ const item2 = element.querySelector('#item2') as UiMenuItem
580
+ await nextFrame()
581
+
582
+ // Initially item2 is selected
583
+ assert.isTrue(item2.selected)
584
+
585
+ // Clear selection
586
+ element.setSelectedItem(null)
587
+ await nextFrame()
588
+
589
+ assert.isFalse(item2.selected)
590
+ assert.isNull(element.selectedItem)
591
+ })
592
+
593
+ it('should handle selecting disabled items', async () => {
594
+ const element = await selectionFixture()
595
+ const item4 = element.querySelector('#item4') as UiMenuItem
596
+ await nextFrame()
597
+
598
+ // Should be able to select disabled items programmatically
599
+ element.setSelectedItem(item4)
600
+ await nextFrame()
601
+
602
+ assert.isTrue(item4.selected)
603
+ assert.equal(element.selectedItem, item4)
604
+ })
605
+
606
+ it('should not throw when selecting an item not in the menu', async () => {
607
+ const element = await selectionFixture()
608
+ const externalItem: UiMenuItem = await fixture(html`<ui-menu-item>External Item</ui-menu-item>`)
609
+ await nextFrame()
610
+
611
+ // Should not throw
612
+ element.setSelectedItem(externalItem)
613
+
614
+ // External item should be selected but not affect the menu's selectedItem
615
+ assert.isTrue(externalItem.selected)
616
+ // The menu should return null since the external item is not in assignedMenuItems
617
+ assert.isNull(element.selectedItem)
618
+ })
619
+ })
620
+
621
+ describe('Selection clearing functionality', () => {
622
+ it('should clear selection from all menu items when setting null', async () => {
623
+ const element = await selectionFixture()
624
+ const item1 = element.querySelector('#item1') as UiMenuItem
625
+ const item2 = element.querySelector('#item2') as UiMenuItem
626
+ const item3 = element.querySelector('#item3') as UiMenuItem
627
+ await nextFrame()
628
+
629
+ // Set multiple items as selected
630
+ item1.selected = true
631
+ item3.selected = true
632
+ await nextFrame()
633
+
634
+ // Initially item2 is selected from fixture, now item1 and item3 are also selected
635
+ assert.isTrue(item1.selected)
636
+ assert.isTrue(item2.selected)
637
+ assert.isTrue(item3.selected)
638
+
639
+ // Clear all selections using the public method
640
+ element.setSelectedItem(null)
641
+ await nextFrame()
642
+
643
+ assert.isFalse(item1.selected)
644
+ assert.isFalse(item2.selected)
645
+ assert.isFalse(item3.selected)
646
+ assert.isNull(element.selectedItem)
647
+ })
648
+
649
+ it('should clear previous selection when selecting new item', async () => {
650
+ const element = await selectionFixture()
651
+ const item1 = element.querySelector('#item1') as UiMenuItem
652
+ const item2 = element.querySelector('#item2') as UiMenuItem
653
+ const item3 = element.querySelector('#item3') as UiMenuItem
654
+ await nextFrame()
655
+
656
+ // Set multiple items as selected manually
657
+ item1.selected = true
658
+ item3.selected = true
659
+ await nextFrame()
660
+
661
+ // Select item2 - this should clear all other selections
662
+ element.setSelectedItem(item2)
663
+ await nextFrame()
664
+
665
+ assert.isFalse(item1.selected)
666
+ assert.isTrue(item2.selected)
667
+ assert.isFalse(item3.selected)
668
+ assert.equal(element.selectedItem, item2)
669
+ })
670
+
671
+ it('should clear selection when notifySelect is called with MenuItem', async () => {
672
+ const element = await selectionFixture()
673
+ const item1 = element.querySelector('#item1') as UiMenuItem
674
+ const item2 = element.querySelector('#item2') as UiMenuItem
675
+ const item3 = element.querySelector('#item3') as UiMenuItem
676
+ await nextFrame()
677
+
678
+ element.show()
679
+ await nextFrame()
680
+
681
+ // Set multiple items as selected manually
682
+ item1.selected = true
683
+ item3.selected = true
684
+ await nextFrame()
685
+
686
+ // Initially item2 is selected from fixture, now all items are selected
687
+ assert.isTrue(item1.selected)
688
+ assert.isTrue(item2.selected)
689
+ assert.isTrue(item3.selected)
690
+
691
+ // Call notifySelect on item1 - should clear all other selections
692
+ element.notifySelect(item1, 0)
693
+
694
+ assert.isTrue(item1.selected) // This one should remain selected
695
+ assert.isFalse(item2.selected) // These should be cleared
696
+ assert.isFalse(item3.selected)
697
+ })
698
+
699
+ it('should handle empty menu items', async () => {
700
+ const element: Menu = await fixture(html`<ui-menu></ui-menu>`)
701
+ await nextFrame()
702
+
703
+ // Should not throw - test by setting and clearing selection
704
+ element.setSelectedItem(null)
705
+ assert.isNull(element.selectedItem)
706
+ })
707
+ })
708
+
709
+ describe('notifySelect method', () => {
710
+ it('should select menu item and hide menu', async () => {
711
+ const element = await selectionFixture()
712
+ const item1 = element.querySelector('#item1') as UiMenuItem
713
+ const item2 = element.querySelector('#item2') as UiMenuItem
714
+ await nextFrame()
715
+
716
+ const hideSpy = sinon.spy(element, 'hide')
717
+ element.show()
718
+ await nextFrame()
719
+
720
+ // Initially item2 is selected
721
+ assert.isTrue(item2.selected)
722
+ assert.isFalse(item1.selected)
723
+
724
+ // Notify selection of item1
725
+ const result = element.notifySelect(item1, 0)
726
+
727
+ assert.isFalse(result) // Should return false (event not canceled)
728
+ assert.isTrue(item1.selected)
729
+ assert.isFalse(item2.selected) // Previous selection should be cleared
730
+ assert.isTrue(hideSpy.calledOnce)
731
+ })
732
+
733
+ it('should handle non-MenuItem selection', async () => {
734
+ const element = await selectionFixture()
735
+ const nonMenuItem = document.createElement('div') as unknown as UiMenuItem
736
+ await nextFrame()
737
+
738
+ const hideSpy = sinon.spy(element, 'hide')
739
+ element.show()
740
+ await nextFrame()
741
+
742
+ // Should not throw and should still hide menu
743
+ // When item is not found in items array, notifySelect returns false
744
+ const result = element.notifySelect(nonMenuItem, 0)
745
+
746
+ assert.isFalse(result) // Returns false when item not found in items
747
+ assert.isTrue(hideSpy.calledOnce)
748
+ })
749
+
750
+ it('should dispatch select event through parent class', async () => {
751
+ const element = await selectionFixture()
752
+ const item1 = element.querySelector('#item1') as UiMenuItem
753
+ await nextFrame()
754
+
755
+ element.show()
756
+ await nextFrame()
757
+
758
+ setTimeout(() => element.notifySelect(item1, 0))
759
+ const event = await oneEvent(element, 'select')
760
+
761
+ assert.instanceOf(event, CustomEvent)
762
+ assert.equal((event as CustomEvent).detail.item, item1)
763
+ assert.equal((event as CustomEvent).detail.index, 0)
764
+ })
765
+
766
+ it('should return true when select event is canceled', async () => {
767
+ const element = await selectionFixture()
768
+ const item1 = element.querySelector('#item1') as UiMenuItem
769
+ await nextFrame()
770
+
771
+ element.show()
772
+ await nextFrame()
773
+
774
+ // Add event listener that cancels the event
775
+ element.addEventListener('select', (e) => {
776
+ e.preventDefault()
777
+ })
778
+
779
+ const result = element.notifySelect(item1, 0)
780
+
781
+ assert.isTrue(result) // Should return true when event is canceled
782
+ assert.isTrue(item1.selected) // Item should still be selected in Menu
783
+ })
784
+
785
+ it('should clear selection from all items before selecting new one', async () => {
786
+ const element = await selectionFixture()
787
+ const item1 = element.querySelector('#item1') as UiMenuItem
788
+ const item2 = element.querySelector('#item2') as UiMenuItem
789
+ const item3 = element.querySelector('#item3') as UiMenuItem
790
+ await nextFrame()
791
+
792
+ // Manually set multiple items as selected
793
+ item1.selected = true
794
+ item3.selected = true
795
+ await nextFrame()
796
+
797
+ element.show()
798
+ await nextFrame()
799
+
800
+ // Notify selection of item2
801
+ element.notifySelect(item2, 1)
802
+
803
+ // Only item2 should be selected
804
+ assert.isFalse(item1.selected)
805
+ assert.isTrue(item2.selected)
806
+ assert.isFalse(item3.selected)
807
+ })
808
+ })
809
+
810
+ describe('Selection integration with keyboard navigation', () => {
811
+ it('should maintain selection state when navigating with arrow keys', async () => {
812
+ const element = await selectionFixture()
813
+ const item2 = element.querySelector('#item2') as UiMenuItem
814
+ await nextFrame()
815
+
816
+ element.show()
817
+ await nextFrame()
818
+
819
+ // Initially item2 is selected
820
+ assert.isTrue(item2.selected)
821
+
822
+ // Navigate with arrow keys (should not affect selection)
823
+ const downEvent = new KeyboardEvent('keydown', { key: 'ArrowDown' })
824
+ element.dispatchEvent(downEvent)
825
+ await nextFrame()
826
+
827
+ // Selection should remain unchanged
828
+ assert.isTrue(item2.selected)
829
+ })
830
+ })
831
+
832
+ describe('Selection state preservation', () => {
833
+ it('should preserve selection when menu is hidden and shown again', async () => {
834
+ const element = await selectionFixture()
835
+ const item2 = element.querySelector('#item2') as UiMenuItem
836
+ await nextFrame()
837
+
838
+ // Initially item2 is selected
839
+ assert.isTrue(item2.selected)
840
+ assert.equal(element.selectedItem, item2)
841
+
842
+ // Show and hide menu
843
+ element.show()
844
+ await nextFrame()
845
+ element.hide()
846
+ await nextFrame()
847
+
848
+ // Selection should be preserved
849
+ assert.isTrue(item2.selected)
850
+ assert.equal(element.selectedItem, item2)
851
+ })
852
+ })
853
+ })
508
854
  })
509
855
  })
@@ -14,6 +14,18 @@ describe('md', () => {
14
14
  return fixture(html`<ui-menu-item>Test Item</ui-menu-item>`)
15
15
  }
16
16
 
17
+ async function selectedFixture(): Promise<UiMenuItem> {
18
+ return fixture(html`<ui-menu-item selected>Selected Item</ui-menu-item>`)
19
+ }
20
+
21
+ async function withValueFixture(): Promise<UiMenuItem> {
22
+ return fixture(html`<ui-menu-item value="test-value">Item with Value</ui-menu-item>`)
23
+ }
24
+
25
+ async function withSelectionIconFixture(): Promise<UiMenuItem> {
26
+ return fixture(html`<ui-menu-item selected showSelectionIcon>Item with Selection Icon</ui-menu-item>`)
27
+ }
28
+
17
29
  async function withSubmenuFixture(): Promise<HTMLElement> {
18
30
  return fixture(html`
19
31
  <div>
@@ -357,5 +369,285 @@ describe('md', () => {
357
369
  assert.isNotNull(ripple)
358
370
  })
359
371
  })
372
+
373
+ describe('Selection functionality', () => {
374
+ it('should have selected property with default value false', async () => {
375
+ const element = await basicFixture()
376
+ assert.isFalse(element.selected)
377
+ })
378
+
379
+ it('should set selected property from attribute', async () => {
380
+ const element = await selectedFixture()
381
+ assert.isTrue(element.selected)
382
+ })
383
+
384
+ it('should reflect selected property to attribute', async () => {
385
+ const element = await basicFixture()
386
+
387
+ element.selected = true
388
+ await nextFrame()
389
+ assert.isTrue(element.hasAttribute('selected'))
390
+
391
+ element.selected = false
392
+ await nextFrame()
393
+ assert.isFalse(element.hasAttribute('selected'))
394
+ })
395
+
396
+ it('should update CSS classes when selected changes', async () => {
397
+ const element = await basicFixture()
398
+
399
+ element.selected = true
400
+ await nextFrame()
401
+ assert.isTrue(element.classList.contains('select'))
402
+
403
+ element.selected = false
404
+ await nextFrame()
405
+ assert.isFalse(element.classList.contains('select'))
406
+ })
407
+
408
+ it('should update aria-selected when selected changes', async () => {
409
+ const element = await basicFixture()
410
+
411
+ element.selected = true
412
+ await nextFrame()
413
+ assert.equal(element.getAttribute('aria-selected'), 'true')
414
+
415
+ element.selected = false
416
+ await nextFrame()
417
+ assert.equal(element.getAttribute('aria-selected'), 'false')
418
+ })
419
+
420
+ it('should initialize selection state on connection', async () => {
421
+ const element = await selectedFixture()
422
+ await nextFrame()
423
+
424
+ assert.isTrue(element.classList.contains('select'))
425
+ assert.equal(element.getAttribute('aria-selected'), 'true')
426
+ })
427
+
428
+ it('should update selection state when selected property changes', async () => {
429
+ const element = await basicFixture()
430
+ const updateSelectionStateSpy = sinon.spy(
431
+ element as UiMenuItem & { updateSelectionState: () => void },
432
+ 'updateSelectionState'
433
+ )
434
+
435
+ element.selected = true
436
+ await nextFrame()
437
+
438
+ assert.isTrue(updateSelectionStateSpy.calledOnce)
439
+ })
440
+ })
441
+
442
+ describe('Value property', () => {
443
+ it('should have value property with default undefined', async () => {
444
+ const element = await basicFixture()
445
+ assert.isUndefined(element.value)
446
+ })
447
+
448
+ it('should set value property from attribute', async () => {
449
+ const element = await withValueFixture()
450
+ assert.equal(element.value, 'test-value')
451
+ })
452
+
453
+ it('should update value property dynamically', async () => {
454
+ const element = await basicFixture()
455
+
456
+ element.value = 'new-value'
457
+ await element.updateComplete
458
+ assert.equal(element.value, 'new-value')
459
+ assert.equal(element.getAttribute('value'), 'new-value')
460
+ })
461
+ })
462
+
463
+ describe('Selection icon functionality', () => {
464
+ it('should have showSelectionIcon property with default value false', async () => {
465
+ const element = await basicFixture()
466
+ assert.isFalse(element.showSelectionIcon)
467
+ })
468
+
469
+ it('should set showSelectionIcon property from attribute', async () => {
470
+ const element = await withSelectionIconFixture()
471
+ assert.isTrue(element.showSelectionIcon)
472
+ })
473
+
474
+ it('should show check icon when selected and showSelectionIcon is true', async () => {
475
+ const element = await withSelectionIconFixture()
476
+ await nextFrame()
477
+
478
+ const checkIcon = element.shadowRoot!.querySelector('.selection-check')
479
+ assert.isNotNull(checkIcon)
480
+ assert.equal(checkIcon!.textContent, 'check')
481
+ })
482
+
483
+ it('should not show check icon when not selected', async () => {
484
+ const element: UiMenuItem = await fixture(
485
+ html`<ui-menu-item showSelectionIcon>Not Selected Item</ui-menu-item>`
486
+ )
487
+ await nextFrame()
488
+
489
+ const checkIcon = element.shadowRoot!.querySelector('.selection-check')
490
+ assert.isNull(checkIcon)
491
+ })
492
+
493
+ it('should not show check icon when selected but showSelectionIcon is false', async () => {
494
+ const element = await selectedFixture()
495
+ await nextFrame()
496
+
497
+ const checkIcon = element.shadowRoot!.querySelector('.selection-check')
498
+ assert.isNull(checkIcon)
499
+ })
500
+
501
+ it('should toggle check icon visibility when selection changes', async () => {
502
+ const element: UiMenuItem = await fixture(html`<ui-menu-item showSelectionIcon>Toggle Item</ui-menu-item>`)
503
+ await nextFrame()
504
+
505
+ // Initially not selected, no icon
506
+ let checkIcon = element.shadowRoot!.querySelector('.selection-check')
507
+ assert.isNull(checkIcon)
508
+
509
+ // Select the item
510
+ element.selected = true
511
+ await nextFrame()
512
+ checkIcon = element.shadowRoot!.querySelector('.selection-check')
513
+ assert.isNotNull(checkIcon)
514
+
515
+ // Deselect the item
516
+ element.selected = false
517
+ await nextFrame()
518
+ checkIcon = element.shadowRoot!.querySelector('.selection-check')
519
+ assert.isNull(checkIcon)
520
+ })
521
+ })
522
+
523
+ describe('Selection events', () => {
524
+ it('should not dispatch select event when regular menu item is clicked', async () => {
525
+ const element = await basicFixture()
526
+ const selectSpy = sinon.spy()
527
+ element.addEventListener('select', selectSpy)
528
+
529
+ element.click()
530
+ await nextFrame()
531
+
532
+ assert.isFalse(selectSpy.called)
533
+ })
534
+
535
+ it('should not dispatch select event when submenu item is clicked', async () => {
536
+ const container = await withSubmenuFixture()
537
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
538
+ await nextFrame()
539
+
540
+ const selectSpy = sinon.spy()
541
+ menuItem.addEventListener('select', selectSpy)
542
+
543
+ menuItem.click()
544
+ await nextFrame()
545
+
546
+ assert.isFalse(selectSpy.called)
547
+ })
548
+
549
+ it('should bubble up select event from sub-menu selections', async () => {
550
+ const element = await basicFixture()
551
+ const selectSpy = sinon.spy()
552
+ element.addEventListener('select', selectSpy)
553
+
554
+ // Simulate a sub-menu selection event
555
+ const subMenuEvent = new CustomEvent('select', {
556
+ detail: { item: element, index: 0 },
557
+ bubbles: true,
558
+ composed: true,
559
+ })
560
+
561
+ // Access the protected method through type assertion
562
+ element['handleSubMenuSelect'](subMenuEvent)
563
+
564
+ assert.isTrue(selectSpy.calledOnce)
565
+ const dispatchedEvent = selectSpy.args[0][0] as CustomEvent
566
+ assert.equal(dispatchedEvent.detail.item, element)
567
+ assert.equal(dispatchedEvent.detail.index, 0)
568
+ assert.isTrue(dispatchedEvent.bubbles)
569
+ assert.isTrue(dispatchedEvent.composed)
570
+ })
571
+ })
572
+
573
+ describe('Accessibility for selection', () => {
574
+ it('should always have aria-selected attribute', async () => {
575
+ const element = await basicFixture()
576
+ await nextFrame()
577
+
578
+ assert.isNotNull(element.getAttribute('aria-selected'))
579
+ })
580
+
581
+ it('should have aria-selected="false" by default', async () => {
582
+ const element = await basicFixture()
583
+ await nextFrame()
584
+
585
+ assert.equal(element.getAttribute('aria-selected'), 'false')
586
+ })
587
+
588
+ it('should have aria-selected="true" when selected', async () => {
589
+ const element = await selectedFixture()
590
+ await nextFrame()
591
+
592
+ assert.equal(element.getAttribute('aria-selected'), 'true')
593
+ })
594
+
595
+ it('should maintain proper aria-selected state when toggling selection', async () => {
596
+ const element = await basicFixture()
597
+ await nextFrame()
598
+
599
+ // Initially false
600
+ assert.equal(element.getAttribute('aria-selected'), 'false')
601
+
602
+ // Select
603
+ element.selected = true
604
+ await nextFrame()
605
+ assert.equal(element.getAttribute('aria-selected'), 'true')
606
+
607
+ // Deselect
608
+ element.selected = false
609
+ await nextFrame()
610
+ assert.equal(element.getAttribute('aria-selected'), 'false')
611
+ })
612
+ })
613
+
614
+ describe('Selection state interaction with other properties', () => {
615
+ it('should maintain selection state when disabled', async () => {
616
+ const element = await selectedFixture()
617
+
618
+ element.disabled = true
619
+ await nextFrame()
620
+
621
+ assert.isTrue(element.selected)
622
+ assert.equal(element.getAttribute('aria-selected'), 'true')
623
+ assert.isTrue(element.classList.contains('select'))
624
+ })
625
+
626
+ it('should preserve value when selection changes', async () => {
627
+ const element = await withValueFixture()
628
+
629
+ element.selected = true
630
+ await nextFrame()
631
+
632
+ assert.equal(element.value, 'test-value')
633
+ })
634
+
635
+ it('should work with complex attribute combinations', async () => {
636
+ const element: UiMenuItem = await fixture(html`
637
+ <ui-menu-item selected showSelectionIcon value="complex-item" disabled> Complex Item </ui-menu-item>
638
+ `)
639
+ await nextFrame()
640
+
641
+ assert.isTrue(element.selected)
642
+ assert.isTrue(element.showSelectionIcon)
643
+ assert.equal(element.value, 'complex-item')
644
+ assert.isTrue(element.disabled)
645
+ assert.equal(element.getAttribute('aria-selected'), 'true')
646
+ assert.isTrue(element.classList.contains('select'))
647
+
648
+ const checkIcon = element.shadowRoot!.querySelector('.selection-check')
649
+ assert.isNotNull(checkIcon)
650
+ })
651
+ })
360
652
  })
361
653
  })