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.
- package/package.json +1 -1
- package/src/components/menu/api.ts +143 -268
- package/src/components/menu/config.ts +84 -40
- package/src/components/menu/features/anchor.ts +159 -0
- package/src/components/menu/features/controller.ts +970 -0
- package/src/components/menu/features/index.ts +4 -0
- package/src/components/menu/index.ts +31 -63
- package/src/components/menu/menu.ts +107 -97
- package/src/components/menu/types.ts +263 -447
- package/src/components/segmented-button/config.ts +59 -20
- package/src/components/segmented-button/index.ts +1 -1
- package/src/components/segmented-button/segment.ts +51 -97
- package/src/components/segmented-button/segmented-button.ts +114 -2
- package/src/components/segmented-button/types.ts +52 -0
- package/src/core/compose/features/icon.ts +15 -13
- package/src/core/dom/classes.ts +81 -9
- package/src/core/dom/create.ts +30 -19
- package/src/core/layout/README.md +531 -166
- package/src/core/layout/array.ts +3 -4
- package/src/core/layout/config.ts +193 -0
- package/src/core/layout/create.ts +1 -2
- package/src/core/layout/index.ts +12 -2
- package/src/core/layout/object.ts +2 -3
- package/src/core/layout/processor.ts +60 -12
- package/src/core/layout/result.ts +1 -2
- package/src/core/layout/types.ts +105 -50
- package/src/core/layout/utils.ts +69 -61
- package/src/index.ts +2 -1
- package/src/styles/components/_button.scss +6 -0
- package/src/styles/components/_chip.scss +4 -5
- package/src/styles/components/_menu.scss +20 -8
- package/src/styles/components/_segmented-button.scss +173 -63
- package/src/styles/main.scss +23 -23
- package/src/styles/utilities/_layout.scss +665 -0
- package/src/components/menu/features/items-manager.ts +0 -457
- package/src/components/menu/features/keyboard-navigation.ts +0 -133
- package/src/components/menu/features/positioning.ts +0 -127
- package/src/components/menu/features/visibility.ts +0 -230
- package/src/components/menu/menu-item.ts +0 -86
- package/src/components/menu/utils.ts +0 -67
- /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
|
-
};
|
|
File without changes
|