mtrl 0.3.6 → 0.3.8
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 +2 -2
- package/src/components/button/api.ts +16 -0
- package/src/components/button/types.ts +9 -0
- package/src/components/menu/api.ts +61 -22
- package/src/components/menu/config.ts +10 -8
- package/src/components/menu/features/anchor.ts +254 -19
- package/src/components/menu/features/controller.ts +724 -271
- package/src/components/menu/features/index.ts +11 -2
- package/src/components/menu/features/position.ts +353 -0
- package/src/components/menu/index.ts +5 -5
- package/src/components/menu/menu.ts +21 -61
- package/src/components/menu/types.ts +30 -16
- package/src/components/select/api.ts +78 -0
- package/src/components/select/config.ts +76 -0
- package/src/components/select/features.ts +331 -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 -45
- 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 +97 -24
- package/src/styles/components/_select.scss +272 -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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/components/select/config.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createComponentConfig,
|
|
5
|
+
createElementConfig
|
|
6
|
+
} from '../../core/config/component-config';
|
|
7
|
+
import { SelectConfig, BaseComponent, ApiOptions } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default configuration for the Select component
|
|
11
|
+
*/
|
|
12
|
+
export const defaultConfig: SelectConfig = {
|
|
13
|
+
options: [],
|
|
14
|
+
variant: 'filled',
|
|
15
|
+
placement: 'bottom-start'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates the base configuration for Select component
|
|
20
|
+
* @param {SelectConfig} config - User provided configuration
|
|
21
|
+
* @returns {SelectConfig} Complete configuration with defaults applied
|
|
22
|
+
*/
|
|
23
|
+
export const createBaseConfig = (config: SelectConfig = {}): SelectConfig =>
|
|
24
|
+
createComponentConfig(defaultConfig, config, 'select') as SelectConfig;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates API configuration for the Select component
|
|
28
|
+
* @param {BaseComponent} comp - Component with select features
|
|
29
|
+
* @returns {ApiOptions} API configuration object
|
|
30
|
+
*/
|
|
31
|
+
export const getApiConfig = (comp: BaseComponent): ApiOptions => ({
|
|
32
|
+
select: {
|
|
33
|
+
getValue: comp.select?.getValue || (() => null),
|
|
34
|
+
setValue: comp.select?.setValue || (() => comp),
|
|
35
|
+
getText: comp.select?.getText || (() => ''),
|
|
36
|
+
getSelectedOption: comp.select?.getSelectedOption || (() => null),
|
|
37
|
+
getOptions: comp.select?.getOptions || (() => []),
|
|
38
|
+
setOptions: comp.select?.setOptions || (() => comp),
|
|
39
|
+
open: comp.select?.open || (() => comp),
|
|
40
|
+
close: comp.select?.close || (() => comp),
|
|
41
|
+
isOpen: comp.select?.isOpen || (() => false)
|
|
42
|
+
},
|
|
43
|
+
events: {
|
|
44
|
+
on: comp.on || (() => comp),
|
|
45
|
+
off: comp.off || (() => comp)
|
|
46
|
+
},
|
|
47
|
+
disabled: {
|
|
48
|
+
enable: () => {
|
|
49
|
+
if (comp.textfield?.enable) {
|
|
50
|
+
comp.textfield.enable();
|
|
51
|
+
}
|
|
52
|
+
return comp;
|
|
53
|
+
},
|
|
54
|
+
disable: () => {
|
|
55
|
+
if (comp.textfield?.disable) {
|
|
56
|
+
comp.textfield.disable();
|
|
57
|
+
}
|
|
58
|
+
return comp;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
lifecycle: {
|
|
62
|
+
destroy: () => {
|
|
63
|
+
if (comp.textfield?.destroy) {
|
|
64
|
+
comp.textfield.destroy();
|
|
65
|
+
}
|
|
66
|
+
if (comp.menu?.destroy) {
|
|
67
|
+
comp.menu.destroy();
|
|
68
|
+
}
|
|
69
|
+
if (comp.lifecycle?.destroy) {
|
|
70
|
+
comp.lifecycle.destroy();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export default defaultConfig;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/components/select/features.ts
|
|
2
|
+
import createTextfield from '../textfield';
|
|
3
|
+
import createMenu from '../menu';
|
|
4
|
+
import { SelectOption, SelectConfig, BaseComponent } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a textfield for the select component
|
|
8
|
+
* @param config - Select configuration
|
|
9
|
+
* @returns Function that enhances a component with textfield functionality
|
|
10
|
+
*/
|
|
11
|
+
export const withTextfield = (config: SelectConfig) =>
|
|
12
|
+
(component: BaseComponent): BaseComponent => {
|
|
13
|
+
// Get option text from value if provided
|
|
14
|
+
let initialText = '';
|
|
15
|
+
if (config.value) {
|
|
16
|
+
const option = (config.options || []).find(opt => opt.id === config.value);
|
|
17
|
+
if (option) {
|
|
18
|
+
initialText = option.text;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create dropdown icon for the textfield
|
|
23
|
+
const dropdownIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7 10l5 5 5-5H7z"/></svg>';
|
|
24
|
+
|
|
25
|
+
// Create textfield component
|
|
26
|
+
const textfield = createTextfield({
|
|
27
|
+
label: config.label,
|
|
28
|
+
variant: config.variant || 'filled',
|
|
29
|
+
value: initialText,
|
|
30
|
+
name: config.name,
|
|
31
|
+
disabled: config.disabled,
|
|
32
|
+
required: config.required,
|
|
33
|
+
supportingText: config.supportingText,
|
|
34
|
+
error: config.error,
|
|
35
|
+
trailingIcon: dropdownIcon,
|
|
36
|
+
readonly: true // Make readonly since selection happens via menu
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Add select-specific class
|
|
40
|
+
textfield.element.classList.add(`${config.prefix || 'mtrl'}-select`);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...component,
|
|
44
|
+
element: textfield.element,
|
|
45
|
+
textfield
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Recursively processes select options to create menu items
|
|
51
|
+
* Ensures all items have proper data structure
|
|
52
|
+
* @param options The options to process
|
|
53
|
+
* @returns Properly structured menu items
|
|
54
|
+
*/
|
|
55
|
+
const processMenuItems = (options) => {
|
|
56
|
+
return options.map(option => {
|
|
57
|
+
if ('type' in option && option.type === 'divider') {
|
|
58
|
+
return option; // Just pass dividers through
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create a basic menu item
|
|
62
|
+
const menuItem = {
|
|
63
|
+
id: option.id,
|
|
64
|
+
text: option.text,
|
|
65
|
+
icon: option.icon,
|
|
66
|
+
disabled: option.disabled,
|
|
67
|
+
hasSubmenu: false,
|
|
68
|
+
data: option
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// If this option has a submenu, process those items recursively
|
|
72
|
+
if (option.hasSubmenu && Array.isArray(option.submenu)) {
|
|
73
|
+
menuItem.hasSubmenu = true;
|
|
74
|
+
menuItem.submenu = processMenuItems(option.submenu);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return menuItem;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a menu for the select component with improved keyboard navigation
|
|
83
|
+
* @param config - Select configuration
|
|
84
|
+
* @returns Function that enhances a component with menu functionality
|
|
85
|
+
*/
|
|
86
|
+
export const withMenu = (config: SelectConfig) =>
|
|
87
|
+
(component: BaseComponent): BaseComponent => {
|
|
88
|
+
if (!component.textfield) {
|
|
89
|
+
console.warn('Cannot add menu: textfield not found');
|
|
90
|
+
return component;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Initialize state
|
|
94
|
+
const state = {
|
|
95
|
+
options: config.options || [],
|
|
96
|
+
selectedOption: null as SelectOption
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Find initial selected option
|
|
100
|
+
if (config.value) {
|
|
101
|
+
state.selectedOption = state.options.find(opt => opt.id === config.value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Convert options to menu items with proper recursive processing
|
|
105
|
+
const menuItems = processMenuItems(state.options);
|
|
106
|
+
|
|
107
|
+
// Create menu component
|
|
108
|
+
const menu = createMenu({
|
|
109
|
+
anchor: component.textfield.element,
|
|
110
|
+
items: menuItems,
|
|
111
|
+
placement: config.placement || 'bottom-start',
|
|
112
|
+
width: '100%', // Match width of textfield
|
|
113
|
+
closeOnSelect: true,
|
|
114
|
+
closeOnClickOutside: true,
|
|
115
|
+
closeOnEscape: true,
|
|
116
|
+
offset: 0 // Set offset to 0 to eliminate gap between textfield and menu
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// This flag helps us know if we need to restore focus after menu closes
|
|
120
|
+
let needsFocusRestore = false;
|
|
121
|
+
|
|
122
|
+
// Handle menu selection
|
|
123
|
+
menu.on('select', (event) => {
|
|
124
|
+
// Safely access data properties with proper type checking
|
|
125
|
+
if (!event.item || event.item.hasSubmenu) {
|
|
126
|
+
return; // Skip processing for submenu items
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Safely extract the option data and validate it
|
|
130
|
+
const option = event.item.data;
|
|
131
|
+
if (!option || !('id' in option) || !('text' in option)) {
|
|
132
|
+
console.warn('Invalid menu selection: missing required data properties');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
state.selectedOption = option;
|
|
137
|
+
|
|
138
|
+
// Update textfield
|
|
139
|
+
component.textfield.setValue(option.text);
|
|
140
|
+
|
|
141
|
+
// Update the selected state in the menu
|
|
142
|
+
menu.setSelected(option.id);
|
|
143
|
+
|
|
144
|
+
// Set flag to restore focus after menu closes
|
|
145
|
+
needsFocusRestore = true;
|
|
146
|
+
|
|
147
|
+
// Emit change event
|
|
148
|
+
if (component.emit) {
|
|
149
|
+
component.emit('change', {
|
|
150
|
+
select: component,
|
|
151
|
+
value: option.id,
|
|
152
|
+
text: option.text,
|
|
153
|
+
option,
|
|
154
|
+
originalEvent: event.originalEvent,
|
|
155
|
+
preventDefault: () => { event.defaultPrevented = true; },
|
|
156
|
+
defaultPrevented: false
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Add keyboard event listener for textfield
|
|
162
|
+
component.textfield.element.addEventListener('keydown', (e) => {
|
|
163
|
+
if (component.textfield.input.disabled) return;
|
|
164
|
+
|
|
165
|
+
// Handle keyboard-based open
|
|
166
|
+
if ((e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') && !menu.isOpen()) {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
|
|
169
|
+
// Set flag to restore focus
|
|
170
|
+
needsFocusRestore = true;
|
|
171
|
+
|
|
172
|
+
// Open menu with keyboard interaction
|
|
173
|
+
menu.open(e, 'keyboard');
|
|
174
|
+
|
|
175
|
+
// Ensure textfield keeps focus
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
// Focus on the textfield to ensure we can capture further keyboard events
|
|
178
|
+
component.textfield.input.focus();
|
|
179
|
+
}, 50);
|
|
180
|
+
|
|
181
|
+
// Emit open event
|
|
182
|
+
if (component.emit) {
|
|
183
|
+
component.emit('open', {
|
|
184
|
+
select: component,
|
|
185
|
+
originalEvent: e,
|
|
186
|
+
preventDefault: () => {},
|
|
187
|
+
defaultPrevented: false
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} else if (e.key === 'Escape' && menu.isOpen()) {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
needsFocusRestore = true;
|
|
193
|
+
menu.close(e);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Update textfield styling when menu opens/closes
|
|
198
|
+
menu.on('open', () => {
|
|
199
|
+
// Add open class to the select component
|
|
200
|
+
component.textfield.element.classList.add(`${config.prefix || 'mtrl'}-select--open`);
|
|
201
|
+
|
|
202
|
+
// Add focused class to the textfield
|
|
203
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
204
|
+
component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
|
|
205
|
+
|
|
206
|
+
// If using the filled variant, we need to add focus styles to ensure the label changes color
|
|
207
|
+
if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
|
|
208
|
+
component.textfield.element.classList.add(`${PREFIX}-textfield--filled-focused`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
menu.on('close', (event) => {
|
|
213
|
+
// Remove open class from the select component
|
|
214
|
+
component.textfield.element.classList.remove(`${config.prefix || 'mtrl'}-select--open`);
|
|
215
|
+
|
|
216
|
+
// Restore focus to textfield if needed
|
|
217
|
+
if (needsFocusRestore) {
|
|
218
|
+
// Small delay to ensure menu is fully closed
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
// Explicitly focus the textfield input
|
|
221
|
+
component.textfield.input.focus();
|
|
222
|
+
|
|
223
|
+
// Keep focused styling
|
|
224
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
225
|
+
component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
|
|
226
|
+
|
|
227
|
+
if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
|
|
228
|
+
component.textfield.element.classList.add(`${PREFIX}-textfield--filled-focused`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Reset the flag
|
|
232
|
+
needsFocusRestore = false;
|
|
233
|
+
}, 50);
|
|
234
|
+
} else {
|
|
235
|
+
// Only remove focus styling if we're not restoring focus
|
|
236
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
237
|
+
component.textfield.element.classList.remove(`${PREFIX}-textfield--focused`);
|
|
238
|
+
|
|
239
|
+
if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
|
|
240
|
+
component.textfield.element.classList.remove(`${PREFIX}-textfield--filled-focused`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Emit close event
|
|
245
|
+
if (component.emit) {
|
|
246
|
+
component.emit('close', {
|
|
247
|
+
select: component,
|
|
248
|
+
originalEvent: event.originalEvent,
|
|
249
|
+
preventDefault: () => {},
|
|
250
|
+
defaultPrevented: false
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Handle special case for showing selected item in menu
|
|
256
|
+
const markSelectedMenuItem = () => {
|
|
257
|
+
if (!state.selectedOption) return;
|
|
258
|
+
|
|
259
|
+
// Use menu's setSelected method to update the selected state
|
|
260
|
+
menu.setSelected(state.selectedOption.id);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Mark selected item when menu opens
|
|
264
|
+
menu.on('open', () => {
|
|
265
|
+
setTimeout(markSelectedMenuItem, 50); // Small delay to ensure DOM is ready
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Expose select API
|
|
269
|
+
return {
|
|
270
|
+
...component,
|
|
271
|
+
menu,
|
|
272
|
+
|
|
273
|
+
// Select controller
|
|
274
|
+
select: {
|
|
275
|
+
getValue: () => state.selectedOption?.id || null,
|
|
276
|
+
|
|
277
|
+
setValue: (value) => {
|
|
278
|
+
const option = state.options.find(opt => 'id' in opt && opt.id === value);
|
|
279
|
+
if (option && 'text' in option) {
|
|
280
|
+
state.selectedOption = option;
|
|
281
|
+
component.textfield.setValue(option.text);
|
|
282
|
+
|
|
283
|
+
// Update selected state using menu's setSelected
|
|
284
|
+
menu.setSelected(option.id);
|
|
285
|
+
}
|
|
286
|
+
return component;
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
getText: () => state.selectedOption?.text || '',
|
|
290
|
+
|
|
291
|
+
getSelectedOption: () => state.selectedOption,
|
|
292
|
+
|
|
293
|
+
getOptions: () => [...state.options],
|
|
294
|
+
|
|
295
|
+
setOptions: (options) => {
|
|
296
|
+
state.options = options;
|
|
297
|
+
|
|
298
|
+
// Process options to menu items with proper handling for submenus
|
|
299
|
+
const menuItems = processMenuItems(options);
|
|
300
|
+
menu.setItems(menuItems);
|
|
301
|
+
|
|
302
|
+
// If previously selected option is no longer available, clear selection
|
|
303
|
+
if (state.selectedOption && !options.find(opt =>
|
|
304
|
+
'id' in opt && opt.id === state.selectedOption.id)
|
|
305
|
+
) {
|
|
306
|
+
state.selectedOption = null;
|
|
307
|
+
component.textfield.setValue('');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return component;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
open: (event?: Event, interactionType: 'mouse' | 'keyboard' = 'mouse') => {
|
|
314
|
+
// Set focus restore flag for keyboard interactions
|
|
315
|
+
if (interactionType === 'keyboard') {
|
|
316
|
+
needsFocusRestore = true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
menu.open(event, interactionType);
|
|
320
|
+
return component;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
close: () => {
|
|
324
|
+
menu.close();
|
|
325
|
+
return component;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
isOpen: () => menu.isOpen()
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/components/select/index.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Select Component Module
|
|
5
|
+
*
|
|
6
|
+
* The Select component provides a dropdown select control,
|
|
7
|
+
* combining a textfield and menu for a complete selection interface.
|
|
8
|
+
*
|
|
9
|
+
* @module components/select
|
|
10
|
+
* @category Components
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Export main component factory
|
|
14
|
+
export { default } from './select';
|
|
15
|
+
|
|
16
|
+
// Export types and interfaces
|
|
17
|
+
export type {
|
|
18
|
+
SelectConfig,
|
|
19
|
+
SelectComponent,
|
|
20
|
+
SelectOption,
|
|
21
|
+
SelectEvent,
|
|
22
|
+
SelectChangeEvent
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Constants for select configuration
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* import { createSelect, SELECT_VARIANTS } from 'mtrl';
|
|
30
|
+
*
|
|
31
|
+
* const select = createSelect({
|
|
32
|
+
* variant: SELECT_VARIANTS.OUTLINED,
|
|
33
|
+
* options: [...]
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* @category Components
|
|
37
|
+
*/
|
|
38
|
+
export { SELECT_VARIANTS } from './types';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/components/select/select.ts
|
|
2
|
+
|
|
3
|
+
import { pipe } from '../../core/compose';
|
|
4
|
+
import { createBase } from '../../core/compose/component';
|
|
5
|
+
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
6
|
+
import { withTextfield, withMenu } from './features';
|
|
7
|
+
import { withAPI } from './api';
|
|
8
|
+
import { SelectConfig, SelectComponent } from './types';
|
|
9
|
+
import { createBaseConfig, getApiConfig } from './config';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new Select component with the specified configuration.
|
|
13
|
+
*
|
|
14
|
+
* The Select component implements a Material Design dropdown select control,
|
|
15
|
+
* combining a textfield and menu to provide a user-friendly selection interface.
|
|
16
|
+
*
|
|
17
|
+
* @param {SelectConfig} config - Configuration options for the select
|
|
18
|
+
* This must include an array of selection options. See {@link SelectConfig} for all available options.
|
|
19
|
+
*
|
|
20
|
+
* @returns {SelectComponent} A fully configured select component
|
|
21
|
+
*
|
|
22
|
+
* @throws {Error} Throws an error if select creation fails
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Create a simple country select
|
|
26
|
+
* const countrySelect = createSelect({
|
|
27
|
+
* label: 'Country',
|
|
28
|
+
* options: [
|
|
29
|
+
* { id: 'us', text: 'United States' },
|
|
30
|
+
* { id: 'ca', text: 'Canada' },
|
|
31
|
+
* { id: 'mx', text: 'Mexico' }
|
|
32
|
+
* ],
|
|
33
|
+
* value: 'us' // Pre-select United States
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Add to the DOM
|
|
37
|
+
* document.body.appendChild(countrySelect.element);
|
|
38
|
+
*
|
|
39
|
+
* // Listen for changes
|
|
40
|
+
* countrySelect.on('change', (event) => {
|
|
41
|
+
* console.log(`Selected: ${event.value} (${event.text})`);
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
const createSelect = (config: SelectConfig): SelectComponent => {
|
|
45
|
+
try {
|
|
46
|
+
// Validate and create the base configuration
|
|
47
|
+
const baseConfig = createBaseConfig(config);
|
|
48
|
+
|
|
49
|
+
// Create the component through functional composition
|
|
50
|
+
const select = pipe(
|
|
51
|
+
createBase,
|
|
52
|
+
withEvents(),
|
|
53
|
+
withTextfield(baseConfig),
|
|
54
|
+
withMenu(baseConfig),
|
|
55
|
+
withLifecycle(),
|
|
56
|
+
comp => withAPI(getApiConfig(comp))(comp)
|
|
57
|
+
)(baseConfig);
|
|
58
|
+
|
|
59
|
+
// Set up initial event handlers if provided
|
|
60
|
+
if (config.on) {
|
|
61
|
+
if (config.on.change) select.on('change', config.on.change);
|
|
62
|
+
if (config.on.open) select.on('open', config.on.open);
|
|
63
|
+
if (config.on.close) select.on('close', config.on.close);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return select;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Select creation error:', error);
|
|
69
|
+
throw new Error(`Failed to create select: ${(error as Error).message}`);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default createSelect;
|