@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
@@ -1,6 +1,7 @@
1
1
  import { html, TemplateResult } from 'lit'
2
2
  import { DemoPage } from '../../../src/demo/DemoPage.js'
3
3
  import reactive from '../../../src/decorators/reactive.js'
4
+ import type { UiMenuItemElement } from '../../../src/md/menu/ui-menu-item.js'
4
5
 
5
6
  import '../../../src/md/menu/ui-menu.js'
6
7
  import '../../../src/md/menu/ui-sub-menu.js'
@@ -17,6 +18,11 @@ class ComponentDemoPage extends DemoPage {
17
18
  @reactive() accessor nestedCount = 0
18
19
  @reactive() accessor overflowMenuOutput = ''
19
20
  @reactive() accessor overflowCount = 0
21
+ @reactive() accessor selectedOption = ''
22
+ @reactive() accessor iconMenuSelectOutput = ''
23
+ @reactive() accessor iconMenuSelectedOption = 'Premium'
24
+ @reactive() accessor autoCheckMenuOutput = ''
25
+ @reactive() accessor autoCheckSelectedOption = 'High'
20
26
 
21
27
  handleBasicMenuSelect(e: CustomEvent): void {
22
28
  const item = e.detail.item as HTMLElement
@@ -39,8 +45,37 @@ class ComponentDemoPage extends DemoPage {
39
45
  this.overflowCount++
40
46
  }
41
47
 
48
+ handleSelectionMenuSelect(e: CustomEvent): void {
49
+ const item = e.detail.item as HTMLElement
50
+ this.selectedOption = item.textContent?.trim() || ''
51
+ }
52
+
53
+ handleIconSelectMenuSelect(e: CustomEvent): void {
54
+ const item = e.detail.item as UiMenuItemElement
55
+ this.iconMenuSelectedOption = item.value || ''
56
+ this.iconMenuSelectOutput = `Selected: ${this.iconMenuSelectedOption}`
57
+ }
58
+
59
+ handleAutoCheckMenuSelect(e: CustomEvent): void {
60
+ const item = e.detail.item as HTMLElement
61
+ this.autoCheckSelectedOption = item.textContent?.trim() || ''
62
+ this.autoCheckMenuOutput = `Selected: ${this.autoCheckSelectedOption}`
63
+ }
64
+
42
65
  contentTemplate(): TemplateResult {
43
- const { basicMenuOutput, nestedMenuOutput, nestedCount, basicCount, overflowMenuOutput, overflowCount } = this
66
+ const {
67
+ basicMenuOutput,
68
+ nestedMenuOutput,
69
+ nestedCount,
70
+ basicCount,
71
+ overflowMenuOutput,
72
+ overflowCount,
73
+ selectedOption,
74
+ iconMenuSelectOutput,
75
+ iconMenuSelectedOption,
76
+ autoCheckMenuOutput,
77
+ autoCheckSelectedOption,
78
+ } = this
44
79
  return html`
45
80
  <a href="../">Back</a>
46
81
 
@@ -361,6 +396,116 @@ class ComponentDemoPage extends DemoPage {
361
396
  <p>Count: ${nestedCount}</p>`
362
397
  : ''}
363
398
  </section>
399
+
400
+ <section class="demo-section">
401
+ <h2 class="title-large">Selection Menu</h2>
402
+ <p>A menu that allows selection of options:</p>
403
+ <ui-button id="selection-menu-trigger" color="filled" popovertarget="selection-menu"
404
+ >Open Selection Menu</ui-button
405
+ >
406
+ <ui-menu id="selection-menu" popover="auto" @select="${this.handleSelectionMenuSelect}">
407
+ <ui-menu-item data-value="option1" ?selected="${selectedOption === 'Option 1'}">
408
+ ${selectedOption === 'Option 1' ? html`<span slot="start"><ui-icon>check</ui-icon></span>` : ''}
409
+ <span>Option 1</span>
410
+ </ui-menu-item>
411
+ <ui-menu-item data-value="option2" ?selected="${selectedOption === 'Option 2'}">
412
+ ${selectedOption === 'Option 2' ? html`<span slot="start"><ui-icon>check</ui-icon></span>` : ''}
413
+ <span>Option 2</span>
414
+ </ui-menu-item>
415
+ <ui-menu-item data-value="option3" ?selected="${selectedOption === 'Option 3'}">
416
+ ${selectedOption === 'Option 3' ? html`<span slot="start"><ui-icon>check</ui-icon></span>` : ''}
417
+ <span>Option 3</span>
418
+ </ui-menu-item>
419
+ </ui-menu>
420
+ ${selectedOption ? html`<p>Selected: ${selectedOption}</p>` : ''}
421
+ </section>
422
+
423
+ <section class="demo-section">
424
+ <h2 class="title-large">Menu with Selection and icons</h2>
425
+ <p>
426
+ A menu that maintains selection state. Select an option and reopen the menu to see the selection preserved:
427
+ </p>
428
+ <ui-button id="single-select-menu-trigger" color="filled" popovertarget="single-select-menu">
429
+ Select Option: ${iconMenuSelectedOption || 'None'}
430
+ </ui-button>
431
+ <ui-menu id="single-select-menu" popover="auto" @select="${this.handleIconSelectMenuSelect}">
432
+ <ui-menu-item value="premium" showSelectionIcon ?selected="${iconMenuSelectedOption === 'premium'}">
433
+ <span slot="start"><ui-icon>star</ui-icon></span>
434
+ <span>Premium</span>
435
+ </ui-menu-item>
436
+ <ui-menu-item value="standard" showSelectionIcon ?selected="${iconMenuSelectedOption === 'standard'}">
437
+ <span slot="start"><ui-icon>account_circle</ui-icon></span>
438
+ <span>Standard</span>
439
+ </ui-menu-item>
440
+ <ui-menu-item value="basic" showSelectionIcon ?selected="${iconMenuSelectedOption === 'basic'}">
441
+ <span slot="start"><ui-icon>public</ui-icon></span>
442
+ <span>Basic</span>
443
+ </ui-menu-item>
444
+ </ui-menu>
445
+ ${iconMenuSelectOutput ? html`<p>${iconMenuSelectOutput}</p>` : ''}
446
+ </section>
447
+
448
+ <section class="demo-section">
449
+ <h2 class="title-large">Menu with Auto-Check Icons</h2>
450
+ <p>
451
+ A menu that automatically shows check icons for selected items when the
452
+ <code>show-selection-icon</code> attribute is used:
453
+ </p>
454
+ <ui-button id="auto-check-menu-trigger" color="filled" popovertarget="auto-check-menu">
455
+ Priority: ${autoCheckSelectedOption || 'None'}
456
+ </ui-button>
457
+ <ui-menu id="auto-check-menu" popover="auto" @select="${this.handleAutoCheckMenuSelect}">
458
+ <ui-menu-item showSelectionIcon>
459
+ <span>Low</span>
460
+ </ui-menu-item>
461
+ <ui-menu-item showSelectionIcon selected>
462
+ <span>High</span>
463
+ </ui-menu-item>
464
+ <ui-menu-item showSelectionIcon>
465
+ <span>Critical</span>
466
+ </ui-menu-item>
467
+ </ui-menu>
468
+ ${autoCheckMenuOutput ? html`<p>${autoCheckMenuOutput}</p>` : ''}
469
+ </section>
470
+
471
+ <section class="demo-section">
472
+ <h2 class="title-large">Edge Positioning Test</h2>
473
+ <p>Test menu positioning at screen edges:</p>
474
+ <div style="display: flex; justify-content: space-between; margin: 20px 0;">
475
+ <ui-button id="left-edge-menu-trigger" color="filled" popovertarget="left-edge-menu">Left Edge</ui-button>
476
+ <ui-button id="right-edge-menu-trigger" color="filled" popovertarget="right-edge-menu">Right Edge</ui-button>
477
+ </div>
478
+
479
+ <ui-menu id="left-edge-menu" popover="auto" @select="${this.handleBasicMenuSelect}">
480
+ <ui-menu-item>
481
+ <span slot="start"><ui-icon>check</ui-icon></span>
482
+ <span>Left Edge Menu Item 1</span>
483
+ </ui-menu-item>
484
+ <ui-menu-item>
485
+ <span slot="start"><ui-icon>check</ui-icon></span>
486
+ <span>Left Edge Menu Item 2</span>
487
+ </ui-menu-item>
488
+ <ui-menu-item>
489
+ <span slot="start"><ui-icon>check</ui-icon></span>
490
+ <span>Left Edge Menu Item 3</span>
491
+ </ui-menu-item>
492
+ </ui-menu>
493
+
494
+ <ui-menu id="right-edge-menu" popover="auto" @select="${this.handleBasicMenuSelect}">
495
+ <ui-menu-item>
496
+ <span slot="start"><ui-icon>check</ui-icon></span>
497
+ <span>Right Edge Menu Item 1</span>
498
+ </ui-menu-item>
499
+ <ui-menu-item>
500
+ <span slot="start"><ui-icon>check</ui-icon></span>
501
+ <span>Right Edge Menu Item 2</span>
502
+ </ui-menu-item>
503
+ <ui-menu-item>
504
+ <span slot="start"><ui-icon>check</ui-icon></span>
505
+ <span>Right Edge Menu Item 3</span>
506
+ </ui-menu-item>
507
+ </ui-menu>
508
+ </section>
364
509
  `
365
510
  }
366
511
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.5.21",
3
+ "version": "0.5.23",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-button-group': UiButtonGroupElement
14
14
  }
15
15
  }
16
+
17
+ export type { MdGroupType } from './internals/group.js'
@@ -16,3 +16,6 @@ declare global {
16
16
  'ui-button': UiButtonElement
17
17
  }
18
18
  }
19
+
20
+ export type { MdButtonColor } from './internals/button.js'
21
+ export type { ButtonType, MdButtonShape, MdButtonSize } from './internals/base.js'
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-chip': UiChipElement
14
14
  }
15
15
  }
16
+
17
+ export { ChipType } from './internals/Chip.js'
@@ -27,8 +27,6 @@ interface DialogEventMap {
27
27
  close: CustomEvent<UiDialogClosingReason>
28
28
  }
29
29
 
30
- export type RenderFunction = () => TemplateResult
31
-
32
30
  /**
33
31
  * Styled dialog using a native `<dialog>` element under the hood.
34
32
  * Note, since native dialog renders in the top layer it is not necessary
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-dialog': UiDialogElement
14
14
  }
15
15
  }
16
+
17
+ export type { UiDialogClosingReason } from './internals/Dialog.js'
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-divider': UiDividerElement
14
14
  }
15
15
  }
16
+
17
+ export { DividerType } from './internals/Divider.element.js'
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-dropdown-list': Element
14
14
  }
15
15
  }
16
+
17
+ export type { UiDropdownListSelection } from './internals/UiDropdownList.js'
@@ -13,3 +13,6 @@ declare global {
13
13
  'ui-icon-button': UiIconButtonElement
14
14
  }
15
15
  }
16
+
17
+ export type { MdIconButtonColor, MdIconButtonWidth } from './internals/IconButton.js'
18
+ export type { ButtonType, MdButtonShape, MdButtonSize } from '../button/internals/base.js'
@@ -123,6 +123,13 @@ export default css`
123
123
  line-height: var(--md-sys-typescale-body-small-height);
124
124
  }
125
125
 
126
+ .start {
127
+ align-self: stretch;
128
+ display: flex;
129
+ justify-content: center;
130
+ align-items: center;
131
+ }
132
+
126
133
  :host slot[name='end']::slotted(*) {
127
134
  color: var(--md-sys-color-on-surface-variant);
128
135
  fill: var(--md-sys-color-on-surface-variant);
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-list-item': UiListItemElement
14
14
  }
15
15
  }
16
+
17
+ export { ListItemImage, ListItemLines } from './internals/ListItem.js'
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-list': Element
14
14
  }
15
15
  }
16
+
17
+ export type { UiListItemsChange, UiListSelection } from './internals/List.js'
@@ -4,7 +4,7 @@ export default css`
4
4
  :host {
5
5
  display: none;
6
6
  position-area: bottom span-right;
7
- position-try: normal flip-block;
7
+ position-try: --menu-fallback-bottom-left, --menu-fallback-top-right, --menu-fallback-top-left, flip-block;
8
8
  position: absolute;
9
9
  margin: 0;
10
10
  padding: 0;
@@ -15,9 +15,21 @@ export default css`
15
15
  overflow: auto;
16
16
  }
17
17
 
18
+ @position-try --menu-fallback-bottom-left {
19
+ position-area: bottom span-left;
20
+ }
21
+
22
+ @position-try --menu-fallback-top-right {
23
+ position-area: top span-right;
24
+ }
25
+
26
+ @position-try --menu-fallback-top-left {
27
+ position-area: top span-left;
28
+ }
29
+
18
30
  :host(:popover-open) {
19
31
  display: block;
20
- background-color: var(--md-sys-color-surface);
32
+ background-color: var(--md-sys-color-surface-container-low);
21
33
  border-radius: var(--md-sys-shape-corner-extra-small);
22
34
  box-shadow: var(--md-sys-elevation-3);
23
35
  }
@@ -15,6 +15,7 @@ import { bound } from '../../../decorators/bound.js'
15
15
  *
16
16
  * @fires select - Dispatched when a menu item is selected
17
17
  * @fires close - Dispatched when the menu is closed
18
+ * @fires open - Dispatched when the menu is opened
18
19
  */
19
20
  export default class Menu extends UiList {
20
21
  /**
@@ -104,41 +105,29 @@ export default class Menu extends UiList {
104
105
  }
105
106
 
106
107
  positionMenu(): void {
107
- // when there's more space above the anchor, position the menu above it
108
+ // Let CSS anchor positioning handle the positioning automatically
109
+ // Only intervene if we need to set max-height for overflow cases
108
110
  const box = this.getBoundingClientRect()
109
- // Now, we determine, whether to position the menu above or below the anchor
110
- // in a way, that if we have enough space below the anchor, we position it below,
111
- // otherwise we position it above the anchor.
112
-
113
- // our starting point is the anchor being positioned below the anchor
114
111
  const menuBottom = box.top + box.height
115
- if (menuBottom <= innerHeight) {
116
- // if the menu fits below the anchor, we leave it as is.
117
- return
112
+ const menuRight = box.left + box.width
113
+
114
+ // Reset any previous manual positioning to let CSS anchor positioning work
115
+ this.style.removeProperty('position-area')
116
+ this.style.removeProperty('max-height')
117
+
118
+ // Only set max-height if the menu would overflow
119
+ if (menuBottom > innerHeight) {
120
+ const availableHeight = innerHeight - box.top
121
+ this.style.maxHeight = `${Math.max(200, availableHeight - 20)}px`
118
122
  }
119
- // we do not make association from the menu to the anchor, so we make an assumption
120
- // that the anchor is 40px high, which is the default height of a button.
121
- // it can be different, but this is a good starting point.
122
- const anchorHeight = 40
123
- const anchorBottom = box.top
124
- const anchorTop = anchorBottom - anchorHeight
125
- const menuHeight = box.height
126
-
127
- const spaceBelow = innerHeight - anchorBottom
128
- const spaceAbove = anchorTop
129
-
130
- const diffBelow = spaceBelow - menuHeight
131
- const diffAbove = spaceAbove - menuHeight
132
- // The initial check ensures the menu does not fit below. Now, check if it fits above.
133
- if (diffAbove >= 0) {
134
- this.style.setProperty('position-area', 'top span-right')
135
- } else if (diffAbove > diffBelow) {
136
- // It doesn't fit in either direction. Choose the one with less overflow (larger, i.e., less negative, diff).
137
- this.style.setProperty('position-area', 'top span-right')
138
- this.style.maxHeight = `${spaceAbove}px`
139
- } else {
140
- this.style.setProperty('position-area', 'bottom span-right')
141
- this.style.maxHeight = `${spaceBelow}px`
123
+
124
+ if (menuRight > innerWidth) {
125
+ const availableWidth = innerWidth - box.left
126
+ // Let CSS anchor positioning handle horizontal flipping
127
+ // We could set max-width if needed
128
+ if (availableWidth < 200) {
129
+ this.style.maxWidth = `${Math.max(180, availableWidth - 20)}px`
130
+ }
142
131
  }
143
132
  }
144
133
 
@@ -213,10 +202,42 @@ export default class Menu extends UiList {
213
202
  }
214
203
 
215
204
  override notifySelect(item: UiListItem, index?: number): boolean {
205
+ // Handle single selection
206
+ if (item instanceof UiMenuItem) {
207
+ this.clearSelection()
208
+ item.selected = true
209
+ }
210
+
216
211
  this.hide()
217
212
  return super.notifySelect(item, index)
218
213
  }
219
214
 
215
+ /**
216
+ * Clears selection from all menu items
217
+ */
218
+ protected clearSelection(): void {
219
+ this.assignedMenuItems.forEach((menuItem) => {
220
+ menuItem.selected = false
221
+ })
222
+ }
223
+
224
+ /**
225
+ * Gets the currently selected menu item
226
+ */
227
+ get selectedItem(): UiMenuItem | null {
228
+ return this.assignedMenuItems.find((item) => item.selected) || null
229
+ }
230
+
231
+ /**
232
+ * Sets the selected menu item
233
+ */
234
+ setSelectedItem(item: UiMenuItem | null): void {
235
+ this.clearSelection()
236
+ if (item) {
237
+ item.selected = true
238
+ }
239
+ }
240
+
220
241
  /**
221
242
  * Handles sub-menu opening
222
243
  */
@@ -54,6 +54,7 @@ export default css`
54
54
  md-focus-ring {
55
55
  --md-focus-ring-color: var(--md-sys-color-primary);
56
56
  --md-focus-ring-width: 2px;
57
+ z-index: 2;
57
58
  }
58
59
 
59
60
  /* Ripple Effect */
@@ -61,4 +62,26 @@ export default css`
61
62
  --md-ripple-color: var(--md-sys-color-primary);
62
63
  --md-ripple-opacity: 0.12;
63
64
  }
65
+
66
+ /* Selected state */
67
+ :host(.select) .menu-item,
68
+ :host([selected]) .menu-item {
69
+ background-color: var(--md-sys-color-secondary-container);
70
+ color: var(--md-sys-color-on-secondary-container);
71
+ }
72
+
73
+ :host(.select) .menu-item:hover,
74
+ :host([selected]) .menu-item:hover {
75
+ background-color: var(--md-sys-color-secondary-container);
76
+ opacity: 0.92;
77
+ }
78
+
79
+ /* Selection check icon */
80
+ .selection-check {
81
+ color: var(--md-sys-color-on-surface-variant);
82
+ fill: var(--md-sys-color-on-surface-variant);
83
+ width: 24px;
84
+ height: 24px;
85
+ margin-right: 8px;
86
+ }
64
87
  `
@@ -8,6 +8,7 @@ import { nanoid } from 'nanoid'
8
8
 
9
9
  import '@material/web/focus/md-focus-ring.js'
10
10
  import '../../ripple/ui-ripple.js'
11
+ import '../../icons/ui-icon.js'
11
12
 
12
13
  /**
13
14
  * Material Design 3 Menu Item component.
@@ -23,6 +24,25 @@ export default class UiMenuItem extends UiListItem {
23
24
  */
24
25
  @property({ type: String }) accessor submenu: string | undefined
25
26
 
27
+ /**
28
+ * Whether this menu item is selected
29
+ * @attribute
30
+ */
31
+ @property({ type: Boolean, reflect: true }) accessor selected = false
32
+
33
+ /**
34
+ * The value associated with this menu item. Use it to identify value associated with the menu item,
35
+ * when selected.
36
+ * @attribute
37
+ */
38
+ @property({ type: String, reflect: true }) accessor value: string | undefined
39
+
40
+ /**
41
+ * Whether to automatically show a check icon when selected
42
+ * @attribute
43
+ */
44
+ @property({ type: Boolean }) accessor showSelectionIcon = false
45
+
26
46
  /**
27
47
  * Whether the menu item has a sub-menu
28
48
  */
@@ -58,6 +78,9 @@ export default class UiMenuItem extends UiListItem {
58
78
  if (!this.id) {
59
79
  this.id = nanoid(6)
60
80
  }
81
+
82
+ // Initialize selection state
83
+ this.updateSelectionState()
61
84
  }
62
85
 
63
86
  protected override updated(changedProperties: PropertyValues<this>): void {
@@ -67,6 +90,10 @@ export default class UiMenuItem extends UiListItem {
67
90
  this.updateAccessibility()
68
91
  this.setupSubmenuConnection()
69
92
  }
93
+
94
+ if (changedProperties.has('selected')) {
95
+ this.updateSelectionState()
96
+ }
70
97
  }
71
98
 
72
99
  /**
@@ -97,6 +124,19 @@ export default class UiMenuItem extends UiListItem {
97
124
  }
98
125
  }
99
126
 
127
+ /**
128
+ * Updates the selection state styling
129
+ */
130
+ protected updateSelectionState(): void {
131
+ if (this.selected) {
132
+ this.classList.add('select')
133
+ this.setAttribute('aria-selected', 'true')
134
+ } else {
135
+ this.classList.remove('select')
136
+ this.setAttribute('aria-selected', 'false')
137
+ }
138
+ }
139
+
100
140
  /**
101
141
  * Handles mouse enter events
102
142
  */
@@ -214,4 +254,13 @@ export default class UiMenuItem extends UiListItem {
214
254
  ${this.hasSubMenu ? html`<ui-icon class="menu-item-arrow">arrow_right</ui-icon>` : ''}
215
255
  </div>`
216
256
  }
257
+
258
+ protected override renderStart(): TemplateResult {
259
+ const showCheckIcon = this.showSelectionIcon && this.selected
260
+
261
+ return html`<div class="start">
262
+ ${showCheckIcon ? html`<ui-icon class="selection-check">check</ui-icon>` : ''}
263
+ <slot name="start" @slotchange=${this.handleSlotChange}></slot>
264
+ </div>`
265
+ }
217
266
  }
@@ -8,10 +8,6 @@ import listStyles from '../list/internals/ListItem.styles.js'
8
8
  * Material Design 3 Menu Item component.
9
9
  *
10
10
  * @element ui-menu-item
11
- * @attribute {Object} data - The menu item data object
12
- * @attribute {boolean} disabled - Whether the menu item is disabled
13
- * @fires select - Dispatched when the menu item is selected
14
- * @fires submenu-open - Dispatched when a sub-menu is opened
15
11
  */
16
12
  @customElement('ui-menu-item')
17
13
  export class UiMenuItemElement extends Element {
@@ -23,3 +19,5 @@ declare global {
23
19
  'ui-menu-item': UiMenuItemElement
24
20
  }
25
21
  }
22
+
23
+ export { ListItemImage, ListItemLines } from '../list/internals/ListItem.js'
@@ -24,3 +24,5 @@ declare global {
24
24
  'ui-menu': UiMenuElement
25
25
  }
26
26
  }
27
+
28
+ export type { UiListItemsChange, UiListSelection } from '../list/internals/List.js'
@@ -13,3 +13,5 @@ declare global {
13
13
  'ui-tabs': UiTabsElement
14
14
  }
15
15
  }
16
+
17
+ export { TabsPriority, type TabSelectionDetail } from './internals/Tabs.js'