mtrl 0.3.5 → 0.3.6

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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/components/menu/api.ts +143 -268
  3. package/src/components/menu/config.ts +84 -40
  4. package/src/components/menu/features/anchor.ts +159 -0
  5. package/src/components/menu/features/controller.ts +970 -0
  6. package/src/components/menu/features/index.ts +4 -0
  7. package/src/components/menu/index.ts +31 -63
  8. package/src/components/menu/menu.ts +107 -97
  9. package/src/components/menu/types.ts +263 -447
  10. package/src/core/dom/classes.ts +81 -9
  11. package/src/core/dom/create.ts +30 -19
  12. package/src/core/layout/README.md +531 -166
  13. package/src/core/layout/array.ts +3 -4
  14. package/src/core/layout/config.ts +193 -0
  15. package/src/core/layout/create.ts +1 -2
  16. package/src/core/layout/index.ts +12 -2
  17. package/src/core/layout/object.ts +2 -3
  18. package/src/core/layout/processor.ts +60 -12
  19. package/src/core/layout/result.ts +1 -2
  20. package/src/core/layout/types.ts +105 -50
  21. package/src/core/layout/utils.ts +69 -61
  22. package/src/index.ts +2 -1
  23. package/src/styles/components/_menu.scss +20 -8
  24. package/src/styles/main.scss +23 -23
  25. package/src/styles/utilities/_layout.scss +665 -0
  26. package/src/components/menu/features/items-manager.ts +0 -457
  27. package/src/components/menu/features/keyboard-navigation.ts +0 -133
  28. package/src/components/menu/features/positioning.ts +0 -127
  29. package/src/components/menu/features/visibility.ts +0 -230
  30. package/src/components/menu/menu-item.ts +0 -86
  31. package/src/components/menu/utils.ts +0 -67
  32. /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
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",
@@ -1,316 +1,191 @@
1
1
  // src/components/menu/api.ts
2
- import { ApiOptions, BaseComponent, MenuComponent, MenuItemConfig, MenuPositionConfig } from './types';
2
+
3
+ import { MenuComponent, MenuContent, MenuPlacement, MenuEvent, MenuSelectEvent } from './types';
3
4
 
4
5
  /**
5
- * Enhances a menu component with public API methods
6
- *
7
- * This is a higher-order function that wraps a base component and exposes
8
- * a standardized public API following the MenuComponent interface. It ensures
9
- * all methods return the component instance for method chaining and handles
10
- * proper resource cleanup during component destruction.
6
+ * API configuration options for menu component
7
+ * @category Components
8
+ * @internal
9
+ */
10
+ interface ApiOptions {
11
+ menu: {
12
+ open: (event?: Event) => any;
13
+ close: (event?: Event) => any;
14
+ toggle: (event?: Event) => any;
15
+ isOpen: () => boolean;
16
+ setItems: (items: MenuContent[]) => any;
17
+ getItems: () => MenuContent[];
18
+ setPlacement: (placement: MenuPlacement) => any;
19
+ getPlacement: () => MenuPlacement;
20
+ };
21
+ anchor: {
22
+ setAnchor: (anchor: HTMLElement | string) => any;
23
+ getAnchor: () => HTMLElement;
24
+ };
25
+ events?: {
26
+ on: <T extends string>(event: T, handler: (event: any) => void) => any;
27
+ off: <T extends string>(event: T, handler: (event: any) => void) => any;
28
+ };
29
+ lifecycle: {
30
+ destroy: () => void;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Component with required elements and methods for API enhancement
36
+ * @category Components
37
+ * @internal
38
+ */
39
+ interface ComponentWithElements {
40
+ element: HTMLElement;
41
+ on?: <T extends string>(event: T, handler: (event: any) => void) => any;
42
+ off?: <T extends string>(event: T, handler: (event: any) => void) => any;
43
+ emit?: (event: string, data: any) => void;
44
+ }
45
+
46
+ /**
47
+ * Enhances a menu component with API methods.
48
+ * This follows the higher-order function pattern to add public API methods
49
+ * to the component, making them available to the end user.
11
50
  *
12
- * @param {ApiOptions} options - API configuration including lifecycle methods
51
+ * @param {ApiOptions} options - API configuration options
13
52
  * @returns {Function} Higher-order function that adds API methods to component
14
- * @internal
15
53
  * @category Components
54
+ * @internal This is an internal utility for the Menu component
16
55
  */
17
- export const withAPI = ({ lifecycle }: ApiOptions) =>
18
- (component: BaseComponent): MenuComponent => ({
56
+ export const withAPI = ({ menu, anchor, events, lifecycle }: ApiOptions) =>
57
+ (component: ComponentWithElements): MenuComponent => ({
19
58
  ...component as any,
20
59
  element: component.element,
21
-
60
+
22
61
  /**
23
- * Shows the menu
24
- *
25
- * Makes the menu visible in the DOM. This method triggers the 'open' event.
26
- * If called when the menu is already visible, it has no effect.
27
- *
28
- * @returns {MenuComponent} Component instance for method chaining
29
- * @example
30
- * ```typescript
31
- * // Show a menu
32
- * menu.show();
33
- *
34
- * // Position and show a menu in one chain
35
- * menu.position(buttonElement).show();
36
- * ```
62
+ * Opens the menu
63
+ * @param event - Optional event that triggered the open
64
+ * @returns Menu component for chaining
37
65
  */
38
- show(): MenuComponent {
39
- component.show?.();
66
+ open(event?: Event) {
67
+ menu.open(event);
40
68
  return this;
41
69
  },
42
-
70
+
43
71
  /**
44
- * Hides the menu
45
- *
46
- * Makes the menu invisible in the DOM. This method triggers the 'close' event.
47
- * If called when the menu is already hidden, it has no effect.
48
- *
49
- * @returns {MenuComponent} Component instance for method chaining
50
- * @example
51
- * ```typescript
52
- * // Hide a menu
53
- * menu.hide();
54
- *
55
- * // Listen for clicks outside the menu to hide it
56
- * document.addEventListener('click', (event) => {
57
- * if (menu.isVisible() && !menu.element.contains(event.target)) {
58
- * menu.hide();
59
- * }
60
- * });
61
- * ```
72
+ * Closes the menu
73
+ * @param event - Optional event that triggered the close
74
+ * @returns Menu component for chaining
62
75
  */
63
- hide(): MenuComponent {
64
- component.hide?.();
76
+ close(event?: Event) {
77
+ menu.close(event);
65
78
  return this;
66
79
  },
67
-
80
+
68
81
  /**
69
- * Positions the menu relative to a target element
70
- *
71
- * Calculates and sets the position of the menu relative to the specified target element.
72
- * This allows precise control over menu placement in the UI.
73
- *
74
- * @param {HTMLElement} target - Target element to position against
75
- * @param {MenuPositionConfig} options - Position configuration
76
- * @returns {MenuComponent} Component instance for method chaining
77
- * @example
78
- * ```typescript
79
- * // Position relative to a button with default alignment (left, bottom)
80
- * menu.position(document.getElementById('menuButton'));
81
- *
82
- * // Position with custom alignment
83
- * menu.position(buttonElement, {
84
- * align: 'right', // Align to the right edge of the target
85
- * vAlign: 'top', // Position above the target
86
- * offsetX: 5, // Add 5px horizontal offset
87
- * offsetY: 10 // Add 10px vertical offset
88
- * });
89
- * ```
82
+ * Toggles the menu's open state
83
+ * @param event - Optional event that triggered the toggle
84
+ * @returns Menu component for chaining
90
85
  */
91
- position(target: HTMLElement, options?: MenuPositionConfig): MenuComponent {
92
- component.position?.(target, options);
86
+ toggle(event?: Event) {
87
+ menu.toggle(event);
93
88
  return this;
94
89
  },
95
-
90
+
96
91
  /**
97
- * Adds an item to the menu
98
- *
99
- * Dynamically adds a new item to the menu. This can be called at any time,
100
- * even after the menu has been rendered and shown.
101
- *
102
- * @param {MenuItemConfig} config - Item configuration
103
- * @returns {MenuComponent} Component instance for method chaining
104
- * @example
105
- * ```typescript
106
- * // Add a standard item
107
- * menu.addItem({ name: 'edit', text: 'Edit' });
108
- *
109
- * // Add a divider
110
- * menu.addItem({ type: 'divider' });
111
- *
112
- * // Add a disabled item
113
- * menu.addItem({ name: 'print', text: 'Print', disabled: true });
114
- *
115
- * // Add an item with a submenu
116
- * menu.addItem({
117
- * name: 'share',
118
- * text: 'Share',
119
- * items: [
120
- * { name: 'email', text: 'Email' },
121
- * { name: 'link', text: 'Copy Link' }
122
- * ]
123
- * });
124
- * ```
92
+ * Checks if the menu is currently open
93
+ * @returns True if the menu is open
125
94
  */
126
- addItem(config: MenuItemConfig): MenuComponent {
127
- component.addItem?.(config);
128
- return this;
95
+ isOpen() {
96
+ return menu.isOpen();
129
97
  },
130
-
98
+
131
99
  /**
132
- * Removes an item from the menu
133
- *
134
- * Dynamically removes an item from the menu by its name identifier.
135
- * If the item doesn't exist, no action is taken.
136
- *
137
- * @param {string} name - Name identifier of the item to remove
138
- * @returns {MenuComponent} Component instance for method chaining
139
- * @example
140
- * ```typescript
141
- * // Remove an item by name
142
- * menu.removeItem('delete');
143
- *
144
- * // Check if a condition is met, then remove an item
145
- * if (!userHasEditPermission) {
146
- * menu.removeItem('edit');
147
- * }
148
- * ```
100
+ * Updates the menu items
101
+ * @param items - New array of menu items and dividers
102
+ * @returns Menu component for chaining
149
103
  */
150
- removeItem(name: string): MenuComponent {
151
- component.removeItem?.(name);
104
+ setItems(items: MenuContent[]) {
105
+ menu.setItems(items);
152
106
  return this;
153
107
  },
154
-
108
+
155
109
  /**
156
- * Gets all registered menu items
157
- *
158
- * Returns a Map containing all the current menu items, indexed by their name.
159
- * Each entry contains the item's DOM element and configuration.
160
- *
161
- * @returns {Map<string, MenuItemData>} Map of item names to their data
162
- * @example
163
- * ```typescript
164
- * // Get all items
165
- * const items = menu.getItems();
166
- *
167
- * // Check if a specific item exists
168
- * if (items.has('delete')) {
169
- * console.log('Delete item exists');
170
- * }
171
- *
172
- * // Get the element for a specific item
173
- * const editItem = items.get('edit');
174
- * if (editItem) {
175
- * console.log('Edit item element:', editItem.element);
176
- * console.log('Edit item config:', editItem.config);
177
- * }
178
- * ```
110
+ * Gets the current menu items
111
+ * @returns Array of current menu items and dividers
179
112
  */
180
113
  getItems() {
181
- return component.getItems?.() || new Map();
114
+ return menu.getItems();
182
115
  },
183
-
116
+
184
117
  /**
185
- * Checks if the menu is currently visible
186
- *
187
- * Returns true if the menu is currently visible in the DOM,
188
- * false otherwise.
189
- *
190
- * @returns {boolean} Whether the menu is visible
191
- * @example
192
- * ```typescript
193
- * // Check if menu is visible before performing an action
194
- * if (menu.isVisible()) {
195
- * menu.hide();
196
- * } else {
197
- * menu.position(button).show();
198
- * }
199
- *
200
- * // Toggle menu visibility
201
- * toggleButton.addEventListener('click', () => {
202
- * if (menu.isVisible()) {
203
- * menu.hide();
204
- * } else {
205
- * menu.position(toggleButton).show();
206
- * }
207
- * });
208
- * ```
118
+ * Updates the menu's anchor element
119
+ * @param anchorElement - New anchor element or selector
120
+ * @returns Menu component for chaining
209
121
  */
210
- isVisible(): boolean {
211
- return component.isVisible?.() || false;
122
+ setAnchor(anchorElement: HTMLElement | string) {
123
+ anchor.setAnchor(anchorElement);
124
+ return this;
212
125
  },
213
-
126
+
127
+ /**
128
+ * Gets the current anchor element
129
+ * @returns Current anchor element
130
+ */
131
+ getAnchor() {
132
+ return anchor.getAnchor();
133
+ },
134
+
214
135
  /**
215
- * Registers an event handler
216
- *
217
- * Subscribes to menu events like 'select', 'open', 'close', etc.
218
- * The handler will be called whenever the specified event occurs.
219
- *
220
- * @param {string} event - Event name to listen for
221
- * @param {Function} handler - Callback function to execute when the event occurs
222
- * @returns {MenuComponent} Component instance for method chaining
223
- * @example
224
- * ```typescript
225
- * // Listen for item selection
226
- * menu.on('select', (event) => {
227
- * console.log(`Selected item: ${event.name}`);
228
- *
229
- * if (event.name === 'delete') {
230
- * confirmDelete();
231
- * }
232
- * });
233
- *
234
- * // Listen for menu opening
235
- * menu.on('open', () => {
236
- * console.log('Menu opened');
237
- * analytics.trackMenuOpen();
238
- * });
239
- *
240
- * // Listen for submenu opening
241
- * menu.on('submenuOpen', (data) => {
242
- * console.log('Submenu opened:', data);
243
- * });
244
- * ```
136
+ * Updates the menu's placement
137
+ * @param placement - New placement value
138
+ * @returns Menu component for chaining
245
139
  */
246
- on(event: string, handler: Function): MenuComponent {
247
- component.on?.(event, handler);
140
+ setPlacement(placement: MenuPlacement) {
141
+ menu.setPlacement(placement);
248
142
  return this;
249
143
  },
250
-
144
+
251
145
  /**
252
- * Unregisters an event handler
253
- *
254
- * Removes a previously registered event handler.
255
- * If the handler is not found, no action is taken.
256
- *
257
- * @param {string} event - Event name to stop listening for
258
- * @param {Function} handler - The handler function to remove
259
- * @returns {MenuComponent} Component instance for method chaining
260
- * @example
261
- * ```typescript
262
- * // Define a handler function
263
- * const handleSelection = (event) => {
264
- * console.log(`Selected: ${event.name}`);
265
- * };
266
- *
267
- * // Register the handler
268
- * menu.on('select', handleSelection);
269
- *
270
- * // Later, unregister the handler
271
- * menu.off('select', handleSelection);
272
- * ```
146
+ * Gets the current menu placement
147
+ * @returns Current placement
273
148
  */
274
- off(event: string, handler: Function): MenuComponent {
275
- component.off?.(event, handler);
149
+ getPlacement() {
150
+ return menu.getPlacement();
151
+ },
152
+
153
+ /**
154
+ * Adds an event listener to the menu
155
+ * @param event - Event name ('open', 'close', 'select')
156
+ * @param handler - Event handler function
157
+ * @returns Menu component for chaining
158
+ */
159
+ on(event, handler) {
160
+ if (events?.on) {
161
+ events.on(event, handler);
162
+ } else if (component.on) {
163
+ component.on(event, handler);
164
+ }
276
165
  return this;
277
166
  },
278
-
167
+
279
168
  /**
280
- * Destroys the menu component and cleans up resources
281
- *
282
- * Performs complete cleanup by removing event listeners, DOM elements,
283
- * and references to prevent memory leaks. After calling this method,
284
- * the menu instance should not be used again.
285
- *
286
- * @returns {MenuComponent} Component instance
287
- * @example
288
- * ```typescript
289
- * // When no longer needed, destroy the menu
290
- * menu.destroy();
291
- *
292
- * // When navigating away or changing views
293
- * function changeView() {
294
- * // Clean up existing components
295
- * contextMenu.destroy();
296
- *
297
- * // Set up new view
298
- * setupNewView();
299
- * }
300
- * ```
169
+ * Removes an event listener from the menu
170
+ * @param event - Event name
171
+ * @param handler - Event handler function
172
+ * @returns Menu component for chaining
301
173
  */
302
- destroy(): MenuComponent {
303
- // First close any open submenus
304
- component.hide?.();
305
-
306
- // Then destroy the component
307
- lifecycle.destroy?.();
308
-
309
- // Final cleanup - forcibly remove from DOM if still attached
310
- if (component.element && component.element.parentNode) {
311
- component.element.remove();
174
+ off(event, handler) {
175
+ if (events?.off) {
176
+ events.off(event, handler);
177
+ } else if (component.off) {
178
+ component.off(event, handler);
312
179
  }
313
-
314
180
  return this;
181
+ },
182
+
183
+ /**
184
+ * Destroys the menu component and cleans up resources
185
+ */
186
+ destroy() {
187
+ lifecycle.destroy();
315
188
  }
316
- });
189
+ });
190
+
191
+ export default withAPI;
@@ -1,80 +1,124 @@
1
1
  // src/components/menu/config.ts
2
- import { PREFIX } from '../../core/config';
2
+
3
3
  import {
4
4
  createComponentConfig,
5
5
  createElementConfig,
6
6
  BaseComponentConfig
7
7
  } from '../../core/config/component-config';
8
- import { MenuConfig, BaseComponent, ApiOptions } from './types';
8
+ import { MenuConfig, MENU_PLACEMENT } from './types';
9
9
 
10
10
  /**
11
11
  * Default configuration for the Menu component
12
+ * These values will be used when not explicitly specified by the user.
12
13
  *
13
- * Defines the standard behavior and initial state for menus.
14
- * These defaults are merged with user-provided configuration options.
15
- *
16
- * @internal
17
14
  * @category Components
18
15
  */
19
16
  export const defaultConfig: MenuConfig = {
20
- /** Empty initial items array */
21
17
  items: [],
22
-
23
- /** By default, menus close when an item is selected */
24
- stayOpenOnSelect: false
18
+ placement: MENU_PLACEMENT.BOTTOM_START,
19
+ closeOnSelect: true,
20
+ closeOnClickOutside: true,
21
+ closeOnEscape: true,
22
+ openSubmenuOnHover: true,
23
+ offset: 8,
24
+ autoFlip: true,
25
+ visible: false
25
26
  };
26
27
 
27
28
  /**
28
- * Creates the base configuration for Menu component
29
- *
30
- * Merges user-provided configuration with default values
31
- * to ensure all required properties are available.
29
+ * Creates the base configuration for Menu component by merging user-provided
30
+ * config with default values.
32
31
  *
33
32
  * @param {MenuConfig} config - User provided configuration
34
33
  * @returns {MenuConfig} Complete configuration with defaults applied
35
- * @internal
36
34
  * @category Components
35
+ * @internal
37
36
  */
38
- export const createBaseConfig = (config: MenuConfig = {}): MenuConfig =>
39
- createComponentConfig(defaultConfig, config, 'menu') as MenuConfig;
37
+ export const createBaseConfig = (config: MenuConfig): MenuConfig => {
38
+ // First, ensure we have an anchor element
39
+ if (!config.anchor) {
40
+ throw new Error('Menu component requires an anchor element or selector');
41
+ }
42
+
43
+ // Apply default configuration
44
+ return createComponentConfig(defaultConfig, config, 'menu') as MenuConfig;
45
+ };
40
46
 
41
47
  /**
42
- * Generates element configuration for the Menu component
43
- *
44
- * Creates the configuration needed for the DOM element creation.
45
- * Sets up proper accessibility attributes like ARIA roles and states.
48
+ * Generates element configuration for the Menu component.
49
+ * This function creates the necessary attributes and configuration
50
+ * for the DOM element creation process.
46
51
  *
47
52
  * @param {MenuConfig} config - Menu configuration
48
53
  * @returns {Object} Element configuration object for withElement
49
- * @internal
50
54
  * @category Components
55
+ * @internal
51
56
  */
52
- export const getElementConfig = (config: MenuConfig) =>
53
- createElementConfig(config, {
57
+ export const getElementConfig = (config: MenuConfig) => {
58
+ // Custom styles based on configuration
59
+ const styles: Record<string, string> = {};
60
+
61
+ if (config.width) {
62
+ styles.width = config.width;
63
+ }
64
+
65
+ if (config.maxHeight) {
66
+ styles.maxHeight = config.maxHeight;
67
+ }
68
+
69
+ // Element attributes
70
+ const attrs: Record<string, any> = {
71
+ role: 'menu',
72
+ tabindex: '-1',
73
+ 'aria-hidden': (!config.visible).toString(),
74
+ style: Object.entries(styles)
75
+ .map(([key, value]) => `${key}: ${value}`)
76
+ .join(';')
77
+ };
78
+
79
+ return createElementConfig(config, {
54
80
  tag: 'div',
55
- componentName: 'menu',
56
- attrs: {
57
- role: 'menu',
58
- tabindex: '-1',
59
- 'aria-hidden': 'true'
60
- },
61
- className: config.class
81
+ attrs,
82
+ className: [
83
+ config.visible ? 'menu--visible' : null,
84
+ config.class
85
+ ].filter(Boolean),
86
+ forwardEvents: {
87
+ keydown: true
88
+ }
62
89
  });
90
+ };
63
91
 
64
92
  /**
65
- * Creates API configuration for the Menu component
66
- *
67
- * Builds the configuration needed for the withAPI feature,
68
- * including lifecycle methods for proper resource cleanup.
93
+ * Creates API configuration for the Menu component.
94
+ * This connects the core component features to the public API.
69
95
  *
70
- * @param {BaseComponent} comp - Component with lifecycle feature
71
- * @returns {ApiOptions} API configuration object
72
- * @internal
96
+ * @param {Object} component - Component with menu features
97
+ * @returns {Object} API configuration object
73
98
  * @category Components
99
+ * @internal
74
100
  */
75
- export const getApiConfig = (comp: BaseComponent): ApiOptions => ({
101
+ export const getApiConfig = (component) => ({
102
+ menu: {
103
+ open: () => component.menu?.open(),
104
+ close: () => component.menu?.close(),
105
+ toggle: () => component.menu?.toggle(),
106
+ isOpen: () => component.menu?.isOpen() || false,
107
+ setItems: (items) => component.menu?.setItems(items),
108
+ getItems: () => component.menu?.getItems() || [],
109
+ setPlacement: (placement) => component.menu?.setPlacement(placement),
110
+ getPlacement: () => component.menu?.getPlacement()
111
+ },
112
+ anchor: {
113
+ setAnchor: (anchor) => component.anchor?.setAnchor(anchor),
114
+ getAnchor: () => component.anchor?.getAnchor()
115
+ },
116
+ events: {
117
+ on: (event, handler) => component.on?.(event, handler),
118
+ off: (event, handler) => component.off?.(event, handler)
119
+ },
76
120
  lifecycle: {
77
- destroy: comp.lifecycle?.destroy || (() => {})
121
+ destroy: () => component.lifecycle?.destroy()
78
122
  }
79
123
  });
80
124