mtrl 0.2.6 → 0.2.8

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 (226) hide show
  1. package/demo/build.ts +349 -0
  2. package/demo/index.html +110 -0
  3. package/demo/main.js +448 -0
  4. package/demo/styles.css +239 -0
  5. package/index.ts +18 -0
  6. package/package.json +14 -3
  7. package/server.ts +86 -0
  8. package/src/components/badge/api.ts +70 -63
  9. package/src/components/badge/badge.ts +16 -2
  10. package/src/components/badge/config.ts +66 -13
  11. package/src/components/badge/features.ts +51 -42
  12. package/src/components/badge/index.ts +27 -2
  13. package/src/components/badge/types.ts +62 -30
  14. package/src/components/bottom-app-bar/bottom-app-bar.ts +154 -0
  15. package/src/components/bottom-app-bar/config.ts +29 -0
  16. package/src/components/bottom-app-bar/index.ts +17 -0
  17. package/src/components/bottom-app-bar/types.ts +114 -0
  18. package/src/components/button/api.ts +5 -0
  19. package/src/components/button/button.ts +0 -1
  20. package/src/components/button/config.ts +6 -2
  21. package/src/components/button/index.ts +10 -2
  22. package/src/components/button/types.ts +20 -2
  23. package/src/components/card/card.ts +13 -25
  24. package/src/components/card/config.ts +83 -30
  25. package/src/components/card/content.ts +8 -10
  26. package/src/components/card/features.ts +4 -3
  27. package/src/components/card/index.ts +29 -2
  28. package/src/components/card/types.ts +33 -22
  29. package/src/components/checkbox/config.ts +3 -4
  30. package/src/components/checkbox/index.ts +1 -2
  31. package/src/components/checkbox/types.ts +12 -3
  32. package/src/components/chip/api.ts +170 -221
  33. package/src/components/chip/chip.ts +34 -302
  34. package/src/components/chip/config.ts +1 -2
  35. package/src/components/chip/index.ts +10 -2
  36. package/src/components/chip/types.ts +224 -35
  37. package/src/components/datepicker/api.ts +265 -0
  38. package/src/components/datepicker/config.ts +141 -0
  39. package/src/components/datepicker/datepicker.ts +341 -0
  40. package/src/components/datepicker/index.ts +12 -0
  41. package/src/components/datepicker/render.ts +450 -0
  42. package/src/components/datepicker/types.ts +397 -0
  43. package/src/components/datepicker/utils.ts +289 -0
  44. package/src/components/dialog/api.ts +55 -21
  45. package/src/components/dialog/config.ts +12 -9
  46. package/src/components/dialog/dialog.ts +6 -3
  47. package/src/components/dialog/features.ts +345 -151
  48. package/src/components/dialog/index.ts +38 -8
  49. package/src/components/dialog/types.ts +40 -14
  50. package/src/components/divider/config.ts +81 -0
  51. package/src/components/divider/divider.ts +37 -0
  52. package/src/components/divider/features.ts +207 -0
  53. package/src/components/divider/index.ts +9 -0
  54. package/src/components/divider/types.ts +55 -0
  55. package/src/components/extended-fab/api.ts +141 -0
  56. package/src/components/extended-fab/config.ts +112 -0
  57. package/src/components/extended-fab/extended-fab.ts +125 -0
  58. package/src/components/extended-fab/index.ts +9 -0
  59. package/src/components/extended-fab/types.ts +304 -0
  60. package/src/components/fab/api.ts +97 -0
  61. package/src/components/fab/config.ts +93 -0
  62. package/src/components/fab/fab.ts +67 -0
  63. package/src/components/fab/index.ts +9 -0
  64. package/src/components/fab/types.ts +251 -0
  65. package/src/components/list/config.ts +4 -5
  66. package/src/components/list/features.ts +6 -7
  67. package/src/components/list/index.ts +7 -9
  68. package/src/components/list/list-item.ts +12 -13
  69. package/src/components/list/types.ts +50 -5
  70. package/src/components/list/utils.ts +30 -3
  71. package/src/components/menu/features/items-manager.ts +9 -9
  72. package/src/components/menu/features/positioning.ts +7 -7
  73. package/src/components/menu/features/visibility.ts +7 -7
  74. package/src/components/menu/index.ts +7 -9
  75. package/src/components/menu/menu-item.ts +6 -6
  76. package/src/components/menu/menu.ts +22 -0
  77. package/src/components/menu/types.ts +29 -10
  78. package/src/components/menu/utils.ts +67 -0
  79. package/src/components/navigation/api.ts +78 -50
  80. package/src/components/navigation/config.ts +22 -10
  81. package/src/components/navigation/features/items.ts +284 -0
  82. package/src/components/navigation/index.ts +0 -6
  83. package/src/components/navigation/nav-item.ts +70 -33
  84. package/src/components/navigation/navigation.ts +53 -3
  85. package/src/components/navigation/types.ts +117 -70
  86. package/src/components/progress/api.ts +2 -3
  87. package/src/components/progress/config.ts +2 -3
  88. package/src/components/progress/index.ts +0 -1
  89. package/src/components/progress/progress.ts +1 -2
  90. package/src/components/progress/types.ts +186 -33
  91. package/src/components/radios/config.ts +1 -1
  92. package/src/components/radios/index.ts +0 -1
  93. package/src/components/radios/types.ts +0 -7
  94. package/src/components/search/api.ts +203 -0
  95. package/src/components/search/config.ts +86 -0
  96. package/src/components/search/features/index.ts +4 -0
  97. package/src/components/search/features/search.ts +717 -0
  98. package/src/components/search/features/states.ts +169 -0
  99. package/src/components/search/features/structure.ts +197 -0
  100. package/src/components/search/index.ts +7 -0
  101. package/src/components/search/search.ts +52 -0
  102. package/src/components/search/types.ts +175 -0
  103. package/src/components/segmented-button/config.ts +80 -0
  104. package/src/components/segmented-button/index.ts +4 -0
  105. package/src/components/segmented-button/segment.ts +154 -0
  106. package/src/components/segmented-button/segmented-button.ts +249 -0
  107. package/src/components/segmented-button/types.ts +254 -0
  108. package/src/components/slider/accessibility.md +5 -5
  109. package/src/components/slider/api.ts +41 -120
  110. package/src/components/slider/config.ts +51 -47
  111. package/src/components/slider/features/handlers.ts +495 -0
  112. package/src/components/slider/features/index.ts +1 -2
  113. package/src/components/slider/features/slider.ts +66 -84
  114. package/src/components/slider/features/states.ts +195 -0
  115. package/src/components/slider/features/structure.ts +136 -206
  116. package/src/components/slider/features/ui.ts +145 -206
  117. package/src/components/slider/index.ts +2 -11
  118. package/src/components/slider/slider.ts +9 -12
  119. package/src/components/slider/types.ts +67 -26
  120. package/src/components/snackbar/config.ts +2 -3
  121. package/src/components/snackbar/constants.ts +0 -32
  122. package/src/components/snackbar/index.ts +0 -1
  123. package/src/components/snackbar/position.ts +9 -1
  124. package/src/components/snackbar/types.ts +122 -46
  125. package/src/components/switch/config.ts +2 -3
  126. package/src/components/switch/index.ts +0 -1
  127. package/src/components/switch/types.ts +3 -2
  128. package/src/components/tabs/config.ts +3 -4
  129. package/src/components/tabs/features.ts +4 -2
  130. package/src/components/tabs/index.ts +0 -15
  131. package/src/components/tabs/indicator.ts +73 -13
  132. package/src/components/tabs/tab-api.ts +12 -4
  133. package/src/components/tabs/tab.ts +18 -6
  134. package/src/components/tabs/types.ts +23 -5
  135. package/src/components/textfield/config.ts +2 -3
  136. package/src/components/textfield/index.ts +0 -1
  137. package/src/components/textfield/types.ts +17 -3
  138. package/src/components/timepicker/README.md +277 -0
  139. package/src/components/timepicker/api.ts +632 -0
  140. package/src/components/timepicker/clockdial.ts +482 -0
  141. package/src/components/timepicker/config.ts +228 -0
  142. package/src/components/timepicker/index.ts +3 -0
  143. package/src/components/timepicker/render.ts +613 -0
  144. package/src/components/timepicker/timepicker.ts +117 -0
  145. package/src/components/timepicker/types.ts +336 -0
  146. package/src/components/timepicker/utils.ts +241 -0
  147. package/src/components/tooltip/api.ts +1 -1
  148. package/src/components/tooltip/config.ts +27 -6
  149. package/src/components/tooltip/index.ts +0 -1
  150. package/src/components/tooltip/types.ts +13 -3
  151. package/src/components/top-app-bar/config.ts +83 -0
  152. package/src/components/top-app-bar/index.ts +11 -0
  153. package/src/components/top-app-bar/top-app-bar.ts +316 -0
  154. package/src/components/top-app-bar/types.ts +140 -0
  155. package/src/core/build/_ripple.scss +6 -6
  156. package/src/core/build/ripple.ts +72 -95
  157. package/src/core/compose/features/icon.ts +3 -1
  158. package/src/core/compose/features/ripple.ts +4 -1
  159. package/src/core/compose/features/textlabel.ts +23 -2
  160. package/src/core/dom/create.ts +5 -0
  161. package/src/index.ts +9 -0
  162. package/src/styles/abstract/_theme.scss +9 -1
  163. package/src/styles/components/_badge.scss +182 -0
  164. package/src/styles/components/_bottom-app-bar.scss +103 -0
  165. package/src/{components/button/_styles.scss → styles/components/_button.scss} +0 -10
  166. package/src/{components/checkbox/_styles.scss → styles/components/_checkbox.scss} +0 -2
  167. package/src/styles/components/_datepicker.scss +358 -0
  168. package/src/styles/components/_dialog.scss +259 -0
  169. package/src/styles/components/_divider.scss +57 -0
  170. package/src/styles/components/_extended-fab.scss +267 -0
  171. package/src/styles/components/_fab.scss +225 -0
  172. package/src/{components/navigation/_styles.scss → styles/components/_navigation.scss} +1 -0
  173. package/src/styles/components/_search.scss +306 -0
  174. package/src/styles/components/_segmented-button.scss +117 -0
  175. package/src/{components/slider/_styles.scss → styles/components/_slider.scss} +83 -24
  176. package/src/{components/switch/_styles.scss → styles/components/_switch.scss} +0 -2
  177. package/src/{components/tabs/_styles.scss → styles/components/_tabs.scss} +95 -33
  178. package/src/{components/textfield/_styles.scss → styles/components/_textfield.scss} +70 -67
  179. package/src/styles/components/_timepicker.scss +451 -0
  180. package/src/styles/components/_top-app-bar.scss +225 -0
  181. package/src/styles/main.scss +98 -49
  182. package/src/styles/themes/_autumn.scss +21 -0
  183. package/src/styles/themes/_base-theme.scss +61 -0
  184. package/src/styles/themes/_baseline.scss +58 -0
  185. package/src/styles/themes/_bluekhaki.scss +125 -0
  186. package/src/styles/themes/_brownbeige.scss +125 -0
  187. package/src/styles/themes/_browngreen.scss +125 -0
  188. package/src/styles/themes/_forest.scss +6 -0
  189. package/src/styles/themes/_greenbeige.scss +125 -0
  190. package/src/styles/themes/_material.scss +125 -0
  191. package/src/styles/themes/_ocean.scss +6 -0
  192. package/src/styles/themes/_sageivory.scss +125 -0
  193. package/src/styles/themes/_spring.scss +6 -0
  194. package/src/styles/themes/_summer.scss +5 -0
  195. package/src/styles/themes/_sunset.scss +5 -0
  196. package/src/styles/themes/_tealcaramel.scss +125 -0
  197. package/src/styles/themes/_winter.scss +6 -0
  198. package/src/components/badge/_styles.scss +0 -174
  199. package/src/components/badge/constants.ts +0 -30
  200. package/src/components/button/constants.ts +0 -11
  201. package/src/components/card/constants.ts +0 -84
  202. package/src/components/dialog/_styles.scss +0 -213
  203. package/src/components/dialog/constants.ts +0 -32
  204. package/src/components/menu/constants.ts +0 -154
  205. package/src/components/navigation/constants.ts +0 -200
  206. package/src/components/navigation/features/items.js +0 -192
  207. package/src/components/progress/constants.ts +0 -29
  208. package/src/components/slider/features/appearance.ts +0 -94
  209. package/src/components/slider/features/disabled.ts +0 -68
  210. package/src/components/slider/features/events.ts +0 -164
  211. package/src/components/slider/features/interactions.ts +0 -396
  212. package/src/components/slider/features/keyboard.ts +0 -233
  213. package/src/components/switch/constants.ts +0 -80
  214. package/src/components/tabs/constants.ts +0 -89
  215. package/src/core/collection/adapters/mongodb.js +0 -232
  216. /package/src/{components/card/_styles.scss → styles/components/_card.scss} +0 -0
  217. /package/src/{components/carousel/_styles.scss → styles/components/_carousel.scss} +0 -0
  218. /package/src/{components/chip/_styles.scss → styles/components/_chip.scss} +0 -0
  219. /package/src/{components/list/_styles.scss → styles/components/_list.scss} +0 -0
  220. /package/src/{components/menu/_styles.scss → styles/components/_menu.scss} +0 -0
  221. /package/src/{components/progress/_styles.scss → styles/components/_progress.scss} +0 -0
  222. /package/src/{components/radios/_styles.scss → styles/components/_radios.scss} +0 -0
  223. /package/src/{components/sheet/_styles.scss → styles/components/_sheet.scss} +0 -0
  224. /package/src/{components/snackbar/_styles.scss → styles/components/_snackbar.scss} +0 -0
  225. /package/src/{components/tooltip/_styles.scss → styles/components/_tooltip.scss} +0 -0
  226. /package/src/styles/utilities/{_color.scss → _colors.scss} +0 -0
@@ -0,0 +1,284 @@
1
+ // src/components/navigation/features/items.ts
2
+ import { createNavItem, getAllNestedItems } from '../nav-item';
3
+ import { NavItemConfig, NavItemData, BaseComponent, NavClass } from '../types';
4
+
5
+ /**
6
+ * Interface for a component with items management capabilities
7
+ * @internal
8
+ */
9
+ interface ItemsComponent extends BaseComponent {
10
+ items: Map<string, NavItemData>;
11
+ addItem: (config: NavItemConfig) => ItemsComponent;
12
+ removeItem: (id: string) => ItemsComponent;
13
+ getItem: (id: string) => NavItemData | undefined;
14
+ getAllItems: () => NavItemData[];
15
+ getActive: () => NavItemData | null;
16
+ getItemPath: (id: string) => string[];
17
+ setActive: (id: string) => ItemsComponent;
18
+ }
19
+
20
+ /**
21
+ * Interface for navigation configuration
22
+ * @internal
23
+ */
24
+ interface NavigationConfig {
25
+ prefix?: string;
26
+ items?: NavItemConfig[];
27
+ [key: string]: any;
28
+ }
29
+
30
+ /**
31
+ * Adds navigation items management to a component
32
+ * @param {NavigationConfig} config - Navigation configuration
33
+ * @returns {Function} Component enhancer function
34
+ */
35
+ export const withNavItems = (config: NavigationConfig) => (component: BaseComponent): ItemsComponent => {
36
+ const items = new Map<string, NavItemData>();
37
+ let activeItem: NavItemData | null = null;
38
+ const prefix = config.prefix || 'mtrl';
39
+
40
+ /**
41
+ * Recursively stores items in the items Map
42
+ * @param {NavItemConfig} itemConfig - Item configuration
43
+ * @param {HTMLElement} item - Created item element
44
+ */
45
+ const storeItem = (itemConfig: NavItemConfig, item: HTMLElement): void => {
46
+ items.set(itemConfig.id, { element: item, config: itemConfig });
47
+
48
+ if (itemConfig.items?.length) {
49
+ itemConfig.items.forEach(nestedConfig => {
50
+ const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
51
+ if (container) {
52
+ const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
53
+ if (nestedContainer) {
54
+ const nestedItem = nestedContainer.querySelector(`[data-id="${nestedConfig.id}"]`) as HTMLElement;
55
+ if (nestedItem) {
56
+ storeItem(nestedConfig, nestedItem);
57
+ }
58
+ }
59
+ }
60
+ });
61
+ }
62
+ };
63
+
64
+ /**
65
+ * Updates the active state for an item
66
+ * @param {HTMLElement} item - Item element to activate
67
+ * @param {NavItemData} itemData - Item data
68
+ * @param {boolean} active - Whether to make active or inactive
69
+ */
70
+ const updateActiveState = (item: HTMLElement, itemData: NavItemData, active: boolean): void => {
71
+ // Determine the correct active attribute based on role
72
+ const role = item.getAttribute('role');
73
+
74
+ if (active) {
75
+ item.classList.add(`${prefix}-${NavClass.ITEM}--active`);
76
+
77
+ // Set appropriate attribute based on role
78
+ if (role === 'tab') {
79
+ item.setAttribute('aria-selected', 'true');
80
+ item.setAttribute('tabindex', '0');
81
+ } else if (!item.getAttribute('aria-haspopup')) {
82
+ // Use aria-current for navigation items that aren't expandable
83
+ item.setAttribute('aria-current', 'page');
84
+ }
85
+ } else {
86
+ item.classList.remove(`${prefix}-${NavClass.ITEM}--active`);
87
+
88
+ // Remove appropriate attribute based on role
89
+ if (role === 'tab') {
90
+ item.setAttribute('aria-selected', 'false');
91
+ item.setAttribute('tabindex', '-1');
92
+ } else if (item.hasAttribute('aria-current')) {
93
+ item.removeAttribute('aria-current');
94
+ }
95
+ }
96
+ };
97
+
98
+ // Create initial items
99
+ if (config.items) {
100
+ config.items.forEach(itemConfig => {
101
+ const item = createNavItem(itemConfig, component.element, prefix);
102
+ storeItem(itemConfig, item);
103
+
104
+ if (itemConfig.active) {
105
+ activeItem = { element: item, config: itemConfig };
106
+ updateActiveState(item, activeItem, true);
107
+ }
108
+ });
109
+ }
110
+
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;
135
+
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)
143
+ });
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;
162
+
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);
170
+
171
+ // Move up to next level
172
+ parentContainer = parentItemContainer.closest(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
173
+ }
174
+
175
+ return path;
176
+ };
177
+
178
+ // Clean up when component is destroyed
179
+ if (component.lifecycle) {
180
+ const originalDestroy = component.lifecycle.destroy;
181
+ component.lifecycle.destroy = () => {
182
+ items.clear();
183
+ if (originalDestroy) {
184
+ originalDestroy();
185
+ }
186
+ };
187
+ }
188
+
189
+ return {
190
+ ...component,
191
+ items,
192
+
193
+ addItem(itemConfig: NavItemConfig) {
194
+ if (items.has(itemConfig.id)) return this;
195
+
196
+ const item = createNavItem(itemConfig, component.element, prefix);
197
+ storeItem(itemConfig, item);
198
+
199
+ if (itemConfig.active) {
200
+ this.setActive(itemConfig.id);
201
+ }
202
+
203
+ if (component.emit) {
204
+ component.emit('itemAdded', {
205
+ id: itemConfig.id,
206
+ item: { element: item, config: itemConfig }
207
+ });
208
+ }
209
+ return this;
210
+ },
211
+
212
+ removeItem(id: string) {
213
+ const item = items.get(id);
214
+ if (!item) return this;
215
+
216
+ // Remove all nested items first
217
+ const nestedItems = getAllNestedItems(item.element, prefix);
218
+ nestedItems.forEach(nestedItem => {
219
+ const nestedId = nestedItem.dataset.id;
220
+ if (nestedId) items.delete(nestedId);
221
+ });
222
+
223
+ if (activeItem?.config.id === id) {
224
+ activeItem = null;
225
+ }
226
+
227
+ // Remove the entire item container
228
+ const container = item.element.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
229
+ if (container) {
230
+ container.remove();
231
+ }
232
+ items.delete(id);
233
+
234
+ if (component.emit) {
235
+ component.emit('itemRemoved', { id, item });
236
+ }
237
+ return this;
238
+ },
239
+
240
+ getItem: (id: string) => items.get(id),
241
+ getAllItems: () => Array.from(items.values()),
242
+ getActive: () => activeItem,
243
+ getItemPath: (id: string) => getItemPath(id),
244
+
245
+ setActive(id: string) {
246
+ const item = items.get(id);
247
+ if (!item || item.config.disabled) return this;
248
+
249
+ if (activeItem) {
250
+ updateActiveState(activeItem.element, activeItem, false);
251
+ }
252
+
253
+ updateActiveState(item.element, item, true);
254
+ activeItem = item;
255
+
256
+ // Ensure all parent items are expanded
257
+ const path = getItemPath(id);
258
+ path.forEach(parentId => {
259
+ const parentItem = items.get(parentId);
260
+ if (parentItem) {
261
+ const parentButton = parentItem.element;
262
+ const container = parentButton.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
263
+ if (container) {
264
+ const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
265
+ if (nestedContainer) {
266
+ parentButton.setAttribute('aria-expanded', 'true');
267
+ nestedContainer.hidden = false;
268
+ }
269
+ }
270
+ }
271
+ });
272
+
273
+ if (component.emit) {
274
+ component.emit('activeChanged', {
275
+ id,
276
+ item,
277
+ previousItem: activeItem,
278
+ path: getItemPath(id)
279
+ });
280
+ }
281
+ return this;
282
+ }
283
+ };
284
+ };
@@ -1,11 +1,5 @@
1
1
  // src/components/navigation/index.ts
2
2
  export { default } from './navigation'
3
- export {
4
- NAV_VARIANTS,
5
- NAV_POSITIONS,
6
- NAV_BEHAVIORS,
7
- NAV_STATES
8
- } from './constants'
9
3
  export {
10
4
  NavigationConfig,
11
5
  NavigationComponent,
@@ -1,5 +1,5 @@
1
1
  // src/components/navigation/nav-item.ts
2
- import { NavItemConfig } from './types';
2
+ import { NavItemConfig, NavClass } from './types';
3
3
 
4
4
  /**
5
5
  * Creates an expand/collapse icon element
@@ -8,7 +8,7 @@ import { NavItemConfig } from './types';
8
8
  */
9
9
  export const createExpandIcon = (prefix: string): HTMLElement => {
10
10
  const icon = document.createElement('span');
11
- icon.className = `${prefix}-nav-expand-icon`;
11
+ icon.className = `${prefix}-${NavClass.EXPAND_ICON}`;
12
12
  icon.innerHTML = `
13
13
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
14
14
  <polyline points="9 18 15 12 9 6"></polyline>
@@ -30,8 +30,10 @@ export const createNestedContainer = (
30
30
  createItem: (config: NavItemConfig, container: HTMLElement, prefix: string) => HTMLElement
31
31
  ): HTMLElement => {
32
32
  const container = document.createElement('div');
33
- container.className = `${prefix}-nav-nested-container`;
34
- container.setAttribute('role', 'group');
33
+ container.className = `${prefix}-${NavClass.NESTED_CONTAINER}`;
34
+
35
+ // Use appropriate role for nested menu
36
+ container.setAttribute('role', 'menu');
35
37
  container.hidden = true;
36
38
 
37
39
  items.forEach(itemConfig => {
@@ -54,67 +56,102 @@ export const createNavItem = (
54
56
  prefix: string
55
57
  ): HTMLElement => {
56
58
  const itemContainer = document.createElement('div');
57
- itemContainer.className = `${prefix}-nav-item-container`;
58
-
59
- const item = document.createElement('button');
60
- item.className = `${prefix}-nav-item`;
61
- item.setAttribute('role', config.items?.length ? 'button' : 'menuitem');
62
- item.setAttribute('aria-selected', 'false');
59
+ itemContainer.className = `${prefix}-${NavClass.ITEM_CONTAINER}`;
60
+
61
+ // Determine if parent container uses tabs or menu role pattern
62
+ const isMenuContext = container.getAttribute('role') === 'menu';
63
+ const isTabContext = container.getAttribute('role') === 'tablist';
64
+ const isDrawerVariant = container.closest(`.${prefix}-nav--drawer, .${prefix}-nav--drawer-modal, .${prefix}-nav--drawer-standard`) !== null;
65
+
66
+ // Create the item element
67
+ const itemElement = document.createElement('button');
68
+ itemElement.className = `${prefix}-${NavClass.ITEM}`;
69
+ itemElement.type = 'button'; // Ensure it's a button type for proper behavior
70
+
71
+ // Set appropriate role based on context and items
72
+ if (config.items?.length) {
73
+ if (isDrawerVariant) {
74
+ // For expandable drawer items with nested items
75
+ itemElement.setAttribute('role', 'button');
76
+ itemElement.setAttribute('aria-expanded', config.expanded ? 'true' : 'false');
77
+ itemElement.setAttribute('aria-haspopup', 'menu');
78
+ } else {
79
+ // For non-drawer variants with nested items
80
+ itemElement.setAttribute('role', 'button');
81
+ }
82
+ } else if (isMenuContext) {
83
+ // For menu items
84
+ itemElement.setAttribute('role', 'menuitem');
85
+ } else if (isTabContext) {
86
+ // For tab items
87
+ itemElement.setAttribute('role', 'tab');
88
+ itemElement.setAttribute('aria-selected', config.active ? 'true' : 'false');
89
+ itemElement.setAttribute('tabindex', config.active ? '0' : '-1');
90
+ }
91
+ // For plain navigation buttons, we don't need to set the role since buttons have inherent semantics
63
92
 
64
93
  if (config.id) {
65
- item.dataset.id = config.id;
94
+ itemElement.dataset.id = config.id;
66
95
  }
67
96
 
68
97
  if (config.disabled) {
69
- item.disabled = true;
70
- item.setAttribute('aria-disabled', 'true');
98
+ itemElement.disabled = true;
99
+ itemElement.setAttribute('aria-disabled', 'true');
71
100
  }
72
101
 
73
102
  // Add icon if provided
74
103
  if (config.icon) {
75
104
  const icon = document.createElement('span');
76
- icon.className = `${prefix}-nav-item-icon`;
105
+ icon.className = `${prefix}-${NavClass.ICON}`;
77
106
  icon.innerHTML = config.icon;
78
- item.appendChild(icon);
107
+ itemElement.appendChild(icon);
79
108
  }
80
109
 
81
110
  // Add label if provided
82
111
  if (config.label) {
83
112
  const label = document.createElement('span');
84
- label.className = `${prefix}-nav-item-label`;
113
+ label.className = `${prefix}-${NavClass.LABEL}`;
85
114
  label.textContent = config.label;
86
- item.appendChild(label);
87
- item.setAttribute('aria-label', config.label);
115
+ itemElement.appendChild(label);
116
+ itemElement.setAttribute('aria-label', config.label);
88
117
  }
89
118
 
90
119
  // Add badge if provided
91
120
  if (config.badge) {
92
121
  const badge = document.createElement('span');
93
- badge.className = `${prefix}-nav-item-badge`;
122
+ badge.className = `${prefix}-${NavClass.BADGE}`;
94
123
  badge.textContent = config.badge;
124
+ // Use appropriate aria labeling
95
125
  badge.setAttribute('aria-label', `${config.badge} notifications`);
96
- item.appendChild(badge);
126
+ itemElement.appendChild(badge);
127
+ }
128
+
129
+ // Mark active state with appropriate semantics
130
+ if (config.active && !config.items?.length) {
131
+ itemElement.classList.add(`${prefix}-${NavClass.ITEM}--active`);
132
+
133
+ // Use aria-current for standard navigation
134
+ if (!isTabContext) {
135
+ itemElement.setAttribute('aria-current', 'page');
136
+ }
97
137
  }
98
138
 
99
- itemContainer.appendChild(item);
139
+ itemContainer.appendChild(itemElement);
100
140
 
101
141
  // Handle nested items - only for drawer variant
102
- if (config.items?.length && container.closest('.mtrl-nav--drawer, .mtrl-nav--drawer-modal, .mtrl-nav--drawer-standard')) {
142
+ if (config.items?.length && isDrawerVariant) {
103
143
  const expandIcon = createExpandIcon(prefix);
104
- item.appendChild(expandIcon);
105
-
106
- item.setAttribute('aria-expanded', config.expanded ? 'true' : 'false');
107
- item.setAttribute('aria-haspopup', 'true');
144
+ itemElement.appendChild(expandIcon);
108
145
 
109
146
  const nestedContainer = createNestedContainer(config.items, prefix, createNavItem);
110
147
  nestedContainer.hidden = !config.expanded;
111
148
  itemContainer.appendChild(nestedContainer);
112
149
 
113
150
  // Handle expand/collapse
114
- item.addEventListener('click', (event) => {
151
+ itemElement.addEventListener('click', (event) => {
115
152
  event.stopPropagation();
116
- const isExpanded = item.getAttribute('aria-expanded') === 'true';
117
- item.setAttribute('aria-expanded', (!isExpanded).toString());
153
+ const isExpanded = itemElement.getAttribute('aria-expanded') === 'true';
154
+ itemElement.setAttribute('aria-expanded', (!isExpanded).toString());
118
155
  nestedContainer.hidden = isExpanded;
119
156
 
120
157
  // Toggle expand icon rotation
@@ -125,7 +162,7 @@ export const createNavItem = (
125
162
  }
126
163
 
127
164
  container.appendChild(itemContainer);
128
- return item;
165
+ return itemElement;
129
166
  };
130
167
 
131
168
  /**
@@ -135,13 +172,13 @@ export const createNavItem = (
135
172
  * @returns {Array<HTMLElement>} Array of all nested items
136
173
  */
137
174
  export const getAllNestedItems = (item: HTMLElement, prefix: string): HTMLElement[] => {
138
- const container = item.closest(`.${prefix}-nav-item-container`);
175
+ const container = item.closest(`.${prefix}-${NavClass.ITEM_CONTAINER}`);
139
176
  if (!container) return [];
140
177
 
141
- const nestedContainer = container.querySelector(`.${prefix}-nav-nested-container`);
178
+ const nestedContainer = container.querySelector(`.${prefix}-${NavClass.NESTED_CONTAINER}`);
142
179
  if (!nestedContainer) return [];
143
180
 
144
- const items = Array.from(nestedContainer.querySelectorAll(`.${prefix}-nav-item`)) as HTMLElement[];
181
+ const items = Array.from(nestedContainer.querySelectorAll(`.${prefix}-${NavClass.ITEM}`)) as HTMLElement[];
145
182
  return items.reduce((acc: HTMLElement[], nestedItem: HTMLElement) => {
146
183
  return [...acc, nestedItem, ...getAllNestedItems(nestedItem, prefix)];
147
184
  }, []);
@@ -6,17 +6,57 @@ import {
6
6
  withDisabled,
7
7
  withLifecycle,
8
8
  withVariant,
9
- withPosition // Import core position feature
9
+ withPosition
10
10
  } from '../../core/compose/features';
11
11
  import { withAPI } from './api';
12
12
  import { withNavItems } from './features/items';
13
- import { NavigationConfig, NavigationComponent } from './types';
13
+ import { NavigationConfig, NavigationComponent, NavVariant } from './types';
14
14
  import {
15
15
  createBaseConfig,
16
16
  getElementConfig,
17
17
  getApiConfig
18
18
  } from './config';
19
19
 
20
+ /**
21
+ * Sets up proper ARIA roles based on navigation variant
22
+ * @param {NavigationComponent} nav - Navigation component
23
+ * @param {NavigationConfig} config - Navigation configuration
24
+ */
25
+ const setupAccessibility = (nav: NavigationComponent, config: NavigationConfig): void => {
26
+ const { element } = nav;
27
+ const variant = config.variant || 'rail';
28
+ const prefix = config.prefix || 'mtrl';
29
+
30
+ // Set appropriate aria-label
31
+ element.setAttribute('aria-label', config.ariaLabel || 'Main Navigation');
32
+
33
+ // For bar navigation (bottom or top nav)
34
+ if (variant === 'bar') {
35
+ // If bar navigation is acting as tabs
36
+ const hasNestedItems = config.items?.some(item => item.items?.length) || false;
37
+
38
+ if (!hasNestedItems) {
39
+ element.setAttribute('role', 'tablist');
40
+ element.setAttribute('aria-orientation', 'horizontal');
41
+ } else {
42
+ element.setAttribute('role', 'menubar');
43
+ element.setAttribute('aria-orientation', 'horizontal');
44
+ }
45
+ }
46
+ // For rail and drawer navigation
47
+ else {
48
+ // Use standard navigation landmark
49
+ element.setAttribute('role', 'navigation');
50
+ }
51
+
52
+ // Set hidden state for modal drawers if needed
53
+ if ((variant === 'modal' ||
54
+ (variant === 'drawer' && config.behavior === 'dismissible')) &&
55
+ !config.expanded) {
56
+ element.classList.add(`${prefix}-nav--hidden`);
57
+ }
58
+ };
59
+
20
60
  /**
21
61
  * Creates a new Navigation component
22
62
  * @param {NavigationConfig} config - Navigation configuration
@@ -40,7 +80,17 @@ const createNavigation = (config: NavigationConfig = {}): NavigationComponent =>
40
80
  comp => withAPI(getApiConfig(comp))(comp)
41
81
  )(baseConfig);
42
82
 
43
- return navigation as NavigationComponent;
83
+ const nav = navigation as NavigationComponent;
84
+
85
+ // Set up proper ARIA roles and relationships
86
+ setupAccessibility(nav, baseConfig);
87
+
88
+ // Implement any initialization logic
89
+ if (baseConfig.disabled) {
90
+ nav.disable();
91
+ }
92
+
93
+ return nav;
44
94
  } catch (error) {
45
95
  console.error('Navigation creation error:', error instanceof Error ? error.message : String(error));
46
96
  throw new Error(`Failed to create navigation: ${error instanceof Error ? error.message : String(error)}`);