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.
- package/package.json +1 -1
- package/src/components/button/api.ts +16 -0
- package/src/components/button/types.ts +9 -0
- package/src/components/menu/api.ts +15 -13
- package/src/components/menu/config.ts +5 -5
- package/src/components/menu/features/anchor.ts +99 -15
- package/src/components/menu/features/controller.ts +418 -221
- package/src/components/menu/features/index.ts +2 -1
- package/src/components/menu/features/position.ts +353 -0
- package/src/components/menu/index.ts +5 -5
- package/src/components/menu/menu.ts +18 -60
- package/src/components/menu/types.ts +17 -16
- package/src/components/select/api.ts +78 -0
- package/src/components/select/config.ts +76 -0
- package/src/components/select/features.ts +317 -0
- package/src/components/select/index.ts +38 -0
- package/src/components/select/select.ts +73 -0
- package/src/components/select/types.ts +355 -0
- package/src/components/textfield/api.ts +78 -6
- package/src/components/textfield/features/index.ts +17 -0
- package/src/components/textfield/features/leading-icon.ts +127 -0
- package/src/components/textfield/features/placement.ts +149 -0
- package/src/components/textfield/features/prefix-text.ts +107 -0
- package/src/components/textfield/features/suffix-text.ts +100 -0
- package/src/components/textfield/features/supporting-text.ts +113 -0
- package/src/components/textfield/features/trailing-icon.ts +108 -0
- package/src/components/textfield/textfield.ts +51 -15
- package/src/components/textfield/types.ts +70 -0
- package/src/core/collection/adapters/base.ts +62 -0
- package/src/core/collection/collection.ts +300 -0
- package/src/core/collection/index.ts +57 -0
- package/src/core/collection/list-manager.ts +333 -0
- package/src/index.ts +4 -1
- package/src/styles/abstract/_variables.scss +18 -0
- package/src/styles/components/_button.scss +21 -5
- package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
- package/src/styles/components/_menu.scss +103 -24
- package/src/styles/components/_select.scss +265 -0
- package/src/styles/components/_textfield.scss +233 -42
- package/src/styles/main.scss +2 -1
- package/src/components/textfield/features.ts +0 -322
- package/src/core/collection/adapters/base.js +0 -26
- package/src/core/collection/collection.js +0 -259
- package/src/core/collection/list-manager.js +0 -157
- /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
package/package.json
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
137
|
-
* @param
|
|
138
|
+
* Updates the menu's position
|
|
139
|
+
* @param position - New position value
|
|
138
140
|
* @returns Menu component for chaining
|
|
139
141
|
*/
|
|
140
|
-
|
|
141
|
-
menu.
|
|
142
|
+
setPosition(position: MenuPosition) {
|
|
143
|
+
menu.setPosition(position);
|
|
142
144
|
return this;
|
|
143
145
|
},
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
|
-
* Gets the current menu
|
|
147
|
-
* @returns Current
|
|
148
|
+
* Gets the current menu position
|
|
149
|
+
* @returns Current position
|
|
148
150
|
*/
|
|
149
|
-
|
|
150
|
-
return menu.
|
|
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,
|
|
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
|
-
|
|
18
|
+
position: MENU_POSITION.BOTTOM_START,
|
|
19
19
|
closeOnSelect: true,
|
|
20
20
|
closeOnClickOutside: true,
|
|
21
21
|
closeOnEscape: true,
|
|
22
22
|
openSubmenuOnHover: true,
|
|
23
|
-
offset:
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
107
|
-
|
|
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
|
|
211
|
+
* @param anchor - New anchor element, selector, or component
|
|
138
212
|
* @returns Component for chaining
|
|
139
213
|
*/
|
|
140
|
-
setAnchor(anchor: HTMLElement | string) {
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
setupAnchorEvents(
|
|
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
|
};
|