mtrl 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/index.ts +18 -0
  2. package/package.json +1 -1
  3. package/src/components/badge/_styles.scss +123 -115
  4. package/src/components/badge/api.ts +57 -59
  5. package/src/components/badge/badge.ts +16 -2
  6. package/src/components/badge/config.ts +65 -11
  7. package/src/components/badge/constants.ts +22 -12
  8. package/src/components/badge/features.ts +44 -40
  9. package/src/components/badge/types.ts +42 -30
  10. package/src/components/bottom-app-bar/_styles.scss +103 -0
  11. package/src/components/bottom-app-bar/bottom-app-bar.ts +196 -0
  12. package/src/components/bottom-app-bar/config.ts +73 -0
  13. package/src/components/bottom-app-bar/index.ts +11 -0
  14. package/src/components/bottom-app-bar/types.ts +108 -0
  15. package/src/components/button/_styles.scss +0 -66
  16. package/src/components/button/api.ts +5 -0
  17. package/src/components/button/button.ts +0 -2
  18. package/src/components/button/config.ts +5 -0
  19. package/src/components/button/constants.ts +0 -6
  20. package/src/components/button/index.ts +2 -2
  21. package/src/components/button/types.ts +7 -7
  22. package/src/components/card/_styles.scss +67 -25
  23. package/src/components/card/api.ts +54 -3
  24. package/src/components/card/card.ts +25 -6
  25. package/src/components/card/config.ts +189 -22
  26. package/src/components/card/constants.ts +20 -19
  27. package/src/components/card/content.ts +299 -2
  28. package/src/components/card/features.ts +158 -4
  29. package/src/components/card/index.ts +31 -9
  30. package/src/components/card/types.ts +166 -15
  31. package/src/components/checkbox/_styles.scss +0 -2
  32. package/src/components/chip/chip.ts +1 -9
  33. package/src/components/chip/constants.ts +0 -10
  34. package/src/components/chip/index.ts +1 -1
  35. package/src/components/chip/types.ts +1 -4
  36. package/src/components/datepicker/_styles.scss +358 -0
  37. package/src/components/datepicker/api.ts +272 -0
  38. package/src/components/datepicker/config.ts +144 -0
  39. package/src/components/datepicker/constants.ts +98 -0
  40. package/src/components/datepicker/datepicker.ts +346 -0
  41. package/src/components/datepicker/index.ts +9 -0
  42. package/src/components/datepicker/render.ts +452 -0
  43. package/src/components/datepicker/types.ts +268 -0
  44. package/src/components/datepicker/utils.ts +290 -0
  45. package/src/components/dialog/_styles.scss +174 -128
  46. package/src/components/dialog/api.ts +48 -13
  47. package/src/components/dialog/config.ts +9 -5
  48. package/src/components/dialog/dialog.ts +6 -3
  49. package/src/components/dialog/features.ts +290 -130
  50. package/src/components/dialog/types.ts +7 -4
  51. package/src/components/divider/_styles.scss +57 -0
  52. package/src/components/divider/config.ts +81 -0
  53. package/src/components/divider/divider.ts +37 -0
  54. package/src/components/divider/features.ts +207 -0
  55. package/src/components/divider/index.ts +5 -0
  56. package/src/components/divider/types.ts +55 -0
  57. package/src/components/extended-fab/_styles.scss +267 -0
  58. package/src/components/extended-fab/api.ts +141 -0
  59. package/src/components/extended-fab/config.ts +108 -0
  60. package/src/components/extended-fab/constants.ts +36 -0
  61. package/src/components/extended-fab/extended-fab.ts +125 -0
  62. package/src/components/extended-fab/index.ts +4 -0
  63. package/src/components/extended-fab/types.ts +287 -0
  64. package/src/components/fab/_styles.scss +225 -0
  65. package/src/components/fab/api.ts +97 -0
  66. package/src/components/fab/config.ts +94 -0
  67. package/src/components/fab/constants.ts +41 -0
  68. package/src/components/fab/fab.ts +67 -0
  69. package/src/components/fab/index.ts +4 -0
  70. package/src/components/fab/types.ts +234 -0
  71. package/src/components/navigation/_styles.scss +1 -0
  72. package/src/components/navigation/api.ts +78 -50
  73. package/src/components/navigation/features/items.ts +280 -0
  74. package/src/components/navigation/nav-item.ts +72 -23
  75. package/src/components/navigation/navigation.ts +54 -2
  76. package/src/components/navigation/types.ts +210 -188
  77. package/src/components/progress/_styles.scss +0 -65
  78. package/src/components/progress/config.ts +1 -2
  79. package/src/components/progress/constants.ts +0 -14
  80. package/src/components/progress/index.ts +1 -1
  81. package/src/components/progress/progress.ts +1 -4
  82. package/src/components/progress/types.ts +1 -4
  83. package/src/components/radios/_styles.scss +0 -45
  84. package/src/components/radios/api.ts +85 -60
  85. package/src/components/radios/config.ts +1 -2
  86. package/src/components/radios/constants.ts +0 -9
  87. package/src/components/radios/index.ts +1 -1
  88. package/src/components/radios/radio.ts +34 -11
  89. package/src/components/radios/radios.ts +2 -1
  90. package/src/components/radios/types.ts +1 -7
  91. package/src/components/search/_styles.scss +306 -0
  92. package/src/components/search/api.ts +203 -0
  93. package/src/components/search/config.ts +87 -0
  94. package/src/components/search/constants.ts +21 -0
  95. package/src/components/search/features/index.ts +4 -0
  96. package/src/components/search/features/search.ts +718 -0
  97. package/src/components/search/features/states.ts +165 -0
  98. package/src/components/search/features/structure.ts +198 -0
  99. package/src/components/search/index.ts +10 -0
  100. package/src/components/search/search.ts +52 -0
  101. package/src/components/search/types.ts +163 -0
  102. package/src/components/segmented-button/_styles.scss +117 -0
  103. package/src/components/segmented-button/config.ts +67 -0
  104. package/src/components/segmented-button/constants.ts +42 -0
  105. package/src/components/segmented-button/index.ts +4 -0
  106. package/src/components/segmented-button/segment.ts +155 -0
  107. package/src/components/segmented-button/segmented-button.ts +250 -0
  108. package/src/components/segmented-button/types.ts +219 -0
  109. package/src/components/slider/_styles.scss +221 -168
  110. package/src/components/slider/accessibility.md +59 -0
  111. package/src/components/slider/api.ts +41 -120
  112. package/src/components/slider/config.ts +51 -49
  113. package/src/components/slider/features/handlers.ts +495 -0
  114. package/src/components/slider/features/index.ts +1 -2
  115. package/src/components/slider/features/slider.ts +66 -84
  116. package/src/components/slider/features/states.ts +195 -0
  117. package/src/components/slider/features/structure.ts +141 -184
  118. package/src/components/slider/features/ui.ts +150 -201
  119. package/src/components/slider/index.ts +2 -11
  120. package/src/components/slider/slider.ts +9 -12
  121. package/src/components/slider/types.ts +39 -24
  122. package/src/components/switch/_styles.scss +0 -2
  123. package/src/components/tabs/_styles.scss +346 -154
  124. package/src/components/tabs/api.ts +178 -400
  125. package/src/components/tabs/config.ts +46 -52
  126. package/src/components/tabs/constants.ts +85 -8
  127. package/src/components/tabs/features.ts +403 -0
  128. package/src/components/tabs/index.ts +60 -3
  129. package/src/components/tabs/indicator.ts +285 -0
  130. package/src/components/tabs/responsive.ts +144 -0
  131. package/src/components/tabs/scroll-indicators.ts +149 -0
  132. package/src/components/tabs/state.ts +186 -0
  133. package/src/components/tabs/tab-api.ts +258 -0
  134. package/src/components/tabs/tab.ts +255 -0
  135. package/src/components/tabs/tabs.ts +50 -31
  136. package/src/components/tabs/types.ts +332 -128
  137. package/src/components/tabs/utils.ts +107 -0
  138. package/src/components/textfield/_styles.scss +0 -98
  139. package/src/components/textfield/config.ts +2 -3
  140. package/src/components/textfield/constants.ts +0 -14
  141. package/src/components/textfield/index.ts +2 -2
  142. package/src/components/textfield/textfield.ts +0 -2
  143. package/src/components/textfield/types.ts +1 -4
  144. package/src/components/timepicker/README.md +277 -0
  145. package/src/components/timepicker/_styles.scss +451 -0
  146. package/src/components/timepicker/api.ts +632 -0
  147. package/src/components/timepicker/clockdial.ts +482 -0
  148. package/src/components/timepicker/config.ts +130 -0
  149. package/src/components/timepicker/constants.ts +138 -0
  150. package/src/components/timepicker/index.ts +8 -0
  151. package/src/components/timepicker/render.ts +613 -0
  152. package/src/components/timepicker/timepicker.ts +117 -0
  153. package/src/components/timepicker/types.ts +336 -0
  154. package/src/components/timepicker/utils.ts +241 -0
  155. package/src/components/top-app-bar/_styles.scss +225 -0
  156. package/src/components/top-app-bar/config.ts +83 -0
  157. package/src/components/top-app-bar/index.ts +11 -0
  158. package/src/components/top-app-bar/top-app-bar.ts +316 -0
  159. package/src/components/top-app-bar/types.ts +140 -0
  160. package/src/core/build/_ripple.scss +6 -6
  161. package/src/core/build/ripple.ts +72 -95
  162. package/src/core/compose/component.ts +1 -1
  163. package/src/core/compose/features/badge.ts +79 -0
  164. package/src/core/compose/features/icon.ts +3 -1
  165. package/src/core/compose/features/index.ts +3 -1
  166. package/src/core/compose/features/ripple.ts +4 -1
  167. package/src/core/compose/features/textlabel.ts +26 -2
  168. package/src/core/dom/create.ts +5 -0
  169. package/src/index.ts +9 -0
  170. package/src/styles/abstract/_theme.scss +115 -3
  171. package/src/styles/themes/_autumn.scss +21 -0
  172. package/src/styles/themes/_base-theme.scss +61 -0
  173. package/src/styles/themes/_baseline.scss +58 -0
  174. package/src/styles/themes/_bluekhaki.scss +125 -0
  175. package/src/styles/themes/_brownbeige.scss +125 -0
  176. package/src/styles/themes/_browngreen.scss +125 -0
  177. package/src/styles/themes/_forest.scss +6 -0
  178. package/src/styles/themes/_greenbeige.scss +125 -0
  179. package/src/styles/themes/_material.scss +125 -0
  180. package/src/styles/themes/_ocean.scss +6 -0
  181. package/src/styles/themes/_sageivory.scss +125 -0
  182. package/src/styles/themes/_spring.scss +6 -0
  183. package/src/styles/themes/_summer.scss +5 -0
  184. package/src/styles/themes/_sunset.scss +5 -0
  185. package/src/styles/themes/_tealcaramel.scss +125 -0
  186. package/src/styles/themes/_winter.scss +6 -0
  187. package/src/components/card/actions.ts +0 -48
  188. package/src/components/card/header.ts +0 -88
  189. package/src/components/card/media.ts +0 -52
  190. package/src/components/navigation/features/items.js +0 -192
  191. package/src/components/slider/features/appearance.ts +0 -94
  192. package/src/components/slider/features/disabled.ts +0 -43
  193. package/src/components/slider/features/events.ts +0 -164
  194. package/src/components/slider/features/interactions.ts +0 -261
  195. package/src/components/slider/features/keyboard.ts +0 -112
  196. package/src/core/collection/adapters/mongodb.js +0 -232
@@ -0,0 +1,234 @@
1
+ // src/components/fab/types.ts
2
+ import { FAB_VARIANTS, FAB_SIZES } from './constants';
3
+
4
+ /**
5
+ * Configuration interface for the FAB component
6
+ * @category Components
7
+ */
8
+ export interface FabConfig {
9
+ /**
10
+ * FAB variant that determines visual styling
11
+ * @default 'primary'
12
+ */
13
+ variant?: keyof typeof FAB_VARIANTS | string;
14
+
15
+ /**
16
+ * FAB size variant
17
+ * @default 'default'
18
+ */
19
+ size?: keyof typeof FAB_SIZES | string;
20
+
21
+ /**
22
+ * Whether the FAB is initially disabled
23
+ * @default false
24
+ */
25
+ disabled?: boolean;
26
+
27
+ /**
28
+ * FAB icon HTML content
29
+ * @example '<svg>...</svg>'
30
+ */
31
+ icon?: string;
32
+
33
+ /**
34
+ * Icon size in pixels or other CSS units
35
+ * @example '24px'
36
+ */
37
+ iconSize?: string;
38
+
39
+ /**
40
+ * Additional CSS classes to add to the FAB
41
+ * @example 'home-fab bottom-right'
42
+ */
43
+ class?: string;
44
+
45
+ /**
46
+ * Button value attribute
47
+ */
48
+ value?: string;
49
+
50
+ /**
51
+ * Position of the FAB on the screen
52
+ * @example 'bottom-right'
53
+ */
54
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | string;
55
+
56
+ /**
57
+ * Button type attribute
58
+ * @default 'button'
59
+ */
60
+ type?: 'button' | 'submit' | 'reset';
61
+
62
+ /**
63
+ * Accessible label for screen readers
64
+ */
65
+ ariaLabel?: string;
66
+
67
+ /**
68
+ * Whether to enable ripple effect
69
+ * @default true
70
+ */
71
+ ripple?: boolean;
72
+
73
+ /**
74
+ * Component prefix for class names
75
+ * @default 'mtrl'
76
+ */
77
+ prefix?: string;
78
+
79
+ /**
80
+ * Component name used in class generation
81
+ */
82
+ componentName?: string;
83
+
84
+ /**
85
+ * Ripple effect configuration
86
+ */
87
+ rippleConfig?: {
88
+ /** Duration of the ripple animation in milliseconds */
89
+ duration?: number;
90
+ /** Timing function for the ripple animation */
91
+ timing?: string;
92
+ /** Opacity values for ripple start and end [start, end] */
93
+ opacity?: [string, string];
94
+ };
95
+
96
+ /**
97
+ * Whether to show the FAB with an entrance animation
98
+ * @default false
99
+ */
100
+ animate?: boolean;
101
+ }
102
+
103
+ /**
104
+ * FAB component interface
105
+ * @category Components
106
+ */
107
+ export interface FabComponent {
108
+ /** The FAB's DOM element */
109
+ element: HTMLButtonElement;
110
+
111
+ /** API for managing FAB icons */
112
+ icon: {
113
+ /** Sets the icon HTML content */
114
+ setIcon: (html: string) => any;
115
+ /** Gets the current icon HTML content */
116
+ getIcon: () => string;
117
+ /** Gets the icon DOM element */
118
+ getElement: () => HTMLElement | null;
119
+ };
120
+
121
+ /** API for managing disabled state */
122
+ disabled: {
123
+ /** Enables the FAB */
124
+ enable: () => void;
125
+ /** Disables the FAB */
126
+ disable: () => void;
127
+ /** Checks if the FAB is disabled */
128
+ isDisabled: () => boolean;
129
+ };
130
+
131
+ /** API for managing component lifecycle */
132
+ lifecycle: {
133
+ /** Destroys the component and cleans up resources */
134
+ destroy: () => void;
135
+ };
136
+
137
+ /**
138
+ * Gets a class name with the component's prefix
139
+ * @param name - Base class name
140
+ * @returns Prefixed class name
141
+ */
142
+ getClass: (name: string) => string;
143
+
144
+ /**
145
+ * Gets the FAB's value attribute
146
+ * @returns FAB value
147
+ */
148
+ getValue: () => string;
149
+
150
+ /**
151
+ * Sets the FAB's value attribute
152
+ * @param value - New value
153
+ * @returns The FAB component for chaining
154
+ */
155
+ setValue: (value: string) => FabComponent;
156
+
157
+ /**
158
+ * Enables the FAB (removes disabled attribute)
159
+ * @returns The FAB component for chaining
160
+ */
161
+ enable: () => FabComponent;
162
+
163
+ /**
164
+ * Disables the FAB (adds disabled attribute)
165
+ * @returns The FAB component for chaining
166
+ */
167
+ disable: () => FabComponent;
168
+
169
+ /**
170
+ * Sets the FAB's icon
171
+ * @param icon - Icon HTML content
172
+ * @returns The FAB component for chaining
173
+ */
174
+ setIcon: (icon: string) => FabComponent;
175
+
176
+ /**
177
+ * Gets the FAB's icon HTML content
178
+ * @returns Icon HTML
179
+ */
180
+ getIcon: () => string;
181
+
182
+ /**
183
+ * Sets the FAB's position
184
+ * @param position - Position value ('top-right', 'bottom-left', etc.)
185
+ * @returns The FAB component for chaining
186
+ */
187
+ setPosition: (position: string) => FabComponent;
188
+
189
+ /**
190
+ * Gets the current position of the FAB
191
+ * @returns Current position
192
+ */
193
+ getPosition: () => string | null;
194
+
195
+ /**
196
+ * Lowers the FAB (useful for pressed state)
197
+ * @returns The FAB component for chaining
198
+ */
199
+ lower: () => FabComponent;
200
+
201
+ /**
202
+ * Raises the FAB back to its default elevation
203
+ * @returns The FAB component for chaining
204
+ */
205
+ raise: () => FabComponent;
206
+
207
+ /**
208
+ * Destroys the FAB component and cleans up resources
209
+ */
210
+ destroy: () => void;
211
+
212
+ /**
213
+ * Adds an event listener to the FAB
214
+ * @param event - Event name ('click', 'focus', etc.)
215
+ * @param handler - Event handler function
216
+ * @returns The FAB component for chaining
217
+ */
218
+ on: (event: string, handler: Function) => FabComponent;
219
+
220
+ /**
221
+ * Removes an event listener from the FAB
222
+ * @param event - Event name
223
+ * @param handler - Event handler function
224
+ * @returns The FAB component for chaining
225
+ */
226
+ off: (event: string, handler: Function) => FabComponent;
227
+
228
+ /**
229
+ * Adds CSS classes to the FAB element
230
+ * @param classes - One or more class names to add
231
+ * @returns The FAB component for chaining
232
+ */
233
+ addClass: (...classes: string[]) => FabComponent;
234
+ }
@@ -249,6 +249,7 @@ $component: '#{base.$prefix}-nav';
249
249
  padding: 12px 0;
250
250
  transition: transform v.motion('duration-short2') v.motion('easing-standard');
251
251
  transform: translateX(0);
252
+ overflow-y: auto;
252
253
 
253
254
  // Hidden state
254
255
  &.#{$component}--hidden {
@@ -1,79 +1,107 @@
1
- // src/components/navigation/api.ts
2
- import {
3
- BaseComponent,
4
- NavigationComponent,
5
- ApiOptions,
6
- NavItemConfig,
7
- NavItemData
8
- } from './types';
1
+ // src/components/button/api.ts
2
+ import { ButtonComponent } from './types';
3
+
4
+ interface ApiOptions {
5
+ disabled: {
6
+ enable: () => void;
7
+ disable: () => void;
8
+ };
9
+ lifecycle: {
10
+ destroy: () => void;
11
+ };
12
+ }
13
+
14
+ interface ComponentWithElements {
15
+ element: HTMLElement;
16
+ text: {
17
+ setText: (content: string) => any;
18
+ getText: () => string;
19
+ getElement: () => HTMLElement | null;
20
+ };
21
+ icon: {
22
+ setIcon: (html: string) => any;
23
+ getIcon: () => string;
24
+ getElement: () => HTMLElement | null;
25
+ };
26
+ getClass: (name: string) => string;
27
+ }
9
28
 
10
29
  /**
11
- * Enhances navigation component with API methods
12
- * @param {ApiOptions} options - API configuration
30
+ * Enhances a button component with API methods
31
+ * @param {ApiOptions} options - API configuration options
13
32
  * @returns {Function} Higher-order function that adds API methods to component
33
+ * @internal This is an internal utility for the Button component
14
34
  */
15
35
  export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
16
- (component: BaseComponent): NavigationComponent => ({
36
+ (component: ComponentWithElements): ButtonComponent => ({
17
37
  ...component as any,
18
- element: component.element,
19
- items: component.items as Map<string, NavItemData>,
20
-
21
- // Item management
22
- addItem(config: NavItemConfig): NavigationComponent {
23
- component.addItem?.(config);
24
- return this;
25
- },
38
+ element: component.element as HTMLButtonElement,
39
+
40
+ getValue: () => component.element.value,
26
41
 
27
- removeItem(id: string): NavigationComponent {
28
- component.removeItem?.(id);
42
+ setValue(value: string) {
43
+ component.element.value = value;
29
44
  return this;
30
45
  },
31
46
 
32
- getItem(id: string): NavItemData | undefined {
33
- return component.getItem?.(id);
47
+ enable() {
48
+ disabled.enable();
49
+ return this;
34
50
  },
35
51
 
36
- getAllItems(): NavItemData[] {
37
- return component.getAllItems?.() || [];
52
+ disable() {
53
+ disabled.disable();
54
+ return this;
38
55
  },
39
56
 
40
- getItemPath(id: string): string[] {
41
- return component.getItemPath?.(id) || [];
42
- },
43
-
44
- // Active state management
45
- setActive(id: string): NavigationComponent {
46
- component.setActive?.(id);
57
+ setText(content: string) {
58
+ component.text.setText(content);
59
+ this.updateCircularStyle();
60
+
61
+ // If removing text from a button with an icon, ensure it has an accessible name
62
+ if (!content && component.icon.getElement()) {
63
+ if (!this.element.getAttribute('aria-label')) {
64
+ const className = this.element.className.split(' ')
65
+ .find(cls => !cls.startsWith(`${component.getClass('button')}`));
66
+
67
+ if (className) {
68
+ this.element.setAttribute('aria-label', className);
69
+ }
70
+ }
71
+ }
72
+
47
73
  return this;
48
74
  },
49
75
 
50
- getActive(): NavItemData | null {
51
- return component.getActive?.() || null;
52
- },
53
-
54
- // Event handling
55
- on(event: string, handler: Function): NavigationComponent {
56
- component.on?.(event, handler);
57
- return this;
76
+ getText() {
77
+ return component.text.getText();
58
78
  },
59
79
 
60
- off(event: string, handler: Function): NavigationComponent {
61
- component.off?.(event, handler);
80
+ setIcon(icon: string) {
81
+ component.icon.setIcon(icon);
82
+ this.updateCircularStyle();
62
83
  return this;
63
84
  },
64
-
65
- // State management
66
- enable(): NavigationComponent {
67
- disabled.enable();
68
- return this;
85
+
86
+ getIcon() {
87
+ return component.icon.getIcon();
69
88
  },
70
89
 
71
- disable(): NavigationComponent {
72
- disabled.disable();
90
+ setAriaLabel(label: string) {
91
+ component.element.setAttribute('aria-label', label);
73
92
  return this;
74
93
  },
75
94
 
76
- destroy(): void {
95
+ destroy() {
77
96
  lifecycle.destroy();
97
+ },
98
+
99
+ updateCircularStyle() {
100
+ const hasText = component.text.getText();
101
+ if (!hasText && component.icon.getElement()) {
102
+ component.element.classList.add(`${component.getClass('button')}--circular`);
103
+ } else {
104
+ component.element.classList.remove(`${component.getClass('button')}--circular`);
105
+ }
78
106
  }
79
107
  });
@@ -0,0 +1,280 @@
1
+ // src/components/navigation/features/items.ts
2
+ import { createNavItem, getAllNestedItems } from '../nav-item';
3
+ import { NavItemConfig, NavItemData } from '../types';
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 {
16
+ items: Map<string, NavItemData>;
17
+ addItem: (config: NavItemConfig) => ItemsComponent;
18
+ removeItem: (id: string) => ItemsComponent;
19
+ getItem: (id: string) => NavItemData | undefined;
20
+ getAllItems: () => NavItemData[];
21
+ getActive: () => NavItemData | null;
22
+ getItemPath: (id: string) => string[];
23
+ setActive: (id: string) => ItemsComponent;
24
+ }
25
+
26
+ interface NavigationConfig {
27
+ prefix?: string;
28
+ items?: NavItemConfig[];
29
+ [key: string]: any;
30
+ }
31
+
32
+ export const withNavItems = (config: NavigationConfig) => (component: Component): ItemsComponent => {
33
+ const items = new Map<string, NavItemData>();
34
+ let activeItem: NavItemData | null = null;
35
+
36
+ /**
37
+ * Recursively stores items in the items Map
38
+ * @param {NavItemConfig} itemConfig - Item configuration
39
+ * @param {HTMLElement} item - Created item element
40
+ */
41
+ const storeItem = (itemConfig: NavItemConfig, item: HTMLElement): void => {
42
+ items.set(itemConfig.id, { element: item, config: itemConfig });
43
+
44
+ if (itemConfig.items?.length) {
45
+ itemConfig.items.forEach(nestedConfig => {
46
+ const container = item.closest(`.${config.prefix}-nav-item-container`);
47
+ if (container) {
48
+ const nestedContainer = container.querySelector(`.${config.prefix}-nav-nested-container`);
49
+ if (nestedContainer) {
50
+ const nestedItem = nestedContainer.querySelector(`[data-id="${nestedConfig.id}"]`) as HTMLElement;
51
+ if (nestedItem) {
52
+ storeItem(nestedConfig, nestedItem);
53
+ }
54
+ }
55
+ }
56
+ });
57
+ }
58
+ };
59
+
60
+ /**
61
+ * Updates the active state for an item
62
+ * @param {HTMLElement} item - Item element to activate
63
+ * @param {NavItemData} itemData - Item data
64
+ * @param {boolean} active - Whether to make active or inactive
65
+ */
66
+ const updateActiveState = (item: HTMLElement, itemData: NavItemData, active: boolean): void => {
67
+ // Determine the correct active attribute based on role
68
+ const role = item.getAttribute('role');
69
+
70
+ if (active) {
71
+ item.classList.add(`${config.prefix}-nav-item--active`);
72
+
73
+ // Set appropriate attribute based on role
74
+ if (role === 'tab') {
75
+ item.setAttribute('aria-selected', 'true');
76
+ item.setAttribute('tabindex', '0');
77
+ } else if (!item.getAttribute('aria-haspopup')) {
78
+ // Use aria-current for navigation items that aren't expandable
79
+ item.setAttribute('aria-current', 'page');
80
+ }
81
+ } else {
82
+ item.classList.remove(`${config.prefix}-nav-item--active`);
83
+
84
+ // Remove appropriate attribute based on role
85
+ if (role === 'tab') {
86
+ item.setAttribute('aria-selected', 'false');
87
+ item.setAttribute('tabindex', '-1');
88
+ } else if (item.hasAttribute('aria-current')) {
89
+ item.removeAttribute('aria-current');
90
+ }
91
+ }
92
+ };
93
+
94
+ // Create initial items
95
+ if (config.items) {
96
+ config.items.forEach(itemConfig => {
97
+ const item = createNavItem(itemConfig, component.element, config.prefix || 'mtrl');
98
+ storeItem(itemConfig, item);
99
+
100
+ if (itemConfig.active) {
101
+ activeItem = { element: item, config: itemConfig };
102
+ updateActiveState(item, activeItem, true);
103
+ }
104
+ });
105
+ }
106
+
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;
131
+
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)
139
+ });
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;
164
+
165
+ path.unshift(parentId);
166
+
167
+ // Move up to next level
168
+ parentContainer = parentItemContainer.closest(`.${config.prefix}-nav-nested-container`);
169
+ }
170
+
171
+ return path;
172
+ };
173
+
174
+ // Clean up when component is destroyed
175
+ if (component.lifecycle) {
176
+ const originalDestroy = component.lifecycle.destroy;
177
+ component.lifecycle.destroy = () => {
178
+ items.clear();
179
+ if (originalDestroy) {
180
+ originalDestroy();
181
+ }
182
+ };
183
+ }
184
+
185
+ return {
186
+ ...component,
187
+ items,
188
+
189
+ addItem(itemConfig: NavItemConfig) {
190
+ if (items.has(itemConfig.id)) return this;
191
+
192
+ const item = createNavItem(itemConfig, component.element, config.prefix || 'mtrl');
193
+ storeItem(itemConfig, item);
194
+
195
+ if (itemConfig.active) {
196
+ this.setActive(itemConfig.id);
197
+ }
198
+
199
+ if (component.emit) {
200
+ component.emit('itemAdded', {
201
+ id: itemConfig.id,
202
+ item: { element: item, config: itemConfig }
203
+ });
204
+ }
205
+ return this;
206
+ },
207
+
208
+ removeItem(id: string) {
209
+ const item = items.get(id);
210
+ if (!item) return this;
211
+
212
+ // Remove all nested items first
213
+ const nestedItems = getAllNestedItems(item.element, config.prefix || 'mtrl');
214
+ nestedItems.forEach(nestedItem => {
215
+ const nestedId = nestedItem.dataset.id;
216
+ if (nestedId) items.delete(nestedId);
217
+ });
218
+
219
+ if (activeItem?.config.id === id) {
220
+ activeItem = null;
221
+ }
222
+
223
+ // Remove the entire item container
224
+ const container = item.element.closest(`.${config.prefix}-nav-item-container`);
225
+ if (container) {
226
+ container.remove();
227
+ }
228
+ items.delete(id);
229
+
230
+ if (component.emit) {
231
+ component.emit('itemRemoved', { id, item });
232
+ }
233
+ return this;
234
+ },
235
+
236
+ getItem: (id: string) => items.get(id),
237
+ getAllItems: () => Array.from(items.values()),
238
+ getActive: () => activeItem,
239
+ getItemPath: (id: string) => getItemPath(id),
240
+
241
+ setActive(id: string) {
242
+ const item = items.get(id);
243
+ if (!item || item.config.disabled) return this;
244
+
245
+ if (activeItem) {
246
+ updateActiveState(activeItem.element, activeItem, false);
247
+ }
248
+
249
+ updateActiveState(item.element, item, true);
250
+ activeItem = item;
251
+
252
+ // Ensure all parent items are expanded
253
+ const path = getItemPath(id);
254
+ path.forEach(parentId => {
255
+ const parentItem = items.get(parentId);
256
+ if (parentItem) {
257
+ const parentButton = parentItem.element;
258
+ const container = parentButton.closest(`.${config.prefix}-nav-item-container`);
259
+ if (container) {
260
+ const nestedContainer = container.querySelector(`.${config.prefix}-nav-nested-container`);
261
+ if (nestedContainer) {
262
+ parentButton.setAttribute('aria-expanded', 'true');
263
+ nestedContainer.hidden = false;
264
+ }
265
+ }
266
+ }
267
+ });
268
+
269
+ if (component.emit) {
270
+ component.emit('activeChanged', {
271
+ id,
272
+ item,
273
+ previousItem: activeItem,
274
+ path: getItemPath(id)
275
+ });
276
+ }
277
+ return this;
278
+ }
279
+ };
280
+ };