mtrl 0.3.5 → 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 (65) 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 +144 -267
  5. package/src/components/menu/config.ts +84 -40
  6. package/src/components/menu/features/anchor.ts +243 -0
  7. package/src/components/menu/features/controller.ts +1167 -0
  8. package/src/components/menu/features/index.ts +5 -0
  9. package/src/components/menu/features/position.ts +353 -0
  10. package/src/components/menu/index.ts +31 -63
  11. package/src/components/menu/menu.ts +72 -104
  12. package/src/components/menu/types.ts +264 -447
  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/core/dom/classes.ts +81 -9
  34. package/src/core/dom/create.ts +30 -19
  35. package/src/core/layout/README.md +531 -166
  36. package/src/core/layout/array.ts +3 -4
  37. package/src/core/layout/config.ts +193 -0
  38. package/src/core/layout/create.ts +1 -2
  39. package/src/core/layout/index.ts +12 -2
  40. package/src/core/layout/object.ts +2 -3
  41. package/src/core/layout/processor.ts +60 -12
  42. package/src/core/layout/result.ts +1 -2
  43. package/src/core/layout/types.ts +105 -50
  44. package/src/core/layout/utils.ts +69 -61
  45. package/src/index.ts +6 -2
  46. package/src/styles/abstract/_variables.scss +18 -0
  47. package/src/styles/components/_button.scss +21 -5
  48. package/src/styles/components/{_chip.scss → _chips.scss} +118 -4
  49. package/src/styles/components/_menu.scss +109 -18
  50. package/src/styles/components/_select.scss +265 -0
  51. package/src/styles/components/_textfield.scss +233 -42
  52. package/src/styles/main.scss +24 -23
  53. package/src/styles/utilities/_layout.scss +665 -0
  54. package/src/components/menu/features/items-manager.ts +0 -457
  55. package/src/components/menu/features/keyboard-navigation.ts +0 -133
  56. package/src/components/menu/features/positioning.ts +0 -127
  57. package/src/components/menu/features/visibility.ts +0 -230
  58. package/src/components/menu/menu-item.ts +0 -86
  59. package/src/components/menu/utils.ts +0 -67
  60. package/src/components/textfield/features.ts +0 -322
  61. package/src/core/collection/adapters/base.js +0 -26
  62. package/src/core/collection/collection.js +0 -259
  63. package/src/core/collection/list-manager.js +0 -157
  64. /package/src/core/collection/adapters/{route.js → route.ts} +0 -0
  65. /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
- };
@@ -1,322 +0,0 @@
1
- // src/components/textfield/features.ts
2
- import { BaseComponent, ElementComponent } from '../../core/compose/component';
3
-
4
- /**
5
- * Configuration for leading icon feature
6
- */
7
- export interface LeadingIconConfig {
8
- /**
9
- * Leading icon HTML content
10
- */
11
- leadingIcon?: string;
12
-
13
- /**
14
- * CSS class prefix
15
- */
16
- prefix?: string;
17
-
18
- /**
19
- * Component name
20
- */
21
- componentName?: string;
22
-
23
- [key: string]: any;
24
- }
25
-
26
- /**
27
- * Configuration for trailing icon feature
28
- */
29
- export interface TrailingIconConfig {
30
- /**
31
- * Trailing icon HTML content
32
- */
33
- trailingIcon?: string;
34
-
35
- /**
36
- * CSS class prefix
37
- */
38
- prefix?: string;
39
-
40
- /**
41
- * Component name
42
- */
43
- componentName?: string;
44
-
45
- [key: string]: any;
46
- }
47
-
48
- /**
49
- * Configuration for supporting text feature
50
- */
51
- export interface SupportingTextConfig {
52
- /**
53
- * Supporting text content
54
- */
55
- supportingText?: string;
56
-
57
- /**
58
- * Whether supporting text indicates an error
59
- */
60
- error?: boolean;
61
-
62
- /**
63
- * CSS class prefix
64
- */
65
- prefix?: string;
66
-
67
- /**
68
- * Component name
69
- */
70
- componentName?: string;
71
-
72
- [key: string]: any;
73
- }
74
-
75
- /**
76
- * Component with leading icon capabilities
77
- */
78
- export interface LeadingIconComponent extends BaseComponent {
79
- /**
80
- * Leading icon element
81
- */
82
- leadingIcon: HTMLElement | null;
83
-
84
- /**
85
- * Sets leading icon content
86
- * @param html - HTML content for the icon
87
- * @returns Component instance for chaining
88
- */
89
- setLeadingIcon: (html: string) => LeadingIconComponent;
90
-
91
- /**
92
- * Removes leading icon
93
- * @returns Component instance for chaining
94
- */
95
- removeLeadingIcon: () => LeadingIconComponent;
96
- }
97
-
98
- /**
99
- * Component with trailing icon capabilities
100
- */
101
- export interface TrailingIconComponent extends BaseComponent {
102
- /**
103
- * Trailing icon element
104
- */
105
- trailingIcon: HTMLElement | null;
106
-
107
- /**
108
- * Sets trailing icon content
109
- * @param html - HTML content for the icon
110
- * @returns Component instance for chaining
111
- */
112
- setTrailingIcon: (html: string) => TrailingIconComponent;
113
-
114
- /**
115
- * Removes trailing icon
116
- * @returns Component instance for chaining
117
- */
118
- removeTrailingIcon: () => TrailingIconComponent;
119
- }
120
-
121
- /**
122
- * Component with supporting text capabilities
123
- */
124
- export interface SupportingTextComponent extends BaseComponent {
125
- /**
126
- * Supporting text element
127
- */
128
- supportingTextElement: HTMLElement | null;
129
-
130
- /**
131
- * Sets supporting text content
132
- * @param text - Text content
133
- * @param isError - Whether text represents an error
134
- * @returns Component instance for chaining
135
- */
136
- setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
137
-
138
- /**
139
- * Removes supporting text
140
- * @returns Component instance for chaining
141
- */
142
- removeSupportingText: () => SupportingTextComponent;
143
- }
144
-
145
- /**
146
- * Creates and manages a leading icon for a component
147
- * @param config - Configuration object with leading icon settings
148
- * @returns Function that enhances a component with leading icon functionality
149
- */
150
- export const withLeadingIcon = <T extends LeadingIconConfig>(config: T) =>
151
- <C extends ElementComponent>(component: C): C & LeadingIconComponent => {
152
- if (!config.leadingIcon) {
153
- return component as C & LeadingIconComponent;
154
- }
155
-
156
- // Create icon element
157
- const PREFIX = config.prefix || 'mtrl';
158
- const iconElement = document.createElement('span');
159
- iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
160
- iconElement.innerHTML = config.leadingIcon;
161
-
162
- // Add leading icon to the component
163
- component.element.appendChild(iconElement);
164
-
165
- // Add leading-icon class to the component
166
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
167
-
168
- // When there's a leading icon, adjust input padding
169
- if (component.input) {
170
- component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
171
- }
172
-
173
- // Add lifecycle integration if available
174
- if ('lifecycle' in component && component.lifecycle?.destroy) {
175
- const originalDestroy = component.lifecycle.destroy;
176
- component.lifecycle.destroy = () => {
177
- iconElement.remove();
178
- originalDestroy.call(component.lifecycle);
179
- };
180
- }
181
-
182
- return {
183
- ...component,
184
- leadingIcon: iconElement,
185
-
186
- setLeadingIcon(html: string) {
187
- iconElement.innerHTML = html;
188
- return this;
189
- },
190
-
191
- removeLeadingIcon() {
192
- if (iconElement.parentNode) {
193
- iconElement.remove();
194
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
195
- if (component.input) {
196
- component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
197
- }
198
- this.leadingIcon = null;
199
- }
200
- return this;
201
- }
202
- };
203
- };
204
-
205
- /**
206
- * Creates and manages a trailing icon for a component
207
- * @param config - Configuration object with trailing icon settings
208
- * @returns Function that enhances a component with trailing icon functionality
209
- */
210
- export const withTrailingIcon = <T extends TrailingIconConfig>(config: T) =>
211
- <C extends ElementComponent>(component: C): C & TrailingIconComponent => {
212
- if (!config.trailingIcon) {
213
- return component as C & TrailingIconComponent;
214
- }
215
-
216
- // Create icon element
217
- const PREFIX = config.prefix || 'mtrl';
218
- const iconElement = document.createElement('span');
219
- iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-trailing-icon`;
220
- iconElement.innerHTML = config.trailingIcon;
221
-
222
- // Add trailing icon to the component
223
- component.element.appendChild(iconElement);
224
-
225
- // Add trailing-icon class to the component
226
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
227
-
228
- // When there's a trailing icon, adjust input padding
229
- if (component.input) {
230
- component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
231
- }
232
-
233
- // Add lifecycle integration if available
234
- if ('lifecycle' in component && component.lifecycle?.destroy) {
235
- const originalDestroy = component.lifecycle.destroy;
236
- component.lifecycle.destroy = () => {
237
- iconElement.remove();
238
- originalDestroy.call(component.lifecycle);
239
- };
240
- }
241
-
242
- return {
243
- ...component,
244
- trailingIcon: iconElement,
245
-
246
- setTrailingIcon(html: string) {
247
- iconElement.innerHTML = html;
248
- return this;
249
- },
250
-
251
- removeTrailingIcon() {
252
- if (iconElement.parentNode) {
253
- iconElement.remove();
254
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
255
- if (component.input) {
256
- component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
257
- }
258
- this.trailingIcon = null;
259
- }
260
- return this;
261
- }
262
- };
263
- };
264
-
265
- /**
266
- * Creates and manages supporting text for a component
267
- * @param config - Configuration object with supporting text settings
268
- * @returns Function that enhances a component with supporting text functionality
269
- */
270
- export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
271
- <C extends ElementComponent>(component: C): C & SupportingTextComponent => {
272
- if (!config.supportingText) {
273
- return component as C & SupportingTextComponent;
274
- }
275
-
276
- // Create supporting text element
277
- const PREFIX = config.prefix || 'mtrl';
278
- const supportingElement = document.createElement('div');
279
- supportingElement.className = `${PREFIX}-${config.componentName || 'textfield'}-helper`;
280
- supportingElement.textContent = config.supportingText;
281
-
282
- if (config.error) {
283
- supportingElement.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`);
284
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--error`);
285
- }
286
-
287
- // Add supporting text to the component
288
- component.element.appendChild(supportingElement);
289
-
290
- // Add lifecycle integration if available
291
- if ('lifecycle' in component && component.lifecycle?.destroy) {
292
- const originalDestroy = component.lifecycle.destroy;
293
- component.lifecycle.destroy = () => {
294
- supportingElement.remove();
295
- originalDestroy.call(component.lifecycle);
296
- };
297
- }
298
-
299
- return {
300
- ...component,
301
- supportingTextElement: supportingElement,
302
-
303
- setSupportingText(text: string, isError = false) {
304
- supportingElement.textContent = text;
305
-
306
- // Handle error state
307
- supportingElement.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`, isError);
308
- component.element.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}--error`, isError);
309
-
310
- return this;
311
- },
312
-
313
- removeSupportingText() {
314
- if (supportingElement.parentNode) {
315
- supportingElement.remove();
316
- this.supportingTextElement = null;
317
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--error`);
318
- }
319
- return this;
320
- }
321
- };
322
- };