mtrl 0.3.5 → 0.3.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 (65) hide show
  1. package/package.json +1 -1
  2. package/src/components/button/api.ts +16 -0
  3. package/src/components/button/types.ts +9 -0
  4. package/src/components/menu/api.ts +144 -267
  5. package/src/components/menu/config.ts +84 -40
  6. package/src/components/menu/features/anchor.ts +243 -0
  7. package/src/components/menu/features/controller.ts +1167 -0
  8. package/src/components/menu/features/index.ts +5 -0
  9. package/src/components/menu/features/position.ts +353 -0
  10. package/src/components/menu/index.ts +31 -63
  11. package/src/components/menu/menu.ts +72 -104
  12. package/src/components/menu/types.ts +264 -447
  13. package/src/components/select/api.ts +78 -0
  14. package/src/components/select/config.ts +76 -0
  15. package/src/components/select/features.ts +317 -0
  16. package/src/components/select/index.ts +38 -0
  17. package/src/components/select/select.ts +73 -0
  18. package/src/components/select/types.ts +355 -0
  19. package/src/components/textfield/api.ts +78 -6
  20. package/src/components/textfield/features/index.ts +17 -0
  21. package/src/components/textfield/features/leading-icon.ts +127 -0
  22. package/src/components/textfield/features/placement.ts +149 -0
  23. package/src/components/textfield/features/prefix-text.ts +107 -0
  24. package/src/components/textfield/features/suffix-text.ts +100 -0
  25. package/src/components/textfield/features/supporting-text.ts +113 -0
  26. package/src/components/textfield/features/trailing-icon.ts +108 -0
  27. package/src/components/textfield/textfield.ts +51 -15
  28. package/src/components/textfield/types.ts +70 -0
  29. package/src/core/collection/adapters/base.ts +62 -0
  30. package/src/core/collection/collection.ts +300 -0
  31. package/src/core/collection/index.ts +57 -0
  32. package/src/core/collection/list-manager.ts +333 -0
  33. package/src/core/dom/classes.ts +81 -9
  34. package/src/core/dom/create.ts +30 -19
  35. package/src/core/layout/README.md +531 -166
  36. package/src/core/layout/array.ts +3 -4
  37. package/src/core/layout/config.ts +193 -0
  38. package/src/core/layout/create.ts +1 -2
  39. package/src/core/layout/index.ts +12 -2
  40. package/src/core/layout/object.ts +2 -3
  41. package/src/core/layout/processor.ts +60 -12
  42. package/src/core/layout/result.ts +1 -2
  43. package/src/core/layout/types.ts +105 -50
  44. package/src/core/layout/utils.ts +69 -61
  45. package/src/index.ts +6 -2
  46. package/src/styles/abstract/_variables.scss +18 -0
  47. package/src/styles/components/_button.scss +21 -5
  48. package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
  49. package/src/styles/components/_menu.scss +109 -18
  50. package/src/styles/components/_select.scss +265 -0
  51. package/src/styles/components/_textfield.scss +233 -42
  52. package/src/styles/main.scss +24 -23
  53. package/src/styles/utilities/_layout.scss +665 -0
  54. package/src/components/menu/features/items-manager.ts +0 -457
  55. package/src/components/menu/features/keyboard-navigation.ts +0 -133
  56. package/src/components/menu/features/positioning.ts +0 -127
  57. package/src/components/menu/features/visibility.ts +0 -230
  58. package/src/components/menu/menu-item.ts +0 -86
  59. package/src/components/menu/utils.ts +0 -67
  60. package/src/components/textfield/features.ts +0 -322
  61. package/src/core/collection/adapters/base.js +0 -26
  62. package/src/core/collection/collection.js +0 -259
  63. package/src/core/collection/list-manager.js +0 -157
  64. /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
  65. /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
@@ -0,0 +1,355 @@
1
+ // src/components/select/types.ts
2
+
3
+ /**
4
+ * Available Select variants
5
+ */
6
+ export type SelectVariant = 'filled' | 'outlined';
7
+
8
+ /**
9
+ * Select variant constants
10
+ */
11
+ export const SELECT_VARIANTS = {
12
+ FILLED: 'filled',
13
+ OUTLINED: 'outlined'
14
+ } as const;
15
+
16
+ /**
17
+ * Select option interface
18
+ */
19
+ export interface SelectOption {
20
+ /**
21
+ * Unique identifier for the option
22
+ */
23
+ id: string;
24
+
25
+ /**
26
+ * Display text for the option
27
+ */
28
+ text: string;
29
+
30
+ /**
31
+ * Whether the option is disabled
32
+ */
33
+ disabled?: boolean;
34
+
35
+ /**
36
+ * Optional icon to display with the option
37
+ */
38
+ icon?: string;
39
+
40
+ /**
41
+ * Whether this option has a submenu
42
+ */
43
+ hasSubmenu?: boolean;
44
+
45
+ /**
46
+ * Optional array of submenu options
47
+ * Only used when hasSubmenu is true
48
+ */
49
+ submenu?: SelectOption[];
50
+
51
+ /**
52
+ * Additional data associated with the option
53
+ */
54
+ data?: any;
55
+ }
56
+
57
+ /**
58
+ * Configuration interface for the Select component
59
+ */
60
+ export interface SelectConfig {
61
+ /**
62
+ * Array of options to display in the select menu
63
+ */
64
+ options: SelectOption[];
65
+
66
+ /**
67
+ * Currently selected value (option id)
68
+ */
69
+ value?: string;
70
+
71
+ /**
72
+ * Visual variant (filled, outlined)
73
+ */
74
+ variant?: SelectVariant | string;
75
+
76
+ /**
77
+ * Label text
78
+ */
79
+ label?: string;
80
+
81
+ /**
82
+ * Input name attribute
83
+ */
84
+ name?: string;
85
+
86
+ /**
87
+ * Whether select is required
88
+ */
89
+ required?: boolean;
90
+
91
+ /**
92
+ * Whether select is disabled
93
+ */
94
+ disabled?: boolean;
95
+
96
+ /**
97
+ * Supporting text content
98
+ */
99
+ supportingText?: string;
100
+
101
+ /**
102
+ * Whether supporting text indicates an error
103
+ */
104
+ error?: boolean;
105
+
106
+ /**
107
+ * Menu placement relative to the textfield
108
+ */
109
+ placement?: string;
110
+
111
+ /**
112
+ * Additional CSS classes
113
+ */
114
+ class?: string;
115
+
116
+ /**
117
+ * Prefix for class names
118
+ */
119
+ prefix?: string;
120
+
121
+ /**
122
+ * Component name
123
+ */
124
+ componentName?: string;
125
+
126
+ /**
127
+ * Event callbacks
128
+ */
129
+ on?: {
130
+ /**
131
+ * Called when the select value changes
132
+ */
133
+ change?: (event: SelectChangeEvent) => void;
134
+
135
+ /**
136
+ * Called when the select menu opens
137
+ */
138
+ open?: (event: SelectEvent) => void;
139
+
140
+ /**
141
+ * Called when the select menu closes
142
+ */
143
+ close?: (event: SelectEvent) => void;
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Select component interface
149
+ */
150
+ export interface SelectComponent {
151
+ /**
152
+ * The root element of the select
153
+ */
154
+ element: HTMLElement;
155
+
156
+ /**
157
+ * The textfield component
158
+ */
159
+ textfield: any;
160
+
161
+ /**
162
+ * The menu component
163
+ */
164
+ menu: any;
165
+
166
+ /**
167
+ * Gets the select's current value (selected option id)
168
+ */
169
+ getValue: () => string | null;
170
+
171
+ /**
172
+ * Sets the select's value (by option id)
173
+ * @param value - Option id to select
174
+ * @returns Select component for chaining
175
+ */
176
+ setValue: (value: string) => SelectComponent;
177
+
178
+ /**
179
+ * Gets the select's current displayed text
180
+ */
181
+ getText: () => string;
182
+
183
+ /**
184
+ * Gets the selected option object
185
+ */
186
+ getSelectedOption: () => SelectOption | null;
187
+
188
+ /**
189
+ * Gets all available options
190
+ */
191
+ getOptions: () => SelectOption[];
192
+
193
+ /**
194
+ * Sets new options
195
+ * @param options - New options array
196
+ * @returns Select component for chaining
197
+ */
198
+ setOptions: (options: SelectOption[]) => SelectComponent;
199
+
200
+ /**
201
+ * Opens the select menu
202
+ * @param interactionType - The type of interaction ('mouse' or 'keyboard')
203
+ * @returns Select component for chaining
204
+ */
205
+ open: (interactionType?: 'mouse' | 'keyboard') => SelectComponent;
206
+
207
+ /**
208
+ * Closes the select menu
209
+ * @returns Select component for chaining
210
+ */
211
+ close: () => SelectComponent;
212
+
213
+ /**
214
+ * Checks if the menu is open
215
+ */
216
+ isOpen: () => boolean;
217
+
218
+ /**
219
+ * Adds an event listener
220
+ * @param event - Event name
221
+ * @param handler - Event handler
222
+ * @returns Select component for chaining
223
+ */
224
+ on: <T extends keyof SelectEvents>(event: T, handler: SelectEvents[T]) => SelectComponent;
225
+
226
+ /**
227
+ * Removes an event listener
228
+ * @param event - Event name
229
+ * @param handler - Event handler
230
+ * @returns Select component for chaining
231
+ */
232
+ off: <T extends keyof SelectEvents>(event: T, handler: SelectEvents[T]) => SelectComponent;
233
+
234
+ /**
235
+ * Enables the select
236
+ * @returns Select component for chaining
237
+ */
238
+ enable: () => SelectComponent;
239
+
240
+ /**
241
+ * Disables the select
242
+ * @returns Select component for chaining
243
+ */
244
+ disable: () => SelectComponent;
245
+
246
+ /**
247
+ * Destroys the select component
248
+ */
249
+ destroy: () => void;
250
+ }
251
+
252
+ /**
253
+ * Select event interface
254
+ */
255
+ export interface SelectEvent {
256
+ /**
257
+ * The select component
258
+ */
259
+ select: SelectComponent;
260
+
261
+ /**
262
+ * Original DOM event if available
263
+ */
264
+ originalEvent?: Event;
265
+
266
+ /**
267
+ * Function to prevent default behavior
268
+ */
269
+ preventDefault: () => void;
270
+
271
+ /**
272
+ * Whether default behavior was prevented
273
+ */
274
+ defaultPrevented: boolean;
275
+ }
276
+
277
+ /**
278
+ * Select change event interface
279
+ */
280
+ export interface SelectChangeEvent extends SelectEvent {
281
+ /**
282
+ * The selected option id
283
+ */
284
+ value: string;
285
+
286
+ /**
287
+ * The selected option text
288
+ */
289
+ text: string;
290
+
291
+ /**
292
+ * The complete selected option object
293
+ */
294
+ option: SelectOption;
295
+ }
296
+
297
+ /**
298
+ * Select events interface for type-checking
299
+ * @internal
300
+ */
301
+ export interface SelectEvents {
302
+ 'change': (event: SelectChangeEvent) => void;
303
+ 'open': (event: SelectEvent) => void;
304
+ 'close': (event: SelectEvent) => void;
305
+ }
306
+
307
+ /**
308
+ * API options interface
309
+ * @internal
310
+ */
311
+ export interface ApiOptions {
312
+ select: {
313
+ getValue: () => string | null;
314
+ setValue: (value: string) => any;
315
+ getText: () => string;
316
+ getSelectedOption: () => SelectOption | null;
317
+ getOptions: () => SelectOption[];
318
+ setOptions: (options: SelectOption[]) => any;
319
+ open: () => any;
320
+ close: () => any;
321
+ isOpen: () => boolean;
322
+ };
323
+ events?: {
324
+ on: <T extends string>(event: T, handler: (event: any) => void) => any;
325
+ off: <T extends string>(event: T, handler: (event: any) => void) => any;
326
+ };
327
+ disabled: {
328
+ enable: () => any;
329
+ disable: () => any;
330
+ };
331
+ lifecycle: {
332
+ destroy: () => void;
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Base component interface
338
+ * @internal
339
+ */
340
+ export interface BaseComponent {
341
+ element: HTMLElement;
342
+ textfield?: any;
343
+ menu?: any;
344
+ on?: (event: string, handler: Function) => any;
345
+ off?: (event: string, handler: Function) => any;
346
+ emit?: (event: string, data: any) => void;
347
+ disabled?: {
348
+ enable: () => any;
349
+ disable: () => any;
350
+ };
351
+ lifecycle?: {
352
+ destroy: () => void;
353
+ };
354
+ [key: string]: any;
355
+ }
@@ -1,18 +1,14 @@
1
1
  // src/components/textfield/api.ts
2
2
  import { BaseComponent, TextfieldComponent, ApiOptions } from './types';
3
+ import { PlacementComponent } from './features/placement';
3
4
 
4
- /**
5
- * Enhances textfield component with API methods
6
- * @param {ApiOptions} options - API configuration
7
- * @returns {Function} Higher-order function that adds API methods to component
8
- */
9
5
  /**
10
6
  * Enhances textfield component with API methods
11
7
  * @param {ApiOptions} options - API configuration
12
8
  * @returns {Function} Higher-order function that adds API methods to component
13
9
  */
14
10
  export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
15
- (component: BaseComponent): TextfieldComponent => ({
11
+ (component: BaseComponent & Partial<PlacementComponent>): TextfieldComponent => ({
16
12
  ...component as any,
17
13
  element: component.element,
18
14
  input: component.input as HTMLInputElement | HTMLTextAreaElement,
@@ -42,6 +38,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
42
38
  // Label management
43
39
  setLabel(text: string): TextfieldComponent {
44
40
  component.label?.setText(text);
41
+ // Update positions after changing label
42
+ if (component.updateElementPositions) {
43
+ setTimeout(() => component.updateElementPositions(), 10);
44
+ }
45
45
  return this;
46
46
  },
47
47
 
@@ -54,6 +54,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
54
54
  setLeadingIcon(html: string): TextfieldComponent {
55
55
  if (component.setLeadingIcon) {
56
56
  component.setLeadingIcon(html);
57
+ // Update positions after changing icon
58
+ if (component.updateElementPositions) {
59
+ setTimeout(() => component.updateElementPositions(), 10);
60
+ }
57
61
  }
58
62
  return this;
59
63
  },
@@ -61,6 +65,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
61
65
  removeLeadingIcon(): TextfieldComponent {
62
66
  if (component.removeLeadingIcon) {
63
67
  component.removeLeadingIcon();
68
+ // Update positions after removing icon
69
+ if (component.updateElementPositions) {
70
+ setTimeout(() => component.updateElementPositions(), 10);
71
+ }
64
72
  }
65
73
  return this;
66
74
  },
@@ -70,6 +78,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
70
78
  setTrailingIcon(html: string): TextfieldComponent {
71
79
  if (component.setTrailingIcon) {
72
80
  component.setTrailingIcon(html);
81
+ // Update positions after changing icon
82
+ if (component.updateElementPositions) {
83
+ setTimeout(() => component.updateElementPositions(), 10);
84
+ }
73
85
  }
74
86
  return this;
75
87
  },
@@ -77,6 +89,10 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
77
89
  removeTrailingIcon(): TextfieldComponent {
78
90
  if (component.removeTrailingIcon) {
79
91
  component.removeTrailingIcon();
92
+ // Update positions after removing icon
93
+ if (component.updateElementPositions) {
94
+ setTimeout(() => component.updateElementPositions(), 10);
95
+ }
80
96
  }
81
97
  return this;
82
98
  },
@@ -96,6 +112,62 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
96
112
  }
97
113
  return this;
98
114
  },
115
+
116
+ // Prefix text management (if present)
117
+ prefixTextElement: component.prefixTextElement || null,
118
+ setPrefixText(text: string): TextfieldComponent {
119
+ if (component.setPrefixText) {
120
+ component.setPrefixText(text);
121
+ // Update positions after changing prefix
122
+ if (component.updateElementPositions) {
123
+ setTimeout(() => component.updateElementPositions(), 10);
124
+ }
125
+ }
126
+ return this;
127
+ },
128
+
129
+ removePrefixText(): TextfieldComponent {
130
+ if (component.removePrefixText) {
131
+ component.removePrefixText();
132
+ // Update positions after removing prefix
133
+ if (component.updateElementPositions) {
134
+ setTimeout(() => component.updateElementPositions(), 10);
135
+ }
136
+ }
137
+ return this;
138
+ },
139
+
140
+ // Suffix text management (if present)
141
+ suffixTextElement: component.suffixTextElement || null,
142
+ setSuffixText(text: string): TextfieldComponent {
143
+ if (component.setSuffixText) {
144
+ component.setSuffixText(text);
145
+ // Update positions after changing suffix
146
+ if (component.updateElementPositions) {
147
+ setTimeout(() => component.updateElementPositions(), 10);
148
+ }
149
+ }
150
+ return this;
151
+ },
152
+
153
+ removeSuffixText(): TextfieldComponent {
154
+ if (component.removeSuffixText) {
155
+ component.removeSuffixText();
156
+ // Update positions after removing suffix
157
+ if (component.updateElementPositions) {
158
+ setTimeout(() => component.updateElementPositions(), 10);
159
+ }
160
+ }
161
+ return this;
162
+ },
163
+
164
+ // Update positioning manually (useful after DOM updates)
165
+ updatePositions(): TextfieldComponent {
166
+ if (component.updateElementPositions) {
167
+ component.updateElementPositions();
168
+ }
169
+ return this;
170
+ },
99
171
 
100
172
  // Event handling
101
173
  on(event: string, handler: Function): TextfieldComponent {
@@ -0,0 +1,17 @@
1
+ // src/components/textfield/features/index.ts
2
+
3
+ // Export features
4
+ export { withLeadingIcon } from './leading-icon';
5
+ export { withTrailingIcon } from './trailing-icon';
6
+ export { withPrefixText } from './prefix-text';
7
+ export { withSuffixText } from './suffix-text';
8
+ export { withSupportingText } from './supporting-text';
9
+ export { withPlacement } from './placement';
10
+
11
+ // Export interfaces
12
+ export type { LeadingIconComponent, LeadingIconConfig } from './leading-icon';
13
+ export type { TrailingIconComponent, TrailingIconConfig } from './trailing-icon';
14
+ export type { PrefixTextComponent, PrefixTextConfig } from './prefix-text';
15
+ export type { SuffixTextComponent, SuffixTextConfig } from './suffix-text';
16
+ export type { SupportingTextComponent, SupportingTextConfig } from './supporting-text';
17
+ export type { PlacementComponent } from './placement';
@@ -0,0 +1,127 @@
1
+ // src/components/textfield/features/leading-icon.ts
2
+
3
+ import { BaseComponent, ElementComponent } from '../../../core/compose/component';
4
+
5
+ /**
6
+ * Configuration for leading icon feature
7
+ */
8
+ export interface LeadingIconConfig {
9
+ /**
10
+ * Leading icon HTML content
11
+ */
12
+ leadingIcon?: string;
13
+
14
+ /**
15
+ * CSS class prefix
16
+ */
17
+ prefix?: string;
18
+
19
+ /**
20
+ * Component name
21
+ */
22
+ componentName?: string;
23
+
24
+ [key: string]: any;
25
+ }
26
+
27
+ /**
28
+ * Component with leading icon capabilities
29
+ */
30
+ export interface LeadingIconComponent extends BaseComponent {
31
+ /**
32
+ * Leading icon element
33
+ */
34
+ leadingIcon: HTMLElement | null;
35
+
36
+ /**
37
+ * Sets leading icon content
38
+ * @param html - HTML content for the icon
39
+ * @returns Component instance for chaining
40
+ */
41
+ setLeadingIcon: (html: string) => LeadingIconComponent;
42
+
43
+ /**
44
+ * Removes leading icon
45
+ * @returns Component instance for chaining
46
+ */
47
+ removeLeadingIcon: () => LeadingIconComponent;
48
+ }
49
+
50
+ /**
51
+ * Adds leading icon to a textfield component
52
+ * @param config - Configuration with leading icon settings
53
+ * @returns Function that enhances a component with leading icon
54
+ */
55
+ export const withLeadingIcon = <T extends LeadingIconConfig>(config: T) =>
56
+ <C extends ElementComponent>(component: C): C & LeadingIconComponent => {
57
+ if (!config.leadingIcon) {
58
+ return component as any;
59
+ }
60
+
61
+ // Create icon element
62
+ const PREFIX = config.prefix || 'mtrl';
63
+ const iconElement = document.createElement('span');
64
+ iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
65
+ iconElement.innerHTML = config.leadingIcon;
66
+
67
+ // Add leading icon to the component
68
+ component.element.appendChild(iconElement);
69
+
70
+ // Add leading-icon class to the component
71
+ component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
72
+
73
+ // When there's a leading icon, adjust input padding
74
+ if (component.input) {
75
+ component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
76
+ }
77
+
78
+ // Add lifecycle integration if available
79
+ if ('lifecycle' in component && component.lifecycle?.destroy) {
80
+ const originalDestroy = component.lifecycle.destroy;
81
+ component.lifecycle.destroy = () => {
82
+ iconElement.remove();
83
+ originalDestroy.call(component.lifecycle);
84
+ };
85
+ }
86
+
87
+ // Update label position based on icon
88
+ setTimeout(() => {
89
+ const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
90
+ if (labelEl) {
91
+ if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
92
+ (labelEl as HTMLElement).style.left = '44px';
93
+ }
94
+ }
95
+ }, 10);
96
+
97
+ return {
98
+ ...component,
99
+ leadingIcon: iconElement,
100
+
101
+ setLeadingIcon(html: string) {
102
+ iconElement.innerHTML = html;
103
+ return this;
104
+ },
105
+
106
+ removeLeadingIcon() {
107
+ if (iconElement.parentNode) {
108
+ iconElement.remove();
109
+ component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
110
+ if (component.input) {
111
+ component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
112
+ }
113
+
114
+ // Reset label position if no prefix
115
+ if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
116
+ const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
117
+ if (labelEl) {
118
+ (labelEl as HTMLElement).style.left = '';
119
+ }
120
+ }
121
+
122
+ this.leadingIcon = null;
123
+ }
124
+ return this;
125
+ }
126
+ };
127
+ };