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.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/src/components/button/api.ts +16 -0
  3. package/src/components/button/types.ts +9 -0
  4. package/src/components/menu/api.ts +144 -267
  5. package/src/components/menu/config.ts +84 -40
  6. package/src/components/menu/features/anchor.ts +243 -0
  7. package/src/components/menu/features/controller.ts +1167 -0
  8. package/src/components/menu/features/index.ts +5 -0
  9. package/src/components/menu/features/position.ts +353 -0
  10. package/src/components/menu/index.ts +31 -63
  11. package/src/components/menu/menu.ts +72 -104
  12. package/src/components/menu/types.ts +264 -447
  13. package/src/components/select/api.ts +78 -0
  14. package/src/components/select/config.ts +76 -0
  15. package/src/components/select/features.ts +317 -0
  16. package/src/components/select/index.ts +38 -0
  17. package/src/components/select/select.ts +73 -0
  18. package/src/components/select/types.ts +355 -0
  19. package/src/components/textfield/api.ts +78 -6
  20. package/src/components/textfield/features/index.ts +17 -0
  21. package/src/components/textfield/features/leading-icon.ts +127 -0
  22. package/src/components/textfield/features/placement.ts +149 -0
  23. package/src/components/textfield/features/prefix-text.ts +107 -0
  24. package/src/components/textfield/features/suffix-text.ts +100 -0
  25. package/src/components/textfield/features/supporting-text.ts +113 -0
  26. package/src/components/textfield/features/trailing-icon.ts +108 -0
  27. package/src/components/textfield/textfield.ts +51 -15
  28. package/src/components/textfield/types.ts +70 -0
  29. package/src/core/collection/adapters/base.ts +62 -0
  30. package/src/core/collection/collection.ts +300 -0
  31. package/src/core/collection/index.ts +57 -0
  32. package/src/core/collection/list-manager.ts +333 -0
  33. package/src/core/dom/classes.ts +81 -9
  34. package/src/core/dom/create.ts +30 -19
  35. package/src/core/layout/README.md +531 -166
  36. package/src/core/layout/array.ts +3 -4
  37. package/src/core/layout/config.ts +193 -0
  38. package/src/core/layout/create.ts +1 -2
  39. package/src/core/layout/index.ts +12 -2
  40. package/src/core/layout/object.ts +2 -3
  41. package/src/core/layout/processor.ts +60 -12
  42. package/src/core/layout/result.ts +1 -2
  43. package/src/core/layout/types.ts +105 -50
  44. package/src/core/layout/utils.ts +69 -61
  45. package/src/index.ts +6 -2
  46. package/src/styles/abstract/_variables.scss +18 -0
  47. package/src/styles/components/_button.scss +21 -5
  48. package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
  49. package/src/styles/components/_menu.scss +109 -18
  50. package/src/styles/components/_select.scss +265 -0
  51. package/src/styles/components/_textfield.scss +233 -42
  52. package/src/styles/main.scss +24 -23
  53. package/src/styles/utilities/_layout.scss +665 -0
  54. package/src/components/menu/features/items-manager.ts +0 -457
  55. package/src/components/menu/features/keyboard-navigation.ts +0 -133
  56. package/src/components/menu/features/positioning.ts +0 -127
  57. package/src/components/menu/features/visibility.ts +0 -230
  58. package/src/components/menu/menu-item.ts +0 -86
  59. package/src/components/menu/utils.ts +0 -67
  60. package/src/components/textfield/features.ts +0 -322
  61. package/src/core/collection/adapters/base.js +0 -26
  62. package/src/core/collection/collection.js +0 -259
  63. package/src/core/collection/list-manager.js +0 -157
  64. /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
  65. /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;