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.
- package/README.md +70 -22
- package/index.ts +33 -0
- package/package.json +14 -5
- package/src/components/button/{styles.scss → _styles.scss} +2 -2
- package/src/components/button/api.ts +89 -0
- package/src/components/button/button.ts +50 -0
- package/src/components/button/config.ts +75 -0
- package/src/components/button/constants.ts +17 -0
- package/src/components/button/index.ts +4 -0
- package/src/components/button/types.ts +118 -0
- package/src/components/card/_styles.scss +359 -0
- package/src/components/card/actions.ts +48 -0
- package/src/components/card/api.ts +102 -0
- package/src/components/card/card.ts +41 -0
- package/src/components/card/config.ts +99 -0
- package/src/components/card/constants.ts +69 -0
- package/src/components/card/content.ts +48 -0
- package/src/components/card/features.ts +228 -0
- package/src/components/card/header.ts +88 -0
- package/src/components/card/index.ts +19 -0
- package/src/components/card/media.ts +52 -0
- package/src/components/card/types.ts +174 -0
- package/src/components/checkbox/api.ts +82 -0
- package/src/components/checkbox/checkbox.ts +75 -0
- package/src/components/checkbox/config.ts +90 -0
- package/src/components/checkbox/index.ts +4 -0
- package/src/components/checkbox/types.ts +146 -0
- package/src/components/chip/_styles.scss +372 -0
- package/src/components/chip/api.ts +115 -0
- package/src/components/chip/chip-set.ts +225 -0
- package/src/components/chip/chip.ts +82 -0
- package/src/components/chip/config.ts +92 -0
- package/src/components/chip/constants.ts +38 -0
- package/src/components/chip/index.ts +4 -0
- package/src/components/chip/types.ts +172 -0
- package/src/components/list/api.ts +72 -0
- package/src/components/list/config.ts +43 -0
- package/src/components/list/{constants.js → constants.ts} +34 -7
- package/src/components/list/features.ts +224 -0
- package/src/components/list/index.ts +14 -0
- package/src/components/list/list-item.ts +120 -0
- package/src/components/list/list.ts +37 -0
- package/src/components/list/types.ts +179 -0
- package/src/components/list/utils.ts +47 -0
- package/src/components/menu/api.ts +119 -0
- package/src/components/menu/config.ts +54 -0
- package/src/components/menu/constants.ts +154 -0
- package/src/components/menu/features/items-manager.ts +457 -0
- package/src/components/menu/features/keyboard-navigation.ts +133 -0
- package/src/components/menu/features/positioning.ts +127 -0
- package/src/components/menu/features/{visibility.js → visibility.ts} +66 -64
- package/src/components/menu/index.ts +14 -0
- package/src/components/menu/menu-item.ts +43 -0
- package/src/components/menu/menu.ts +53 -0
- package/src/components/menu/types.ts +178 -0
- package/src/components/navigation/api.ts +79 -0
- package/src/components/navigation/config.ts +61 -0
- package/src/components/navigation/{constants.js → constants.ts} +10 -10
- package/src/components/navigation/index.ts +14 -0
- package/src/components/navigation/nav-item.ts +148 -0
- package/src/components/navigation/navigation.ts +50 -0
- package/src/components/navigation/types.ts +212 -0
- package/src/components/progress/_styles.scss +204 -0
- package/src/components/progress/api.ts +179 -0
- package/src/components/progress/config.ts +124 -0
- package/src/components/progress/constants.ts +43 -0
- package/src/components/progress/index.ts +5 -0
- package/src/components/progress/progress.ts +163 -0
- package/src/components/progress/types.ts +102 -0
- package/src/components/snackbar/api.ts +162 -0
- package/src/components/snackbar/config.ts +62 -0
- package/src/components/snackbar/{constants.js → constants.ts} +21 -4
- package/src/components/snackbar/features.ts +76 -0
- package/src/components/snackbar/index.ts +4 -0
- package/src/components/snackbar/position.ts +71 -0
- package/src/components/snackbar/queue.ts +76 -0
- package/src/components/snackbar/snackbar.ts +60 -0
- package/src/components/snackbar/types.ts +58 -0
- package/src/components/switch/api.ts +77 -0
- package/src/components/switch/config.ts +74 -0
- package/src/components/switch/index.ts +4 -0
- package/src/components/switch/switch.ts +52 -0
- package/src/components/switch/types.ts +142 -0
- package/src/components/textfield/api.ts +72 -0
- package/src/components/textfield/config.ts +54 -0
- package/src/components/textfield/{constants.js → constants.ts} +38 -5
- package/src/components/textfield/index.ts +4 -0
- package/src/components/textfield/textfield.ts +50 -0
- package/src/components/textfield/types.ts +139 -0
- package/src/core/compose/base.ts +43 -0
- package/src/core/compose/component.ts +247 -0
- package/src/core/compose/features/checkable.ts +155 -0
- package/src/core/compose/features/disabled.ts +116 -0
- package/src/core/compose/features/events.ts +65 -0
- package/src/core/compose/features/icon.ts +67 -0
- package/src/core/compose/features/index.ts +35 -0
- package/src/core/compose/features/input.ts +174 -0
- package/src/core/compose/features/lifecycle.ts +139 -0
- package/src/core/compose/features/position.ts +94 -0
- package/src/core/compose/features/ripple.ts +55 -0
- package/src/core/compose/features/size.ts +29 -0
- package/src/core/compose/features/style.ts +31 -0
- package/src/core/compose/features/text.ts +44 -0
- package/src/core/compose/features/textinput.ts +225 -0
- package/src/core/compose/features/textlabel.ts +92 -0
- package/src/core/compose/features/track.ts +84 -0
- package/src/core/compose/features/variant.ts +29 -0
- package/src/core/compose/features/withEvents.ts +137 -0
- package/src/core/compose/index.ts +54 -0
- package/src/core/compose/{pipe.js → pipe.ts} +16 -11
- package/src/core/config/component-config.ts +136 -0
- package/src/core/config.ts +211 -0
- package/src/core/dom/{attributes.js → attributes.ts} +11 -11
- package/src/core/dom/classes.ts +60 -0
- package/src/core/dom/create.ts +188 -0
- package/src/core/dom/events.ts +209 -0
- package/src/core/dom/index.ts +10 -0
- package/src/core/dom/utils.ts +97 -0
- package/src/core/index.ts +111 -0
- package/src/core/state/disabled.ts +81 -0
- package/src/core/state/emitter.ts +94 -0
- package/src/core/state/events.ts +88 -0
- package/src/core/state/index.ts +16 -0
- package/src/core/state/lifecycle.ts +131 -0
- package/src/core/state/store.ts +197 -0
- package/src/core/utils/index.ts +45 -0
- package/src/core/utils/{mobile.js → mobile.ts} +48 -24
- package/src/core/utils/object.ts +41 -0
- package/src/core/utils/validate.ts +234 -0
- package/src/{index.js → index.ts} +4 -2
- package/index.js +0 -11
- package/src/components/button/api.js +0 -54
- package/src/components/button/button.js +0 -81
- package/src/components/button/config.js +0 -10
- package/src/components/button/constants.js +0 -63
- package/src/components/button/index.js +0 -2
- package/src/components/checkbox/api.js +0 -45
- package/src/components/checkbox/checkbox.js +0 -96
- package/src/components/checkbox/index.js +0 -2
- package/src/components/container/api.js +0 -42
- package/src/components/container/container.js +0 -45
- package/src/components/container/index.js +0 -2
- package/src/components/container/styles.scss +0 -66
- package/src/components/list/index.js +0 -2
- package/src/components/list/list-item.js +0 -147
- package/src/components/list/list.js +0 -267
- package/src/components/menu/api.js +0 -117
- package/src/components/menu/constants.js +0 -42
- package/src/components/menu/features/items-manager.js +0 -375
- package/src/components/menu/features/keyboard-navigation.js +0 -129
- package/src/components/menu/features/positioning.js +0 -125
- package/src/components/menu/index.js +0 -2
- package/src/components/menu/menu-item.js +0 -41
- package/src/components/menu/menu.js +0 -54
- package/src/components/navigation/api.js +0 -43
- package/src/components/navigation/index.js +0 -2
- package/src/components/navigation/nav-item.js +0 -137
- package/src/components/navigation/navigation.js +0 -55
- package/src/components/snackbar/api.js +0 -125
- package/src/components/snackbar/features.js +0 -69
- package/src/components/snackbar/index.js +0 -2
- package/src/components/snackbar/position.js +0 -63
- package/src/components/snackbar/queue.js +0 -74
- package/src/components/snackbar/snackbar.js +0 -70
- package/src/components/switch/api.js +0 -44
- package/src/components/switch/index.js +0 -2
- package/src/components/switch/switch.js +0 -71
- package/src/components/textfield/api.js +0 -49
- package/src/components/textfield/index.js +0 -2
- package/src/components/textfield/textfield.js +0 -68
- package/src/core/build/_ripple.scss +0 -79
- package/src/core/build/constants.js +0 -51
- package/src/core/build/icon.js +0 -78
- package/src/core/build/ripple.js +0 -159
- package/src/core/build/text.js +0 -54
- package/src/core/compose/base.js +0 -8
- package/src/core/compose/component.js +0 -225
- package/src/core/compose/features/checkable.js +0 -114
- package/src/core/compose/features/disabled.js +0 -64
- package/src/core/compose/features/events.js +0 -48
- package/src/core/compose/features/icon.js +0 -33
- package/src/core/compose/features/index.js +0 -20
- package/src/core/compose/features/input.js +0 -100
- package/src/core/compose/features/lifecycle.js +0 -69
- package/src/core/compose/features/position.js +0 -60
- package/src/core/compose/features/ripple.js +0 -32
- package/src/core/compose/features/size.js +0 -9
- package/src/core/compose/features/style.js +0 -12
- package/src/core/compose/features/text.js +0 -17
- package/src/core/compose/features/textinput.js +0 -114
- package/src/core/compose/features/textlabel.js +0 -28
- package/src/core/compose/features/track.js +0 -49
- package/src/core/compose/features/variant.js +0 -9
- package/src/core/compose/features/withEvents.js +0 -67
- package/src/core/compose/index.js +0 -16
- package/src/core/config.js +0 -140
- package/src/core/dom/classes.js +0 -70
- package/src/core/dom/create.js +0 -132
- package/src/core/dom/events.js +0 -175
- package/src/core/dom/index.js +0 -5
- package/src/core/dom/utils.js +0 -22
- package/src/core/index.js +0 -23
- package/src/core/state/disabled.js +0 -51
- package/src/core/state/emitter.js +0 -63
- package/src/core/state/events.js +0 -29
- package/src/core/state/index.js +0 -6
- package/src/core/state/lifecycle.js +0 -64
- package/src/core/state/store.js +0 -112
- package/src/core/utils/index.js +0 -39
- package/src/core/utils/object.js +0 -22
- package/src/core/utils/validate.js +0 -37
- /package/src/components/checkbox/{styles.scss → _styles.scss} +0 -0
- /package/src/components/checkbox/{constants.js → constants.ts} +0 -0
- /package/src/components/list/{styles.scss → _styles.scss} +0 -0
- /package/src/components/menu/{styles.scss → _styles.scss} +0 -0
- /package/src/components/navigation/{styles.scss → _styles.scss} +0 -0
- /package/src/components/snackbar/{styles.scss → _styles.scss} +0 -0
- /package/src/components/switch/{styles.scss → _styles.scss} +0 -0
- /package/src/components/switch/{constants.js → constants.ts} +0 -0
- /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,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
|
-
}
|