mtrl 0.3.5 → 0.3.7
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/package.json +1 -1
- package/src/components/button/api.ts +16 -0
- package/src/components/button/types.ts +9 -0
- package/src/components/menu/api.ts +144 -267
- package/src/components/menu/config.ts +84 -40
- package/src/components/menu/features/anchor.ts +243 -0
- package/src/components/menu/features/controller.ts +1167 -0
- package/src/components/menu/features/index.ts +5 -0
- package/src/components/menu/features/position.ts +353 -0
- package/src/components/menu/index.ts +31 -63
- package/src/components/menu/menu.ts +72 -104
- package/src/components/menu/types.ts +264 -447
- package/src/components/select/api.ts +78 -0
- package/src/components/select/config.ts +76 -0
- package/src/components/select/features.ts +317 -0
- package/src/components/select/index.ts +38 -0
- package/src/components/select/select.ts +73 -0
- package/src/components/select/types.ts +355 -0
- package/src/components/textfield/api.ts +78 -6
- package/src/components/textfield/features/index.ts +17 -0
- package/src/components/textfield/features/leading-icon.ts +127 -0
- package/src/components/textfield/features/placement.ts +149 -0
- package/src/components/textfield/features/prefix-text.ts +107 -0
- package/src/components/textfield/features/suffix-text.ts +100 -0
- package/src/components/textfield/features/supporting-text.ts +113 -0
- package/src/components/textfield/features/trailing-icon.ts +108 -0
- package/src/components/textfield/textfield.ts +51 -15
- package/src/components/textfield/types.ts +70 -0
- package/src/core/collection/adapters/base.ts +62 -0
- package/src/core/collection/collection.ts +300 -0
- package/src/core/collection/index.ts +57 -0
- package/src/core/collection/list-manager.ts +333 -0
- package/src/core/dom/classes.ts +81 -9
- package/src/core/dom/create.ts +30 -19
- package/src/core/layout/README.md +531 -166
- package/src/core/layout/array.ts +3 -4
- package/src/core/layout/config.ts +193 -0
- package/src/core/layout/create.ts +1 -2
- package/src/core/layout/index.ts +12 -2
- package/src/core/layout/object.ts +2 -3
- package/src/core/layout/processor.ts +60 -12
- package/src/core/layout/result.ts +1 -2
- package/src/core/layout/types.ts +105 -50
- package/src/core/layout/utils.ts +69 -61
- package/src/index.ts +6 -2
- package/src/styles/abstract/_variables.scss +18 -0
- package/src/styles/components/_button.scss +21 -5
- package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
- package/src/styles/components/_menu.scss +109 -18
- package/src/styles/components/_select.scss +265 -0
- package/src/styles/components/_textfield.scss +233 -42
- package/src/styles/main.scss +24 -23
- package/src/styles/utilities/_layout.scss +665 -0
- package/src/components/menu/features/items-manager.ts +0 -457
- package/src/components/menu/features/keyboard-navigation.ts +0 -133
- package/src/components/menu/features/positioning.ts +0 -127
- package/src/components/menu/features/visibility.ts +0 -230
- package/src/components/menu/menu-item.ts +0 -86
- package/src/components/menu/utils.ts +0 -67
- package/src/components/textfield/features.ts +0 -322
- package/src/core/collection/adapters/base.js +0 -26
- package/src/core/collection/collection.js +0 -259
- package/src/core/collection/list-manager.js +0 -157
- /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
- /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// src/components/menu/features/anchor.ts
|
|
2
|
+
|
|
3
|
+
import { MenuConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds anchor functionality to menu component
|
|
7
|
+
* Manages the relationship between menu and its anchor element
|
|
8
|
+
*
|
|
9
|
+
* @param config - Menu configuration
|
|
10
|
+
* @returns Component enhancer with anchor management functionality
|
|
11
|
+
*/
|
|
12
|
+
export const withAnchor = (config: MenuConfig) => component => {
|
|
13
|
+
if (!component.element) {
|
|
14
|
+
console.warn('Cannot initialize menu anchor: missing element');
|
|
15
|
+
return component;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Track anchor state
|
|
19
|
+
const state = {
|
|
20
|
+
anchorElement: null as HTMLElement,
|
|
21
|
+
anchorComponent: null as any,
|
|
22
|
+
activeClass: '' // Store the appropriate active class based on element type
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the anchor element from string, direct reference, or component
|
|
27
|
+
*/
|
|
28
|
+
const resolveAnchor = (anchor: HTMLElement | string | { element: HTMLElement }): HTMLElement => {
|
|
29
|
+
if (!anchor) return null;
|
|
30
|
+
|
|
31
|
+
// Handle string selector
|
|
32
|
+
if (typeof anchor === 'string') {
|
|
33
|
+
const element = document.querySelector(anchor);
|
|
34
|
+
if (!element) {
|
|
35
|
+
console.warn(`Menu anchor not found: ${anchor}`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return element as HTMLElement;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle component with element property
|
|
42
|
+
if (typeof anchor === 'object' && anchor !== null && 'element' in anchor) {
|
|
43
|
+
return anchor.element;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle direct HTML element
|
|
47
|
+
return anchor as HTMLElement;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Determine the appropriate active class based on element type
|
|
52
|
+
*/
|
|
53
|
+
const determineActiveClass = (element: HTMLElement): string => {
|
|
54
|
+
// Check if this is one of our component types
|
|
55
|
+
const classPrefix = component.getClass('').split('-')[0];
|
|
56
|
+
|
|
57
|
+
// Check element tag and classes to determine appropriate active class
|
|
58
|
+
if (element.tagName === 'BUTTON') {
|
|
59
|
+
return `${classPrefix}-button--active`;
|
|
60
|
+
} else if (element.classList.contains(`${classPrefix}-chip`)) {
|
|
61
|
+
return `${classPrefix}-chip--selected`;
|
|
62
|
+
} else if (element.classList.contains(`${classPrefix}-textfield`) ||
|
|
63
|
+
element.classList.contains(`${classPrefix}-select`)) {
|
|
64
|
+
return `${classPrefix}-textfield--focused`;
|
|
65
|
+
} else {
|
|
66
|
+
// Default active class for other elements
|
|
67
|
+
return `${classPrefix}-menu-anchor--active`;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sets up anchor click handler for toggling menu
|
|
73
|
+
*/
|
|
74
|
+
const setupAnchorEvents = (anchorElement: HTMLElement, originalAnchor?: any): void => {
|
|
75
|
+
if (!anchorElement) return;
|
|
76
|
+
|
|
77
|
+
// Remove previously attached event if any
|
|
78
|
+
if (state.anchorElement && state.anchorElement !== anchorElement) {
|
|
79
|
+
cleanup();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Store references
|
|
83
|
+
state.anchorElement = anchorElement;
|
|
84
|
+
|
|
85
|
+
// Store reference to component if it was provided
|
|
86
|
+
if (originalAnchor && typeof originalAnchor === 'object' && 'element' in originalAnchor) {
|
|
87
|
+
state.anchorComponent = originalAnchor;
|
|
88
|
+
} else {
|
|
89
|
+
state.anchorComponent = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Determine the appropriate active class for this anchor
|
|
93
|
+
state.activeClass = determineActiveClass(anchorElement);
|
|
94
|
+
|
|
95
|
+
// Add click handler
|
|
96
|
+
anchorElement.addEventListener('click', handleAnchorClick);
|
|
97
|
+
|
|
98
|
+
// Add ARIA attributes
|
|
99
|
+
anchorElement.setAttribute('aria-haspopup', 'true');
|
|
100
|
+
anchorElement.setAttribute('aria-expanded', 'false');
|
|
101
|
+
|
|
102
|
+
// Get menu ID or generate one
|
|
103
|
+
let menuId = component.element.id;
|
|
104
|
+
if (!menuId) {
|
|
105
|
+
menuId = `menu-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
106
|
+
component.element.id = menuId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Connect menu and anchor with ARIA
|
|
110
|
+
anchorElement.setAttribute('aria-controls', menuId);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Applies active visual state to anchor
|
|
115
|
+
*/
|
|
116
|
+
const setAnchorActive = (active: boolean): void => {
|
|
117
|
+
if (!state.anchorElement) return;
|
|
118
|
+
|
|
119
|
+
// For component with setActive method (our button component has this)
|
|
120
|
+
if (state.anchorComponent && typeof state.anchorComponent.setActive === 'function') {
|
|
121
|
+
state.anchorComponent.setActive(active);
|
|
122
|
+
}
|
|
123
|
+
// For component with .selected property (like our chip component)
|
|
124
|
+
else if (state.anchorComponent && 'selected' in state.anchorComponent) {
|
|
125
|
+
state.anchorComponent.selected = active;
|
|
126
|
+
}
|
|
127
|
+
// Standard DOM element fallback
|
|
128
|
+
else if (state.anchorElement.classList) {
|
|
129
|
+
if (active) {
|
|
130
|
+
// Add the appropriate active class
|
|
131
|
+
state.anchorElement.classList.add(state.activeClass);
|
|
132
|
+
} else {
|
|
133
|
+
// Remove active class
|
|
134
|
+
state.anchorElement.classList.remove(state.activeClass);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Handles anchor element click
|
|
141
|
+
*/
|
|
142
|
+
const handleAnchorClick = (e: MouseEvent): void => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
|
|
145
|
+
// Toggle menu visibility
|
|
146
|
+
if (component.menu) {
|
|
147
|
+
const isOpen = component.menu.isOpen();
|
|
148
|
+
|
|
149
|
+
if (isOpen) {
|
|
150
|
+
component.menu.close(e);
|
|
151
|
+
} else {
|
|
152
|
+
component.menu.open(e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Removes event listeners from anchor
|
|
159
|
+
*/
|
|
160
|
+
const cleanup = (): void => {
|
|
161
|
+
if (state.anchorElement) {
|
|
162
|
+
state.anchorElement.removeEventListener('click', handleAnchorClick);
|
|
163
|
+
state.anchorElement.removeAttribute('aria-haspopup');
|
|
164
|
+
state.anchorElement.removeAttribute('aria-expanded');
|
|
165
|
+
state.anchorElement.removeAttribute('aria-controls');
|
|
166
|
+
|
|
167
|
+
// Clean up active state if present
|
|
168
|
+
setAnchorActive(false);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Reset state
|
|
172
|
+
state.anchorComponent = null;
|
|
173
|
+
state.activeClass = '';
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Initialize with provided anchor
|
|
177
|
+
const initialAnchor = config.anchor;
|
|
178
|
+
const initialElement = resolveAnchor(initialAnchor);
|
|
179
|
+
setupAnchorEvents(initialElement, initialAnchor);
|
|
180
|
+
|
|
181
|
+
// Register with lifecycle if available
|
|
182
|
+
if (component.lifecycle) {
|
|
183
|
+
const originalDestroy = component.lifecycle.destroy || (() => {});
|
|
184
|
+
component.lifecycle.destroy = () => {
|
|
185
|
+
cleanup();
|
|
186
|
+
originalDestroy();
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Listen for menu state changes to update anchor
|
|
191
|
+
component.on('open', () => {
|
|
192
|
+
if (state.anchorElement) {
|
|
193
|
+
state.anchorElement.setAttribute('aria-expanded', 'true');
|
|
194
|
+
setAnchorActive(true);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
component.on('close', () => {
|
|
199
|
+
if (state.anchorElement) {
|
|
200
|
+
state.anchorElement.setAttribute('aria-expanded', 'false');
|
|
201
|
+
setAnchorActive(false);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Return enhanced component
|
|
206
|
+
return {
|
|
207
|
+
...component,
|
|
208
|
+
anchor: {
|
|
209
|
+
/**
|
|
210
|
+
* Sets a new anchor element
|
|
211
|
+
* @param anchor - New anchor element, selector, or component
|
|
212
|
+
* @returns Component for chaining
|
|
213
|
+
*/
|
|
214
|
+
setAnchor(anchor: HTMLElement | string | { element: HTMLElement }) {
|
|
215
|
+
const newElement = resolveAnchor(anchor);
|
|
216
|
+
if (newElement) {
|
|
217
|
+
setupAnchorEvents(newElement, anchor);
|
|
218
|
+
}
|
|
219
|
+
return component;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Gets the current anchor element
|
|
224
|
+
* @returns Current anchor element
|
|
225
|
+
*/
|
|
226
|
+
getAnchor() {
|
|
227
|
+
return state.anchorElement;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sets the active state of the anchor
|
|
232
|
+
* @param active - Whether anchor should appear active
|
|
233
|
+
* @returns Component for chaining
|
|
234
|
+
*/
|
|
235
|
+
setActive(active: boolean) {
|
|
236
|
+
setAnchorActive(active);
|
|
237
|
+
return component;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export default withAnchor;
|