@api-client/ui 0.4.1 → 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,361 @@
1
+ import { assert, fixture, html, nextFrame, oneEvent } from '@open-wc/testing'
2
+ import sinon from 'sinon'
3
+ import UiMenuItem from '../../../src/md/menu/internal/MenuItem.js'
4
+ import UiSubMenu from '../../../src/md/menu/internal/SubMenu.js'
5
+
6
+ import '../../../src/md/menu/ui-menu.js'
7
+ import '../../../src/md/menu/ui-menu-item.js'
8
+ import '../../../src/md/menu/ui-sub-menu.js'
9
+ import '../../../src/md/icons/ui-icon.js'
10
+
11
+ describe('md', () => {
12
+ describe('MenuItem', () => {
13
+ async function basicFixture(): Promise<UiMenuItem> {
14
+ return fixture(html`<ui-menu-item>Test Item</ui-menu-item>`)
15
+ }
16
+
17
+ async function withSubmenuFixture(): Promise<HTMLElement> {
18
+ return fixture(html`
19
+ <div>
20
+ <ui-menu-item id="parent-item" submenu="test-submenu">
21
+ <span slot="start"><ui-icon>folder</ui-icon></span>
22
+ <span>Parent Item</span>
23
+ </ui-menu-item>
24
+ <ui-sub-menu id="test-submenu" anchor="parent-item">
25
+ <ui-menu-item>Child Item 1</ui-menu-item>
26
+ <ui-menu-item>Child Item 2</ui-menu-item>
27
+ </ui-sub-menu>
28
+ </div>
29
+ `)
30
+ }
31
+
32
+ async function disabledFixture(): Promise<UiMenuItem> {
33
+ return fixture(html`<ui-menu-item disabled>Disabled Item</ui-menu-item>`)
34
+ }
35
+
36
+ async function withSlotsFixture(): Promise<UiMenuItem> {
37
+ return fixture(html`
38
+ <ui-menu-item>
39
+ <span slot="start"><ui-icon>add</ui-icon></span>
40
+ <span>Main Content</span>
41
+ <span slot="end"><ui-icon>arrow_right</ui-icon></span>
42
+ <span slot="end-text">Ctrl+N</span>
43
+ </ui-menu-item>
44
+ `)
45
+ }
46
+
47
+ describe('Basic functionality', () => {
48
+ it('should create menu item element', async () => {
49
+ const element = await basicFixture()
50
+ assert.instanceOf(element, UiMenuItem)
51
+ assert.equal(element.tagName.toLowerCase(), 'ui-menu-item')
52
+ })
53
+
54
+ it('should set correct ARIA attributes', async () => {
55
+ const element = await basicFixture()
56
+ assert.equal(element.getAttribute('role'), 'menuitem')
57
+ })
58
+
59
+ it('should generate ID if not present', async () => {
60
+ const element = await basicFixture()
61
+ assert.isString(element.id)
62
+ assert.isTrue(element.id.length > 0)
63
+ })
64
+
65
+ it('should preserve existing ID', async () => {
66
+ const element: UiMenuItem = await fixture(html`<ui-menu-item id="custom-id">Test</ui-menu-item>`)
67
+ assert.equal(element.id, 'custom-id')
68
+ })
69
+ })
70
+
71
+ describe('Submenu functionality', () => {
72
+ it('should detect submenu presence', async () => {
73
+ const container = await withSubmenuFixture()
74
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
75
+ await nextFrame()
76
+
77
+ assert.isTrue(menuItem.hasSubMenu)
78
+ assert.equal(menuItem.submenu, 'test-submenu')
79
+ })
80
+
81
+ it('should get submenu element reference', async () => {
82
+ const container = await withSubmenuFixture()
83
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
84
+ const submenu = container.querySelector('#test-submenu') as UiSubMenu
85
+ await nextFrame()
86
+
87
+ assert.equal(menuItem.subMenuElement, submenu)
88
+ })
89
+
90
+ it('should open submenu', async () => {
91
+ const container = await withSubmenuFixture()
92
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
93
+ const submenu = container.querySelector('#test-submenu') as UiSubMenu
94
+ const showSpy = sinon.spy(submenu, 'show')
95
+ await nextFrame()
96
+
97
+ menuItem.openSubMenu()
98
+ await nextFrame()
99
+
100
+ assert.isTrue(showSpy.calledOnce)
101
+ assert.equal(menuItem.getAttribute('aria-expanded'), 'true')
102
+ })
103
+
104
+ it('should close submenu', async () => {
105
+ const container = await withSubmenuFixture()
106
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
107
+ const submenu = container.querySelector('#test-submenu') as UiSubMenu
108
+ const hideSpy = sinon.spy(submenu, 'hide')
109
+ await nextFrame()
110
+
111
+ menuItem.openSubMenu()
112
+ await nextFrame()
113
+ menuItem.closeSubMenu()
114
+ await nextFrame()
115
+
116
+ assert.isTrue(hideSpy.calledOnce)
117
+ assert.equal(menuItem.getAttribute('aria-expanded'), 'false')
118
+ })
119
+
120
+ it('should toggle submenu', async () => {
121
+ const container = await withSubmenuFixture()
122
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
123
+ const submenu = container.querySelector('#test-submenu') as UiSubMenu
124
+ const showSpy = sinon.spy(submenu, 'show')
125
+ const hideSpy = sinon.spy(submenu, 'hide')
126
+ await nextFrame()
127
+
128
+ menuItem.toggleSubMenu()
129
+ await nextFrame()
130
+ assert.isTrue(showSpy.calledOnce)
131
+
132
+ menuItem.toggleSubMenu()
133
+ await nextFrame()
134
+ assert.isTrue(hideSpy.calledOnce)
135
+ })
136
+
137
+ it('should dispatch submenu-open event', async () => {
138
+ const container = await withSubmenuFixture()
139
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
140
+ await nextFrame()
141
+
142
+ setTimeout(() => menuItem.openSubMenu())
143
+ const event = (await oneEvent(menuItem, 'submenu-open')) as CustomEvent<{ subMenu: UiSubMenu }>
144
+
145
+ assert.instanceOf(event, CustomEvent)
146
+ assert.isTrue(event.bubbles)
147
+ assert.isTrue(event.composed)
148
+ assert.isNotNull(event.detail.subMenu)
149
+ })
150
+ })
151
+
152
+ describe('Click handling', () => {
153
+ it('should handle click on regular menu item', async () => {
154
+ const element = await basicFixture()
155
+ const clickSpy = sinon.spy()
156
+ element.addEventListener('click', clickSpy)
157
+
158
+ element.click()
159
+ await nextFrame()
160
+
161
+ assert.isTrue(clickSpy.calledOnce)
162
+ })
163
+
164
+ it('should prevent default on submenu item click', async () => {
165
+ const container = await withSubmenuFixture()
166
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
167
+ await nextFrame()
168
+
169
+ const event = new MouseEvent('click', { bubbles: true, cancelable: true })
170
+ let defaultPrevented = false
171
+ let propagationStopped = false
172
+
173
+ // Override preventDefault and stopPropagation to check if they were called
174
+ const originalPreventDefault = event.preventDefault
175
+ const originalStopPropagation = event.stopPropagation
176
+
177
+ event.preventDefault = () => {
178
+ defaultPrevented = true
179
+ originalPreventDefault.call(event)
180
+ }
181
+
182
+ event.stopPropagation = () => {
183
+ propagationStopped = true
184
+ originalStopPropagation.call(event)
185
+ }
186
+
187
+ menuItem.dispatchEvent(event)
188
+
189
+ assert.isTrue(defaultPrevented)
190
+ assert.isTrue(propagationStopped)
191
+ })
192
+ })
193
+
194
+ describe('Mouse interaction', () => {
195
+ it('should open submenu on mouse enter', async () => {
196
+ const container = await withSubmenuFixture()
197
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
198
+ const openSpy = sinon.spy(menuItem, 'openSubMenu')
199
+ await nextFrame()
200
+
201
+ const event = new MouseEvent('mouseenter')
202
+ menuItem.dispatchEvent(event)
203
+
204
+ assert.isTrue(openSpy.calledOnce)
205
+ })
206
+
207
+ it('should not open submenu on mouse enter if no submenu', async () => {
208
+ const element = await basicFixture()
209
+ const openSpy = sinon.spy(element, 'openSubMenu')
210
+
211
+ const event = new MouseEvent('mouseenter')
212
+ element.dispatchEvent(event)
213
+
214
+ assert.isFalse(openSpy.called)
215
+ })
216
+ })
217
+
218
+ describe('Accessibility', () => {
219
+ it('should set aria-haspopup for items with submenu', async () => {
220
+ const container = await withSubmenuFixture()
221
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
222
+ await nextFrame()
223
+
224
+ assert.equal(menuItem.getAttribute('aria-haspopup'), 'true')
225
+ assert.equal(menuItem.getAttribute('aria-expanded'), 'false')
226
+ })
227
+
228
+ it('should not set aria-haspopup for items without submenu', async () => {
229
+ const element = await basicFixture()
230
+ await nextFrame()
231
+
232
+ assert.isNull(element.getAttribute('aria-haspopup'))
233
+ assert.isNull(element.getAttribute('aria-expanded'))
234
+ })
235
+
236
+ it('should update aria-expanded when submenu opens/closes', async () => {
237
+ const container = await withSubmenuFixture()
238
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
239
+ await nextFrame()
240
+
241
+ assert.equal(menuItem.getAttribute('aria-expanded'), 'false')
242
+
243
+ menuItem.openSubMenu()
244
+ await nextFrame()
245
+ assert.equal(menuItem.getAttribute('aria-expanded'), 'true')
246
+
247
+ menuItem.closeSubMenu()
248
+ await nextFrame()
249
+ assert.equal(menuItem.getAttribute('aria-expanded'), 'false')
250
+ })
251
+ })
252
+
253
+ describe('Disabled state', () => {
254
+ it('should handle disabled attribute', async () => {
255
+ const element = await disabledFixture()
256
+ await nextFrame()
257
+
258
+ assert.isTrue(element.disabled)
259
+ assert.isTrue(element.hasAttribute('disabled'))
260
+ })
261
+
262
+ it('should reflect disabled property to attribute', async () => {
263
+ const element = await basicFixture()
264
+
265
+ element.disabled = true
266
+ await nextFrame()
267
+ assert.equal(element.getAttribute('disabled'), '')
268
+
269
+ element.disabled = false
270
+ await nextFrame()
271
+ assert.isFalse(element.hasAttribute('disabled'))
272
+ })
273
+ })
274
+
275
+ describe('Slots', () => {
276
+ it('should render content in slots', async () => {
277
+ const element = await withSlotsFixture()
278
+ await nextFrame()
279
+
280
+ const startSlot = element.shadowRoot!.querySelector('slot[name="start"]')
281
+ const endSlot = element.shadowRoot!.querySelector('slot[name="end"]')
282
+ const endTextSlot = element.shadowRoot!.querySelector('slot[name="end-text"]')
283
+
284
+ assert.isNotNull(startSlot)
285
+ assert.isNotNull(endSlot)
286
+ assert.isNotNull(endTextSlot)
287
+ })
288
+
289
+ it('should show arrow icon for items with submenu', async () => {
290
+ const container = await withSubmenuFixture()
291
+ const menuItem = container.querySelector('#parent-item') as UiMenuItem
292
+ await nextFrame()
293
+
294
+ const arrow = menuItem.shadowRoot!.querySelector('.menu-item-arrow')
295
+ assert.isNotNull(arrow)
296
+ assert.equal(arrow!.textContent, 'arrow_right')
297
+ })
298
+
299
+ it('should not show arrow icon for items without submenu', async () => {
300
+ const element = await basicFixture()
301
+ await nextFrame()
302
+
303
+ const arrow = element.shadowRoot!.querySelector('.menu-item-arrow')
304
+ assert.isNull(arrow)
305
+ })
306
+ })
307
+
308
+ describe('Edge cases', () => {
309
+ it('should handle missing submenu element gracefully', async () => {
310
+ const element: UiMenuItem = await fixture(html`<ui-menu-item submenu="nonexistent">Test</ui-menu-item>`)
311
+ await nextFrame()
312
+
313
+ assert.isFalse(element.hasSubMenu)
314
+ assert.isNull(element.subMenuElement)
315
+
316
+ // Should not throw
317
+ element.openSubMenu()
318
+ element.closeSubMenu()
319
+ element.toggleSubMenu()
320
+ })
321
+
322
+ it('should handle multiple submenu property changes', async () => {
323
+ const element = await basicFixture()
324
+
325
+ element.submenu = 'test1'
326
+ await nextFrame()
327
+
328
+ element.submenu = 'test2'
329
+ await nextFrame()
330
+
331
+ element.submenu = undefined
332
+ await nextFrame()
333
+
334
+ // Should not throw or cause issues
335
+ assert.isUndefined(element.submenu)
336
+ })
337
+ })
338
+
339
+ describe('Rendering', () => {
340
+ it('should render with correct CSS classes', async () => {
341
+ const element = await basicFixture()
342
+ await nextFrame()
343
+
344
+ const surface = element.shadowRoot!.querySelector('.surface')
345
+ assert.isNotNull(surface)
346
+ assert.isTrue(surface!.classList.contains('menu-item'))
347
+ })
348
+
349
+ it('should render focus ring and ripple', async () => {
350
+ const element = await basicFixture()
351
+ await nextFrame()
352
+
353
+ const focusRing = element.shadowRoot!.querySelector('md-focus-ring')
354
+ const ripple = element.shadowRoot!.querySelector('ui-ripple')
355
+
356
+ assert.isNotNull(focusRing)
357
+ assert.isNotNull(ripple)
358
+ })
359
+ })
360
+ })
361
+ })