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,179 +1,181 @@
|
|
|
1
|
-
// src/components/menu/features/visibility.
|
|
2
|
-
import {
|
|
1
|
+
// src/components/menu/features/visibility.ts
|
|
2
|
+
import { BaseComponent, MenuConfig } from '../types';
|
|
3
|
+
import { MENU_EVENTS } from '../constants';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Adds visibility management functionality to a menu component
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {string} config.prefix - CSS class prefix
|
|
7
|
+
* @param {MenuConfig} config - Menu configuration
|
|
8
8
|
* @returns {Function} Component enhancer
|
|
9
9
|
*/
|
|
10
|
-
export const withVisibility = (config) => (component) => {
|
|
11
|
-
let isVisible = false
|
|
12
|
-
let outsideClickHandler = null
|
|
13
|
-
let keydownHandler = null
|
|
10
|
+
export const withVisibility = (config: MenuConfig) => (component: BaseComponent): BaseComponent => {
|
|
11
|
+
let isVisible = false;
|
|
12
|
+
let outsideClickHandler: ((event: MouseEvent) => void) | null = null;
|
|
13
|
+
let keydownHandler: ((event: KeyboardEvent) => void) | null = null;
|
|
14
|
+
const prefix = config.prefix || 'mtrl';
|
|
14
15
|
|
|
15
16
|
// Create the component interface with hide/show methods first
|
|
16
|
-
const enhancedComponent = {
|
|
17
|
+
const enhancedComponent: BaseComponent = {
|
|
17
18
|
...component,
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Shows the menu
|
|
21
22
|
*/
|
|
22
|
-
show
|
|
23
|
-
if (isVisible) return this
|
|
23
|
+
show() {
|
|
24
|
+
if (isVisible) return this;
|
|
24
25
|
|
|
25
26
|
// First set visibility to true to prevent multiple calls
|
|
26
|
-
isVisible = true
|
|
27
|
+
isVisible = true;
|
|
27
28
|
|
|
28
29
|
// Make sure the element is in the DOM
|
|
29
30
|
if (!component.element.parentNode) {
|
|
30
|
-
document.body.appendChild(component.element)
|
|
31
|
+
document.body.appendChild(component.element);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// Always clean up previous handlers before adding new ones
|
|
34
35
|
if (outsideClickHandler) {
|
|
35
|
-
document.removeEventListener('mousedown', outsideClickHandler)
|
|
36
|
+
document.removeEventListener('mousedown', outsideClickHandler);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// Setup outside click handler for closing
|
|
39
|
-
outsideClickHandler = handleOutsideClick
|
|
40
|
+
outsideClickHandler = handleOutsideClick;
|
|
40
41
|
|
|
41
42
|
// Use setTimeout to ensure the handler is not triggered immediately
|
|
42
43
|
setTimeout(() => {
|
|
43
|
-
document.addEventListener('mousedown', outsideClickHandler)
|
|
44
|
-
}, 0)
|
|
44
|
+
document.addEventListener('mousedown', outsideClickHandler!);
|
|
45
|
+
}, 0);
|
|
45
46
|
|
|
46
47
|
// Setup keyboard navigation
|
|
47
48
|
if (!keydownHandler) {
|
|
48
|
-
keydownHandler = handleKeydown
|
|
49
|
-
document.addEventListener('keydown', keydownHandler)
|
|
49
|
+
keydownHandler = handleKeydown;
|
|
50
|
+
document.addEventListener('keydown', keydownHandler);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// Add display block first for transition to work
|
|
53
|
-
component.element.style.display = 'block'
|
|
54
|
+
component.element.style.display = 'block';
|
|
54
55
|
|
|
55
56
|
// Force a reflow before adding the visible class for animation
|
|
56
|
-
|
|
57
|
-
component.element.
|
|
58
|
-
component.element.
|
|
57
|
+
// eslint-disable-next-line no-void
|
|
58
|
+
void component.element.offsetHeight;
|
|
59
|
+
component.element.classList.add(`${prefix}-menu--visible`);
|
|
60
|
+
component.element.setAttribute('aria-hidden', 'false');
|
|
59
61
|
|
|
60
62
|
// Emit open event
|
|
61
|
-
component.emit(MENU_EVENTS.OPEN)
|
|
63
|
+
component.emit?.(MENU_EVENTS.OPEN, {});
|
|
62
64
|
|
|
63
|
-
return this
|
|
65
|
+
return this;
|
|
64
66
|
},
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Hides the menu
|
|
68
70
|
*/
|
|
69
|
-
hide
|
|
71
|
+
hide() {
|
|
70
72
|
// Return early if already hidden
|
|
71
|
-
if (!isVisible) return this
|
|
73
|
+
if (!isVisible) return this;
|
|
72
74
|
|
|
73
75
|
// First set the visibility flag to false
|
|
74
|
-
isVisible = false
|
|
76
|
+
isVisible = false;
|
|
75
77
|
|
|
76
78
|
// Close any open submenus first
|
|
77
79
|
if (component.closeSubmenus) {
|
|
78
|
-
component.closeSubmenus()
|
|
80
|
+
component.closeSubmenus();
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
// Remove ALL event listeners
|
|
82
84
|
if (outsideClickHandler) {
|
|
83
|
-
document.removeEventListener('mousedown', outsideClickHandler)
|
|
84
|
-
outsideClickHandler = null
|
|
85
|
+
document.removeEventListener('mousedown', outsideClickHandler);
|
|
86
|
+
outsideClickHandler = null;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
if (keydownHandler) {
|
|
88
|
-
document.removeEventListener('keydown', keydownHandler)
|
|
89
|
-
keydownHandler = null
|
|
90
|
+
document.removeEventListener('keydown', keydownHandler);
|
|
91
|
+
keydownHandler = null;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
// Hide the menu with visual indication first
|
|
93
|
-
component.element.classList.remove(`${
|
|
94
|
-
component.element.setAttribute('aria-hidden', 'true')
|
|
95
|
+
component.element.classList.remove(`${prefix}-menu--visible`);
|
|
96
|
+
component.element.setAttribute('aria-hidden', 'true');
|
|
95
97
|
|
|
96
98
|
// Define a reliable cleanup function
|
|
97
99
|
const cleanupElement = () => {
|
|
98
100
|
// Safety check to prevent errors
|
|
99
101
|
if (component.element) {
|
|
100
|
-
component.element.style.display = 'none'
|
|
102
|
+
component.element.style.display = 'none';
|
|
101
103
|
|
|
102
104
|
// Remove from DOM if still attached
|
|
103
105
|
if (component.element.parentNode) {
|
|
104
|
-
component.element.remove()
|
|
106
|
+
component.element.remove();
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
}
|
|
109
|
+
};
|
|
108
110
|
|
|
109
111
|
// Try to use transition end for smooth animation
|
|
110
|
-
const handleTransitionEnd = (e) => {
|
|
112
|
+
const handleTransitionEnd = (e: TransitionEvent) => {
|
|
111
113
|
if (e.propertyName === 'opacity' || e.propertyName === 'transform') {
|
|
112
|
-
component.element.removeEventListener('transitionend', handleTransitionEnd)
|
|
113
|
-
cleanupElement()
|
|
114
|
+
component.element.removeEventListener('transitionend', handleTransitionEnd);
|
|
115
|
+
cleanupElement();
|
|
114
116
|
}
|
|
115
|
-
}
|
|
117
|
+
};
|
|
116
118
|
|
|
117
|
-
component.element.addEventListener('transitionend', handleTransitionEnd)
|
|
119
|
+
component.element.addEventListener('transitionend', handleTransitionEnd);
|
|
118
120
|
|
|
119
121
|
// Fallback timeout in case transition events don't fire
|
|
120
122
|
// This ensures the menu always gets removed
|
|
121
|
-
setTimeout(cleanupElement, 300)
|
|
123
|
+
setTimeout(cleanupElement, 300);
|
|
122
124
|
|
|
123
125
|
// Emit close event
|
|
124
|
-
component.emit(MENU_EVENTS.CLOSE)
|
|
126
|
+
component.emit?.(MENU_EVENTS.CLOSE, {});
|
|
125
127
|
|
|
126
|
-
return this
|
|
128
|
+
return this;
|
|
127
129
|
},
|
|
128
130
|
|
|
129
131
|
/**
|
|
130
132
|
* Returns whether the menu is currently visible
|
|
131
133
|
* @returns {boolean} Visibility state
|
|
132
134
|
*/
|
|
133
|
-
isVisible
|
|
134
|
-
return isVisible
|
|
135
|
+
isVisible() {
|
|
136
|
+
return isVisible;
|
|
135
137
|
}
|
|
136
|
-
}
|
|
138
|
+
};
|
|
137
139
|
|
|
138
140
|
/**
|
|
139
141
|
* Handles clicks outside the menu
|
|
140
142
|
* @param {MouseEvent} event - Mouse event
|
|
141
143
|
*/
|
|
142
|
-
const handleOutsideClick = (event) => {
|
|
143
|
-
if (!isVisible) return
|
|
144
|
+
const handleOutsideClick = (event: MouseEvent) => {
|
|
145
|
+
if (!isVisible) return;
|
|
144
146
|
|
|
145
147
|
// Store the opening button if available
|
|
146
|
-
const openingButton = config.openingButton?.element
|
|
148
|
+
const openingButton = config.openingButton?.element;
|
|
147
149
|
|
|
148
150
|
// Check if click is outside the menu but not on the opening button
|
|
149
|
-
const clickedElement = event.target
|
|
151
|
+
const clickedElement = event.target as Node;
|
|
150
152
|
|
|
151
153
|
// Don't close if the click is inside the menu
|
|
152
154
|
if (component.element.contains(clickedElement)) {
|
|
153
|
-
return
|
|
155
|
+
return;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
// Don't close if the click is on the opening button (it will handle opening/closing)
|
|
157
159
|
if (openingButton && (openingButton === clickedElement || openingButton.contains(clickedElement))) {
|
|
158
|
-
return
|
|
160
|
+
return;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
// If we got here, close the menu
|
|
162
|
-
enhancedComponent.hide()
|
|
163
|
-
}
|
|
164
|
+
enhancedComponent.hide?.();
|
|
165
|
+
};
|
|
164
166
|
|
|
165
167
|
/**
|
|
166
168
|
* Handles keyboard events
|
|
167
169
|
* @param {KeyboardEvent} event - Keyboard event
|
|
168
170
|
*/
|
|
169
|
-
const handleKeydown = (event) => {
|
|
170
|
-
if (!isVisible) return
|
|
171
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
172
|
+
if (!isVisible) return;
|
|
171
173
|
|
|
172
174
|
if (event.key === 'Escape') {
|
|
173
|
-
event.preventDefault()
|
|
174
|
-
enhancedComponent.hide()
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
enhancedComponent.hide?.();
|
|
175
177
|
}
|
|
176
|
-
}
|
|
178
|
+
};
|
|
177
179
|
|
|
178
|
-
return enhancedComponent
|
|
179
|
-
}
|
|
180
|
+
return enhancedComponent;
|
|
181
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/components/menu/index.ts
|
|
2
|
+
export { default } from './menu'
|
|
3
|
+
export {
|
|
4
|
+
MENU_ALIGN,
|
|
5
|
+
MENU_VERTICAL_ALIGN,
|
|
6
|
+
MENU_ITEM_TYPES,
|
|
7
|
+
MENU_EVENTS
|
|
8
|
+
} from './constants'
|
|
9
|
+
export {
|
|
10
|
+
MenuConfig,
|
|
11
|
+
MenuComponent,
|
|
12
|
+
MenuItemConfig,
|
|
13
|
+
MenuPositionConfig
|
|
14
|
+
} from './types'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/components/menu/menu-item.ts
|
|
2
|
+
import { MenuItemConfig } from './types';
|
|
3
|
+
import { MENU_ITEM_TYPES } from './constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a menu item element
|
|
7
|
+
* @param {MenuItemConfig} itemConfig - Item configuration
|
|
8
|
+
* @param {string} prefix - CSS class prefix
|
|
9
|
+
* @returns {HTMLElement} Menu item element
|
|
10
|
+
*/
|
|
11
|
+
export const createMenuItem = (itemConfig: MenuItemConfig, prefix: string): HTMLElement => {
|
|
12
|
+
const item = document.createElement('li');
|
|
13
|
+
item.className = `${prefix}-menu-item`;
|
|
14
|
+
|
|
15
|
+
if (itemConfig.type === MENU_ITEM_TYPES.DIVIDER) {
|
|
16
|
+
item.className = `${prefix}-menu-divider`;
|
|
17
|
+
return item;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (itemConfig.class) {
|
|
21
|
+
item.className += ` ${itemConfig.class}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (itemConfig.disabled) {
|
|
25
|
+
item.setAttribute('aria-disabled', 'true');
|
|
26
|
+
item.className += ` ${prefix}-menu-item--disabled`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (itemConfig.name) {
|
|
30
|
+
item.setAttribute('data-name', itemConfig.name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
item.textContent = itemConfig.text || '';
|
|
34
|
+
|
|
35
|
+
if (itemConfig.items?.length) {
|
|
36
|
+
item.className += ` ${prefix}-menu-item--submenu`;
|
|
37
|
+
item.setAttribute('aria-haspopup', 'true');
|
|
38
|
+
item.setAttribute('aria-expanded', 'false');
|
|
39
|
+
// We don't need to add a submenu indicator as it's handled by CSS ::after
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return item;
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/components/menu/menu.ts
|
|
2
|
+
import { pipe } from '../../core/compose';
|
|
3
|
+
import { createBase, withElement } from '../../core/compose/component';
|
|
4
|
+
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
5
|
+
import { withAPI } from './api';
|
|
6
|
+
import { withVisibility } from './features/visibility';
|
|
7
|
+
import { withItemsManager } from './features/items-manager';
|
|
8
|
+
import { withPositioning } from './features/positioning';
|
|
9
|
+
import { withKeyboardNavigation } from './features/keyboard-navigation';
|
|
10
|
+
import { MenuConfig, MenuComponent } from './types';
|
|
11
|
+
import {
|
|
12
|
+
createBaseConfig,
|
|
13
|
+
getElementConfig,
|
|
14
|
+
getApiConfig
|
|
15
|
+
} from './config';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new Menu component
|
|
19
|
+
* @param {MenuConfig} config - Menu configuration
|
|
20
|
+
* @returns {MenuComponent} Menu component instance
|
|
21
|
+
*/
|
|
22
|
+
const createMenu = (config: MenuConfig = {}): MenuComponent => {
|
|
23
|
+
const baseConfig = createBaseConfig(config);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Create menu component
|
|
27
|
+
const menu = pipe(
|
|
28
|
+
createBase,
|
|
29
|
+
withEvents(),
|
|
30
|
+
withElement(getElementConfig(baseConfig)),
|
|
31
|
+
withLifecycle(),
|
|
32
|
+
withItemsManager(baseConfig),
|
|
33
|
+
withVisibility(baseConfig),
|
|
34
|
+
withPositioning,
|
|
35
|
+
withKeyboardNavigation(baseConfig),
|
|
36
|
+
comp => withAPI(getApiConfig(comp))(comp)
|
|
37
|
+
)(baseConfig);
|
|
38
|
+
|
|
39
|
+
// Handle circular dependency for submenus
|
|
40
|
+
// This is needed because we need the complete menu factory function
|
|
41
|
+
// to create submenus, but we can't import it directly in items-manager
|
|
42
|
+
if (menu.setCreateSubmenuFunction) {
|
|
43
|
+
menu.setCreateSubmenuFunction(createMenu);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return menu as MenuComponent;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Menu creation error:', error instanceof Error ? error.message : String(error));
|
|
49
|
+
throw new Error(`Failed to create menu: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default createMenu;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/components/menu/types.ts
|
|
2
|
+
import {
|
|
3
|
+
MENU_ALIGN,
|
|
4
|
+
MENU_VERTICAL_ALIGN,
|
|
5
|
+
MENU_ITEM_TYPES
|
|
6
|
+
} from './constants';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Menu item configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface MenuItemConfig {
|
|
12
|
+
/** Unique identifier for the item */
|
|
13
|
+
name: string;
|
|
14
|
+
|
|
15
|
+
/** Text content for the item */
|
|
16
|
+
text: string;
|
|
17
|
+
|
|
18
|
+
/** Type of menu item */
|
|
19
|
+
type?: keyof typeof MENU_ITEM_TYPES | string;
|
|
20
|
+
|
|
21
|
+
/** Whether the item is disabled */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
|
|
24
|
+
/** Additional CSS classes */
|
|
25
|
+
class?: string;
|
|
26
|
+
|
|
27
|
+
/** Submenu items */
|
|
28
|
+
items?: MenuItemConfig[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Position configuration
|
|
33
|
+
*/
|
|
34
|
+
export interface MenuPositionConfig {
|
|
35
|
+
/** Horizontal alignment */
|
|
36
|
+
align?: keyof typeof MENU_ALIGN | string;
|
|
37
|
+
|
|
38
|
+
/** Vertical alignment */
|
|
39
|
+
vAlign?: keyof typeof MENU_VERTICAL_ALIGN | string;
|
|
40
|
+
|
|
41
|
+
/** Horizontal offset in pixels */
|
|
42
|
+
offsetX?: number;
|
|
43
|
+
|
|
44
|
+
/** Vertical offset in pixels */
|
|
45
|
+
offsetY?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Position result
|
|
50
|
+
*/
|
|
51
|
+
export interface MenuPosition {
|
|
52
|
+
/** Left position in pixels */
|
|
53
|
+
left: number;
|
|
54
|
+
|
|
55
|
+
/** Top position in pixels */
|
|
56
|
+
top: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Stored item data
|
|
61
|
+
*/
|
|
62
|
+
export interface MenuItemData {
|
|
63
|
+
/** DOM element for the item */
|
|
64
|
+
element: HTMLElement;
|
|
65
|
+
|
|
66
|
+
/** Item configuration */
|
|
67
|
+
config: MenuItemConfig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Menu selection event data
|
|
72
|
+
*/
|
|
73
|
+
export interface MenuSelectEvent {
|
|
74
|
+
/** Name of the selected item */
|
|
75
|
+
name: string;
|
|
76
|
+
|
|
77
|
+
/** Text content of the selected item */
|
|
78
|
+
text: string;
|
|
79
|
+
|
|
80
|
+
/** Path of parent item names (for submenus) */
|
|
81
|
+
path?: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Configuration interface for the Menu component
|
|
86
|
+
*/
|
|
87
|
+
export interface MenuConfig {
|
|
88
|
+
/** Initial menu items */
|
|
89
|
+
items?: MenuItemConfig[];
|
|
90
|
+
|
|
91
|
+
/** Additional CSS classes */
|
|
92
|
+
class?: string;
|
|
93
|
+
|
|
94
|
+
/** Whether to keep menu open after selection */
|
|
95
|
+
stayOpenOnSelect?: boolean;
|
|
96
|
+
|
|
97
|
+
/** Button element that opens the menu */
|
|
98
|
+
openingButton?: HTMLElement | { element: HTMLElement };
|
|
99
|
+
|
|
100
|
+
/** Parent item element (for submenus) */
|
|
101
|
+
parentItem?: HTMLElement;
|
|
102
|
+
|
|
103
|
+
/** Prefix for class names */
|
|
104
|
+
prefix?: string;
|
|
105
|
+
|
|
106
|
+
/** Component name */
|
|
107
|
+
componentName?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Menu component interface
|
|
112
|
+
*/
|
|
113
|
+
export interface MenuComponent {
|
|
114
|
+
/** The root element of the menu */
|
|
115
|
+
element: HTMLElement;
|
|
116
|
+
|
|
117
|
+
/** Shows the menu */
|
|
118
|
+
show: () => MenuComponent;
|
|
119
|
+
|
|
120
|
+
/** Hides the menu */
|
|
121
|
+
hide: () => MenuComponent;
|
|
122
|
+
|
|
123
|
+
/** Checks if the menu is visible */
|
|
124
|
+
isVisible: () => boolean;
|
|
125
|
+
|
|
126
|
+
/** Positions the menu relative to a target */
|
|
127
|
+
position: (target: HTMLElement, options?: MenuPositionConfig) => MenuComponent;
|
|
128
|
+
|
|
129
|
+
/** Adds a menu item */
|
|
130
|
+
addItem: (config: MenuItemConfig) => MenuComponent;
|
|
131
|
+
|
|
132
|
+
/** Removes a menu item by name */
|
|
133
|
+
removeItem: (name: string) => MenuComponent;
|
|
134
|
+
|
|
135
|
+
/** Gets all menu items */
|
|
136
|
+
getItems: () => Map<string, MenuItemData>;
|
|
137
|
+
|
|
138
|
+
/** Adds event listener */
|
|
139
|
+
on: (event: string, handler: Function) => MenuComponent;
|
|
140
|
+
|
|
141
|
+
/** Removes event listener */
|
|
142
|
+
off: (event: string, handler: Function) => MenuComponent;
|
|
143
|
+
|
|
144
|
+
/** Destroys the menu component and cleans up resources */
|
|
145
|
+
destroy: () => MenuComponent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Base component interface
|
|
150
|
+
*/
|
|
151
|
+
export interface BaseComponent {
|
|
152
|
+
element: HTMLElement;
|
|
153
|
+
emit?: (event: string, data: any) => void;
|
|
154
|
+
on?: (event: string, handler: Function) => any;
|
|
155
|
+
off?: (event: string, handler: Function) => any;
|
|
156
|
+
show?: () => any;
|
|
157
|
+
hide?: () => any;
|
|
158
|
+
isVisible?: () => boolean;
|
|
159
|
+
position?: (target: HTMLElement, options?: MenuPositionConfig) => any;
|
|
160
|
+
addItem?: (config: MenuItemConfig) => any;
|
|
161
|
+
removeItem?: (name: string) => any;
|
|
162
|
+
getItems?: () => Map<string, MenuItemData>;
|
|
163
|
+
closeSubmenus?: () => any;
|
|
164
|
+
refreshHoverHandlers?: () => any;
|
|
165
|
+
lifecycle?: {
|
|
166
|
+
destroy: () => void;
|
|
167
|
+
};
|
|
168
|
+
[key: string]: any;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* API options interface
|
|
173
|
+
*/
|
|
174
|
+
export interface ApiOptions {
|
|
175
|
+
lifecycle: {
|
|
176
|
+
destroy: () => void;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/components/navigation/api.ts
|
|
2
|
+
import {
|
|
3
|
+
BaseComponent,
|
|
4
|
+
NavigationComponent,
|
|
5
|
+
ApiOptions,
|
|
6
|
+
NavItemConfig,
|
|
7
|
+
NavItemData
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enhances navigation component with API methods
|
|
12
|
+
* @param {ApiOptions} options - API configuration
|
|
13
|
+
* @returns {Function} Higher-order function that adds API methods to component
|
|
14
|
+
*/
|
|
15
|
+
export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
16
|
+
(component: BaseComponent): NavigationComponent => ({
|
|
17
|
+
...component as any,
|
|
18
|
+
element: component.element,
|
|
19
|
+
items: component.items as Map<string, NavItemData>,
|
|
20
|
+
|
|
21
|
+
// Item management
|
|
22
|
+
addItem(config: NavItemConfig): NavigationComponent {
|
|
23
|
+
component.addItem?.(config);
|
|
24
|
+
return this;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
removeItem(id: string): NavigationComponent {
|
|
28
|
+
component.removeItem?.(id);
|
|
29
|
+
return this;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getItem(id: string): NavItemData | undefined {
|
|
33
|
+
return component.getItem?.(id);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
getAllItems(): NavItemData[] {
|
|
37
|
+
return component.getAllItems?.() || [];
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
getItemPath(id: string): string[] {
|
|
41
|
+
return component.getItemPath?.(id) || [];
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Active state management
|
|
45
|
+
setActive(id: string): NavigationComponent {
|
|
46
|
+
component.setActive?.(id);
|
|
47
|
+
return this;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
getActive(): NavItemData | null {
|
|
51
|
+
return component.getActive?.() || null;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Event handling
|
|
55
|
+
on(event: string, handler: Function): NavigationComponent {
|
|
56
|
+
component.on?.(event, handler);
|
|
57
|
+
return this;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
off(event: string, handler: Function): NavigationComponent {
|
|
61
|
+
component.off?.(event, handler);
|
|
62
|
+
return this;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// State management
|
|
66
|
+
enable(): NavigationComponent {
|
|
67
|
+
disabled.enable();
|
|
68
|
+
return this;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
disable(): NavigationComponent {
|
|
72
|
+
disabled.disable();
|
|
73
|
+
return this;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
destroy(): void {
|
|
77
|
+
lifecycle.destroy();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/components/navigation/config.ts
|
|
2
|
+
import {
|
|
3
|
+
createComponentConfig,
|
|
4
|
+
createElementConfig,
|
|
5
|
+
BaseComponentConfig
|
|
6
|
+
} from '../../core/config/component-config';
|
|
7
|
+
import { NavigationConfig, BaseComponent, ApiOptions } from './types';
|
|
8
|
+
import { NAV_VARIANTS, NAV_POSITIONS, NAV_BEHAVIORS } from './constants';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default configuration for the Navigation component
|
|
12
|
+
*/
|
|
13
|
+
export const defaultConfig: NavigationConfig = {
|
|
14
|
+
variant: NAV_VARIANTS.RAIL,
|
|
15
|
+
position: NAV_POSITIONS.LEFT,
|
|
16
|
+
behavior: NAV_BEHAVIORS.FIXED,
|
|
17
|
+
items: [],
|
|
18
|
+
showLabels: true,
|
|
19
|
+
scrimEnabled: false
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates the base configuration for Navigation component
|
|
24
|
+
* @param {NavigationConfig} config - User provided configuration
|
|
25
|
+
* @returns {NavigationConfig} Complete configuration with defaults applied
|
|
26
|
+
*/
|
|
27
|
+
export const createBaseConfig = (config: NavigationConfig = {}): NavigationConfig =>
|
|
28
|
+
createComponentConfig(defaultConfig, config, 'nav') as NavigationConfig;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generates element configuration for the Navigation component
|
|
32
|
+
* @param {NavigationConfig} config - Navigation configuration
|
|
33
|
+
* @returns {Object} Element configuration object for withElement
|
|
34
|
+
*/
|
|
35
|
+
export const getElementConfig = (config: NavigationConfig) =>
|
|
36
|
+
createElementConfig(config, {
|
|
37
|
+
tag: 'nav',
|
|
38
|
+
componentName: 'nav',
|
|
39
|
+
attrs: {
|
|
40
|
+
role: 'navigation',
|
|
41
|
+
'aria-label': config.ariaLabel || 'Main Navigation'
|
|
42
|
+
},
|
|
43
|
+
className: config.class
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates API configuration for the Navigation component
|
|
48
|
+
* @param {BaseComponent} comp - Component with disabled and lifecycle features
|
|
49
|
+
* @returns {ApiOptions} API configuration object
|
|
50
|
+
*/
|
|
51
|
+
export const getApiConfig = (comp: BaseComponent): ApiOptions => ({
|
|
52
|
+
disabled: {
|
|
53
|
+
enable: comp.disabled?.enable,
|
|
54
|
+
disable: comp.disabled?.disable
|
|
55
|
+
},
|
|
56
|
+
lifecycle: {
|
|
57
|
+
destroy: comp.lifecycle?.destroy
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default defaultConfig;
|