mtrl 0.1.2 → 0.2.0

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 (220) hide show
  1. package/README.md +70 -22
  2. package/index.ts +33 -0
  3. package/package.json +14 -5
  4. package/src/components/button/{styles.scss → _styles.scss} +2 -2
  5. package/src/components/button/api.ts +89 -0
  6. package/src/components/button/button.ts +50 -0
  7. package/src/components/button/config.ts +75 -0
  8. package/src/components/button/constants.ts +17 -0
  9. package/src/components/button/index.ts +4 -0
  10. package/src/components/button/types.ts +118 -0
  11. package/src/components/card/_styles.scss +359 -0
  12. package/src/components/card/actions.ts +48 -0
  13. package/src/components/card/api.ts +102 -0
  14. package/src/components/card/card.ts +41 -0
  15. package/src/components/card/config.ts +99 -0
  16. package/src/components/card/constants.ts +69 -0
  17. package/src/components/card/content.ts +48 -0
  18. package/src/components/card/features.ts +228 -0
  19. package/src/components/card/header.ts +88 -0
  20. package/src/components/card/index.ts +19 -0
  21. package/src/components/card/media.ts +52 -0
  22. package/src/components/card/types.ts +174 -0
  23. package/src/components/checkbox/api.ts +82 -0
  24. package/src/components/checkbox/checkbox.ts +75 -0
  25. package/src/components/checkbox/config.ts +90 -0
  26. package/src/components/checkbox/index.ts +4 -0
  27. package/src/components/checkbox/types.ts +146 -0
  28. package/src/components/chip/_styles.scss +372 -0
  29. package/src/components/chip/api.ts +115 -0
  30. package/src/components/chip/chip-set.ts +225 -0
  31. package/src/components/chip/chip.ts +82 -0
  32. package/src/components/chip/config.ts +92 -0
  33. package/src/components/chip/constants.ts +38 -0
  34. package/src/components/chip/index.ts +4 -0
  35. package/src/components/chip/types.ts +172 -0
  36. package/src/components/list/api.ts +72 -0
  37. package/src/components/list/config.ts +43 -0
  38. package/src/components/list/{constants.js → constants.ts} +34 -7
  39. package/src/components/list/features.ts +224 -0
  40. package/src/components/list/index.ts +14 -0
  41. package/src/components/list/list-item.ts +120 -0
  42. package/src/components/list/list.ts +37 -0
  43. package/src/components/list/types.ts +179 -0
  44. package/src/components/list/utils.ts +47 -0
  45. package/src/components/menu/api.ts +119 -0
  46. package/src/components/menu/config.ts +54 -0
  47. package/src/components/menu/constants.ts +154 -0
  48. package/src/components/menu/features/items-manager.ts +457 -0
  49. package/src/components/menu/features/keyboard-navigation.ts +133 -0
  50. package/src/components/menu/features/positioning.ts +127 -0
  51. package/src/components/menu/features/{visibility.js → visibility.ts} +66 -64
  52. package/src/components/menu/index.ts +14 -0
  53. package/src/components/menu/menu-item.ts +43 -0
  54. package/src/components/menu/menu.ts +53 -0
  55. package/src/components/menu/types.ts +178 -0
  56. package/src/components/navigation/api.ts +79 -0
  57. package/src/components/navigation/config.ts +61 -0
  58. package/src/components/navigation/{constants.js → constants.ts} +10 -10
  59. package/src/components/navigation/index.ts +14 -0
  60. package/src/components/navigation/nav-item.ts +148 -0
  61. package/src/components/navigation/navigation.ts +50 -0
  62. package/src/components/navigation/types.ts +212 -0
  63. package/src/components/progress/_styles.scss +204 -0
  64. package/src/components/progress/api.ts +179 -0
  65. package/src/components/progress/config.ts +124 -0
  66. package/src/components/progress/constants.ts +43 -0
  67. package/src/components/progress/index.ts +5 -0
  68. package/src/components/progress/progress.ts +163 -0
  69. package/src/components/progress/types.ts +102 -0
  70. package/src/components/snackbar/api.ts +162 -0
  71. package/src/components/snackbar/config.ts +62 -0
  72. package/src/components/snackbar/{constants.js → constants.ts} +21 -4
  73. package/src/components/snackbar/features.ts +76 -0
  74. package/src/components/snackbar/index.ts +4 -0
  75. package/src/components/snackbar/position.ts +71 -0
  76. package/src/components/snackbar/queue.ts +76 -0
  77. package/src/components/snackbar/snackbar.ts +60 -0
  78. package/src/components/snackbar/types.ts +58 -0
  79. package/src/components/switch/api.ts +77 -0
  80. package/src/components/switch/config.ts +74 -0
  81. package/src/components/switch/index.ts +4 -0
  82. package/src/components/switch/switch.ts +52 -0
  83. package/src/components/switch/types.ts +142 -0
  84. package/src/components/textfield/api.ts +72 -0
  85. package/src/components/textfield/config.ts +54 -0
  86. package/src/components/textfield/{constants.js → constants.ts} +38 -5
  87. package/src/components/textfield/index.ts +4 -0
  88. package/src/components/textfield/textfield.ts +50 -0
  89. package/src/components/textfield/types.ts +139 -0
  90. package/src/core/compose/base.ts +43 -0
  91. package/src/core/compose/component.ts +247 -0
  92. package/src/core/compose/features/checkable.ts +155 -0
  93. package/src/core/compose/features/disabled.ts +116 -0
  94. package/src/core/compose/features/events.ts +65 -0
  95. package/src/core/compose/features/icon.ts +67 -0
  96. package/src/core/compose/features/index.ts +35 -0
  97. package/src/core/compose/features/input.ts +174 -0
  98. package/src/core/compose/features/lifecycle.ts +139 -0
  99. package/src/core/compose/features/position.ts +94 -0
  100. package/src/core/compose/features/ripple.ts +55 -0
  101. package/src/core/compose/features/size.ts +29 -0
  102. package/src/core/compose/features/style.ts +31 -0
  103. package/src/core/compose/features/text.ts +44 -0
  104. package/src/core/compose/features/textinput.ts +225 -0
  105. package/src/core/compose/features/textlabel.ts +92 -0
  106. package/src/core/compose/features/track.ts +84 -0
  107. package/src/core/compose/features/variant.ts +29 -0
  108. package/src/core/compose/features/withEvents.ts +137 -0
  109. package/src/core/compose/index.ts +54 -0
  110. package/src/core/compose/{pipe.js → pipe.ts} +16 -11
  111. package/src/core/config/component-config.ts +136 -0
  112. package/src/core/config.ts +211 -0
  113. package/src/core/dom/{attributes.js → attributes.ts} +11 -11
  114. package/src/core/dom/classes.ts +60 -0
  115. package/src/core/dom/create.ts +188 -0
  116. package/src/core/dom/events.ts +209 -0
  117. package/src/core/dom/index.ts +10 -0
  118. package/src/core/dom/utils.ts +97 -0
  119. package/src/core/index.ts +111 -0
  120. package/src/core/state/disabled.ts +81 -0
  121. package/src/core/state/emitter.ts +94 -0
  122. package/src/core/state/events.ts +88 -0
  123. package/src/core/state/index.ts +16 -0
  124. package/src/core/state/lifecycle.ts +131 -0
  125. package/src/core/state/store.ts +197 -0
  126. package/src/core/utils/index.ts +45 -0
  127. package/src/core/utils/{mobile.js → mobile.ts} +48 -24
  128. package/src/core/utils/object.ts +41 -0
  129. package/src/core/utils/validate.ts +234 -0
  130. package/src/{index.js → index.ts} +4 -2
  131. package/index.js +0 -11
  132. package/src/components/button/api.js +0 -54
  133. package/src/components/button/button.js +0 -81
  134. package/src/components/button/config.js +0 -10
  135. package/src/components/button/constants.js +0 -63
  136. package/src/components/button/index.js +0 -2
  137. package/src/components/checkbox/api.js +0 -45
  138. package/src/components/checkbox/checkbox.js +0 -96
  139. package/src/components/checkbox/index.js +0 -2
  140. package/src/components/container/api.js +0 -42
  141. package/src/components/container/container.js +0 -45
  142. package/src/components/container/index.js +0 -2
  143. package/src/components/container/styles.scss +0 -66
  144. package/src/components/list/index.js +0 -2
  145. package/src/components/list/list-item.js +0 -147
  146. package/src/components/list/list.js +0 -267
  147. package/src/components/menu/api.js +0 -117
  148. package/src/components/menu/constants.js +0 -42
  149. package/src/components/menu/features/items-manager.js +0 -375
  150. package/src/components/menu/features/keyboard-navigation.js +0 -129
  151. package/src/components/menu/features/positioning.js +0 -125
  152. package/src/components/menu/index.js +0 -2
  153. package/src/components/menu/menu-item.js +0 -41
  154. package/src/components/menu/menu.js +0 -54
  155. package/src/components/navigation/api.js +0 -43
  156. package/src/components/navigation/index.js +0 -2
  157. package/src/components/navigation/nav-item.js +0 -137
  158. package/src/components/navigation/navigation.js +0 -55
  159. package/src/components/snackbar/api.js +0 -125
  160. package/src/components/snackbar/features.js +0 -69
  161. package/src/components/snackbar/index.js +0 -2
  162. package/src/components/snackbar/position.js +0 -63
  163. package/src/components/snackbar/queue.js +0 -74
  164. package/src/components/snackbar/snackbar.js +0 -70
  165. package/src/components/switch/api.js +0 -44
  166. package/src/components/switch/index.js +0 -2
  167. package/src/components/switch/switch.js +0 -71
  168. package/src/components/textfield/api.js +0 -49
  169. package/src/components/textfield/index.js +0 -2
  170. package/src/components/textfield/textfield.js +0 -68
  171. package/src/core/build/_ripple.scss +0 -79
  172. package/src/core/build/constants.js +0 -51
  173. package/src/core/build/icon.js +0 -78
  174. package/src/core/build/ripple.js +0 -159
  175. package/src/core/build/text.js +0 -54
  176. package/src/core/compose/base.js +0 -8
  177. package/src/core/compose/component.js +0 -225
  178. package/src/core/compose/features/checkable.js +0 -114
  179. package/src/core/compose/features/disabled.js +0 -64
  180. package/src/core/compose/features/events.js +0 -48
  181. package/src/core/compose/features/icon.js +0 -33
  182. package/src/core/compose/features/index.js +0 -20
  183. package/src/core/compose/features/input.js +0 -100
  184. package/src/core/compose/features/lifecycle.js +0 -69
  185. package/src/core/compose/features/position.js +0 -60
  186. package/src/core/compose/features/ripple.js +0 -32
  187. package/src/core/compose/features/size.js +0 -9
  188. package/src/core/compose/features/style.js +0 -12
  189. package/src/core/compose/features/text.js +0 -17
  190. package/src/core/compose/features/textinput.js +0 -114
  191. package/src/core/compose/features/textlabel.js +0 -28
  192. package/src/core/compose/features/track.js +0 -49
  193. package/src/core/compose/features/variant.js +0 -9
  194. package/src/core/compose/features/withEvents.js +0 -67
  195. package/src/core/compose/index.js +0 -16
  196. package/src/core/config.js +0 -140
  197. package/src/core/dom/classes.js +0 -70
  198. package/src/core/dom/create.js +0 -132
  199. package/src/core/dom/events.js +0 -175
  200. package/src/core/dom/index.js +0 -5
  201. package/src/core/dom/utils.js +0 -22
  202. package/src/core/index.js +0 -23
  203. package/src/core/state/disabled.js +0 -51
  204. package/src/core/state/emitter.js +0 -63
  205. package/src/core/state/events.js +0 -29
  206. package/src/core/state/index.js +0 -6
  207. package/src/core/state/lifecycle.js +0 -64
  208. package/src/core/state/store.js +0 -112
  209. package/src/core/utils/index.js +0 -39
  210. package/src/core/utils/object.js +0 -22
  211. package/src/core/utils/validate.js +0 -37
  212. /package/src/components/checkbox/{styles.scss → _styles.scss} +0 -0
  213. /package/src/components/checkbox/{constants.js → constants.ts} +0 -0
  214. /package/src/components/list/{styles.scss → _styles.scss} +0 -0
  215. /package/src/components/menu/{styles.scss → _styles.scss} +0 -0
  216. /package/src/components/navigation/{styles.scss → _styles.scss} +0 -0
  217. /package/src/components/snackbar/{styles.scss → _styles.scss} +0 -0
  218. /package/src/components/switch/{styles.scss → _styles.scss} +0 -0
  219. /package/src/components/switch/{constants.js → constants.ts} +0 -0
  220. /package/src/components/textfield/{styles.scss → _styles.scss} +0 -0
@@ -1,375 +0,0 @@
1
- // src/components/menu/features/items-manager.js
2
-
3
- import { createMenuItem } from '../menu-item'
4
- import { MENU_EVENTS } from '../constants'
5
- import createMenu from '../menu'
6
-
7
- /**
8
- * Adds menu items management functionality to a component
9
- * @param {Object} config - Menu configuration
10
- * @returns {Function} Component enhancer
11
- */
12
- export const withItemsManager = (config) => (component) => {
13
- const submenus = new Map()
14
- const itemsMap = new Map()
15
- let activeSubmenu = null
16
- let currentHoveredItem = null
17
-
18
- // Create items container
19
- const list = document.createElement('ul')
20
- list.className = `${config.prefix}-menu-list`
21
- list.setAttribute('role', 'menu')
22
- component.element.appendChild(list)
23
-
24
- /**
25
- * Creates a submenu for a menu item
26
- * @param {string} name - Item name
27
- * @param {HTMLElement} item - Menu item element
28
- * @returns {Object} Submenu component
29
- */
30
- const createSubmenu = (name, item) => {
31
- const itemConfig = itemsMap.get(name)
32
- if (!itemConfig?.items) return null
33
-
34
- const submenu = createMenu({
35
- ...config,
36
- items: itemConfig.items,
37
- class: `${config.prefix}-menu--submenu`,
38
- parentItem: item
39
- })
40
-
41
- // Handle submenu selection
42
- submenu.on(MENU_EVENTS.SELECT, (detail) => {
43
- component.emit(MENU_EVENTS.SELECT, {
44
- name: `${name}:${detail.name}`,
45
- text: detail.text,
46
- path: [name, detail.name]
47
- })
48
- })
49
-
50
- return submenu
51
- }
52
-
53
- /**
54
- * Opens a submenu
55
- * @param {string} name - Item name
56
- * @param {HTMLElement} item - Menu item element
57
- */
58
- const openSubmenu = (name, item) => {
59
- // Close any open submenu that's different
60
- if (activeSubmenu && submenus.get(name) !== activeSubmenu) {
61
- const activeItem = list.querySelector('[aria-expanded="true"]')
62
- if (activeItem && activeItem !== item) {
63
- activeItem.setAttribute('aria-expanded', 'false')
64
- }
65
- activeSubmenu.hide()
66
- activeSubmenu = null
67
- }
68
-
69
- // If submenu doesn't exist yet, create it
70
- if (!submenus.has(name)) {
71
- const submenu = createSubmenu(name, item)
72
- if (submenu) {
73
- submenus.set(name, submenu)
74
- } else {
75
- return // No items to show
76
- }
77
- }
78
-
79
- // Get submenu and show it if not already showing
80
- const submenu = submenus.get(name)
81
- if (submenu && (activeSubmenu !== submenu || !item.getAttribute('aria-expanded') === 'true')) {
82
- item.setAttribute('aria-expanded', 'true')
83
- activeSubmenu = submenu
84
-
85
- // Position submenu relative to item
86
- submenu.show().position(item, {
87
- align: 'right',
88
- vAlign: 'top',
89
- offsetX: 0,
90
- offsetY: 0
91
- })
92
- }
93
- }
94
-
95
- /**
96
- * Closes a submenu
97
- * @param {string} name - Item name
98
- * @param {boolean} force - Whether to force close even if submenu is hovered
99
- */
100
- const closeSubmenu = (name, force = false) => {
101
- const submenu = submenus.get(name)
102
- if (!submenu || activeSubmenu !== submenu) return
103
-
104
- // Don't close if submenu is currently being hovered, unless forced
105
- if (!force && submenu.element && submenu.element.matches(':hover')) {
106
- return
107
- }
108
-
109
- const item = list.querySelector(`[data-name="${name}"][aria-expanded="true"]`)
110
- if (item) {
111
- item.setAttribute('aria-expanded', 'false')
112
- }
113
-
114
- submenu.hide()
115
- activeSubmenu = null
116
- }
117
-
118
- /**
119
- * Handles mouseenter for submenu items
120
- * @param {Event} event - Mouse event
121
- */
122
- const handleMouseEnter = (event) => {
123
- const item = event.target.closest(`.${config.prefix}-menu-item--submenu`)
124
- if (!item) return
125
-
126
- const name = item.getAttribute('data-name')
127
- if (name) {
128
- openSubmenu(name, item)
129
- currentHoveredItem = item
130
- }
131
- }
132
-
133
- /**
134
- * Handles mouseleave for submenu items
135
- * @param {Event} event - Mouse event
136
- */
137
- const handleMouseLeave = (event) => {
138
- const item = event.target.closest(`.${config.prefix}-menu-item--submenu`)
139
- if (!item) return
140
-
141
- const name = item.getAttribute('data-name')
142
- if (!name) return
143
-
144
- // Only close if we're not entering the submenu
145
- const submenu = submenus.get(name)
146
- if (submenu && submenu.element) {
147
- // Use setTimeout to allow checking if mouse moved to submenu
148
- setTimeout(() => {
149
- if (!submenu.element.matches(':hover') &&
150
- !item.matches(':hover')) {
151
- closeSubmenu(name)
152
- }
153
- }, 100)
154
- }
155
-
156
- currentHoveredItem = null
157
- }
158
-
159
- // Add hover handlers for all submenu items
160
- const addHoverHandlers = () => {
161
- // First remove any existing handlers to prevent duplicates
162
- list.querySelectorAll(`.${config.prefix}-menu-item--submenu`).forEach(item => {
163
- item.removeEventListener('mouseenter', handleMouseEnter)
164
- item.removeEventListener('mouseleave', handleMouseLeave)
165
-
166
- // Add the event listeners
167
- item.addEventListener('mouseenter', handleMouseEnter)
168
- item.addEventListener('mouseleave', handleMouseLeave)
169
- })
170
- }
171
-
172
- /**
173
- * Handles click events on menu items
174
- * @param {Event} event - Click event
175
- */
176
- const handleItemClick = (event) => {
177
- const item = event.target.closest(`.${config.prefix}-menu-item`)
178
- if (!item || item.getAttribute('aria-disabled') === 'true') return
179
-
180
- // For submenu items, toggle submenu
181
- if (item.classList.contains(`${config.prefix}-menu-item--submenu`)) {
182
- const name = item.getAttribute('data-name')
183
- if (!name) return
184
-
185
- // If expanded, close it
186
- if (item.getAttribute('aria-expanded') === 'true') {
187
- closeSubmenu(name, true) // Force close
188
- } else {
189
- // Otherwise open it
190
- openSubmenu(name, item)
191
- }
192
- return
193
- }
194
-
195
- // For regular items, emit select event
196
- const name = item.getAttribute('data-name')
197
- if (name) {
198
- component.emit(MENU_EVENTS.SELECT, { name, text: item.textContent })
199
- // Hide menu after selection unless configured otherwise
200
- if (!config.stayOpenOnSelect) {
201
- component.hide?.()
202
- }
203
- }
204
- }
205
-
206
- // Handle item clicks
207
- list.addEventListener('click', handleItemClick)
208
-
209
- // Create initial items
210
- if (config.items) {
211
- config.items.forEach(itemConfig => {
212
- const item = createMenuItem(itemConfig, config.prefix)
213
- list.appendChild(item)
214
-
215
- // Store item config for later use
216
- if (itemConfig.name) {
217
- itemsMap.set(itemConfig.name, itemConfig)
218
- }
219
- })
220
- }
221
-
222
- // Add hover handlers after all items are created
223
- addHoverHandlers()
224
-
225
- // Override show method to reset state and ensure hover handlers
226
- const originalShow = component.show
227
- component.show = function (...args) {
228
- // Reset state when showing menu
229
- currentHoveredItem = null
230
-
231
- // Ensure all items have hover handlers
232
- setTimeout(addHoverHandlers, 0)
233
-
234
- return originalShow.apply(this, args)
235
- }
236
-
237
- // Override hide method to close all submenus
238
- const originalHide = component.hide
239
- component.hide = function (...args) {
240
- // Close all submenus
241
- if (activeSubmenu) {
242
- activeSubmenu.hide()
243
- activeSubmenu = null
244
-
245
- const expandedItems = list.querySelectorAll('[aria-expanded="true"]')
246
- expandedItems.forEach(item => {
247
- item.setAttribute('aria-expanded', 'false')
248
- })
249
- }
250
-
251
- // Reset state
252
- currentHoveredItem = null
253
-
254
- return originalHide.apply(this, args)
255
- }
256
-
257
- // Add cleanup
258
- const originalDestroy = component.lifecycle?.destroy
259
- if (component.lifecycle) {
260
- component.lifecycle.destroy = () => {
261
- // Remove hover handlers from all items
262
- list.querySelectorAll(`.${config.prefix}-menu-item--submenu`).forEach(item => {
263
- item.removeEventListener('mouseenter', handleMouseEnter)
264
- item.removeEventListener('mouseleave', handleMouseLeave)
265
- })
266
-
267
- // Remove click listener
268
- list.removeEventListener('click', handleItemClick)
269
-
270
- // Reset state
271
- currentHoveredItem = null
272
-
273
- // Destroy all submenus
274
- submenus.forEach(submenu => submenu.destroy())
275
- submenus.clear()
276
- itemsMap.clear()
277
-
278
- if (originalDestroy) {
279
- originalDestroy()
280
- }
281
- }
282
- }
283
-
284
- return {
285
- ...component,
286
-
287
- /**
288
- * Closes any open submenus
289
- */
290
- closeSubmenus () {
291
- if (activeSubmenu) {
292
- activeSubmenu.hide()
293
- activeSubmenu = null
294
-
295
- const expandedItems = list.querySelectorAll('[aria-expanded="true"]')
296
- expandedItems.forEach(item => {
297
- item.setAttribute('aria-expanded', 'false')
298
- })
299
- }
300
- return this
301
- },
302
-
303
- /**
304
- * Adds an item to the menu
305
- * @param {Object} itemConfig - Item configuration
306
- */
307
- addItem (itemConfig) {
308
- if (!itemConfig) return this
309
-
310
- const item = createMenuItem(itemConfig, config.prefix)
311
- list.appendChild(item)
312
-
313
- // Store item config for later use
314
- if (itemConfig.name) {
315
- itemsMap.set(itemConfig.name, itemConfig)
316
- }
317
-
318
- // If it's a submenu item, add hover handlers
319
- if (itemConfig.items?.length) {
320
- item.addEventListener('mouseenter', handleMouseEnter)
321
- item.addEventListener('mouseleave', handleMouseLeave)
322
- }
323
-
324
- return this
325
- },
326
-
327
- /**
328
- * Removes an item from the menu
329
- * @param {string} name - Item name
330
- */
331
- removeItem (name) {
332
- if (!name) return this
333
-
334
- // First, ensure we remove the item from our internal map
335
- itemsMap.delete(name)
336
-
337
- // Now try to remove the item from the DOM
338
- const item = list.querySelector(`[data-name="${name}"]`)
339
- if (item) {
340
- // Remove event listeners
341
- item.removeEventListener('mouseenter', handleMouseEnter)
342
- item.removeEventListener('mouseleave', handleMouseLeave)
343
-
344
- // Close any submenu associated with this item
345
- if (submenus.has(name)) {
346
- const submenu = submenus.get(name)
347
- submenu.destroy()
348
- submenus.delete(name)
349
- }
350
-
351
- // Remove the item from the DOM
352
- item.remove()
353
- }
354
-
355
- return this
356
- },
357
-
358
- /**
359
- * Gets all registered items
360
- * @returns {Map} Map of item names to configurations
361
- */
362
- getItems () {
363
- return new Map(itemsMap)
364
- },
365
-
366
- /**
367
- * Refreshes all hover handlers
368
- * @returns {Object} Component instance
369
- */
370
- refreshHoverHandlers () {
371
- addHoverHandlers()
372
- return this
373
- }
374
- }
375
- }
@@ -1,129 +0,0 @@
1
- // src/components/menu/features/keyboard-navigation.js
2
-
3
- /**
4
- * Adds keyboard navigation functionality to a menu component
5
- * @param {Object} config - Menu configuration
6
- * @returns {Function} Component enhancer
7
- */
8
- export const withKeyboardNavigation = (config) => (component) => {
9
- // Store the component's existing methods
10
- const componentMethods = {
11
- show: component.show,
12
- hide: component.hide,
13
- destroy: component.lifecycle?.destroy
14
- }
15
-
16
- let keydownHandler = null
17
-
18
- /**
19
- * Handles keyboard navigation
20
- * @param {KeyboardEvent} event - Keyboard event
21
- */
22
- const handleKeydown = (event) => {
23
- if (!component.isVisible?.()) return
24
-
25
- const focusedItem = document.activeElement
26
- const list = component.element.querySelector(`.${config.prefix}-menu-list`)
27
- const isMenuItem = focusedItem.classList?.contains(`${config.prefix}-menu-item`)
28
- const items = Array.from(list.querySelectorAll(`.${config.prefix}-menu-item:not([aria-disabled="true"])`))
29
-
30
- switch (event.key) {
31
- case 'ArrowDown':
32
- event.preventDefault()
33
- if (!isMenuItem) {
34
- items[0]?.focus()
35
- } else {
36
- const currentIndex = items.indexOf(focusedItem)
37
- const nextItem = items[currentIndex + 1] || items[0]
38
- nextItem.focus()
39
- }
40
- break
41
-
42
- case 'ArrowUp':
43
- event.preventDefault()
44
- if (!isMenuItem) {
45
- items[items.length - 1]?.focus()
46
- } else {
47
- const currentIndex = items.indexOf(focusedItem)
48
- const prevItem = items[currentIndex - 1] || items[items.length - 1]
49
- prevItem.focus()
50
- }
51
- break
52
-
53
- case 'ArrowRight':
54
- if (isMenuItem && focusedItem.classList.contains(`${config.prefix}-menu-item--submenu`)) {
55
- event.preventDefault()
56
- const submenuEvent = new MouseEvent('click', {
57
- bubbles: true,
58
- cancelable: true
59
- })
60
- focusedItem.dispatchEvent(submenuEvent)
61
- }
62
- break
63
-
64
- case 'ArrowLeft':
65
- if (config.parentItem) {
66
- event.preventDefault()
67
- component.hide()
68
- config.parentItem.focus()
69
- }
70
- break
71
-
72
- case 'Enter':
73
- case ' ':
74
- if (isMenuItem) {
75
- event.preventDefault()
76
- focusedItem.click()
77
- }
78
- break
79
- }
80
- }
81
-
82
- /**
83
- * Enables keyboard navigation
84
- */
85
- const enableKeyboardNavigation = () => {
86
- if (!keydownHandler) {
87
- keydownHandler = handleKeydown
88
- document.addEventListener('keydown', keydownHandler)
89
- }
90
- }
91
-
92
- /**
93
- * Disables keyboard navigation
94
- */
95
- const disableKeyboardNavigation = () => {
96
- if (keydownHandler) {
97
- document.removeEventListener('keydown', keydownHandler)
98
- keydownHandler = null
99
- }
100
- }
101
-
102
- // Enhanced component with navigation capabilities
103
- const enhancedComponent = {
104
- ...component,
105
-
106
- show () {
107
- const result = componentMethods.show.call(this)
108
- enableKeyboardNavigation()
109
- return result
110
- },
111
-
112
- hide () {
113
- disableKeyboardNavigation()
114
- return componentMethods.hide.call(this)
115
- }
116
- }
117
-
118
- // Add cleanup to lifecycle
119
- if (component.lifecycle) {
120
- component.lifecycle.destroy = () => {
121
- disableKeyboardNavigation()
122
- if (componentMethods.destroy) {
123
- componentMethods.destroy()
124
- }
125
- }
126
- }
127
-
128
- return enhancedComponent
129
- }
@@ -1,125 +0,0 @@
1
- // src/components/menu/features/positioning.js
2
-
3
- /**
4
- * Positions a menu element relative to a target element
5
- * @param {HTMLElement} menuElement - Menu element to position
6
- * @param {HTMLElement} target - Target element to position against
7
- * @param {Object} options - Positioning options
8
- * @param {string} [options.align='left'] - Horizontal alignment: 'left', 'right', 'center'
9
- * @param {string} [options.vAlign='bottom'] - Vertical alignment: 'top', 'bottom', 'middle'
10
- * @param {number} [options.offsetX=0] - Horizontal offset in pixels
11
- * @param {number} [options.offsetY=0] - Vertical offset in pixels
12
- * @returns {Object} The final position {left, top}
13
- */
14
- export const positionMenu = (menuElement, target, options = {}) => {
15
- if (!target || !menuElement) return { left: 0, top: 0 }
16
-
17
- // Force the menu to be visible temporarily to get accurate dimensions
18
- const originalDisplay = menuElement.style.display
19
- const originalVisibility = menuElement.style.visibility
20
- const originalOpacity = menuElement.style.opacity
21
-
22
- menuElement.style.display = 'block'
23
- menuElement.style.visibility = 'hidden'
24
- menuElement.style.opacity = '0'
25
-
26
- const targetRect = target.getBoundingClientRect()
27
- const menuRect = menuElement.getBoundingClientRect()
28
-
29
- // Restore original styles
30
- menuElement.style.display = originalDisplay
31
- menuElement.style.visibility = originalVisibility
32
- menuElement.style.opacity = originalOpacity
33
-
34
- const {
35
- align = 'left',
36
- vAlign = 'bottom',
37
- offsetX = 0,
38
- offsetY = 0
39
- } = options
40
-
41
- let left = targetRect.left + offsetX
42
- let top = targetRect.bottom + offsetY
43
-
44
- // Handle horizontal alignment
45
- if (align === 'right') {
46
- left = targetRect.right - menuRect.width + offsetX
47
- } else if (align === 'center') {
48
- left = targetRect.left + (targetRect.width - menuRect.width) / 2 + offsetX
49
- }
50
-
51
- // Handle vertical alignment
52
- if (vAlign === 'top') {
53
- top = targetRect.top - menuRect.height + offsetY
54
- } else if (vAlign === 'middle') {
55
- top = targetRect.top + (targetRect.height - menuRect.height) / 2 + offsetY
56
- }
57
-
58
- // Determine if this is a submenu
59
- const isSubmenu = menuElement.classList.contains('mtrl-menu--submenu')
60
-
61
- // Special positioning for submenus
62
- if (isSubmenu) {
63
- // By default, position to the right of the parent item
64
- left = targetRect.right + 2 // Add a small gap
65
- top = targetRect.top
66
-
67
- // Check if submenu would go off-screen to the right
68
- const viewportWidth = window.innerWidth
69
- if (left + menuRect.width > viewportWidth) {
70
- // Position to the left of the parent item instead
71
- left = targetRect.left - menuRect.width - 2
72
- }
73
-
74
- // Check if submenu would go off-screen at the bottom
75
- const viewportHeight = window.innerHeight
76
- if (top + menuRect.height > viewportHeight) {
77
- // Align with bottom of viewport
78
- top = Math.max(0, viewportHeight - menuRect.height)
79
- }
80
- } else {
81
- // Standard menu positioning and boundary checking
82
- const viewportWidth = window.innerWidth
83
- const viewportHeight = window.innerHeight
84
-
85
- if (left + menuRect.width > viewportWidth) {
86
- left = Math.max(0, viewportWidth - menuRect.width)
87
- }
88
-
89
- if (left < 0) left = 0
90
-
91
- if (top + menuRect.height > viewportHeight) {
92
- top = Math.max(0, targetRect.top - menuRect.height + offsetY)
93
- }
94
-
95
- if (top < 0) top = 0
96
- }
97
-
98
- // Apply position
99
- menuElement.style.left = `${left}px`
100
- menuElement.style.top = `${top}px`
101
-
102
- return { left, top }
103
- }
104
-
105
- /**
106
- * Adds positioning functionality to a menu component
107
- * @param {Object} component - Menu component
108
- * @returns {Object} Enhanced component with positioning methods
109
- */
110
- export const withPositioning = (component) => {
111
- return {
112
- ...component,
113
-
114
- /**
115
- * Positions the menu relative to a target element
116
- * @param {HTMLElement} target - Target element
117
- * @param {Object} options - Position options
118
- * @returns {Object} Component instance
119
- */
120
- position (target, options) {
121
- positionMenu(component.element, target, options)
122
- return this
123
- }
124
- }
125
- }
@@ -1,2 +0,0 @@
1
- // src/components/menu/index.js
2
- export { default } from './menu.js'
@@ -1,41 +0,0 @@
1
- // src/components/menu/menu-item.js
2
-
3
- /**
4
- * Creates a menu item element
5
- * @param {Object} itemConfig - Item configuration
6
- * @param {string} prefix - CSS class prefix
7
- * @returns {HTMLElement} Menu item element
8
- */
9
- export const createMenuItem = (itemConfig, prefix) => {
10
- const item = document.createElement('li')
11
- item.className = `${prefix}-menu-item`
12
-
13
- if (itemConfig.type === 'divider') {
14
- item.className = `${prefix}-menu-divider`
15
- return item
16
- }
17
-
18
- if (itemConfig.class) {
19
- item.className += ` ${itemConfig.class}`
20
- }
21
-
22
- if (itemConfig.disabled) {
23
- item.setAttribute('aria-disabled', 'true')
24
- item.className += ` ${prefix}-menu-item--disabled`
25
- }
26
-
27
- if (itemConfig.name) {
28
- item.setAttribute('data-name', itemConfig.name)
29
- }
30
-
31
- item.textContent = itemConfig.text || ''
32
-
33
- if (itemConfig.items?.length) {
34
- item.className += ` ${prefix}-menu-item--submenu`
35
- item.setAttribute('aria-haspopup', 'true')
36
- item.setAttribute('aria-expanded', 'false')
37
- // We don't need to add a submenu indicator as it's handled by CSS ::after
38
- }
39
-
40
- return item
41
- }