mtrl 0.2.7 → 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 (190) hide show
  1. package/index.ts +2 -0
  2. package/package.json +14 -3
  3. package/src/components/badge/api.ts +23 -14
  4. package/src/components/badge/badge.ts +2 -2
  5. package/src/components/badge/config.ts +10 -11
  6. package/src/components/badge/features.ts +15 -10
  7. package/src/components/badge/index.ts +27 -2
  8. package/src/components/badge/types.ts +28 -8
  9. package/src/components/bottom-app-bar/bottom-app-bar.ts +2 -44
  10. package/src/components/bottom-app-bar/config.ts +1 -45
  11. package/src/components/bottom-app-bar/index.ts +7 -1
  12. package/src/components/bottom-app-bar/types.ts +7 -1
  13. package/src/components/button/button.ts +0 -1
  14. package/src/components/button/config.ts +1 -2
  15. package/src/components/button/index.ts +10 -2
  16. package/src/components/button/types.ts +14 -2
  17. package/src/components/card/config.ts +17 -9
  18. package/src/components/card/content.ts +8 -10
  19. package/src/components/card/features.ts +4 -6
  20. package/src/components/card/index.ts +29 -2
  21. package/src/components/card/types.ts +6 -23
  22. package/src/components/checkbox/config.ts +3 -4
  23. package/src/components/checkbox/index.ts +1 -2
  24. package/src/components/checkbox/types.ts +12 -3
  25. package/src/components/chip/api.ts +170 -221
  26. package/src/components/chip/chip.ts +34 -302
  27. package/src/components/chip/config.ts +1 -2
  28. package/src/components/chip/index.ts +10 -2
  29. package/src/components/chip/types.ts +224 -35
  30. package/src/components/datepicker/api.ts +18 -25
  31. package/src/components/datepicker/config.ts +9 -12
  32. package/src/components/datepicker/datepicker.ts +7 -12
  33. package/src/components/datepicker/index.ts +10 -7
  34. package/src/components/datepicker/render.ts +16 -18
  35. package/src/components/datepicker/types.ts +164 -35
  36. package/src/components/datepicker/utils.ts +1 -2
  37. package/src/components/dialog/api.ts +7 -8
  38. package/src/components/dialog/config.ts +3 -4
  39. package/src/components/dialog/features.ts +56 -22
  40. package/src/components/dialog/index.ts +38 -8
  41. package/src/components/dialog/types.ts +33 -10
  42. package/src/components/divider/index.ts +5 -1
  43. package/src/components/extended-fab/config.ts +6 -2
  44. package/src/components/extended-fab/index.ts +7 -2
  45. package/src/components/extended-fab/types.ts +21 -4
  46. package/src/components/fab/config.ts +3 -4
  47. package/src/components/fab/fab.ts +1 -1
  48. package/src/components/fab/index.ts +7 -2
  49. package/src/components/fab/types.ts +21 -4
  50. package/src/components/list/config.ts +4 -5
  51. package/src/components/list/features.ts +6 -7
  52. package/src/components/list/index.ts +7 -9
  53. package/src/components/list/list-item.ts +12 -13
  54. package/src/components/list/types.ts +50 -5
  55. package/src/components/list/utils.ts +30 -3
  56. package/src/components/menu/features/items-manager.ts +9 -9
  57. package/src/components/menu/features/positioning.ts +7 -7
  58. package/src/components/menu/features/visibility.ts +7 -7
  59. package/src/components/menu/index.ts +7 -9
  60. package/src/components/menu/menu-item.ts +6 -6
  61. package/src/components/menu/menu.ts +22 -0
  62. package/src/components/menu/types.ts +29 -10
  63. package/src/components/menu/utils.ts +67 -0
  64. package/src/components/navigation/api.ts +131 -96
  65. package/src/components/navigation/config.ts +22 -10
  66. package/src/components/navigation/features/controller.ts +273 -0
  67. package/src/components/navigation/features/items.ts +160 -87
  68. package/src/components/navigation/index.ts +0 -6
  69. package/src/components/navigation/nav-item.ts +12 -24
  70. package/src/components/navigation/navigation.ts +21 -8
  71. package/src/components/navigation/system-types.ts +124 -0
  72. package/src/components/navigation/system.ts +776 -0
  73. package/src/components/navigation/types.ts +228 -203
  74. package/src/components/progress/api.ts +2 -3
  75. package/src/components/progress/config.ts +2 -3
  76. package/src/components/progress/index.ts +0 -1
  77. package/src/components/progress/progress.ts +1 -2
  78. package/src/components/progress/types.ts +186 -33
  79. package/src/components/radios/config.ts +1 -1
  80. package/src/components/radios/index.ts +0 -1
  81. package/src/components/radios/types.ts +0 -7
  82. package/src/components/search/config.ts +1 -2
  83. package/src/components/search/features/search.ts +14 -15
  84. package/src/components/search/features/states.ts +5 -1
  85. package/src/components/search/features/structure.ts +3 -4
  86. package/src/components/search/index.ts +0 -3
  87. package/src/components/search/types.ts +18 -6
  88. package/src/components/segmented-button/config.ts +20 -7
  89. package/src/components/segmented-button/segment.ts +6 -7
  90. package/src/components/segmented-button/segmented-button.ts +4 -5
  91. package/src/components/segmented-button/types.ts +37 -2
  92. package/src/components/slider/config.ts +20 -2
  93. package/src/components/slider/features/controller.ts +761 -0
  94. package/src/components/slider/features/handlers.ts +18 -15
  95. package/src/components/slider/features/index.ts +3 -2
  96. package/src/components/slider/features/range.ts +104 -0
  97. package/src/components/slider/slider.ts +34 -14
  98. package/src/components/slider/structure.ts +152 -0
  99. package/src/components/slider/types.ts +34 -8
  100. package/src/components/snackbar/config.ts +2 -3
  101. package/src/components/snackbar/constants.ts +0 -32
  102. package/src/components/snackbar/index.ts +0 -1
  103. package/src/components/snackbar/position.ts +9 -1
  104. package/src/components/snackbar/types.ts +122 -46
  105. package/src/components/switch/config.ts +2 -3
  106. package/src/components/switch/index.ts +0 -1
  107. package/src/components/switch/types.ts +3 -2
  108. package/src/components/tabs/config.ts +3 -4
  109. package/src/components/tabs/index.ts +0 -15
  110. package/src/components/tabs/tab-api.ts +12 -4
  111. package/src/components/tabs/tab.ts +18 -6
  112. package/src/components/tabs/types.ts +13 -3
  113. package/src/components/textfield/api.ts +53 -0
  114. package/src/components/textfield/config.ts +2 -3
  115. package/src/components/textfield/features.ts +322 -0
  116. package/src/components/textfield/index.ts +0 -1
  117. package/src/components/textfield/textfield.ts +8 -0
  118. package/src/components/textfield/types.ts +29 -6
  119. package/src/components/timepicker/api.ts +1 -1
  120. package/src/components/timepicker/clockdial.ts +2 -5
  121. package/src/components/timepicker/config.ts +102 -4
  122. package/src/components/timepicker/index.ts +1 -6
  123. package/src/components/timepicker/render.ts +1 -1
  124. package/src/components/timepicker/timepicker.ts +1 -1
  125. package/src/components/tooltip/api.ts +1 -1
  126. package/src/components/tooltip/config.ts +27 -6
  127. package/src/components/tooltip/index.ts +0 -1
  128. package/src/components/tooltip/types.ts +13 -3
  129. package/src/core/compose/features/textinput.ts +15 -2
  130. package/src/core/compose/features/textlabel.ts +0 -3
  131. package/src/core/composition/features/dom.ts +33 -0
  132. package/src/core/composition/features/icon.ts +131 -0
  133. package/src/core/composition/features/index.ts +11 -0
  134. package/src/core/composition/features/label.ts +156 -0
  135. package/src/core/composition/features/structure.ts +22 -0
  136. package/src/core/composition/index.ts +26 -0
  137. package/src/core/index.ts +1 -1
  138. package/src/core/structure.ts +288 -0
  139. package/src/index.ts +1 -0
  140. package/src/styles/components/_navigation-mobile.scss +244 -0
  141. package/src/styles/components/_navigation-system.scss +151 -0
  142. package/src/{components/tabs/_styles.scss → styles/components/_tabs.scss} +1 -1
  143. package/src/{components/textfield/_styles.scss → styles/components/_textfield.scss} +314 -72
  144. package/src/styles/main.scss +98 -49
  145. package/src/components/badge/constants.ts +0 -40
  146. package/src/components/button/constants.ts +0 -11
  147. package/src/components/card/constants.ts +0 -84
  148. package/src/components/datepicker/constants.ts +0 -98
  149. package/src/components/dialog/constants.ts +0 -32
  150. package/src/components/extended-fab/constants.ts +0 -36
  151. package/src/components/fab/constants.ts +0 -41
  152. package/src/components/menu/constants.ts +0 -154
  153. package/src/components/navigation/constants.ts +0 -200
  154. package/src/components/progress/constants.ts +0 -29
  155. package/src/components/search/constants.ts +0 -21
  156. package/src/components/segmented-button/constants.ts +0 -42
  157. package/src/components/slider/features/slider.ts +0 -318
  158. package/src/components/slider/features/structure.ts +0 -181
  159. package/src/components/slider/features/ui.ts +0 -388
  160. package/src/components/switch/constants.ts +0 -80
  161. package/src/components/tabs/constants.ts +0 -89
  162. package/src/components/textfield/constants.ts +0 -100
  163. package/src/components/timepicker/constants.ts +0 -138
  164. /package/src/{components/badge/_styles.scss → styles/components/_badge.scss} +0 -0
  165. /package/src/{components/bottom-app-bar/_styles.scss → styles/components/_bottom-app-bar.scss} +0 -0
  166. /package/src/{components/button/_styles.scss → styles/components/_button.scss} +0 -0
  167. /package/src/{components/card/_styles.scss → styles/components/_card.scss} +0 -0
  168. /package/src/{components/carousel/_styles.scss → styles/components/_carousel.scss} +0 -0
  169. /package/src/{components/checkbox/_styles.scss → styles/components/_checkbox.scss} +0 -0
  170. /package/src/{components/chip/_styles.scss → styles/components/_chip.scss} +0 -0
  171. /package/src/{components/datepicker/_styles.scss → styles/components/_datepicker.scss} +0 -0
  172. /package/src/{components/dialog/_styles.scss → styles/components/_dialog.scss} +0 -0
  173. /package/src/{components/divider/_styles.scss → styles/components/_divider.scss} +0 -0
  174. /package/src/{components/extended-fab/_styles.scss → styles/components/_extended-fab.scss} +0 -0
  175. /package/src/{components/fab/_styles.scss → styles/components/_fab.scss} +0 -0
  176. /package/src/{components/list/_styles.scss → styles/components/_list.scss} +0 -0
  177. /package/src/{components/menu/_styles.scss → styles/components/_menu.scss} +0 -0
  178. /package/src/{components/navigation/_styles.scss → styles/components/_navigation.scss} +0 -0
  179. /package/src/{components/progress/_styles.scss → styles/components/_progress.scss} +0 -0
  180. /package/src/{components/radios/_styles.scss → styles/components/_radios.scss} +0 -0
  181. /package/src/{components/search/_styles.scss → styles/components/_search.scss} +0 -0
  182. /package/src/{components/segmented-button/_styles.scss → styles/components/_segmented-button.scss} +0 -0
  183. /package/src/{components/sheet/_styles.scss → styles/components/_sheet.scss} +0 -0
  184. /package/src/{components/slider/_styles.scss → styles/components/_slider.scss} +0 -0
  185. /package/src/{components/snackbar/_styles.scss → styles/components/_snackbar.scss} +0 -0
  186. /package/src/{components/switch/_styles.scss → styles/components/_switch.scss} +0 -0
  187. /package/src/{components/timepicker/_styles.scss → styles/components/_timepicker.scss} +0 -0
  188. /package/src/{components/tooltip/_styles.scss → styles/components/_tooltip.scss} +0 -0
  189. /package/src/{components/top-app-bar/_styles.scss → styles/components/_top-app-bar.scss} +0 -0
  190. /package/src/styles/utilities/{_color.scss → _colors.scss} +0 -0
@@ -5,15 +5,14 @@ import {
5
5
  BaseComponentConfig
6
6
  } from '../../core/config/component-config';
7
7
  import { NavigationConfig, BaseComponent, ApiOptions } from './types';
8
- import { NAV_VARIANTS, NAV_POSITIONS, NAV_BEHAVIORS } from './constants';
9
8
 
10
9
  /**
11
10
  * Default configuration for the Navigation component
12
11
  */
13
12
  export const defaultConfig: NavigationConfig = {
14
- variant: NAV_VARIANTS.RAIL,
15
- position: NAV_POSITIONS.LEFT,
16
- behavior: NAV_BEHAVIORS.FIXED,
13
+ variant: 'rail',
14
+ position: 'left',
15
+ behavior: 'fixed',
17
16
  items: [],
18
17
  showLabels: true,
19
18
  scrimEnabled: false
@@ -32,16 +31,29 @@ export const createBaseConfig = (config: NavigationConfig = {}): NavigationConfi
32
31
  * @param {NavigationConfig} config - Navigation configuration
33
32
  * @returns {Object} Element configuration object for withElement
34
33
  */
35
- export const getElementConfig = (config: NavigationConfig) =>
36
- createElementConfig(config, {
34
+ export const getElementConfig = (config: NavigationConfig) => {
35
+ // Build class list - start with the variant class
36
+ const variantClass = config.variant ? `${config.prefix}-nav--${config.variant}` : '';
37
+
38
+ // Add position class if specified
39
+ const positionClass = config.position ? `${config.prefix}-nav--${config.position}` : '';
40
+
41
+ // Add user-provided classes
42
+ const userClass = config.class || '';
43
+
44
+ // Combine all classes
45
+ const classNames = [variantClass, positionClass, userClass].filter(Boolean);
46
+
47
+ return createElementConfig(config, {
37
48
  tag: 'nav',
38
49
  componentName: 'nav',
39
50
  attrs: {
40
51
  role: 'navigation',
41
52
  'aria-label': config.ariaLabel || 'Main Navigation'
42
53
  },
43
- className: config.class
54
+ className: classNames
44
55
  });
56
+ };
45
57
 
46
58
  /**
47
59
  * Creates API configuration for the Navigation component
@@ -50,11 +62,11 @@ export const getElementConfig = (config: NavigationConfig) =>
50
62
  */
51
63
  export const getApiConfig = (comp: BaseComponent): ApiOptions => ({
52
64
  disabled: {
53
- enable: comp.disabled?.enable,
54
- disable: comp.disabled?.disable
65
+ enable: comp.disabled?.enable || (() => {}),
66
+ disable: comp.disabled?.disable || (() => {})
55
67
  },
56
68
  lifecycle: {
57
- destroy: comp.lifecycle?.destroy
69
+ destroy: comp.lifecycle?.destroy || (() => {})
58
70
  }
59
71
  });
60
72
 
@@ -0,0 +1,273 @@
1
+ // src/components/navigation/features/controller.ts
2
+ import { BaseComponent, NavClass, NavItemData } from '../types';
3
+
4
+ /**
5
+ * Configuration interface for controller feature
6
+ */
7
+ interface ControllerConfig {
8
+ /** Component prefix for class names */
9
+ prefix?: string;
10
+
11
+ /** Debug mode flag */
12
+ debug?: boolean;
13
+
14
+ /** Component name */
15
+ componentName?: string;
16
+
17
+ /** Additional configuration options */
18
+ [key: string]: any;
19
+ }
20
+
21
+ /**
22
+ * Enhanced component with controller capabilities
23
+ */
24
+ interface ControllerComponent extends BaseComponent {
25
+ /** Handler method for item click events */
26
+ handleItemClick: (id: string) => void;
27
+ }
28
+
29
+ /**
30
+ * Adds event delegation controller to a navigation component
31
+ * This centralizes event handling for all navigation items
32
+ *
33
+ * @param {ControllerConfig} config - Controller configuration
34
+ * @returns {Function} Component enhancer function
35
+ */
36
+ export const withController = (config: ControllerConfig) => (component: BaseComponent): ControllerComponent => {
37
+ const prefix = config.prefix || 'mtrl';
38
+
39
+ /**
40
+ * Updates the active state for an item
41
+ * @param {HTMLElement} item - Item element to activate
42
+ * @param {boolean} active - Whether to make active or inactive
43
+ */
44
+ const updateItemState = (item: HTMLElement, active: boolean): void => {
45
+ // Safety check - ensure item exists
46
+ if (!item) return;
47
+
48
+ // Determine the correct active attribute based on role
49
+ const role = item.getAttribute('role');
50
+
51
+ if (active) {
52
+ item.classList.add(`${prefix}-${NavClass.ITEM}--active`);
53
+
54
+ // Set appropriate attribute based on role
55
+ if (role === 'tab') {
56
+ item.setAttribute('aria-selected', 'true');
57
+ item.setAttribute('tabindex', '0');
58
+ } else if (!item.getAttribute('aria-haspopup')) {
59
+ // Use aria-current for navigation items that aren't expandable
60
+ item.setAttribute('aria-current', 'page');
61
+ }
62
+ } else {
63
+ item.classList.remove(`${prefix}-${NavClass.ITEM}--active`);
64
+
65
+ // Remove appropriate attribute based on role
66
+ if (role === 'tab') {
67
+ item.setAttribute('aria-selected', 'false');
68
+ item.setAttribute('tabindex', '-1');
69
+ } else if (item.hasAttribute('aria-current')) {
70
+ item.removeAttribute('aria-current');
71
+ }
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Handle expandable item toggle
77
+ * @param {HTMLElement} item - Expandable item element
78
+ * @returns {boolean} Whether the item was expandable and handled
79
+ */
80
+ const handleExpandableItem = (item: HTMLElement): boolean => {
81
+ const isExpandable = item.getAttribute('aria-expanded') !== null;
82
+ if (!isExpandable) return false;
83
+
84
+ // Toggle expanded state
85
+ const isExpanded = item.getAttribute('aria-expanded') === 'true';
86
+ item.setAttribute('aria-expanded', (!isExpanded).toString());
87
+
88
+ // Find and toggle nested container - use flexible selectors
89
+ const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}, .mtrl-nav-item-container`);
90
+ if (container) {
91
+ const nestedContainer = container.querySelector(
92
+ `.${prefix}-${NavClass.NESTED_CONTAINER}, .mtrl-nav-nested-container`
93
+ );
94
+ if (nestedContainer) {
95
+ nestedContainer.hidden = isExpanded;
96
+
97
+ // Also toggle expand icon rotation if present
98
+ const expandIcon = item.querySelector(
99
+ `.${prefix}-${NavClass.EXPAND_ICON}, .mtrl-nav-expand-icon`
100
+ );
101
+ if (expandIcon && expandIcon.style) {
102
+ expandIcon.style.transform = isExpanded ? '' : 'rotate(90deg)';
103
+ }
104
+ }
105
+ }
106
+
107
+ // For expandable items, we still emit a change event
108
+ const id = item.dataset.id;
109
+ if (id && component.emit) {
110
+ component.emit('expandToggle', {
111
+ id,
112
+ expanded: !isExpanded
113
+ });
114
+ }
115
+
116
+ return true;
117
+ };
118
+
119
+ // Create the enhanced component with handleItemClick method
120
+ const enhancedComponent: ControllerComponent = {
121
+ ...component,
122
+
123
+ /**
124
+ * Handler method for item click events
125
+ * @param {string} id - ID of the clicked item
126
+ */
127
+ handleItemClick(id: string) {
128
+ if (!component.items) {
129
+ return;
130
+ }
131
+
132
+ const itemData = component.items.get(id);
133
+ if (!itemData) {
134
+ return;
135
+ }
136
+
137
+ // Find the currently active item by DOM query instead of relying on getActive
138
+ const activeElement = component.element.querySelector(`.${prefix}-${NavClass.ITEM}--active, .mtrl-nav-item--active`);
139
+
140
+ // Check if this item is already active - prevent infinite loops
141
+ // if (activeElement && activeElement === itemData.element) {
142
+ // return;
143
+ // }
144
+
145
+ // Deactivate previous active item if found
146
+ if (activeElement) {
147
+ updateItemState(activeElement as HTMLElement, false);
148
+ }
149
+
150
+ // Make sure itemData.element exists before updating
151
+ if (itemData.element) {
152
+ updateItemState(itemData.element, true);
153
+ }
154
+
155
+ // Emit change event
156
+ if (component.emit) {
157
+ // Get the path to the item (for nested items)
158
+ const path = component.getItemPath ? component.getItemPath(id) : [];
159
+
160
+ component.emit('change', {
161
+ id,
162
+ item: itemData,
163
+ previousItem: activeElement ? {
164
+ element: activeElement as HTMLElement,
165
+ config: { id: activeElement.dataset.id }
166
+ } : null,
167
+ path,
168
+ source: 'userAction'
169
+ });
170
+ }
171
+ }
172
+ };
173
+
174
+ // Set up click event delegation for all navigation items
175
+ component.element.addEventListener('click', (event: Event) => {
176
+ const target = event.target as HTMLElement;
177
+
178
+ // Use more flexible selectors that match actual DOM structure
179
+ const item = target.closest(`.${prefix}-${NavClass.ITEM}, .mtrl-nav-item`) as HTMLElement;
180
+
181
+ if (!item) {
182
+ // Fallback to elements with data-id attribute
183
+ const itemByDataId = target.closest('[data-id]') as HTMLElement;
184
+ if (!itemByDataId) {
185
+ return;
186
+ }
187
+ }
188
+
189
+ // Use the found item or fallback
190
+ const navItem = item || target.closest('[data-id]') as HTMLElement;
191
+
192
+ if (navItem.hasAttribute('disabled') || navItem.getAttribute('aria-disabled') === 'true') {
193
+ return;
194
+ }
195
+
196
+ // Get the ID from the data attribute
197
+ const id = navItem.dataset.id;
198
+ if (!id) {
199
+ return;
200
+ }
201
+
202
+ // Handle expandable items first
203
+ if (handleExpandableItem(navItem)) {
204
+ return;
205
+ }
206
+
207
+ // Let the enhanced component handle normal item activation
208
+ enhancedComponent.handleItemClick(id);
209
+ });
210
+
211
+ // Add keyboard support for navigation
212
+ component.element.addEventListener('keydown', (event: KeyboardEvent) => {
213
+ // Only handle specific keys
214
+ if (!['Enter', ' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
215
+ return;
216
+ }
217
+
218
+ const isVertical = ['rail', 'drawer'].includes(component.variant || '');
219
+ const isHorizontal = ['bar'].includes(component.variant || '');
220
+
221
+ // Handle Enter/Space for activation
222
+ if (event.key === 'Enter' || event.key === ' ') {
223
+ const item = document.activeElement as HTMLElement;
224
+ if (item && item.classList.contains(`${prefix}-${NavClass.ITEM}`)) {
225
+ event.preventDefault();
226
+
227
+ const id = item.dataset.id;
228
+ if (id) {
229
+ item.click(); // Trigger a click event for the item
230
+ }
231
+ }
232
+ return;
233
+ }
234
+
235
+ // Get all focusable navigation items - use flexible selector
236
+ const items = Array.from(
237
+ component.element.querySelectorAll(
238
+ `.${prefix}-${NavClass.ITEM}:not([disabled]):not([aria-disabled="true"]),
239
+ .mtrl-nav-item:not([disabled]):not([aria-disabled="true"])`
240
+ )
241
+ ) as HTMLElement[];
242
+
243
+ if (items.length === 0) return;
244
+
245
+ // Find the currently focused item
246
+ const focusedItem = document.activeElement as HTMLElement;
247
+ const focusedIndex = items.indexOf(focusedItem);
248
+
249
+ // Handle navigation keys
250
+ let newIndex = -1;
251
+
252
+ if ((isVertical && (event.key === 'ArrowDown' || event.key === 'ArrowRight')) ||
253
+ (isHorizontal && event.key === 'ArrowRight')) {
254
+ newIndex = focusedIndex < 0 ? 0 : (focusedIndex + 1) % items.length;
255
+ } else if ((isVertical && (event.key === 'ArrowUp' || event.key === 'ArrowLeft')) ||
256
+ (isHorizontal && event.key === 'ArrowLeft')) {
257
+ newIndex = focusedIndex < 0 ? items.length - 1 : (focusedIndex - 1 + items.length) % items.length;
258
+ } else if (event.key === 'Home') {
259
+ newIndex = 0;
260
+ } else if (event.key === 'End') {
261
+ newIndex = items.length - 1;
262
+ }
263
+
264
+ if (newIndex >= 0) {
265
+ event.preventDefault();
266
+ items[newIndex].focus();
267
+ }
268
+ });
269
+
270
+ return enhancedComponent;
271
+ };
272
+
273
+ export default withController;
@@ -1,18 +1,12 @@
1
1
  // src/components/navigation/features/items.ts
2
2
  import { createNavItem, getAllNestedItems } from '../nav-item';
3
- import { NavItemConfig, NavItemData } from '../types';
3
+ import { NavItemConfig, NavItemData, BaseComponent, NavClass } from '../types';
4
4
 
5
- // Type definitions to help with TypeScript conversion
6
- interface Component {
7
- element: HTMLElement;
8
- emit?: (event: string, data: any) => void;
9
- lifecycle?: {
10
- destroy: () => void;
11
- };
12
- [key: string]: any;
13
- }
14
-
15
- interface ItemsComponent extends Component {
5
+ /**
6
+ * Interface for a component with items management capabilities
7
+ * @internal
8
+ */
9
+ interface ItemsComponent extends BaseComponent {
16
10
  items: Map<string, NavItemData>;
17
11
  addItem: (config: NavItemConfig) => ItemsComponent;
18
12
  removeItem: (id: string) => ItemsComponent;
@@ -23,15 +17,44 @@ interface ItemsComponent extends Component {
23
17
  setActive: (id: string) => ItemsComponent;
24
18
  }
25
19
 
20
+ /**
21
+ * Interface for navigation configuration
22
+ * @internal
23
+ */
26
24
  interface NavigationConfig {
27
25
  prefix?: string;
28
26
  items?: NavItemConfig[];
27
+ debug?: boolean;
29
28
  [key: string]: any;
30
29
  }
31
30
 
32
- export const withNavItems = (config: NavigationConfig) => (component: Component): ItemsComponent => {
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
+
49
+ /**
50
+ * Adds navigation items management to a component
51
+ * @param {NavigationConfig} config - Navigation configuration
52
+ * @returns {Function} Component enhancer function
53
+ */
54
+ export const withNavItems = (config: NavigationConfig) => (component: BaseComponent): ItemsComponent => {
33
55
  const items = new Map<string, NavItemData>();
34
56
  let activeItem: NavItemData | null = null;
57
+ const prefix = config.prefix || 'mtrl';
35
58
 
36
59
  /**
37
60
  * Recursively stores items in the items Map
@@ -43,9 +66,9 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
43
66
 
44
67
  if (itemConfig.items?.length) {
45
68
  itemConfig.items.forEach(nestedConfig => {
46
- const container = item.closest(`.${config.prefix}-nav-item-container`);
69
+ const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
47
70
  if (container) {
48
- const nestedContainer = container.querySelector(`.${config.prefix}-nav-nested-container`);
71
+ const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
49
72
  if (nestedContainer) {
50
73
  const nestedItem = nestedContainer.querySelector(`[data-id="${nestedConfig.id}"]`) as HTMLElement;
51
74
  if (nestedItem) {
@@ -68,7 +91,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
68
91
  const role = item.getAttribute('role');
69
92
 
70
93
  if (active) {
71
- item.classList.add(`${config.prefix}-nav-item--active`);
94
+ item.classList.add(`${prefix}-${NavClass.ITEM}--active`);
72
95
 
73
96
  // Set appropriate attribute based on role
74
97
  if (role === 'tab') {
@@ -79,7 +102,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
79
102
  item.setAttribute('aria-current', 'page');
80
103
  }
81
104
  } else {
82
- item.classList.remove(`${config.prefix}-nav-item--active`);
105
+ item.classList.remove(`${prefix}-${NavClass.ITEM}--active`);
83
106
 
84
107
  // Remove appropriate attribute based on role
85
108
  if (role === 'tab') {
@@ -94,7 +117,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
94
117
  // Create initial items
95
118
  if (config.items) {
96
119
  config.items.forEach(itemConfig => {
97
- const item = createNavItem(itemConfig, component.element, config.prefix || 'mtrl');
120
+ const item = createNavItem(itemConfig, component.element, prefix);
98
121
  storeItem(itemConfig, item);
99
122
 
100
123
  if (itemConfig.active) {
@@ -104,72 +127,52 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
104
127
  });
105
128
  }
106
129
 
107
- // Handle item clicks
108
- component.element.addEventListener('click', (event: Event) => {
109
- const item = (event.target as HTMLElement).closest(`.${config.prefix}-nav-item`) as HTMLElement;
110
- if (!item || (item as any).disabled || item.getAttribute('aria-haspopup') === 'menu') return;
111
-
112
- const id = item.dataset.id;
113
- if (!id) return;
114
-
115
- const itemData = items.get(id);
116
- if (!itemData) return;
117
-
118
- // Skip if this is an expandable item
119
- if (item.getAttribute('aria-expanded') !== null) return;
120
-
121
- // Store previous item before updating
122
- const previousItem = activeItem;
123
-
124
- // Update active state
125
- if (activeItem) {
126
- updateActiveState(activeItem.element, activeItem, false);
127
- }
128
-
129
- updateActiveState(item, itemData, true);
130
- 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 });
131
150
 
132
- // Emit change event with item data
133
- if (component.emit) {
134
- component.emit('change', {
135
- id,
136
- item: itemData,
137
- previousItem,
138
- 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
139
159
  });
140
- }
141
- });
142
-
143
- /**
144
- * Gets the path to an item (parent IDs)
145
- * @param {string} id - Item ID to get path for
146
- * @returns {Array<string>} Array of parent item IDs
147
- */
148
- const getItemPath = (id: string): string[] => {
149
- const path: string[] = [];
150
- let currentItem = items.get(id);
151
-
152
- if (!currentItem) return path;
153
-
154
- let parentContainer = currentItem.element.closest(`.${config.prefix}-nav-nested-container`);
155
- while (parentContainer) {
156
- const parentItemContainer = parentContainer.parentElement;
157
- if (!parentItemContainer) break;
158
-
159
- const parentItem = parentItemContainer.querySelector(`.${config.prefix}-nav-item`);
160
- if (!parentItem) break;
161
-
162
- const parentId = parentItem.getAttribute('data-id');
163
- if (!parentId) break;
160
+ }, { passive: true });
164
161
 
165
- 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';
166
167
 
167
- // Move up to next level
168
- parentContainer = parentItemContainer.closest(`.${config.prefix}-nav-nested-container`);
169
- }
170
-
171
- return path;
172
- };
168
+ component.emit('mouseleave', {
169
+ clientX: event.clientX,
170
+ clientY: event.clientY,
171
+ relatedTargetId,
172
+ id: componentId
173
+ });
174
+ });
175
+ }
173
176
 
174
177
  // Clean up when component is destroyed
175
178
  if (component.lifecycle) {
@@ -182,6 +185,76 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
182
185
  };
183
186
  }
184
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
+
185
258
  return {
186
259
  ...component,
187
260
  items,
@@ -189,7 +262,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
189
262
  addItem(itemConfig: NavItemConfig) {
190
263
  if (items.has(itemConfig.id)) return this;
191
264
 
192
- const item = createNavItem(itemConfig, component.element, config.prefix || 'mtrl');
265
+ const item = createNavItem(itemConfig, component.element, prefix);
193
266
  storeItem(itemConfig, item);
194
267
 
195
268
  if (itemConfig.active) {
@@ -208,9 +281,9 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
208
281
  removeItem(id: string) {
209
282
  const item = items.get(id);
210
283
  if (!item) return this;
211
-
284
+
212
285
  // Remove all nested items first
213
- const nestedItems = getAllNestedItems(item.element, config.prefix || 'mtrl');
286
+ const nestedItems = getAllNestedItems(item.element, prefix);
214
287
  nestedItems.forEach(nestedItem => {
215
288
  const nestedId = nestedItem.dataset.id;
216
289
  if (nestedId) items.delete(nestedId);
@@ -221,7 +294,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
221
294
  }
222
295
 
223
296
  // Remove the entire item container
224
- const container = item.element.closest(`.${config.prefix}-nav-item-container`);
297
+ const container = item.element.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
225
298
  if (container) {
226
299
  container.remove();
227
300
  }
@@ -241,7 +314,7 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
241
314
  setActive(id: string) {
242
315
  const item = items.get(id);
243
316
  if (!item || item.config.disabled) return this;
244
-
317
+
245
318
  if (activeItem) {
246
319
  updateActiveState(activeItem.element, activeItem, false);
247
320
  }
@@ -255,9 +328,9 @@ export const withNavItems = (config: NavigationConfig) => (component: Component)
255
328
  const parentItem = items.get(parentId);
256
329
  if (parentItem) {
257
330
  const parentButton = parentItem.element;
258
- const container = parentButton.closest(`.${config.prefix}-nav-item-container`);
331
+ const container = parentButton.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
259
332
  if (container) {
260
- const nestedContainer = container.querySelector(`.${config.prefix}-nav-nested-container`);
333
+ const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
261
334
  if (nestedContainer) {
262
335
  parentButton.setAttribute('aria-expanded', 'true');
263
336
  nestedContainer.hidden = false;