mtrl 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/index.ts +4 -0
  2. package/package.json +1 -1
  3. package/src/components/button/button.ts +34 -5
  4. package/src/components/navigation/api.ts +131 -96
  5. package/src/components/navigation/features/controller.ts +273 -0
  6. package/src/components/navigation/features/items.ts +133 -64
  7. package/src/components/navigation/navigation.ts +17 -2
  8. package/src/components/navigation/system/core.ts +302 -0
  9. package/src/components/navigation/system/events.ts +240 -0
  10. package/src/components/navigation/system/index.ts +184 -0
  11. package/src/components/navigation/system/mobile.ts +278 -0
  12. package/src/components/navigation/system/state.ts +77 -0
  13. package/src/components/navigation/system/types.ts +364 -0
  14. package/src/components/slider/config.ts +20 -2
  15. package/src/components/slider/features/controller.ts +737 -0
  16. package/src/components/slider/features/handlers.ts +18 -16
  17. package/src/components/slider/features/index.ts +3 -2
  18. package/src/components/slider/features/range.ts +104 -0
  19. package/src/components/slider/schema.ts +141 -0
  20. package/src/components/slider/slider.ts +34 -13
  21. package/src/components/switch/api.ts +16 -0
  22. package/src/components/switch/config.ts +1 -18
  23. package/src/components/switch/features.ts +198 -0
  24. package/src/components/switch/index.ts +1 -0
  25. package/src/components/switch/switch.ts +3 -3
  26. package/src/components/switch/types.ts +14 -2
  27. package/src/components/textfield/api.ts +53 -0
  28. package/src/components/textfield/features.ts +322 -0
  29. package/src/components/textfield/textfield.ts +8 -0
  30. package/src/components/textfield/types.ts +12 -3
  31. package/src/components/timepicker/clockdial.ts +1 -4
  32. package/src/core/compose/features/textinput.ts +15 -2
  33. package/src/core/composition/features/dom.ts +45 -0
  34. package/src/core/composition/features/icon.ts +131 -0
  35. package/src/core/composition/features/index.ts +12 -0
  36. package/src/core/composition/features/label.ts +155 -0
  37. package/src/core/composition/features/layout.ts +47 -0
  38. package/src/core/composition/index.ts +26 -0
  39. package/src/core/index.ts +1 -1
  40. package/src/core/layout/README.md +350 -0
  41. package/src/core/layout/array.ts +181 -0
  42. package/src/core/layout/create.ts +55 -0
  43. package/src/core/layout/index.ts +26 -0
  44. package/src/core/layout/object.ts +124 -0
  45. package/src/core/layout/processor.ts +58 -0
  46. package/src/core/layout/result.ts +85 -0
  47. package/src/core/layout/types.ts +125 -0
  48. package/src/core/layout/utils.ts +136 -0
  49. package/src/index.ts +1 -0
  50. package/src/styles/abstract/_variables.scss +28 -0
  51. package/src/styles/components/_navigation-mobile.scss +244 -0
  52. package/src/styles/components/_navigation-system.scss +151 -0
  53. package/src/styles/components/_switch.scss +133 -69
  54. package/src/styles/components/_textfield.scss +259 -27
  55. package/demo/build.ts +0 -349
  56. package/demo/index.html +0 -110
  57. package/demo/main.js +0 -448
  58. package/demo/styles.css +0 -239
  59. package/server.ts +0 -86
  60. package/src/components/slider/features/slider.ts +0 -318
  61. package/src/components/slider/features/structure.ts +0 -181
  62. package/src/components/slider/features/ui.ts +0 -388
  63. package/src/components/textfield/constants.ts +0 -100
  64. package/src/core/layout/index.js +0 -95
@@ -24,9 +24,28 @@ interface ItemsComponent extends BaseComponent {
24
24
  interface NavigationConfig {
25
25
  prefix?: string;
26
26
  items?: NavItemConfig[];
27
+ debug?: boolean;
27
28
  [key: string]: any;
28
29
  }
29
30
 
31
+ /**
32
+ * Helper to get element ID or component type
33
+ * @internal
34
+ */
35
+ function getElementId(element: HTMLElement | null, prefix: string): string | null {
36
+ if (!element) return null;
37
+
38
+ // Try to get data-id
39
+ const id = element.getAttribute('data-id');
40
+ if (id) return id;
41
+
42
+ // Try to identify by component class
43
+ if (element.classList.contains(`${prefix}-nav--rail`)) return 'rail';
44
+ if (element.classList.contains(`${prefix}-nav--drawer`)) return 'drawer';
45
+
46
+ return null;
47
+ }
48
+
30
49
  /**
31
50
  * Adds navigation items management to a component
32
51
  * @param {NavigationConfig} config - Navigation configuration
@@ -108,72 +127,52 @@ export const withNavItems = (config: NavigationConfig) => (component: BaseCompon
108
127
  });
109
128
  }
110
129
 
111
- // Handle item clicks
112
- component.element.addEventListener('click', (event: Event) => {
113
- const item = (event.target as HTMLElement).closest(`.${prefix}-${NavClass.ITEM}`) as HTMLElement;
114
- if (!item || (item as any).disabled || item.getAttribute('aria-haspopup') === 'menu') return;
115
-
116
- const id = item.dataset.id;
117
- if (!id) return;
118
-
119
- const itemData = items.get(id);
120
- if (!itemData) return;
121
-
122
- // Skip if this is an expandable item
123
- if (item.getAttribute('aria-expanded') !== null) return;
124
-
125
- // Store previous item before updating
126
- const previousItem = activeItem;
127
-
128
- // Update active state
129
- if (activeItem) {
130
- updateActiveState(activeItem.element, activeItem, false);
131
- }
132
-
133
- updateActiveState(item, itemData, true);
134
- activeItem = itemData;
130
+ // Set up enhanced event handling for mouse events
131
+ if (component.emit) {
132
+ // Mouse over event handler
133
+ component.element.addEventListener('mouseover', (event: MouseEvent) => {
134
+ // Find the closest item with data-id
135
+ const target = event.target as HTMLElement;
136
+ const item = target.closest(`[data-id]`) as HTMLElement;
137
+ if (item) {
138
+ const id = item.dataset.id;
139
+ if (id) {
140
+ component.emit('mouseover', {
141
+ id,
142
+ clientX: event.clientX,
143
+ clientY: event.clientY,
144
+ target: item,
145
+ item: items.get(id)
146
+ });
147
+ }
148
+ }
149
+ }, { passive: true });
135
150
 
136
- // Emit change event with item data
137
- if (component.emit) {
138
- component.emit('change', {
139
- id,
140
- item: itemData,
141
- previousItem,
142
- path: getItemPath(id)
151
+ // Mouse enter event
152
+ component.element.addEventListener('mouseenter', (event: MouseEvent) => {
153
+ const componentId = component.element.dataset.id || component.componentName || 'nav';
154
+
155
+ component.emit('mouseenter', {
156
+ clientX: event.clientX,
157
+ clientY: event.clientY,
158
+ id: componentId
143
159
  });
144
- }
145
- });
146
-
147
- /**
148
- * Gets the path to an item (parent IDs)
149
- * @param {string} id - Item ID to get path for
150
- * @returns {Array<string>} Array of parent item IDs
151
- */
152
- const getItemPath = (id: string): string[] => {
153
- const path: string[] = [];
154
- let currentItem = items.get(id);
155
-
156
- if (!currentItem) return path;
157
-
158
- let parentContainer = currentItem.element.closest(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
159
- while (parentContainer) {
160
- const parentItemContainer = parentContainer.parentElement;
161
- if (!parentItemContainer) break;
160
+ }, { passive: true });
162
161
 
163
- const parentItem = parentItemContainer.querySelector(`.${prefix}-${NavClass.ITEM}`);
164
- if (!parentItem) break;
165
-
166
- const parentId = parentItem.getAttribute('data-id');
167
- if (!parentId) break;
168
-
169
- path.unshift(parentId);
162
+ // Mouse leave event
163
+ component.element.addEventListener('mouseleave', (event: MouseEvent) => {
164
+ const relatedTarget = event.relatedTarget as HTMLElement;
165
+ const relatedTargetId = getElementId(relatedTarget, prefix);
166
+ const componentId = component.element.dataset.id || component.componentName || 'nav';
170
167
 
171
- // Move up to next level
172
- parentContainer = parentItemContainer.closest(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
173
- }
174
-
175
- return path;
176
- };
168
+ component.emit('mouseleave', {
169
+ clientX: event.clientX,
170
+ clientY: event.clientY,
171
+ relatedTargetId,
172
+ id: componentId
173
+ });
174
+ });
175
+ }
177
176
 
178
177
  // Clean up when component is destroyed
179
178
  if (component.lifecycle) {
@@ -186,6 +185,76 @@ export const withNavItems = (config: NavigationConfig) => (component: BaseCompon
186
185
  };
187
186
  }
188
187
 
188
+ /**
189
+ * Gets the path to an item (parent IDs and the item's own ID)
190
+ * @param {string} id - Item ID to get path for
191
+ * @returns {Array<string>} Array of parent item IDs including the item's own ID
192
+ */
193
+ const getItemPath = (id: string): string[] => {
194
+ // Always include the item's own ID in the path
195
+ const path: string[] = [id];
196
+ const currentItem = items.get(id);
197
+
198
+ if (!currentItem) {
199
+ return path; // Still return [id] even if item not found in map
200
+ }
201
+
202
+ // First, find the item container that contains this element
203
+ const itemContainer = currentItem.element.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
204
+ if (!itemContainer) {
205
+ return path;
206
+ }
207
+
208
+ // Then find the parent element of the item container
209
+ const parentElement = itemContainer.parentElement;
210
+
211
+ // If parent element is not a nested container, this is a root-level item
212
+ // Just return the item's own ID
213
+ if (!parentElement || !parentElement.classList.contains(`${prefix}-${NavClass.NESTED_CONTAINER}`)) {
214
+ return path;
215
+ }
216
+
217
+ // We're dealing with a nested item - find its ancestors
218
+ let currentNestedContainer = parentElement;
219
+
220
+ while (currentNestedContainer) {
221
+ // Find the parent item container
222
+ const parentItemContainer = currentNestedContainer.parentElement;
223
+
224
+ if (!parentItemContainer || !parentItemContainer.classList.contains(`${prefix}-${NavClass.ITEM_CONTAINER}`)) {
225
+ break;
226
+ }
227
+
228
+ // Find the parent item button/element
229
+ const parentItem = parentItemContainer.querySelector(`.${prefix}-${NavClass.ITEM}[data-id]`);
230
+
231
+ if (!parentItem) {
232
+ break;
233
+ }
234
+
235
+ const parentId = parentItem.getAttribute('data-id');
236
+
237
+ if (!parentId) {
238
+ break;
239
+ }
240
+
241
+ // Add to the beginning of path (since we're going up the tree)
242
+ // This puts ancestors before the item's own ID
243
+ path.unshift(parentId);
244
+
245
+ // Move up to the next level - find the container of this parent's container
246
+ const grandparentElement = parentItemContainer.parentElement;
247
+
248
+ if (!grandparentElement || !grandparentElement.classList.contains(`${prefix}-${NavClass.NESTED_CONTAINER}`)) {
249
+ break;
250
+ }
251
+
252
+ currentNestedContainer = grandparentElement;
253
+ }
254
+
255
+ return path;
256
+ };
257
+
189
258
  return {
190
259
  ...component,
191
260
  items,
@@ -212,7 +281,7 @@ export const withNavItems = (config: NavigationConfig) => (component: BaseCompon
212
281
  removeItem(id: string) {
213
282
  const item = items.get(id);
214
283
  if (!item) return this;
215
-
284
+
216
285
  // Remove all nested items first
217
286
  const nestedItems = getAllNestedItems(item.element, prefix);
218
287
  nestedItems.forEach(nestedItem => {
@@ -245,7 +314,7 @@ export const withNavItems = (config: NavigationConfig) => (component: BaseCompon
245
314
  setActive(id: string) {
246
315
  const item = items.get(id);
247
316
  if (!item || item.config.disabled) return this;
248
-
317
+
249
318
  if (activeItem) {
250
319
  updateActiveState(activeItem.element, activeItem, false);
251
320
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../core/compose/features';
11
11
  import { withAPI } from './api';
12
12
  import { withNavItems } from './features/items';
13
+ import { withController } from './features/controller';
13
14
  import { NavigationConfig, NavigationComponent, NavVariant } from './types';
14
15
  import {
15
16
  createBaseConfig,
@@ -68,15 +69,21 @@ const createNavigation = (config: NavigationConfig = {}): NavigationComponent =>
68
69
  try {
69
70
  const navigation = pipe(
70
71
  createBase,
71
- // First add events system
72
+ // First add events system - MUST be before other features that use events
72
73
  withEvents(),
73
74
  // Then add the element and other features
74
75
  withElement(getElementConfig(baseConfig)),
76
+ // Add core features
75
77
  withVariant(baseConfig),
76
78
  withPosition(baseConfig),
79
+ // Add navigation-specific features
77
80
  withNavItems(baseConfig),
81
+ // Add controller for event delegation AFTER items are set up
82
+ withController(baseConfig),
83
+ // Add standard component features
78
84
  withDisabled(baseConfig),
79
85
  withLifecycle(),
86
+ // Finally add the API - this must be last to include all features
80
87
  comp => withAPI(getApiConfig(comp))(comp)
81
88
  )(baseConfig);
82
89
 
@@ -89,10 +96,18 @@ const createNavigation = (config: NavigationConfig = {}): NavigationComponent =>
89
96
  if (baseConfig.disabled) {
90
97
  nav.disable();
91
98
  }
99
+
100
+ // Set component variant property for component identification
101
+ nav.variant = baseConfig.variant;
102
+
103
+ // Add explicit component identifier for debugging
104
+ nav.element.dataset.componentType = 'navigation';
105
+ if (baseConfig.variant) {
106
+ nav.element.dataset.variant = baseConfig.variant;
107
+ }
92
108
 
93
109
  return nav;
94
110
  } catch (error) {
95
- console.error('Navigation creation error:', error instanceof Error ? error.message : String(error));
96
111
  throw new Error(`Failed to create navigation: ${error instanceof Error ? error.message : String(error)}`);
97
112
  }
98
113
  };
@@ -0,0 +1,302 @@
1
+ // src/components/navigation/system/core.ts
2
+
3
+ import { NavigationSystemState, NavigationSystemConfig, NavigationItem } from './types';
4
+ import { isMobileDevice } from '../../../core/utils/mobile';
5
+ import createNavigation from '../navigation';
6
+
7
+ /**
8
+ * Update drawer content for a specific section WITHOUT changing visibility
9
+ *
10
+ * @param state - System state
11
+ * @param sectionId - Section ID to display
12
+ * @param showDrawer - Function to show the drawer
13
+ * @param hideDrawer - Function to hide the drawer
14
+ */
15
+ export const updateDrawerContent = (
16
+ state: NavigationSystemState,
17
+ sectionId: string,
18
+ showDrawer: () => void,
19
+ hideDrawer: () => void
20
+ ): void => {
21
+ if (!state.drawer || !sectionId || !state.items[sectionId]) {
22
+ return;
23
+ }
24
+
25
+ // Get section items
26
+ const sectionData = state.items[sectionId];
27
+ const items = sectionData.items || [];
28
+
29
+ // If no items, hide drawer and exit
30
+ if (items.length === 0) {
31
+ hideDrawer();
32
+ return;
33
+ }
34
+
35
+ // Clear existing drawer items first using the API
36
+ const currentItems = state.drawer.getAllItems();
37
+ if (currentItems?.length > 0) {
38
+ currentItems.forEach((item: any) => {
39
+ state.drawer.removeItem(item.config.id);
40
+ });
41
+ }
42
+
43
+ // Add new items to drawer through the API
44
+ items.forEach((item: NavigationItem) => {
45
+ state.drawer.addItem(item);
46
+ });
47
+
48
+ // Show the drawer
49
+ showDrawer();
50
+ };
51
+
52
+ /**
53
+ * Creates the rail navigation component
54
+ *
55
+ * @param state - System state
56
+ * @param config - System configuration
57
+ * @returns Rail navigation component
58
+ */
59
+ export const createRailNavigation = (
60
+ state: NavigationSystemState,
61
+ config: any
62
+ ): any => {
63
+ // Build rail items from sections
64
+ const railItems = Object.keys(state.items || {}).map(sectionId => ({
65
+ id: sectionId,
66
+ label: state.items[sectionId]?.label || sectionId,
67
+ icon: state.items[sectionId]?.icon || '',
68
+ active: sectionId === state.activeSection
69
+ }));
70
+
71
+ // Create the rail component
72
+ const rail = createNavigation({
73
+ variant: 'rail',
74
+ position: 'left',
75
+ showLabels: config.showLabelsOnRail,
76
+ items: railItems,
77
+ ...config.railOptions
78
+ });
79
+
80
+ document.body.appendChild(rail.element);
81
+ return rail;
82
+ };
83
+
84
+ /**
85
+ * Creates the drawer navigation component
86
+ *
87
+ * @param state - System state
88
+ * @param config - System configuration
89
+ * @returns Drawer navigation component
90
+ */
91
+ export const createDrawerNavigation = (
92
+ state: NavigationSystemState,
93
+ config: any
94
+ ): any => {
95
+ // Create the drawer component (initially empty)
96
+ const drawer = createNavigation({
97
+ variant: 'drawer',
98
+ position: 'left',
99
+ items: [], // Start empty
100
+ ...config.drawerOptions
101
+ });
102
+
103
+ document.body.appendChild(drawer.element);
104
+
105
+ // Mark drawer with identifier
106
+ drawer.element.dataset.id = 'drawer';
107
+
108
+ // IMPORTANT: Make drawer initially hidden unless explicitly expanded
109
+ if (!config.expanded) {
110
+ drawer.element.classList.add('mtrl-nav--hidden');
111
+ drawer.element.setAttribute('aria-hidden', 'true');
112
+ }
113
+
114
+ return drawer;
115
+ };
116
+
117
+ /**
118
+ * Shows the drawer with mobile-specific behaviors
119
+ *
120
+ * @param state - System state
121
+ * @param mobileConfig - Mobile configuration
122
+ */
123
+ export const showDrawer = (
124
+ state: NavigationSystemState,
125
+ mobileConfig: any
126
+ ): void => {
127
+ if (!state.drawer) return;
128
+
129
+ state.drawer.element.classList.remove('mtrl-nav--hidden');
130
+ state.drawer.element.setAttribute('aria-hidden', 'false');
131
+
132
+ // Apply mobile-specific behaviors
133
+ if (state.isMobile) {
134
+ if (state.overlayElement) {
135
+ state.overlayElement.classList.add('active');
136
+ state.overlayElement.setAttribute('aria-hidden', 'false');
137
+ }
138
+
139
+ // Lock body scroll if enabled
140
+ if (mobileConfig.lockBodyScroll) {
141
+ document.body.classList.add(mobileConfig.bodyLockClass);
142
+ }
143
+
144
+ // Ensure close button is visible
145
+ if (state.closeButtonElement) {
146
+ state.closeButtonElement.style.display = 'flex';
147
+ }
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Hides the drawer with mobile-specific behaviors
153
+ *
154
+ * @param state - System state
155
+ * @param mobileConfig - Mobile configuration
156
+ */
157
+ export const hideDrawer = (
158
+ state: NavigationSystemState,
159
+ mobileConfig: any
160
+ ): void => {
161
+ if (!state.drawer) return;
162
+
163
+ state.drawer.element.classList.add('mtrl-nav--hidden');
164
+ state.drawer.element.setAttribute('aria-hidden', 'true');
165
+
166
+ // Remove mobile-specific effects
167
+ if (state.overlayElement) {
168
+ state.overlayElement.classList.remove('active');
169
+ state.overlayElement.setAttribute('aria-hidden', 'true');
170
+ }
171
+
172
+ // Unlock body scroll
173
+ if (mobileConfig.lockBodyScroll) {
174
+ document.body.classList.remove(mobileConfig.bodyLockClass);
175
+ }
176
+ };
177
+
178
+ /**
179
+ * Checks if drawer is visible
180
+ *
181
+ * @param state - System state
182
+ * @returns true if drawer is visible
183
+ */
184
+ export const isDrawerVisible = (
185
+ state: NavigationSystemState
186
+ ): boolean => {
187
+ if (!state.drawer) return false;
188
+ return !state.drawer.element.classList.contains('mtrl-nav--hidden');
189
+ };
190
+
191
+ /**
192
+ * Checks and updates the mobile state
193
+ *
194
+ * @param state - System state
195
+ * @param mobileConfig - Mobile configuration
196
+ * @param setupMobileMode - Function to set up mobile mode
197
+ * @param teardownMobileMode - Function to tear down mobile mode
198
+ * @param systemApi - Reference to the public system API
199
+ */
200
+ export const checkMobileState = (
201
+ state: NavigationSystemState,
202
+ mobileConfig: any,
203
+ setupMobileMode: () => void,
204
+ teardownMobileMode: () => void,
205
+ systemApi: any
206
+ ): void => {
207
+ const prevState = state.isMobile;
208
+ state.isMobile = window.innerWidth <= mobileConfig.breakpoint || isMobileDevice();
209
+
210
+ // If state changed, adjust UI
211
+ if (prevState !== state.isMobile) {
212
+ if (state.isMobile) {
213
+ // Switched to mobile mode
214
+ setupMobileMode();
215
+ } else {
216
+ // Switched to desktop mode
217
+ teardownMobileMode();
218
+ }
219
+
220
+ // Emit a view change event
221
+ if (systemApi.onViewChange) {
222
+ systemApi.onViewChange({
223
+ mobile: state.isMobile,
224
+ previousMobile: prevState,
225
+ width: window.innerWidth
226
+ });
227
+ }
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Clean up resources when the system is destroyed
233
+ *
234
+ * @param state - System state
235
+ */
236
+ export const cleanupResources = (
237
+ state: NavigationSystemState
238
+ ): void => {
239
+ // Clean up overlay
240
+ if (state.overlayElement && state.overlayElement.parentNode) {
241
+ state.overlayElement.parentNode.removeChild(state.overlayElement);
242
+ state.overlayElement = null;
243
+ }
244
+
245
+ // Destroy components
246
+ if (state.rail) {
247
+ state.rail.destroy();
248
+ state.rail = null;
249
+ }
250
+
251
+ if (state.drawer) {
252
+ state.drawer.destroy();
253
+ state.drawer = null;
254
+ }
255
+
256
+ // Reset state
257
+ state.activeSection = null;
258
+ state.activeSubsection = null;
259
+ state.mouseInDrawer = false;
260
+ state.mouseInRail = false;
261
+ state.processingChange = false;
262
+ state.isMobile = false;
263
+ };
264
+
265
+ /**
266
+ * Navigate to a specific section and subsection
267
+ *
268
+ * @param state - System state
269
+ * @param section - Section ID
270
+ * @param subsection - Subsection ID (optional)
271
+ * @param silent - Whether to suppress change events
272
+ */
273
+ export const navigateTo = (
274
+ state: NavigationSystemState,
275
+ section: string,
276
+ subsection?: string,
277
+ silent?: boolean
278
+ ): void => {
279
+ // Skip if section doesn't exist
280
+ if (!section || !state.items[section]) {
281
+ return;
282
+ }
283
+
284
+ // Check if we're already on this section and subsection
285
+ if (state.activeSection === section && state.activeSubsection === subsection) {
286
+ return;
287
+ }
288
+
289
+ // Update active section
290
+ state.activeSection = section;
291
+
292
+ // Update rail if it exists
293
+ if (state.rail) {
294
+ state.rail.setActive(section, silent);
295
+ }
296
+
297
+ // Update active subsection if specified
298
+ if (subsection && state.drawer) {
299
+ state.activeSubsection = subsection;
300
+ state.drawer.setActive(subsection, silent);
301
+ }
302
+ };