@api-client/ui 0.4.2 → 0.4.3

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 (109) hide show
  1. package/.vscode/settings.json +1 -0
  2. package/build/src/index.d.ts +3 -0
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/index.js +4 -0
  5. package/build/src/index.js.map +1 -1
  6. package/build/src/lib/Dom.d.ts +5 -0
  7. package/build/src/lib/Dom.d.ts.map +1 -0
  8. package/build/src/lib/Dom.js +24 -0
  9. package/build/src/lib/Dom.js.map +1 -0
  10. package/build/src/md/button/internals/base.d.ts +27 -0
  11. package/build/src/md/button/internals/base.d.ts.map +1 -1
  12. package/build/src/md/button/internals/base.js +90 -1
  13. package/build/src/md/button/internals/base.js.map +1 -1
  14. package/build/src/md/icons/internals/Icon.js +2 -2
  15. package/build/src/md/icons/internals/Icon.js.map +1 -1
  16. package/build/src/md/list/internals/List.d.ts +1 -1
  17. package/build/src/md/list/internals/List.d.ts.map +1 -1
  18. package/build/src/md/list/internals/List.js +4 -4
  19. package/build/src/md/list/internals/List.js.map +1 -1
  20. package/build/src/md/list/internals/ListItem.d.ts +1 -0
  21. package/build/src/md/list/internals/ListItem.d.ts.map +1 -1
  22. package/build/src/md/list/internals/ListItem.js +10 -10
  23. package/build/src/md/list/internals/ListItem.js.map +1 -1
  24. package/build/src/md/list/internals/ListItem.styles.d.ts.map +1 -1
  25. package/build/src/md/list/internals/ListItem.styles.js +2 -20
  26. package/build/src/md/list/internals/ListItem.styles.js.map +1 -1
  27. package/build/src/md/menu/index.d.ts +4 -0
  28. package/build/src/md/menu/index.d.ts.map +1 -0
  29. package/build/src/md/menu/index.js +4 -0
  30. package/build/src/md/menu/index.js.map +1 -0
  31. package/build/src/md/menu/internal/Menu.d.ts +76 -0
  32. package/build/src/md/menu/internal/Menu.d.ts.map +1 -0
  33. package/build/src/md/menu/internal/Menu.js +235 -0
  34. package/build/src/md/menu/internal/Menu.js.map +1 -0
  35. package/build/src/md/menu/internal/Menu.styles.d.ts +3 -0
  36. package/build/src/md/menu/internal/Menu.styles.d.ts.map +1 -0
  37. package/build/src/md/menu/internal/Menu.styles.js +185 -0
  38. package/build/src/md/menu/internal/Menu.styles.js.map +1 -0
  39. package/build/src/md/menu/internal/MenuItem.d.ts +77 -0
  40. package/build/src/md/menu/internal/MenuItem.d.ts.map +1 -0
  41. package/build/src/md/menu/internal/MenuItem.js +216 -0
  42. package/build/src/md/menu/internal/MenuItem.js.map +1 -0
  43. package/build/src/md/menu/internal/MenuItem.styles.d.ts +3 -0
  44. package/build/src/md/menu/internal/MenuItem.styles.d.ts.map +1 -0
  45. package/build/src/md/menu/internal/MenuItem.styles.js +64 -0
  46. package/build/src/md/menu/internal/MenuItem.styles.js.map +1 -0
  47. package/build/src/md/menu/internal/SubMenu.d.ts +56 -0
  48. package/build/src/md/menu/internal/SubMenu.d.ts.map +1 -0
  49. package/build/src/md/menu/internal/SubMenu.js +171 -0
  50. package/build/src/md/menu/internal/SubMenu.js.map +1 -0
  51. package/build/src/md/menu/internal/SubMenu.styles.d.ts +3 -0
  52. package/build/src/md/menu/internal/SubMenu.styles.d.ts.map +1 -0
  53. package/build/src/md/menu/internal/SubMenu.styles.js +8 -0
  54. package/build/src/md/menu/internal/SubMenu.styles.js.map +1 -0
  55. package/build/src/md/menu/ui-menu-item.d.ts +20 -0
  56. package/build/src/md/menu/ui-menu-item.d.ts.map +1 -0
  57. package/build/src/md/menu/ui-menu-item.js +37 -0
  58. package/build/src/md/menu/ui-menu-item.js.map +1 -0
  59. package/build/src/md/menu/ui-menu.d.ts +22 -0
  60. package/build/src/md/menu/ui-menu.d.ts.map +1 -0
  61. package/build/src/md/menu/ui-menu.js +38 -0
  62. package/build/src/md/menu/ui-menu.js.map +1 -0
  63. package/build/src/md/menu/ui-sub-menu.d.ts +20 -0
  64. package/build/src/md/menu/ui-sub-menu.d.ts.map +1 -0
  65. package/build/src/md/menu/ui-sub-menu.js +37 -0
  66. package/build/src/md/menu/ui-sub-menu.js.map +1 -0
  67. package/build/src/mixins/FileDropMixin.d.ts.map +1 -1
  68. package/build/src/mixins/FileDropMixin.js +7 -8
  69. package/build/src/mixins/FileDropMixin.js.map +1 -1
  70. package/build/src/mixins/RenderableMixin.d.ts.map +1 -1
  71. package/build/src/mixins/RenderableMixin.js +2 -3
  72. package/build/src/mixins/RenderableMixin.js.map +1 -1
  73. package/demo/md/index.html +2 -0
  74. package/demo/md/menu/index.html +19 -0
  75. package/demo/md/menu/index.ts +154 -0
  76. package/package.json +2 -3
  77. package/src/index.ts +5 -0
  78. package/src/lib/Dom.ts +26 -0
  79. package/src/md/button/internals/base.ts +77 -0
  80. package/src/md/icons/internals/Icon.ts +2 -2
  81. package/src/md/list/internals/List.ts +4 -4
  82. package/src/md/list/internals/ListItem.styles.ts +2 -20
  83. package/src/md/list/internals/ListItem.ts +11 -11
  84. package/src/md/menu/README.md +253 -0
  85. package/src/md/menu/index.ts +3 -0
  86. package/src/md/menu/internal/Menu.styles.ts +185 -0
  87. package/src/md/menu/internal/Menu.ts +205 -0
  88. package/src/md/menu/internal/MenuItem.styles.ts +64 -0
  89. package/src/md/menu/internal/MenuItem.ts +217 -0
  90. package/src/md/menu/internal/SubMenu.styles.ts +8 -0
  91. package/src/md/menu/internal/SubMenu.ts +179 -0
  92. package/src/md/menu/ui-menu-item.ts +25 -0
  93. package/src/md/menu/ui-menu.ts +26 -0
  94. package/src/md/menu/ui-sub-menu.ts +25 -0
  95. package/src/mixins/FileDropMixin.ts +106 -107
  96. package/src/mixins/RenderableMixin.ts +107 -108
  97. package/test/md/menu/Menu.test.ts +509 -0
  98. package/test/md/menu/MenuIntegration.test.ts +426 -0
  99. package/test/md/menu/MenuItem.test.ts +361 -0
  100. package/test/md/menu/SubMenu.test.ts +411 -0
  101. /package/test/{ui → md}/button/UiButton.test.ts +0 -0
  102. /package/test/{ui → md}/button/UiIconButton.test.ts +0 -0
  103. /package/test/{ui → md}/chip/UiChip.test.ts +0 -0
  104. /package/test/{ui → md}/collapse/UiCollapse.test.ts +0 -0
  105. /package/test/{ui → md}/collapse/flex-layout.test.ts +0 -0
  106. /package/test/{ui → md}/date-time/DateTime.test.ts +0 -0
  107. /package/test/{ui → md}/dialog/UiDialog.test.ts +0 -0
  108. /package/test/{ui → md}/progress/UiProgressElement.test.ts +0 -0
  109. /package/test/{ui → md}/progress/UiRangeElement.test.ts +0 -0
@@ -0,0 +1,217 @@
1
+ import { html, PropertyValues, TemplateResult } from 'lit'
2
+ import { property, state } from 'lit/decorators.js'
3
+ import { classMap } from 'lit/directives/class-map.js'
4
+ import UiListItem from '../../list/internals/ListItem.js'
5
+ import UiSubMenu from './SubMenu.js'
6
+ import { findElementInShadowRoots } from '../../../lib/Dom.js'
7
+ import { nanoid } from 'nanoid'
8
+
9
+ import '@material/web/focus/md-focus-ring.js'
10
+ import '../../ripple/ui-ripple.js'
11
+
12
+ /**
13
+ * Material Design 3 Menu Item component.
14
+ *
15
+ * @slot - The menu item content (label, icon, etc.)
16
+ * @fires select - Dispatched when the menu item is selected
17
+ * @fires submenu-open - Dispatched when a sub-menu is opened
18
+ */
19
+ export default class UiMenuItem extends UiListItem {
20
+ /**
21
+ * The ID of the associated submenu
22
+ * @attribute
23
+ */
24
+ @property({ type: String }) accessor submenu: string | undefined
25
+
26
+ /**
27
+ * Whether the menu item has a sub-menu
28
+ */
29
+ get hasSubMenu(): boolean {
30
+ return !!this.submenu && !!this.subMenuElement
31
+ }
32
+
33
+ /**
34
+ * Reference to the sub-menu element
35
+ */
36
+ get subMenuElement(): UiSubMenu | null {
37
+ if (!this.submenu) return null
38
+ return findElementInShadowRoots(this.submenu, this) as UiSubMenu | null
39
+ }
40
+
41
+ /**
42
+ * Whether the sub-menu is open
43
+ */
44
+ @state() protected accessor subMenuOpen = false
45
+
46
+ constructor() {
47
+ super()
48
+ this.addEventListener('mouseenter', this.handleMouseEnter.bind(this))
49
+ this.addEventListener('mouseleave', this.handleMouseLeave.bind(this))
50
+ this.addEventListener('click', this.handleMenuItemClick.bind(this))
51
+ }
52
+
53
+ override connectedCallback(): void {
54
+ super.connectedCallback()
55
+ this.setAttribute('role', 'menuitem')
56
+
57
+ // Generate ID if not present (needed for submenu anchoring)
58
+ if (!this.id) {
59
+ this.id = nanoid(6)
60
+ }
61
+ }
62
+
63
+ protected override updated(changedProperties: PropertyValues<this>): void {
64
+ super.updated(changedProperties)
65
+
66
+ if (changedProperties.has('submenu')) {
67
+ this.updateAccessibility()
68
+ this.setupSubmenuConnection()
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Sets up the connection between this menu item and its submenu
74
+ */
75
+ protected setupSubmenuConnection(): void {
76
+ if (this.subMenuElement) {
77
+ this.subMenuElement.anchor = this.id
78
+
79
+ // Find parent menu and set it on the submenu
80
+ const parentMenu = this.closest('ui-menu, ui-sub-menu') as UiSubMenu
81
+ if (parentMenu) {
82
+ this.subMenuElement.setParentMenu(parentMenu)
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Updates accessibility attributes
89
+ */
90
+ protected updateAccessibility(): void {
91
+ if (this.hasSubMenu) {
92
+ this.setAttribute('aria-haspopup', 'true')
93
+ this.setAttribute('aria-expanded', String(this.subMenuOpen))
94
+ } else {
95
+ this.removeAttribute('aria-haspopup')
96
+ this.removeAttribute('aria-expanded')
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Handles mouse enter events
102
+ */
103
+ protected handleMouseEnter(): void {
104
+ if (this.hasSubMenu && !this.subMenuOpen) {
105
+ this.openSubMenu()
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Handles mouse leave events
111
+ */
112
+ protected handleMouseLeave(): void {
113
+ // Close sub-menu after a delay to allow moving to sub-menu
114
+ setTimeout(() => {
115
+ if (this.subMenuOpen && !this.matches(':hover') && !this.subMenuElement?.matches(':hover')) {
116
+ this.closeSubMenu()
117
+ }
118
+ }, 100)
119
+ }
120
+
121
+ /**
122
+ * Handles click events
123
+ */
124
+ public override handleClick(e: MouseEvent): void {
125
+ if (this.hasSubMenu) {
126
+ e.preventDefault()
127
+ e.stopPropagation()
128
+ this.toggleSubMenu()
129
+ } else {
130
+ super.handleClick(e)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Handles menu item click events
136
+ */
137
+ protected handleMenuItemClick(e: MouseEvent): void {
138
+ this.handleClick(e)
139
+ }
140
+
141
+ /**
142
+ * Opens the sub-menu
143
+ */
144
+ openSubMenu(): void {
145
+ if (!this.hasSubMenu || this.subMenuOpen) return
146
+
147
+ this.subMenuOpen = true
148
+ this.updateAccessibility()
149
+ this.subMenuElement?.show()
150
+
151
+ this.dispatchEvent(
152
+ new CustomEvent('submenu-open', {
153
+ detail: { subMenu: this.subMenuElement },
154
+ bubbles: true,
155
+ composed: true,
156
+ })
157
+ )
158
+ }
159
+
160
+ /**
161
+ * Closes the sub-menu
162
+ */
163
+ closeSubMenu(): void {
164
+ if (!this.subMenuOpen) return
165
+
166
+ this.subMenuOpen = false
167
+ this.updateAccessibility()
168
+ this.subMenuElement?.hide()
169
+ }
170
+
171
+ /**
172
+ * Toggles the sub-menu
173
+ */
174
+ toggleSubMenu(): void {
175
+ if (this.subMenuOpen) {
176
+ this.closeSubMenu()
177
+ } else {
178
+ this.openSubMenu()
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Handles sub-menu item selection
184
+ */
185
+ protected handleSubMenuSelect(e: CustomEvent): void {
186
+ // Bubble up the selection event
187
+ this.dispatchEvent(
188
+ new CustomEvent('select', {
189
+ detail: e.detail,
190
+ bubbles: true,
191
+ composed: true,
192
+ })
193
+ )
194
+ }
195
+
196
+ override render(): TemplateResult {
197
+ const classes = classMap({
198
+ 'surface': true,
199
+ 'menu-item': true,
200
+ 'menu-item-with-submenu': this.hasSubMenu,
201
+ 'submenu-open': this.subMenuOpen,
202
+ })
203
+
204
+ return html`
205
+ ${this.renderFocusRing()} ${this.renderRipple()}
206
+ <div class=${classes} role="menuitem">${this.renderStart()} ${this.renderBody()} ${this.renderEnd()}</div>
207
+ `
208
+ }
209
+
210
+ protected override renderEnd(): TemplateResult {
211
+ return html`<div class="end">
212
+ <slot name="end" @slotchange=${this.handleSlotChange}></slot>
213
+ <span class="trailing-supporting-text"><slot name="end-text"></slot></span>
214
+ ${this.hasSubMenu ? html`<ui-icon class="menu-item-arrow">arrow_right</ui-icon>` : ''}
215
+ </div>`
216
+ }
217
+ }
@@ -0,0 +1,8 @@
1
+ import { css } from 'lit'
2
+
3
+ export default css`
4
+ :host {
5
+ position-area: span-bottom right;
6
+ position-try-fallbacks: flip-block flip-inline;
7
+ }
8
+ `
@@ -0,0 +1,179 @@
1
+ import { html, PropertyValues, TemplateResult } from 'lit'
2
+ import { property } from 'lit/decorators.js'
3
+ import { classMap } from 'lit/directives/class-map.js'
4
+ import Menu from './Menu.js'
5
+ import UiListItem from '../../list/internals/ListItem.js'
6
+ import { findElementInShadowRoots } from '../../../lib/Dom.js'
7
+ import type { MenuItem } from '../../../index.js'
8
+
9
+ /**
10
+ * Material Design 3 Sub-Menu component.
11
+ * Extends the main Menu component to provide sub-menu functionality.
12
+ * Uses Popover API and Anchor Positioning API for modern positioning.
13
+ *
14
+ * @slot - The sub-menu items
15
+ * @fires select - Dispatched when a sub-menu item is selected
16
+ * @fires close - Dispatched when the sub-menu is closed
17
+ */
18
+ export default class UiSubMenu extends Menu {
19
+ /**
20
+ * The ID of the anchor element (parent menu item)
21
+ * @attribute
22
+ */
23
+ @property({ type: String }) accessor anchor: string | undefined
24
+
25
+ /**
26
+ * Reference to the parent menu
27
+ */
28
+ parentMenu: Menu | null = null
29
+
30
+ /**
31
+ * Reference to the anchor element
32
+ */
33
+ get anchorElement(): MenuItem | null {
34
+ if (!this.anchor) return null
35
+ return findElementInShadowRoots(this.anchor, this) as MenuItem | null
36
+ }
37
+
38
+ override connectedCallback(): void {
39
+ super.connectedCallback()
40
+ this.setAttribute('role', 'menu')
41
+ this.setAttribute('aria-label', 'Submenu')
42
+ }
43
+
44
+ protected override updated(changedProperties: PropertyValues<this>): void {
45
+ super.updated(changedProperties)
46
+
47
+ if (changedProperties.has('anchor')) {
48
+ this.updateAnchorPositioning()
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Updates anchor positioning using CSS Anchor Positioning API
54
+ */
55
+ protected updateAnchorPositioning(): void {
56
+ const anchor = this.anchorElement
57
+ if (!anchor) return
58
+ const anchorName = `--anchor-${this.id}`
59
+
60
+ // Set anchor name on the parent menu item
61
+ anchor.style.setProperty('anchor-name', anchorName)
62
+
63
+ // Set anchor positioning on the submenu
64
+ this.style.setProperty('position-anchor', anchorName)
65
+ }
66
+
67
+ /**
68
+ * Shows the submenu
69
+ */
70
+ override show(): void {
71
+ if (!this.anchorElement) {
72
+ return
73
+ }
74
+
75
+ // Update positioning before showing
76
+ this.updateAnchorPositioning()
77
+
78
+ // Close any other open submenus in the parent menu
79
+ if (this.parentMenu) {
80
+ this.parentMenu.closeSubMenu()
81
+ this.parentMenu.setActiveSubMenu(this)
82
+ }
83
+
84
+ // Show the popover
85
+ this.showPopover()
86
+ this.open = true
87
+ this.focus()
88
+
89
+ this.dispatchEvent(
90
+ new CustomEvent('open', {
91
+ bubbles: true,
92
+ composed: true,
93
+ detail: { submenu: this },
94
+ })
95
+ )
96
+ }
97
+
98
+ /**
99
+ * Hides the submenu
100
+ */
101
+ override hide(): void {
102
+ super.hide()
103
+
104
+ // Clear parent menu's active submenu reference
105
+ const parentMenu = this.parentMenu
106
+ if (parentMenu) {
107
+ parentMenu.setActiveSubMenu(null)
108
+ }
109
+ const anchor = this.anchorElement
110
+ if (anchor) {
111
+ anchor.closeSubMenu()
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Sets the parent menu reference
117
+ */
118
+ setParentMenu(menu: Menu): void {
119
+ this.parentMenu = menu
120
+ }
121
+
122
+ /**
123
+ * Handles selection events - bubbles them up to parent menu
124
+ */
125
+ override notifySelect(item: UiListItem): boolean {
126
+ // First hide this submenu
127
+ this.hide()
128
+
129
+ // If we have a parent menu, hide it too and bubble the selection
130
+ if (this.parentMenu) {
131
+ this.parentMenu.hide()
132
+ }
133
+
134
+ // Call parent implementation to dispatch the select event
135
+ return super.notifySelect(item)
136
+ }
137
+
138
+ /**
139
+ * Handles keyboard navigation specific to submenus
140
+ */
141
+ override handleKeydown(e: KeyboardEvent): void {
142
+ if (!this.open || e.defaultPrevented) return
143
+
144
+ switch (e.key) {
145
+ case 'Escape':
146
+ e.preventDefault()
147
+ this.hide()
148
+ // Return focus to parent menu item
149
+ if (this.anchorElement) {
150
+ this.anchorElement.focus()
151
+ }
152
+ break
153
+ case 'ArrowLeft':
154
+ e.preventDefault()
155
+ this.hide()
156
+ // Return focus to parent menu item
157
+ if (this.anchorElement) {
158
+ this.anchorElement.focus()
159
+ }
160
+ break
161
+ default:
162
+ // Let the parent handle other keys
163
+ super.handleKeydown(e)
164
+ }
165
+ }
166
+
167
+ override render(): TemplateResult {
168
+ const classes = classMap({
169
+ 'submenu-container': true,
170
+ 'submenu-open': this.open,
171
+ })
172
+
173
+ return html`
174
+ <div class=${classes}>
175
+ <slot></slot>
176
+ </div>
177
+ `
178
+ }
179
+ }
@@ -0,0 +1,25 @@
1
+ import type { CSSResultOrNative } from 'lit'
2
+ import { customElement } from 'lit/decorators.js'
3
+ import Element from './internal/MenuItem.js'
4
+ import styles from './internal/MenuItem.styles.js'
5
+ import listStyles from '../list/internals/ListItem.styles.js'
6
+
7
+ /**
8
+ * Material Design 3 Menu Item component.
9
+ *
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
+ */
16
+ @customElement('ui-menu-item')
17
+ export class UiMenuItemElement extends Element {
18
+ static override styles: CSSResultOrNative[] = [styles, listStyles]
19
+ }
20
+
21
+ declare global {
22
+ interface HTMLElementTagNameMap {
23
+ 'ui-menu-item': UiMenuItemElement
24
+ }
25
+ }
@@ -0,0 +1,26 @@
1
+ import type { CSSResultOrNative } from 'lit'
2
+ import { customElement } from 'lit/decorators.js'
3
+ import Element from './internal/Menu.js'
4
+ import styles from './internal/Menu.styles.js'
5
+
6
+ /**
7
+ * Material Design 3 Menu component with sub-menu support.
8
+ *
9
+ * @element ui-menu
10
+ * @attribute {boolean} open - Whether the menu is currently open
11
+ * @attribute {boolean} anchored - Whether the menu should be positioned relative to an anchor element
12
+ * @attribute {boolean} disabled - Whether the menu is disabled
13
+ * @fires select - Dispatched when a menu item is selected
14
+ * @fires close - Dispatched when the menu is closed
15
+ * @fires open - Dispatched when the menu is opened
16
+ */
17
+ @customElement('ui-menu')
18
+ export class UiMenuElement extends Element {
19
+ static override styles: CSSResultOrNative[] = [styles]
20
+ }
21
+
22
+ declare global {
23
+ interface HTMLElementTagNameMap {
24
+ 'ui-menu': UiMenuElement
25
+ }
26
+ }
@@ -0,0 +1,25 @@
1
+ import type { CSSResultOrNative } from 'lit'
2
+ import { customElement } from 'lit/decorators.js'
3
+ import Element from './internal/SubMenu.js'
4
+ import menuStyles from './internal/SubMenu.styles.js'
5
+ import styles from './internal/Menu.styles.js'
6
+
7
+ /**
8
+ * Material Design 3 Sub-Menu component.
9
+ *
10
+ * @element ui-sub-menu
11
+ * @attribute {Object} parentItem - The parent menu item element
12
+ * @attribute {Array} menuItems - The sub-menu items data
13
+ * @fires select - Dispatched when a sub-menu item is selected
14
+ * @fires close - Dispatched when the sub-menu is closed
15
+ */
16
+ @customElement('ui-sub-menu')
17
+ export class UiSubMenuElement extends Element {
18
+ static override styles: CSSResultOrNative[] = [styles, menuStyles]
19
+ }
20
+
21
+ declare global {
22
+ interface HTMLElementTagNameMap {
23
+ 'ui-sub-menu': UiSubMenuElement
24
+ }
25
+ }