mtrl 0.2.8 → 0.2.9

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 (42) hide show
  1. package/index.ts +2 -0
  2. package/package.json +1 -1
  3. package/src/components/navigation/api.ts +131 -96
  4. package/src/components/navigation/features/controller.ts +273 -0
  5. package/src/components/navigation/features/items.ts +133 -64
  6. package/src/components/navigation/navigation.ts +17 -2
  7. package/src/components/navigation/system-types.ts +124 -0
  8. package/src/components/navigation/system.ts +776 -0
  9. package/src/components/slider/config.ts +20 -2
  10. package/src/components/slider/features/controller.ts +761 -0
  11. package/src/components/slider/features/handlers.ts +18 -15
  12. package/src/components/slider/features/index.ts +3 -2
  13. package/src/components/slider/features/range.ts +104 -0
  14. package/src/components/slider/slider.ts +34 -14
  15. package/src/components/slider/structure.ts +152 -0
  16. package/src/components/textfield/api.ts +53 -0
  17. package/src/components/textfield/features.ts +322 -0
  18. package/src/components/textfield/textfield.ts +8 -0
  19. package/src/components/textfield/types.ts +12 -3
  20. package/src/components/timepicker/clockdial.ts +1 -4
  21. package/src/core/compose/features/textinput.ts +15 -2
  22. package/src/core/composition/features/dom.ts +33 -0
  23. package/src/core/composition/features/icon.ts +131 -0
  24. package/src/core/composition/features/index.ts +11 -0
  25. package/src/core/composition/features/label.ts +156 -0
  26. package/src/core/composition/features/structure.ts +22 -0
  27. package/src/core/composition/index.ts +26 -0
  28. package/src/core/index.ts +1 -1
  29. package/src/core/structure.ts +288 -0
  30. package/src/index.ts +1 -0
  31. package/src/styles/components/_navigation-mobile.scss +244 -0
  32. package/src/styles/components/_navigation-system.scss +151 -0
  33. package/src/styles/components/_textfield.scss +250 -11
  34. package/demo/build.ts +0 -349
  35. package/demo/index.html +0 -110
  36. package/demo/main.js +0 -448
  37. package/demo/styles.css +0 -239
  38. package/server.ts +0 -86
  39. package/src/components/slider/features/slider.ts +0 -318
  40. package/src/components/slider/features/structure.ts +0 -181
  41. package/src/components/slider/features/ui.ts +0 -388
  42. package/src/components/textfield/constants.ts +0 -100
@@ -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,124 @@
1
+ // src/components/navigation/system-types.ts
2
+
3
+ import { NavigationComponent, NavItemConfig } from './types';
4
+
5
+ /**
6
+ * Structure for navigation section with its items
7
+ */
8
+ export interface NavigationSection {
9
+ /** Section label */
10
+ label: string;
11
+
12
+ /** Section icon HTML */
13
+ icon?: string;
14
+
15
+ /** Navigation items within this section */
16
+ items: NavItemConfig[];
17
+ }
18
+
19
+ /**
20
+ * Configuration for the navigation system
21
+ */
22
+ export interface NavigationSystemConfig {
23
+ /** Item structure by section ID */
24
+ items: Record<string, NavigationSection>;
25
+
26
+ /** Initial active section */
27
+ activeSection?: string;
28
+
29
+ /** Initial active subsection */
30
+ activeSubsection?: string;
31
+
32
+ /** Whether to animate drawer opening/closing */
33
+ animateDrawer?: boolean;
34
+
35
+ /** Whether to show labels on rail */
36
+ showLabelsOnRail?: boolean;
37
+
38
+ /** Whether to hide drawer when an item is clicked */
39
+ hideDrawerOnClick?: boolean;
40
+
41
+ /** Delay before showing drawer on hover (ms) */
42
+ hoverDelay?: number;
43
+
44
+ /** Delay before hiding drawer on mouse leave (ms) */
45
+ closeDelay?: number;
46
+
47
+ /** Configuration options passed to rail component */
48
+ railOptions?: Record<string, any>;
49
+
50
+ /** Configuration options passed to drawer component */
51
+ drawerOptions?: Record<string, any>;
52
+
53
+ /** Enable debug logging */
54
+ debug?: boolean;
55
+ }
56
+
57
+ /**
58
+ * Event data for section change events
59
+ */
60
+ export interface SectionChangeEvent {
61
+ /** ID of the selected section */
62
+ section: string;
63
+
64
+ /** Section data */
65
+ sectionData: NavigationSection;
66
+ }
67
+
68
+ /**
69
+ * Event data for item selection events
70
+ */
71
+ export interface ItemSelectEvent {
72
+ /** ID of the parent section */
73
+ section: string;
74
+
75
+ /** ID of the selected subsection */
76
+ subsection: string;
77
+
78
+ /** Selected item data */
79
+ item: any;
80
+ }
81
+
82
+ /**
83
+ * Navigation system API
84
+ */
85
+ export interface NavigationSystem {
86
+ /** Initialize the navigation system */
87
+ initialize(): NavigationSystem;
88
+
89
+ /** Clean up resources */
90
+ cleanup(): void;
91
+
92
+ /** Navigate to a specific section and subsection */
93
+ navigateTo(section: string, subsection?: string): void;
94
+
95
+ /** Get the rail component */
96
+ getRail(): NavigationComponent | null;
97
+
98
+ /** Get the drawer component */
99
+ getDrawer(): NavigationComponent | null;
100
+
101
+ /** Get the active section */
102
+ getActiveSection(): string | null;
103
+
104
+ /** Get the active subsection */
105
+ getActiveSubsection(): string | null;
106
+
107
+ /** Show the drawer */
108
+ showDrawer(): void;
109
+
110
+ /** Hide the drawer */
111
+ hideDrawer(): void;
112
+
113
+ /** Check if drawer is visible */
114
+ isDrawerVisible(): boolean;
115
+
116
+ /** Configure the navigation system */
117
+ configure(config: Partial<NavigationSystemConfig>): NavigationSystem;
118
+
119
+ /** Event handler for section changes */
120
+ onSectionChange: ((section: string, subsection?: string) => void) | null;
121
+
122
+ /** Event handler for item selection */
123
+ onItemSelect: ((event: ItemSelectEvent) => void) | null;
124
+ }