mtrl 0.3.6 → 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 (45) 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 +15 -13
  5. package/src/components/menu/config.ts +5 -5
  6. package/src/components/menu/features/anchor.ts +99 -15
  7. package/src/components/menu/features/controller.ts +418 -221
  8. package/src/components/menu/features/index.ts +2 -1
  9. package/src/components/menu/features/position.ts +353 -0
  10. package/src/components/menu/index.ts +5 -5
  11. package/src/components/menu/menu.ts +18 -60
  12. package/src/components/menu/types.ts +17 -16
  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/index.ts +4 -1
  34. package/src/styles/abstract/_variables.scss +18 -0
  35. package/src/styles/components/_button.scss +21 -5
  36. package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
  37. package/src/styles/components/_menu.scss +103 -24
  38. package/src/styles/components/_select.scss +265 -0
  39. package/src/styles/components/_textfield.scss +233 -42
  40. package/src/styles/main.scss +2 -1
  41. package/src/components/textfield/features.ts +0 -322
  42. package/src/core/collection/adapters/base.js +0 -26
  43. package/src/core/collection/collection.js +0 -259
  44. package/src/core/collection/list-manager.js +0 -157
  45. /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "A functional TypeScript/JavaScript component library with composable architecture based on Material Design 3",
5
5
  "author": "floor",
6
6
  "license": "MIT License",
@@ -123,6 +123,22 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
123
123
  return component.icon.getIcon();
124
124
  },
125
125
 
126
+ /**
127
+ * Sets the active state of the button
128
+ * Used to visually indicate the button's active state, such as when it has a menu open
129
+ *
130
+ * @param active - Whether the button should appear active
131
+ * @returns Button component for method chaining
132
+ */
133
+ setActive(active: boolean) {
134
+ if (active) {
135
+ component.element.classList.add(`${component.getClass('button')}--active`);
136
+ } else {
137
+ component.element.classList.remove(`${component.getClass('button')}--active`);
138
+ }
139
+ return this;
140
+ },
141
+
126
142
  /**
127
143
  * Sets the button's aria-label attribute for accessibility
128
144
  * @param label - Accessible label text
@@ -249,6 +249,15 @@ export interface ButtonComponent {
249
249
  */
250
250
  updateCircularStyle: () => void;
251
251
 
252
+ /**
253
+ * Sets the active state of the button
254
+ * Used to visually indicate the button's active state, such as when it has a menu open
255
+ *
256
+ * @param active - Whether the button should appear active
257
+ * @returns The button component for chaining
258
+ */
259
+ setActive: (active: boolean) => ButtonComponent;
260
+
252
261
  /**
253
262
  * Adds an event listener to the button
254
263
  * @param event - Event name ('click', 'focus', etc.)
@@ -1,6 +1,6 @@
1
1
  // src/components/menu/api.ts
2
2
 
3
- import { MenuComponent, MenuContent, MenuPlacement, MenuEvent, MenuSelectEvent } from './types';
3
+ import { MenuComponent, MenuContent, MenuPosition, MenuEvent, MenuSelectEvent } from './types';
4
4
 
5
5
  /**
6
6
  * API configuration options for menu component
@@ -15,8 +15,8 @@ interface ApiOptions {
15
15
  isOpen: () => boolean;
16
16
  setItems: (items: MenuContent[]) => any;
17
17
  getItems: () => MenuContent[];
18
- setPlacement: (placement: MenuPlacement) => any;
19
- getPlacement: () => MenuPlacement;
18
+ setPosition: (position: MenuPosition) => any;
19
+ getPosition: () => MenuPosition;
20
20
  };
21
21
  anchor: {
22
22
  setAnchor: (anchor: HTMLElement | string) => any;
@@ -58,13 +58,15 @@ export const withAPI = ({ menu, anchor, events, lifecycle }: ApiOptions) =>
58
58
  ...component as any,
59
59
  element: component.element,
60
60
 
61
+
61
62
  /**
62
63
  * Opens the menu
63
64
  * @param event - Optional event that triggered the open
65
+ * @param interactionType - The type of interaction that triggered the open ('mouse' or 'keyboard')
64
66
  * @returns Menu component for chaining
65
67
  */
66
- open(event?: Event) {
67
- menu.open(event);
68
+ open(event?: Event, interactionType: 'mouse' | 'keyboard' = 'mouse') {
69
+ menu.open(event, interactionType);
68
70
  return this;
69
71
  },
70
72
 
@@ -133,21 +135,21 @@ export const withAPI = ({ menu, anchor, events, lifecycle }: ApiOptions) =>
133
135
  },
134
136
 
135
137
  /**
136
- * Updates the menu's placement
137
- * @param placement - New placement value
138
+ * Updates the menu's position
139
+ * @param position - New position value
138
140
  * @returns Menu component for chaining
139
141
  */
140
- setPlacement(placement: MenuPlacement) {
141
- menu.setPlacement(placement);
142
+ setPosition(position: MenuPosition) {
143
+ menu.setPosition(position);
142
144
  return this;
143
145
  },
144
146
 
145
147
  /**
146
- * Gets the current menu placement
147
- * @returns Current placement
148
+ * Gets the current menu position
149
+ * @returns Current position
148
150
  */
149
- getPlacement() {
150
- return menu.getPlacement();
151
+ getPosition() {
152
+ return menu.getPosition();
151
153
  },
152
154
 
153
155
  /**
@@ -5,7 +5,7 @@ import {
5
5
  createElementConfig,
6
6
  BaseComponentConfig
7
7
  } from '../../core/config/component-config';
8
- import { MenuConfig, MENU_PLACEMENT } from './types';
8
+ import { MenuConfig, MENU_POSITION } from './types';
9
9
 
10
10
  /**
11
11
  * Default configuration for the Menu component
@@ -15,12 +15,12 @@ import { MenuConfig, MENU_PLACEMENT } from './types';
15
15
  */
16
16
  export const defaultConfig: MenuConfig = {
17
17
  items: [],
18
- placement: MENU_PLACEMENT.BOTTOM_START,
18
+ position: MENU_POSITION.BOTTOM_START,
19
19
  closeOnSelect: true,
20
20
  closeOnClickOutside: true,
21
21
  closeOnEscape: true,
22
22
  openSubmenuOnHover: true,
23
- offset: 8,
23
+ offset: 0,
24
24
  autoFlip: true,
25
25
  visible: false
26
26
  };
@@ -106,8 +106,8 @@ export const getApiConfig = (component) => ({
106
106
  isOpen: () => component.menu?.isOpen() || false,
107
107
  setItems: (items) => component.menu?.setItems(items),
108
108
  getItems: () => component.menu?.getItems() || [],
109
- setPlacement: (placement) => component.menu?.setPlacement(placement),
110
- getPlacement: () => component.menu?.getPlacement()
109
+ setPosition: (position) => component.menu?.setPosition(position),
110
+ getPosition: () => component.menu?.getPosition()
111
111
  },
112
112
  anchor: {
113
113
  setAnchor: (anchor) => component.anchor?.setAnchor(anchor),
@@ -17,15 +17,18 @@ export const withAnchor = (config: MenuConfig) => component => {
17
17
 
18
18
  // Track anchor state
19
19
  const state = {
20
- anchorElement: null as HTMLElement
20
+ anchorElement: null as HTMLElement,
21
+ anchorComponent: null as any,
22
+ activeClass: '' // Store the appropriate active class based on element type
21
23
  };
22
24
 
23
25
  /**
24
- * Resolves the anchor element from string or direct reference
26
+ * Resolves the anchor element from string, direct reference, or component
25
27
  */
26
- const resolveAnchor = (anchor: HTMLElement | string): HTMLElement => {
28
+ const resolveAnchor = (anchor: HTMLElement | string | { element: HTMLElement }): HTMLElement => {
27
29
  if (!anchor) return null;
28
30
 
31
+ // Handle string selector
29
32
  if (typeof anchor === 'string') {
30
33
  const element = document.querySelector(anchor);
31
34
  if (!element) {
@@ -34,14 +37,41 @@ export const withAnchor = (config: MenuConfig) => component => {
34
37
  }
35
38
  return element as HTMLElement;
36
39
  }
40
+
41
+ // Handle component with element property
42
+ if (typeof anchor === 'object' && anchor !== null && 'element' in anchor) {
43
+ return anchor.element;
44
+ }
45
+
46
+ // Handle direct HTML element
47
+ return anchor as HTMLElement;
48
+ };
37
49
 
38
- return anchor;
50
+ /**
51
+ * Determine the appropriate active class based on element type
52
+ */
53
+ const determineActiveClass = (element: HTMLElement): string => {
54
+ // Check if this is one of our component types
55
+ const classPrefix = component.getClass('').split('-')[0];
56
+
57
+ // Check element tag and classes to determine appropriate active class
58
+ if (element.tagName === 'BUTTON') {
59
+ return `${classPrefix}-button--active`;
60
+ } else if (element.classList.contains(`${classPrefix}-chip`)) {
61
+ return `${classPrefix}-chip--selected`;
62
+ } else if (element.classList.contains(`${classPrefix}-textfield`) ||
63
+ element.classList.contains(`${classPrefix}-select`)) {
64
+ return `${classPrefix}-textfield--focused`;
65
+ } else {
66
+ // Default active class for other elements
67
+ return `${classPrefix}-menu-anchor--active`;
68
+ }
39
69
  };
40
70
 
41
71
  /**
42
72
  * Sets up anchor click handler for toggling menu
43
73
  */
44
- const setupAnchorEvents = (anchorElement: HTMLElement): void => {
74
+ const setupAnchorEvents = (anchorElement: HTMLElement, originalAnchor?: any): void => {
45
75
  if (!anchorElement) return;
46
76
 
47
77
  // Remove previously attached event if any
@@ -49,8 +79,18 @@ export const withAnchor = (config: MenuConfig) => component => {
49
79
  cleanup();
50
80
  }
51
81
 
52
- // Store reference
82
+ // Store references
53
83
  state.anchorElement = anchorElement;
84
+
85
+ // Store reference to component if it was provided
86
+ if (originalAnchor && typeof originalAnchor === 'object' && 'element' in originalAnchor) {
87
+ state.anchorComponent = originalAnchor;
88
+ } else {
89
+ state.anchorComponent = null;
90
+ }
91
+
92
+ // Determine the appropriate active class for this anchor
93
+ state.activeClass = determineActiveClass(anchorElement);
54
94
 
55
95
  // Add click handler
56
96
  anchorElement.addEventListener('click', handleAnchorClick);
@@ -70,6 +110,32 @@ export const withAnchor = (config: MenuConfig) => component => {
70
110
  anchorElement.setAttribute('aria-controls', menuId);
71
111
  };
72
112
 
113
+ /**
114
+ * Applies active visual state to anchor
115
+ */
116
+ const setAnchorActive = (active: boolean): void => {
117
+ if (!state.anchorElement) return;
118
+
119
+ // For component with setActive method (our button component has this)
120
+ if (state.anchorComponent && typeof state.anchorComponent.setActive === 'function') {
121
+ state.anchorComponent.setActive(active);
122
+ }
123
+ // For component with .selected property (like our chip component)
124
+ else if (state.anchorComponent && 'selected' in state.anchorComponent) {
125
+ state.anchorComponent.selected = active;
126
+ }
127
+ // Standard DOM element fallback
128
+ else if (state.anchorElement.classList) {
129
+ if (active) {
130
+ // Add the appropriate active class
131
+ state.anchorElement.classList.add(state.activeClass);
132
+ } else {
133
+ // Remove active class
134
+ state.anchorElement.classList.remove(state.activeClass);
135
+ }
136
+ }
137
+ };
138
+
73
139
  /**
74
140
  * Handles anchor element click
75
141
  */
@@ -82,10 +148,8 @@ export const withAnchor = (config: MenuConfig) => component => {
82
148
 
83
149
  if (isOpen) {
84
150
  component.menu.close(e);
85
- state.anchorElement.setAttribute('aria-expanded', 'false');
86
151
  } else {
87
152
  component.menu.open(e);
88
- state.anchorElement.setAttribute('aria-expanded', 'true');
89
153
  }
90
154
  }
91
155
  };
@@ -99,12 +163,20 @@ export const withAnchor = (config: MenuConfig) => component => {
99
163
  state.anchorElement.removeAttribute('aria-haspopup');
100
164
  state.anchorElement.removeAttribute('aria-expanded');
101
165
  state.anchorElement.removeAttribute('aria-controls');
166
+
167
+ // Clean up active state if present
168
+ setAnchorActive(false);
102
169
  }
170
+
171
+ // Reset state
172
+ state.anchorComponent = null;
173
+ state.activeClass = '';
103
174
  };
104
175
 
105
176
  // Initialize with provided anchor
106
- const initialAnchor = resolveAnchor(config.anchor);
107
- setupAnchorEvents(initialAnchor);
177
+ const initialAnchor = config.anchor;
178
+ const initialElement = resolveAnchor(initialAnchor);
179
+ setupAnchorEvents(initialElement, initialAnchor);
108
180
 
109
181
  // Register with lifecycle if available
110
182
  if (component.lifecycle) {
@@ -119,12 +191,14 @@ export const withAnchor = (config: MenuConfig) => component => {
119
191
  component.on('open', () => {
120
192
  if (state.anchorElement) {
121
193
  state.anchorElement.setAttribute('aria-expanded', 'true');
194
+ setAnchorActive(true);
122
195
  }
123
196
  });
124
197
 
125
198
  component.on('close', () => {
126
199
  if (state.anchorElement) {
127
200
  state.anchorElement.setAttribute('aria-expanded', 'false');
201
+ setAnchorActive(false);
128
202
  }
129
203
  });
130
204
 
@@ -134,13 +208,13 @@ export const withAnchor = (config: MenuConfig) => component => {
134
208
  anchor: {
135
209
  /**
136
210
  * Sets a new anchor element
137
- * @param anchor - New anchor element or selector
211
+ * @param anchor - New anchor element, selector, or component
138
212
  * @returns Component for chaining
139
213
  */
140
- setAnchor(anchor: HTMLElement | string) {
141
- const newAnchor = resolveAnchor(anchor);
142
- if (newAnchor) {
143
- setupAnchorEvents(newAnchor);
214
+ setAnchor(anchor: HTMLElement | string | { element: HTMLElement }) {
215
+ const newElement = resolveAnchor(anchor);
216
+ if (newElement) {
217
+ setupAnchorEvents(newElement, anchor);
144
218
  }
145
219
  return component;
146
220
  },
@@ -151,6 +225,16 @@ export const withAnchor = (config: MenuConfig) => component => {
151
225
  */
152
226
  getAnchor() {
153
227
  return state.anchorElement;
228
+ },
229
+
230
+ /**
231
+ * Sets the active state of the anchor
232
+ * @param active - Whether anchor should appear active
233
+ * @returns Component for chaining
234
+ */
235
+ setActive(active: boolean) {
236
+ setAnchorActive(active);
237
+ return component;
154
238
  }
155
239
  }
156
240
  };