mtrl 0.3.3 → 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 (41) 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/components/segmented-button/config.ts +59 -20
  11. package/src/components/segmented-button/index.ts +1 -1
  12. package/src/components/segmented-button/segment.ts +51 -97
  13. package/src/components/segmented-button/segmented-button.ts +114 -2
  14. package/src/components/segmented-button/types.ts +52 -0
  15. package/src/core/compose/features/icon.ts +15 -13
  16. package/src/core/dom/classes.ts +81 -9
  17. package/src/core/dom/create.ts +30 -19
  18. package/src/core/layout/README.md +531 -166
  19. package/src/core/layout/array.ts +3 -4
  20. package/src/core/layout/config.ts +193 -0
  21. package/src/core/layout/create.ts +1 -2
  22. package/src/core/layout/index.ts +12 -2
  23. package/src/core/layout/object.ts +2 -3
  24. package/src/core/layout/processor.ts +60 -12
  25. package/src/core/layout/result.ts +1 -2
  26. package/src/core/layout/types.ts +105 -50
  27. package/src/core/layout/utils.ts +69 -61
  28. package/src/index.ts +2 -1
  29. package/src/styles/components/_button.scss +6 -0
  30. package/src/styles/components/_chip.scss +4 -5
  31. package/src/styles/components/_menu.scss +20 -8
  32. package/src/styles/components/_segmented-button.scss +173 -63
  33. package/src/styles/main.scss +23 -23
  34. package/src/styles/utilities/_layout.scss +665 -0
  35. package/src/components/menu/features/items-manager.ts +0 -457
  36. package/src/components/menu/features/keyboard-navigation.ts +0 -133
  37. package/src/components/menu/features/positioning.ts +0 -127
  38. package/src/components/menu/features/visibility.ts +0 -230
  39. package/src/components/menu/menu-item.ts +0 -86
  40. package/src/components/menu/utils.ts +0 -67
  41. /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
@@ -1,230 +0,0 @@
1
- // src/components/menu/features/visibility.ts
2
- import { BaseComponent, MenuConfig } from '../types';
3
- import { MENU_EVENT, MENU_CLASSES } from '../utils';
4
-
5
- /**
6
- * Adds visibility management functionality to a menu component
7
- *
8
- * This feature adds the ability to show and hide the menu with smooth transitions,
9
- * along with proper event handling for clicks outside the menu and keyboard shortcuts.
10
- * It implements the following functionality:
11
- *
12
- * - Tracking visibility state
13
- * - Showing the menu with animation
14
- * - Hiding the menu with animation
15
- * - Automatic handling of clicks outside the menu
16
- * - Keyboard shortcut (Escape) for dismissing the menu
17
- * - Proper ARIA attributes for accessibility
18
- * - Event emission for component state changes
19
- *
20
- * @param {MenuConfig} config - Menu configuration options
21
- * @returns {Function} Component enhancer function that adds visibility features
22
- *
23
- * @internal
24
- * @category Components
25
- */
26
- export const withVisibility = (config: MenuConfig) => (component: BaseComponent): BaseComponent => {
27
- let isVisible = false;
28
- let outsideClickHandler: ((event: MouseEvent) => void) | null = null;
29
- let keydownHandler: ((event: KeyboardEvent) => void) | null = null;
30
- const prefix = config.prefix || 'mtrl';
31
-
32
- // Create the component interface with hide/show methods first
33
- const enhancedComponent: BaseComponent = {
34
- ...component,
35
-
36
- /**
37
- * Shows the menu
38
- *
39
- * Makes the menu visible with a smooth transition animation.
40
- * Handles DOM insertion, event binding, and ARIA attribute updates.
41
- * Emits an 'open' event when the menu becomes visible.
42
- *
43
- * @returns {BaseComponent} The component instance for method chaining
44
- * @internal
45
- */
46
- show() {
47
- if (isVisible) return this;
48
-
49
- // First set visibility to true to prevent multiple calls
50
- isVisible = true;
51
-
52
- // Make sure the element is in the DOM
53
- if (!component.element.parentNode) {
54
- document.body.appendChild(component.element);
55
- }
56
-
57
- // Always clean up previous handlers before adding new ones
58
- if (outsideClickHandler) {
59
- document.removeEventListener('mousedown', outsideClickHandler);
60
- }
61
-
62
- // Setup outside click handler for closing
63
- outsideClickHandler = handleOutsideClick;
64
-
65
- // Use setTimeout to ensure the handler is not triggered immediately
66
- setTimeout(() => {
67
- document.addEventListener('mousedown', outsideClickHandler!);
68
- }, 0);
69
-
70
- // Setup keyboard navigation
71
- if (!keydownHandler) {
72
- keydownHandler = handleKeydown;
73
- document.addEventListener('keydown', keydownHandler);
74
- }
75
-
76
- // Add display block first for transition to work
77
- component.element.style.display = 'block';
78
-
79
- // Force a reflow before adding the visible class for animation
80
- // eslint-disable-next-line no-void
81
- void component.element.offsetHeight;
82
- component.element.classList.add(`${prefix}-${MENU_CLASSES.VISIBLE}`);
83
- component.element.setAttribute('aria-hidden', 'false');
84
-
85
- // Emit open event
86
- component.emit?.(MENU_EVENT.OPEN, {});
87
-
88
- return this;
89
- },
90
-
91
- /**
92
- * Hides the menu
93
- *
94
- * Makes the menu invisible with a smooth transition animation.
95
- * Handles event cleanup, ARIA attribute updates, and DOM removal.
96
- * Emits a 'close' event when the menu becomes hidden.
97
- * Also ensures any open submenus are closed first.
98
- *
99
- * @returns {BaseComponent} The component instance for method chaining
100
- * @internal
101
- */
102
- hide() {
103
- // Return early if already hidden
104
- if (!isVisible) return this;
105
-
106
- // First set the visibility flag to false
107
- isVisible = false;
108
-
109
- // Close any open submenus first
110
- if (component.closeSubmenus) {
111
- component.closeSubmenus();
112
- }
113
-
114
- // Remove ALL event listeners
115
- if (outsideClickHandler) {
116
- document.removeEventListener('mousedown', outsideClickHandler);
117
- outsideClickHandler = null;
118
- }
119
-
120
- if (keydownHandler) {
121
- document.removeEventListener('keydown', keydownHandler);
122
- keydownHandler = null;
123
- }
124
-
125
- // Hide the menu with visual indication first
126
- component.element.classList.remove(`${prefix}-${MENU_CLASSES.VISIBLE}`);
127
- component.element.setAttribute('aria-hidden', 'true');
128
-
129
- // Define a reliable cleanup function
130
- const cleanupElement = () => {
131
- // Safety check to prevent errors
132
- if (component.element) {
133
- component.element.style.display = 'none';
134
-
135
- // Remove from DOM if still attached
136
- if (component.element.parentNode) {
137
- component.element.remove();
138
- }
139
- }
140
- };
141
-
142
- // Try to use transition end for smooth animation
143
- const handleTransitionEnd = (e: TransitionEvent) => {
144
- if (e.propertyName === 'opacity' || e.propertyName === 'transform') {
145
- component.element.removeEventListener('transitionend', handleTransitionEnd);
146
- cleanupElement();
147
- }
148
- };
149
-
150
- component.element.addEventListener('transitionend', handleTransitionEnd);
151
-
152
- // Fallback timeout in case transition events don't fire
153
- // This ensures the menu always gets removed
154
- setTimeout(cleanupElement, 300);
155
-
156
- // Emit close event
157
- component.emit?.(MENU_EVENT.CLOSE, {});
158
-
159
- return this;
160
- },
161
-
162
- /**
163
- * Returns whether the menu is currently visible
164
- *
165
- * Provides the current visibility state of the menu component.
166
- * This method is used internally and exposed via the public API.
167
- *
168
- * @returns {boolean} True if the menu is visible, false otherwise
169
- * @internal
170
- */
171
- isVisible() {
172
- return isVisible;
173
- }
174
- };
175
-
176
- /**
177
- * Handles clicks outside the menu
178
- *
179
- * Event handler for detecting and responding to mouse clicks outside the menu.
180
- * Checks if the click occurred outside both the menu and its origin element,
181
- * and if so, hides the menu. This provides the auto-dismiss behavior expected
182
- * of temporary surfaces like menus.
183
- *
184
- * @param {MouseEvent} event - Mouse event containing target information
185
- * @internal
186
- */
187
- const handleOutsideClick = (event: MouseEvent) => {
188
- if (!isVisible) return;
189
-
190
- // Store the opening button if available
191
- const origin = config.origin?.element;
192
-
193
- // Check if click is outside the menu but not on the opening button
194
- const clickedElement = event.target as Node;
195
-
196
- // Don't close if the click is inside the menu
197
- if (component.element.contains(clickedElement)) {
198
- return;
199
- }
200
-
201
- // Don't close if the click is on the opening button (it will handle opening/closing)
202
- if (origin && (origin === clickedElement || origin.contains(clickedElement))) {
203
- return;
204
- }
205
-
206
- // If we got here, close the menu
207
- enhancedComponent.hide?.();
208
- };
209
-
210
- /**
211
- * Handles keyboard events for the menu
212
- *
213
- * Event handler for keyboard interactions with the menu.
214
- * Currently implements the Escape key to dismiss the menu,
215
- * following standard interaction patterns for temporary UI surfaces.
216
- *
217
- * @param {KeyboardEvent} event - Keyboard event containing key information
218
- * @internal
219
- */
220
- const handleKeydown = (event: KeyboardEvent) => {
221
- if (!isVisible) return;
222
-
223
- if (event.key === 'Escape') {
224
- event.preventDefault();
225
- enhancedComponent.hide?.();
226
- }
227
- };
228
-
229
- return enhancedComponent;
230
- };
@@ -1,86 +0,0 @@
1
- // src/components/menu/menu-item.ts
2
- import { MenuItemConfig } from './types';
3
- import { MENU_ITEM_TYPE, getMenuClass } from './utils';
4
-
5
- /**
6
- * Creates a DOM element for a menu item
7
- *
8
- * Generates an HTMLElement (li) based on the provided configuration.
9
- * Handles different types of menu items (standard, divider, submenu),
10
- * applies proper CSS classes, and sets appropriate ARIA attributes
11
- * for accessibility.
12
- *
13
- * @param {MenuItemConfig} itemConfig - Item configuration
14
- * @param {string} prefix - CSS class prefix (default: 'mtrl')
15
- * @returns {HTMLElement} Menu item DOM element
16
- *
17
- * @example
18
- * ```typescript
19
- * // Create a standard menu item
20
- * const itemElement = createMenuItem(
21
- * { name: 'edit', text: 'Edit' },
22
- * 'mtrl'
23
- * );
24
- *
25
- * // Create a disabled menu item
26
- * const disabledItem = createMenuItem(
27
- * { name: 'print', text: 'Print', disabled: true },
28
- * 'mtrl'
29
- * );
30
- *
31
- * // Create a divider
32
- * const divider = createMenuItem(
33
- * { type: 'divider' },
34
- * 'mtrl'
35
- * );
36
- *
37
- * // Create an item with submenu indicator
38
- * const submenuItem = createMenuItem(
39
- * {
40
- * name: 'share',
41
- * text: 'Share',
42
- * items: [
43
- * { name: 'email', text: 'Email' },
44
- * { name: 'link', text: 'Copy Link' }
45
- * ]
46
- * },
47
- * 'mtrl'
48
- * );
49
- * ```
50
- *
51
- * @internal
52
- * @category Components
53
- */
54
- export const createMenuItem = (itemConfig: MenuItemConfig, prefix: string): HTMLElement => {
55
- const item = document.createElement('li');
56
- item.className = `${prefix}-${getMenuClass('ITEM')}`;
57
-
58
- if (itemConfig.type === MENU_ITEM_TYPE.DIVIDER) {
59
- item.className = `${prefix}-${getMenuClass('DIVIDER')}`;
60
- return item;
61
- }
62
-
63
- if (itemConfig.class) {
64
- item.className += ` ${itemConfig.class}`;
65
- }
66
-
67
- if (itemConfig.disabled) {
68
- item.setAttribute('aria-disabled', 'true');
69
- item.className += ` ${prefix}-${getMenuClass('ITEM')}--disabled`;
70
- }
71
-
72
- if (itemConfig.name) {
73
- item.setAttribute('data-name', itemConfig.name);
74
- }
75
-
76
- item.textContent = itemConfig.text || '';
77
-
78
- if (itemConfig.items?.length) {
79
- item.className += ` ${prefix}-${getMenuClass('ITEM')}--submenu`;
80
- item.setAttribute('aria-haspopup', 'true');
81
- item.setAttribute('aria-expanded', 'false');
82
- // We don't need to add a submenu indicator as it's handled by CSS ::after
83
- }
84
-
85
- return item;
86
- }
@@ -1,67 +0,0 @@
1
- // src/components/menu/utils.ts
2
-
3
- /**
4
- * Menu alignment constants for internal use
5
- * @internal
6
- */
7
- export const MENU_ALIGNMENT = {
8
- LEFT: 'left',
9
- RIGHT: 'right',
10
- CENTER: 'center'
11
- };
12
-
13
- /**
14
- * Menu vertical alignment constants for internal use
15
- * @internal
16
- */
17
- export const MENU_VERTICAL_ALIGNMENT = {
18
- TOP: 'top',
19
- BOTTOM: 'bottom',
20
- MIDDLE: 'middle'
21
- };
22
-
23
- /**
24
- * Menu item types for internal use
25
- * @internal
26
- */
27
- export const MENU_ITEM_TYPE = {
28
- ITEM: 'item',
29
- DIVIDER: 'divider'
30
- };
31
-
32
- /**
33
- * Menu events for internal use
34
- * @internal
35
- */
36
- export const MENU_EVENT = {
37
- SELECT: 'select',
38
- OPEN: 'open',
39
- CLOSE: 'close',
40
- SUBMENU_OPEN: 'submenuOpen',
41
- SUBMENU_CLOSE: 'submenuClose'
42
- };
43
-
44
- /**
45
- * Menu CSS classes for internal use
46
- * @internal
47
- */
48
- export const MENU_CLASSES = {
49
- ROOT: 'menu',
50
- ITEM: 'menu-item',
51
- ITEM_CONTAINER: 'menu-item-container',
52
- LIST: 'menu-list',
53
- DIVIDER: 'menu-divider',
54
- SUBMENU: 'menu--submenu',
55
- VISIBLE: 'menu--visible',
56
- DISABLED: 'menu--disabled'
57
- };
58
-
59
- /**
60
- * Gets a class name for menu elements
61
- * @param {string} element - Element name from MENU_CLASSES
62
- * @returns {string} The class name
63
- * @internal
64
- */
65
- export const getMenuClass = (element: keyof typeof MENU_CLASSES): string => {
66
- return MENU_CLASSES[element];
67
- };