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
@@ -1,4 +1,5 @@
1
1
  // src/components/menu/features/index.ts
2
2
 
3
3
  export { default as withController } from './controller';
4
- export { default as withAnchor } from './anchor';
4
+ export { default as withAnchor } from './anchor';
5
+ export { default as withPosition } from './position';
@@ -0,0 +1,353 @@
1
+ // src/components/menu/features/position.ts
2
+
3
+ import { MenuConfig } from '../types';
4
+
5
+ /**
6
+ * Menu position helper
7
+ * Provides functions for positioning menus and submenus
8
+ */
9
+ export const createPositioner = (component, config: MenuConfig) => {
10
+ /**
11
+ * Positions the menu relative to its anchor
12
+ * Ensures the menu maintains proper spacing from viewport edges
13
+ * Makes sure the menu stays attached to anchor during scrolling
14
+ *
15
+ * @param menuElement - The menu element to position
16
+ * @param anchorElement - The element to anchor against
17
+ * @param preferredPosition - The preferred position
18
+ * @param isSubmenu - Whether this is a submenu (affects positioning logic)
19
+ */
20
+ const positionElement = (
21
+ menuElement: HTMLElement,
22
+ anchorElement: HTMLElement,
23
+ preferredPosition: string,
24
+ isSubmenu = false
25
+ ): void => {
26
+ if (!menuElement || !anchorElement) return;
27
+
28
+ // Ensure menu is positioned absolutely for proper scroll behavior
29
+ menuElement.style.position = 'absolute';
30
+
31
+ // Get current scroll position - critical for absolute positioning that tracks anchor
32
+ const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
33
+ const scrollY = window.pageYOffset || document.documentElement.scrollTop;
34
+
35
+ // Make a copy of the menu for measurement without affecting the real menu
36
+ const tempMenu = menuElement.cloneNode(true) as HTMLElement;
37
+
38
+ // Make the temp menu visible but not displayed for measurement
39
+ tempMenu.style.visibility = 'hidden';
40
+ tempMenu.style.display = 'block';
41
+ tempMenu.style.position = 'absolute';
42
+ tempMenu.style.top = '0';
43
+ tempMenu.style.left = '0';
44
+ tempMenu.style.transform = 'none';
45
+ tempMenu.style.opacity = '0';
46
+ tempMenu.style.pointerEvents = 'none';
47
+ tempMenu.classList.add(`${component.getClass('menu--visible')}`); // Add visible class for proper dimensions
48
+
49
+ // Add it to the DOM temporarily
50
+ document.body.appendChild(tempMenu);
51
+
52
+ // Get measurements
53
+ const anchorRect = anchorElement.getBoundingClientRect();
54
+ const menuRect = tempMenu.getBoundingClientRect();
55
+ const viewportWidth = window.innerWidth;
56
+ const viewportHeight = window.innerHeight;
57
+
58
+ // Remove the temp element after measurements
59
+ document.body.removeChild(tempMenu);
60
+
61
+ // Get values needed for calculations
62
+ const offset = config.offset !== undefined ? config.offset : 8;
63
+
64
+ // Calculate position based on position
65
+ let top = 0;
66
+ let left = 0;
67
+ let calculatedPosition = preferredPosition;
68
+
69
+ // Different positioning logic for main menu vs submenu
70
+ if (isSubmenu) {
71
+ // Default position is to the right of parent
72
+ calculatedPosition = preferredPosition || 'right-start';
73
+
74
+ // Check if this would push the submenu out of the viewport
75
+ if (calculatedPosition.startsWith('right') && anchorRect.right + menuRect.width + offset > viewportWidth - 16) {
76
+ // Flip to the left side if it doesn't fit on the right
77
+ calculatedPosition = calculatedPosition.replace('right', 'left');
78
+ } else if (calculatedPosition.startsWith('left') && anchorRect.left - menuRect.width - offset < 16) {
79
+ // Flip to the right side if it doesn't fit on the left
80
+ calculatedPosition = calculatedPosition.replace('left', 'right');
81
+ }
82
+
83
+ // Check vertical positioning as well for submenus
84
+ // If submenu would extend beyond the bottom of the viewport, adjust positioning
85
+ if (anchorRect.top + menuRect.height > viewportHeight - 16) {
86
+ if (calculatedPosition === 'right-start') {
87
+ calculatedPosition = 'right-end';
88
+ } else if (calculatedPosition === 'left-start') {
89
+ calculatedPosition = 'left-end';
90
+ }
91
+ }
92
+ } else {
93
+ // For main menu, follow the standard position calculation
94
+ // First determine correct position based on original position
95
+ switch (preferredPosition) {
96
+ case 'top-start':
97
+ case 'top':
98
+ case 'top-end':
99
+ // Check if enough space above
100
+ if (anchorRect.top < menuRect.height + offset + 16) {
101
+ // Not enough space above, flip to bottom
102
+ calculatedPosition = preferredPosition.replace('top', 'bottom');
103
+ }
104
+ break;
105
+
106
+ case 'bottom-start':
107
+ case 'bottom':
108
+ case 'bottom-end':
109
+ // Check if enough space below
110
+ if (anchorRect.bottom + menuRect.height + offset + 16 > viewportHeight) {
111
+ // Not enough space below, check if more space above
112
+ if (anchorRect.top > (viewportHeight - anchorRect.bottom)) {
113
+ // More space above, flip to top
114
+ calculatedPosition = preferredPosition.replace('bottom', 'top');
115
+ }
116
+ }
117
+ break;
118
+
119
+ // Specifically handle right-start, right, left-start, and left positions
120
+ case 'right-start':
121
+ case 'right':
122
+ case 'left-start':
123
+ case 'left':
124
+ // Check if enough space below for these side positions
125
+ if (anchorRect.bottom + menuRect.height > viewportHeight - 16) {
126
+ // Not enough space below, shift the menu upward
127
+ if (preferredPosition === 'right-start') {
128
+ calculatedPosition = 'right-end';
129
+ } else if (preferredPosition === 'left-start') {
130
+ calculatedPosition = 'left-end';
131
+ } else if (preferredPosition === 'right') {
132
+ // For center aligned, shift up by half menu height plus some spacing
133
+ top = anchorRect.top - (menuRect.height - anchorRect.height) - offset;
134
+ } else if (preferredPosition === 'left') {
135
+ // For center aligned, shift up by half menu height plus some spacing
136
+ top = anchorRect.top - (menuRect.height - anchorRect.height) - offset;
137
+ }
138
+ }
139
+ break;
140
+ }
141
+ }
142
+
143
+ // Reset any existing position classes
144
+ const positionClasses = [
145
+ 'position-top', 'position-bottom', 'position-right', 'position-left'
146
+ ];
147
+
148
+ positionClasses.forEach(posClass => {
149
+ menuElement.classList.remove(`${component.getClass('menu')}--${posClass}`);
150
+ });
151
+
152
+ // Determine transform origin based on vertical position
153
+ // Start by checking the calculated position to determine transform origin
154
+ const menuAppearsAboveAnchor =
155
+ calculatedPosition.startsWith('top') ||
156
+ calculatedPosition === 'right-end' ||
157
+ calculatedPosition === 'left-end' ||
158
+ (calculatedPosition === 'right' && top < anchorRect.top) ||
159
+ (calculatedPosition === 'left' && top < anchorRect.top);
160
+
161
+ if (menuAppearsAboveAnchor) {
162
+ menuElement.classList.add(`${component.getClass('menu')}--position-top`);
163
+ } else if (calculatedPosition.startsWith('left')) {
164
+ menuElement.classList.add(`${component.getClass('menu')}--position-left`);
165
+ } else if (calculatedPosition.startsWith('right')) {
166
+ menuElement.classList.add(`${component.getClass('menu')}--position-right`);
167
+ } else {
168
+ menuElement.classList.add(`${component.getClass('menu')}--position-bottom`);
169
+ }
170
+
171
+ // Position calculation - important: getBoundingClientRect() returns values relative to viewport
172
+ // We need to add scroll position to get absolute position
173
+ switch (calculatedPosition) {
174
+ case 'top-start':
175
+ top = anchorRect.top + scrollY - menuRect.height - offset;
176
+ left = anchorRect.left + scrollX;
177
+ break;
178
+ case 'top':
179
+ top = anchorRect.top + scrollY - menuRect.height - offset;
180
+ left = anchorRect.left + scrollX + (anchorRect.width / 2) - (menuRect.width / 2);
181
+ break;
182
+ case 'top-end':
183
+ top = anchorRect.top + scrollY - menuRect.height - offset;
184
+ left = anchorRect.right + scrollX - menuRect.width;
185
+ break;
186
+ case 'right-start':
187
+ top = anchorRect.top + scrollY;
188
+ left = anchorRect.right + scrollX + offset;
189
+ break;
190
+ case 'right':
191
+ // Custom top position might be set above; only set if not already defined
192
+ if (top === 0) {
193
+ top = anchorRect.top + scrollY + (anchorRect.height / 2) - (menuRect.height / 2);
194
+ } else {
195
+ top += scrollY;
196
+ }
197
+ left = anchorRect.right + scrollX + offset;
198
+ break;
199
+ case 'right-end':
200
+ top = anchorRect.bottom + scrollY - menuRect.height;
201
+ left = anchorRect.right + scrollX + offset;
202
+ break;
203
+ case 'bottom-start':
204
+ top = anchorRect.bottom + scrollY + offset;
205
+ left = anchorRect.left + scrollX;
206
+ break;
207
+ case 'bottom':
208
+ top = anchorRect.bottom + scrollY + offset;
209
+ left = anchorRect.left + scrollX + (anchorRect.width / 2) - (menuRect.width / 2);
210
+ break;
211
+ case 'bottom-end':
212
+ top = anchorRect.bottom + scrollY + offset;
213
+ left = anchorRect.right + scrollX - menuRect.width;
214
+ break;
215
+ case 'left-start':
216
+ top = anchorRect.top + scrollY;
217
+ left = anchorRect.left + scrollX - menuRect.width - offset;
218
+ break;
219
+ case 'left':
220
+ // Custom top position might be set above; only set if not already defined
221
+ if (top === 0) {
222
+ top = anchorRect.top + scrollY + (anchorRect.height / 2) - (menuRect.height / 2);
223
+ } else {
224
+ top += scrollY;
225
+ }
226
+ left = anchorRect.left + scrollX - menuRect.width - offset;
227
+ break;
228
+ case 'left-end':
229
+ top = anchorRect.bottom + scrollY - menuRect.height;
230
+ left = anchorRect.left + scrollX - menuRect.width - offset;
231
+ break;
232
+ }
233
+
234
+ // Ensure the menu has proper spacing from viewport edges
235
+
236
+ // Top edge spacing - ensure the menu doesn't go above the viewport + padding
237
+ const minTopSpacing = 16; // Minimum distance from top of viewport
238
+ if (top - scrollY < minTopSpacing) {
239
+ top = minTopSpacing + scrollY;
240
+ }
241
+
242
+ // Bottom edge spacing - ensure the menu doesn't go below the viewport - padding
243
+ const viewportBottomMargin = 16; // Minimum space from bottom of viewport
244
+ const bottomEdge = (top - scrollY) + menuRect.height;
245
+
246
+ if (bottomEdge > viewportHeight - viewportBottomMargin) {
247
+ // Option 1: We could adjust the top position
248
+ // top = scrollY + viewportHeight - viewportBottomMargin - menuRect.height;
249
+
250
+ // Option 2: Instead of moving the menu, adjust its height to fit (better UX)
251
+ const availableHeight = viewportHeight - (top - scrollY) - viewportBottomMargin;
252
+
253
+ // Set a minimum height to prevent tiny menus
254
+ const minMenuHeight = Math.min(menuRect.height, 100);
255
+ const newMaxHeight = Math.max(availableHeight, minMenuHeight);
256
+
257
+ // Update maxHeight to fit within viewport
258
+ menuElement.style.maxHeight = `${newMaxHeight}px`;
259
+
260
+ // If user has explicitly set a maxHeight, respect it if smaller
261
+ if (config.maxHeight) {
262
+ const configMaxHeight = parseInt(config.maxHeight, 10);
263
+ if (!isNaN(configMaxHeight) && configMaxHeight < parseInt(menuElement.style.maxHeight || '0', 10)) {
264
+ menuElement.style.maxHeight = config.maxHeight;
265
+ }
266
+ }
267
+ } else {
268
+ // If there's plenty of space, use the config's maxHeight (if provided)
269
+ if (config.maxHeight) {
270
+ menuElement.style.maxHeight = config.maxHeight;
271
+ }
272
+ }
273
+
274
+ // For 'width: 100%' configuration, match the anchor width
275
+ if (config.width === '100%' && !isSubmenu) {
276
+ menuElement.style.width = `${anchorRect.width}px`;
277
+ }
278
+
279
+ // Apply final positions, ensuring menu stays within viewport
280
+ // The position is absolute, not fixed, so it must account for scroll
281
+ menuElement.style.top = `${Math.max(minTopSpacing + scrollY, top)}px`;
282
+ menuElement.style.left = `${Math.max(16 + scrollX, left)}px`;
283
+
284
+ // Make sure menu doesn't extend past right edge
285
+ if ((left - scrollX) + menuRect.width > viewportWidth - 16) {
286
+ // If we're going past the right edge, set right with fixed distance from edge
287
+ menuElement.style.left = 'auto';
288
+ menuElement.style.right = '16px';
289
+ }
290
+ };
291
+
292
+ /**
293
+ * Positions the main menu relative to its anchor
294
+ */
295
+ const positionMenu = (anchorElement: HTMLElement): void => {
296
+ if (!anchorElement || !component.element) return;
297
+ positionElement(component.element, anchorElement, config.position, false);
298
+ };
299
+
300
+ /**
301
+ * Positions a submenu relative to its parent menu item
302
+ * For deeply nested submenus, alternates side placement (right/left)
303
+ * @param submenuElement - The submenu element to position
304
+ * @param parentItemElement - The parent menu item element
305
+ * @param level - Nesting level for calculating position
306
+ */
307
+ const positionSubmenu = (
308
+ submenuElement: HTMLElement,
309
+ parentItemElement: HTMLElement,
310
+ level: number = 1
311
+ ): void => {
312
+ if (!submenuElement || !parentItemElement) return;
313
+
314
+ // Alternate between right and left positioning for deeper nesting levels
315
+ // This helps prevent menus from cascading off the screen
316
+ const prefPosition = level % 2 === 1 ? 'right-start' : 'left-start';
317
+
318
+ // Use higher z-index for deeper nested menus to ensure proper layering
319
+ submenuElement.style.zIndex = `${1000 + (level * 10)}`;
320
+
321
+ positionElement(submenuElement, parentItemElement, prefPosition, true);
322
+ };
323
+
324
+ return {
325
+ positionMenu,
326
+ positionSubmenu,
327
+ positionElement
328
+ };
329
+ };
330
+
331
+ /**
332
+ * Adds positioning functionality to the menu component
333
+ *
334
+ * @param config - Menu configuration options
335
+ * @returns Component enhancer with positioning functionality
336
+ */
337
+ export const withPosition = (config: MenuConfig) => component => {
338
+ // Do nothing if no element
339
+ if (!component.element) {
340
+ return component;
341
+ }
342
+
343
+ // Create the positioner
344
+ const positioner = createPositioner(component, config);
345
+
346
+ // Return enhanced component
347
+ return {
348
+ ...component,
349
+ position: positioner
350
+ };
351
+ };
352
+
353
+ export default withPosition;
@@ -22,23 +22,23 @@ export type {
22
22
  MenuContent,
23
23
  MenuEvent,
24
24
  MenuSelectEvent,
25
- MenuPlacement
25
+ MenuPosition
26
26
  } from './types';
27
27
 
28
28
  /**
29
- * Constants for menu placement values - use these instead of string literals
29
+ * Constants for menu position values - use these instead of string literals
30
30
  * for better code completion and type safety.
31
31
  *
32
32
  * @example
33
- * import { createMenu, MENU_PLACEMENT } from 'mtrl';
33
+ * import { createMenu, MENU_POSITION } from 'mtrl';
34
34
  *
35
35
  * // Create a menu positioned at the bottom-right of its anchor
36
36
  * const menu = createMenu({
37
37
  * anchor: '#dropdown-button',
38
38
  * items: [...],
39
- * placement: MENU_PLACEMENT.BOTTOM_END
39
+ * position: MENU_POSITION.BOTTOM_END
40
40
  * });
41
41
  *
42
42
  * @category Components
43
43
  */
44
- export { MENU_PLACEMENT } from './types';
44
+ export { MENU_POSITION } from './types';
@@ -4,6 +4,7 @@ import { pipe } from '../../core/compose';
4
4
  import { createBase, withElement } from '../../core/compose/component';
5
5
  import { withEvents, withLifecycle } from '../../core/compose/features';
6
6
  import { withController, withAnchor } from './features';
7
+ import { withPosition } from './features/position';
7
8
  import { withAPI } from './api';
8
9
  import { MenuConfig, MenuComponent } from './types';
9
10
  import { createBaseConfig, getElementConfig, getApiConfig } from './config';
@@ -18,6 +19,9 @@ import { createBaseConfig, getElementConfig, getApiConfig } from './config';
18
19
  * Menus are built using a functional composition pattern, applying various
19
20
  * features through the pipe function for a modular architecture.
20
21
  *
22
+ * The menu element is not added to the DOM until it's opened, and it's removed
23
+ * from the DOM when closed, following best practices for dropdown menus.
24
+ *
21
25
  * @param {MenuConfig} config - Configuration options for the menu
22
26
  * This must include an anchor element or selector, and an array of menu items.
23
27
  * See {@link MenuConfig} for all available options.
@@ -44,14 +48,14 @@ import { createBaseConfig, getElementConfig, getApiConfig } from './config';
44
48
  * ]
45
49
  * });
46
50
  *
47
- * // Add the menu to the document
48
- * document.body.appendChild(menu.element);
49
- *
50
51
  * // Add event listener for item selection
51
52
  * menu.on('select', (event) => {
52
53
  * console.log('Selected item:', event.itemId);
53
54
  * });
54
55
  *
56
+ * // Menu will be added to the DOM when opened and removed when closed
57
+ * menuButton.addEventListener('click', () => menu.toggle());
58
+ *
55
59
  * @example
56
60
  * // Create a menu with nested submenus
57
61
  * const menu = createMenu({
@@ -70,68 +74,21 @@ import { createBaseConfig, getElementConfig, getApiConfig } from './config';
70
74
  * { type: 'divider' },
71
75
  * { id: 'delete', text: 'Delete', icon: '<svg>...</svg>' }
72
76
  * ],
73
- * placement: 'bottom-end'
77
+ * position: 'bottom-end'
74
78
  * });
75
79
  *
76
80
  * @example
77
- * // Programmatically control menu visibility
78
- * const userMenu = createMenu({
79
- * anchor: userAvatar,
80
- * items: userMenuItems
81
+ * // Specify a custom position for the menu
82
+ * const filterMenu = createMenu({
83
+ * anchor: filterButton,
84
+ * items: filterOptions,
85
+ * position: MENU_POSITION.TOP_START,
86
+ * width: '240px',
87
+ * maxHeight: '400px'
81
88
  * });
82
89
  *
83
- * // Open the menu programmatically
84
- * userMenu.open();
85
- *
86
- * // Later, close the menu
87
- * userMenu.close();
88
- */
89
- /**
90
- * Creates a new Menu component with the specified configuration.
91
- *
92
- * The Menu component implements the Material Design 3 menu specifications,
93
- * providing a flexible dropdown menu system with support for nested menus,
94
- * keyboard navigation, and ARIA accessibility.
95
- *
96
- * Menus are built using a functional composition pattern, applying various
97
- * features through the pipe function for a modular architecture.
98
- *
99
- * The menu element is not added to the DOM until it's opened, and it's removed
100
- * from the DOM when closed, following best practices for dropdown menus.
101
- *
102
- * @param {MenuConfig} config - Configuration options for the menu
103
- * This must include an anchor element or selector, and an array of menu items.
104
- * See {@link MenuConfig} for all available options.
105
- *
106
- * @returns {MenuComponent} A fully configured menu component instance with
107
- * all requested features applied. The returned component has methods for
108
- * menu manipulation, event handling, and lifecycle management.
109
- *
110
- * @throws {Error} Throws an error if menu creation fails or if required
111
- * configuration (like anchor) is missing.
112
- *
113
- * @category Components
114
- *
115
- * @example
116
- * // Create a simple menu anchored to a button
117
- * const menuButton = document.getElementById('menu-button');
118
- * const menu = createMenu({
119
- * anchor: menuButton,
120
- * items: [
121
- * { id: 'item1', text: 'Option 1' },
122
- * { id: 'item2', text: 'Option 2' },
123
- * { type: 'divider' },
124
- * { id: 'item3', text: 'Option 3' }
125
- * ]
126
- * });
127
- *
128
- * // Add event listener for item selection
129
- * menu.on('select', (event) => {
130
- * console.log('Selected item:', event.itemId);
131
- * });
132
- *
133
- * // Menu will be added to the DOM when opened and removed when closed
134
- * menuButton.addEventListener('click', () => menu.toggle());
90
+ * // Update the menu's position programmatically
91
+ * filterMenu.setPosition(MENU_POSITION.BOTTOM_END);
135
92
  */
136
93
  const createMenu = (config: MenuConfig): MenuComponent => {
137
94
  try {
@@ -143,6 +100,7 @@ const createMenu = (config: MenuConfig): MenuComponent => {
143
100
  createBase, // Base component
144
101
  withEvents(), // Event handling
145
102
  withElement(getElementConfig(baseConfig)), // DOM element
103
+ withPosition(baseConfig), // Position management
146
104
  withController(baseConfig), // Menu controller
147
105
  withAnchor(baseConfig), // Anchor management
148
106
  withLifecycle(), // Lifecycle management
@@ -1,12 +1,12 @@
1
1
  // src/components/menu/types.ts
2
2
 
3
3
  /**
4
- * Menu placement options
4
+ * Menu position options
5
5
  * Controls where the menu will appear relative to its anchor element
6
6
  *
7
7
  * @category Components
8
8
  */
9
- export const MENU_PLACEMENT = {
9
+ export const MENU_POSITION = {
10
10
  /** Places menu below the anchor, aligned to left edge */
11
11
  BOTTOM_START: 'bottom-start',
12
12
  /** Places menu below the anchor, centered */
@@ -34,11 +34,11 @@ export const MENU_PLACEMENT = {
34
34
  } as const;
35
35
 
36
36
  /**
37
- * Alignment options for the menu
37
+ * Position options for the menu
38
38
  *
39
39
  * @category Components
40
40
  */
41
- export type MenuPlacement = typeof MENU_PLACEMENT[keyof typeof MENU_PLACEMENT];
41
+ export type MenuPosition = typeof MENU_POSITION[keyof typeof MENU_POSITION];
42
42
 
43
43
  /**
44
44
  * Configuration interface for a menu item
@@ -126,9 +126,9 @@ export type MenuContent = MenuItem | MenuDivider;
126
126
  export interface MenuConfig {
127
127
  /**
128
128
  * Element to which the menu will be anchored
129
- * Can be an HTML element or a CSS selector string
129
+ * Can be an HTML element, a CSS selector string, or a component with an element property
130
130
  */
131
- anchor: HTMLElement | string;
131
+ anchor: HTMLElement | string | { element: HTMLElement };
132
132
 
133
133
  /**
134
134
  * Array of menu items and dividers to display
@@ -136,10 +136,10 @@ export interface MenuConfig {
136
136
  items: MenuContent[];
137
137
 
138
138
  /**
139
- * Placement of the menu relative to the anchor
139
+ * Position of the menu relative to the anchor
140
140
  * @default 'bottom-start'
141
141
  */
142
- placement?: MenuPlacement;
142
+ position?: MenuPosition;
143
143
 
144
144
  /**
145
145
  * Whether the menu should close when an item is clicked
@@ -184,7 +184,7 @@ export interface MenuConfig {
184
184
  offset?: number;
185
185
 
186
186
  /**
187
- * Whether the menu should automatically flip placement to stay in viewport
187
+ * Whether the menu should automatically flip position to stay in viewport
188
188
  * @default true
189
189
  */
190
190
  autoFlip?: boolean;
@@ -280,9 +280,10 @@ export interface MenuComponent {
280
280
  /**
281
281
  * Opens the menu
282
282
  * @param event - Optional event that triggered the open
283
+ * @param interactionType - The type of interaction that triggered the open ('mouse' or 'keyboard')
283
284
  * @returns The menu component for chaining
284
285
  */
285
- open: (event?: Event) => MenuComponent;
286
+ open: (event?: Event, interactionType?: 'mouse' | 'keyboard') => MenuComponent;
286
287
 
287
288
  /**
288
289
  * Closes the menu
@@ -331,17 +332,17 @@ export interface MenuComponent {
331
332
  getAnchor: () => HTMLElement;
332
333
 
333
334
  /**
334
- * Updates the menu's placement
335
- * @param placement - New placement value
335
+ * Updates the menu's position
336
+ * @param position - New position value
336
337
  * @returns The menu component for chaining
337
338
  */
338
- setPlacement: (placement: MenuPlacement) => MenuComponent;
339
+ setPosition: (position: MenuPosition) => MenuComponent;
339
340
 
340
341
  /**
341
- * Gets the current menu placement
342
- * @returns Current placement
342
+ * Gets the current menu position
343
+ * @returns Current position
343
344
  */
344
- getPlacement: () => MenuPlacement;
345
+ getPosition: () => MenuPosition;
345
346
 
346
347
  /**
347
348
  * Adds an event listener to the menu
@@ -0,0 +1,78 @@
1
+ // src/components/select/api.ts
2
+ import { SelectComponent, ApiOptions, SelectOption } from './types';
3
+
4
+ /**
5
+ * Enhances a select component with API methods
6
+ * @param options - API configuration options
7
+ * @returns Higher-order function that adds API methods to component
8
+ * @internal
9
+ */
10
+ export const withAPI = (options: ApiOptions) =>
11
+ (component: any): SelectComponent => ({
12
+ ...component,
13
+ element: component.element,
14
+ textfield: component.textfield,
15
+ menu: component.menu,
16
+
17
+ getValue: options.select.getValue,
18
+
19
+ setValue(value: string): SelectComponent {
20
+ options.select.setValue(value);
21
+ return this;
22
+ },
23
+
24
+ getText: options.select.getText,
25
+
26
+ getSelectedOption: options.select.getSelectedOption,
27
+
28
+ getOptions: options.select.getOptions,
29
+
30
+ setOptions(options: SelectOption[]): SelectComponent {
31
+ options.select.setOptions(options);
32
+ return this;
33
+ },
34
+
35
+ open(interactionType: 'mouse' | 'keyboard' = 'mouse'): SelectComponent {
36
+ options.select.open(undefined, interactionType);
37
+ return this;
38
+ },
39
+
40
+ close(): SelectComponent {
41
+ options.select.close();
42
+ return this;
43
+ },
44
+
45
+ isOpen: options.select.isOpen,
46
+
47
+ on(event, handler) {
48
+ if (options.events?.on) {
49
+ options.events.on(event, handler);
50
+ } else if (component.on) {
51
+ component.on(event, handler);
52
+ }
53
+ return this;
54
+ },
55
+
56
+ off(event, handler) {
57
+ if (options.events?.off) {
58
+ options.events.off(event, handler);
59
+ } else if (component.off) {
60
+ component.off(event, handler);
61
+ }
62
+ return this;
63
+ },
64
+
65
+ enable(): SelectComponent {
66
+ options.disabled.enable();
67
+ return this;
68
+ },
69
+
70
+ disable(): SelectComponent {
71
+ options.disabled.disable();
72
+ return this;
73
+ },
74
+
75
+ destroy() {
76
+ options.lifecycle.destroy();
77
+ }
78
+ });