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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/components/textfield/features/placement.ts
|
|
2
|
+
|
|
3
|
+
import { BaseComponent, ElementComponent } from '../../../core/compose/component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component with placement management capabilities
|
|
7
|
+
*/
|
|
8
|
+
export interface PlacementComponent extends BaseComponent {
|
|
9
|
+
/**
|
|
10
|
+
* Updates positions of all elements in the textfield
|
|
11
|
+
* @returns The component instance for chaining
|
|
12
|
+
*/
|
|
13
|
+
updateElementPositions: () => PlacementComponent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handles dynamic positioning of textfield elements (label, prefix, suffix)
|
|
18
|
+
* This feature should be added last in the pipe to ensure all elements exist
|
|
19
|
+
*
|
|
20
|
+
* @returns Function that enhances a component with dynamic positioning
|
|
21
|
+
*/
|
|
22
|
+
export const withPlacement = () =>
|
|
23
|
+
<C extends ElementComponent>(component: C): C & PlacementComponent => {
|
|
24
|
+
const PREFIX = component.config.prefix || 'mtrl';
|
|
25
|
+
const COMPONENT = component.config.componentName || 'textfield';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Updates positions of labels and adjusts input padding
|
|
29
|
+
* to accommodate prefix/suffix elements
|
|
30
|
+
*/
|
|
31
|
+
const updateElementPositions = () => {
|
|
32
|
+
if (!component.element || !component.element.isConnected) return component;
|
|
33
|
+
|
|
34
|
+
// Get necessary elements
|
|
35
|
+
const labelEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`) as HTMLElement;
|
|
36
|
+
const prefixEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-prefix`);
|
|
37
|
+
const suffixEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-suffix`);
|
|
38
|
+
|
|
39
|
+
// Get component states
|
|
40
|
+
const isOutlined = component.element.classList.contains(`${PREFIX}-${COMPONENT}--outlined`);
|
|
41
|
+
const isFilled = component.element.classList.contains(`${PREFIX}-${COMPONENT}--filled`);
|
|
42
|
+
const isFocused = component.element.classList.contains(`${PREFIX}-${COMPONENT}--focused`);
|
|
43
|
+
const isEmpty = component.element.classList.contains(`${PREFIX}-${COMPONENT}--empty`);
|
|
44
|
+
const hasLeadingIcon = component.element.classList.contains(`${PREFIX}-${COMPONENT}--with-leading-icon`);
|
|
45
|
+
|
|
46
|
+
// Handle prefix positioning and input padding
|
|
47
|
+
if (prefixEl && component.input) {
|
|
48
|
+
const prefixWidth = prefixEl.getBoundingClientRect().width + 4; // 4px spacing
|
|
49
|
+
const inputPadding = prefixWidth + 12; // 12px additional padding
|
|
50
|
+
|
|
51
|
+
// Update input left padding
|
|
52
|
+
component.input.style.paddingLeft = `${inputPadding}px`;
|
|
53
|
+
|
|
54
|
+
// Update label position if present
|
|
55
|
+
if (labelEl) {
|
|
56
|
+
let labelPosition = inputPadding;
|
|
57
|
+
|
|
58
|
+
// Account for leading icon if present
|
|
59
|
+
if (hasLeadingIcon) {
|
|
60
|
+
labelPosition = Math.max(labelPosition, 44);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Different positioning strategy based on variant and state
|
|
64
|
+
if (!isFocused && isEmpty) {
|
|
65
|
+
// When unfocused and empty, align with prefix/input
|
|
66
|
+
labelEl.style.left = `${labelPosition}px`;
|
|
67
|
+
} else {
|
|
68
|
+
// When focused or filled, move to default position
|
|
69
|
+
labelEl.style.left = '12px';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
} else if (hasLeadingIcon && labelEl) {
|
|
74
|
+
// Handle case with leading icon but no prefix
|
|
75
|
+
if (isOutlined) {
|
|
76
|
+
if (!isFocused && isEmpty) {
|
|
77
|
+
// When unfocused and empty, align with icon
|
|
78
|
+
labelEl.style.left = '44px';
|
|
79
|
+
} else {
|
|
80
|
+
// When focused or filled, move to default position
|
|
81
|
+
labelEl.style.left = '12px';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// For filled variant, the CSS handles this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle suffix positioning and input padding
|
|
88
|
+
if (suffixEl && component.input) {
|
|
89
|
+
const suffixWidth = suffixEl.getBoundingClientRect().width + 4; // 4px spacing
|
|
90
|
+
const inputPadding = suffixWidth + 12; // 12px additional padding
|
|
91
|
+
|
|
92
|
+
// Update input right padding
|
|
93
|
+
component.input.style.paddingRight = `${inputPadding}px`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return component;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Set up event listeners for dynamic positioning
|
|
100
|
+
const setupEventListeners = () => {
|
|
101
|
+
if (component.input) {
|
|
102
|
+
// Update positions when focus state changes
|
|
103
|
+
component.input.addEventListener('focus', () => {
|
|
104
|
+
component.element.classList.add(`${PREFIX}-${COMPONENT}--focused`);
|
|
105
|
+
setTimeout(updateElementPositions, 10);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
component.input.addEventListener('blur', () => {
|
|
109
|
+
component.element.classList.remove(`${PREFIX}-${COMPONENT}--focused`);
|
|
110
|
+
setTimeout(updateElementPositions, 10);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Update positions when empty state changes
|
|
114
|
+
component.input.addEventListener('input', () => {
|
|
115
|
+
const isEmpty = !component.input.value;
|
|
116
|
+
component.element.classList.toggle(`${PREFIX}-${COMPONENT}--empty`, isEmpty);
|
|
117
|
+
setTimeout(updateElementPositions, 10);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Set initial empty state
|
|
121
|
+
if (!component.input.value) {
|
|
122
|
+
component.element.classList.add(`${PREFIX}-${COMPONENT}--empty`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Update positions on window resize
|
|
127
|
+
window.addEventListener('resize', updateElementPositions);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Perform initial setup
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
setupEventListeners();
|
|
133
|
+
updateElementPositions();
|
|
134
|
+
}, 0);
|
|
135
|
+
|
|
136
|
+
// Add lifecycle integration
|
|
137
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
138
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
139
|
+
component.lifecycle.destroy = () => {
|
|
140
|
+
window.removeEventListener('resize', updateElementPositions);
|
|
141
|
+
originalDestroy.call(component.lifecycle);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...component,
|
|
147
|
+
updateElementPositions
|
|
148
|
+
};
|
|
149
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/components/textfield/features/prefix-text.ts
|
|
2
|
+
|
|
3
|
+
import { BaseComponent, ElementComponent } from '../../../core/compose/component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for prefix text feature
|
|
7
|
+
*/
|
|
8
|
+
export interface PrefixTextConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Prefix text content to display before the input
|
|
11
|
+
*/
|
|
12
|
+
prefixText?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CSS class prefix
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Component name
|
|
21
|
+
*/
|
|
22
|
+
componentName?: string;
|
|
23
|
+
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component with prefix text capabilities
|
|
29
|
+
*/
|
|
30
|
+
export interface PrefixTextComponent extends BaseComponent {
|
|
31
|
+
/**
|
|
32
|
+
* Prefix text element
|
|
33
|
+
*/
|
|
34
|
+
prefixTextElement: HTMLElement | null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sets prefix text content
|
|
38
|
+
* @param text - Text content to display before the input
|
|
39
|
+
* @returns Component instance for chaining
|
|
40
|
+
*/
|
|
41
|
+
setPrefixText: (text: string) => PrefixTextComponent;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Removes prefix text
|
|
45
|
+
* @returns Component instance for chaining
|
|
46
|
+
*/
|
|
47
|
+
removePrefixText: () => PrefixTextComponent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds prefix text to a textfield component
|
|
52
|
+
* @param config - Configuration with prefix text settings
|
|
53
|
+
* @returns Function that enhances a component with prefix text
|
|
54
|
+
*/
|
|
55
|
+
export const withPrefixText = <T extends PrefixTextConfig>(config: T) =>
|
|
56
|
+
<C extends ElementComponent>(component: C): C & PrefixTextComponent => {
|
|
57
|
+
if (!config.prefixText) {
|
|
58
|
+
return component as any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create prefix text element
|
|
62
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
63
|
+
const prefixElement = document.createElement('span');
|
|
64
|
+
prefixElement.className = `${PREFIX}-${config.componentName || 'textfield'}-prefix`;
|
|
65
|
+
prefixElement.textContent = config.prefixText;
|
|
66
|
+
|
|
67
|
+
// Add prefix text to the component
|
|
68
|
+
component.element.appendChild(prefixElement);
|
|
69
|
+
|
|
70
|
+
// Add prefix class to the component
|
|
71
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`);
|
|
72
|
+
|
|
73
|
+
// Add lifecycle integration
|
|
74
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
75
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
76
|
+
component.lifecycle.destroy = () => {
|
|
77
|
+
prefixElement.remove();
|
|
78
|
+
originalDestroy.call(component.lifecycle);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...component,
|
|
84
|
+
prefixTextElement: prefixElement,
|
|
85
|
+
|
|
86
|
+
setPrefixText(text: string) {
|
|
87
|
+
prefixElement.textContent = text;
|
|
88
|
+
return this;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
removePrefixText() {
|
|
92
|
+
if (prefixElement.parentNode) {
|
|
93
|
+
prefixElement.remove();
|
|
94
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`);
|
|
95
|
+
|
|
96
|
+
// Reset label position if no prefix
|
|
97
|
+
const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
|
|
98
|
+
if (labelEl) {
|
|
99
|
+
(labelEl as HTMLElement).style.left = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.prefixTextElement = null;
|
|
103
|
+
}
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/components/textfield/features/suffix-text.ts
|
|
2
|
+
|
|
3
|
+
import { BaseComponent, ElementComponent } from '../../../core/compose/component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for suffix text feature
|
|
7
|
+
*/
|
|
8
|
+
export interface SuffixTextConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Suffix text content to display after the input
|
|
11
|
+
*/
|
|
12
|
+
suffixText?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CSS class prefix
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Component name
|
|
21
|
+
*/
|
|
22
|
+
componentName?: string;
|
|
23
|
+
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component with suffix text capabilities
|
|
29
|
+
*/
|
|
30
|
+
export interface SuffixTextComponent extends BaseComponent {
|
|
31
|
+
/**
|
|
32
|
+
* Suffix text element
|
|
33
|
+
*/
|
|
34
|
+
suffixTextElement: HTMLElement | null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sets suffix text content
|
|
38
|
+
* @param text - Text content to display after the input
|
|
39
|
+
* @returns Component instance for chaining
|
|
40
|
+
*/
|
|
41
|
+
setSuffixText: (text: string) => SuffixTextComponent;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Removes suffix text
|
|
45
|
+
* @returns Component instance for chaining
|
|
46
|
+
*/
|
|
47
|
+
removeSuffixText: () => SuffixTextComponent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds suffix text to a textfield component
|
|
52
|
+
* @param config - Configuration with suffix text settings
|
|
53
|
+
* @returns Function that enhances a component with suffix text
|
|
54
|
+
*/
|
|
55
|
+
export const withSuffixText = <T extends SuffixTextConfig>(config: T) =>
|
|
56
|
+
<C extends ElementComponent>(component: C): C & SuffixTextComponent => {
|
|
57
|
+
if (!config.suffixText) {
|
|
58
|
+
return component as any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create suffix text element
|
|
62
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
63
|
+
const suffixElement = document.createElement('span');
|
|
64
|
+
suffixElement.className = `${PREFIX}-${config.componentName || 'textfield'}-suffix`;
|
|
65
|
+
suffixElement.textContent = config.suffixText;
|
|
66
|
+
|
|
67
|
+
// Add suffix text to the component
|
|
68
|
+
component.element.appendChild(suffixElement);
|
|
69
|
+
|
|
70
|
+
// Add suffix class to the component
|
|
71
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-suffix`);
|
|
72
|
+
|
|
73
|
+
// Add lifecycle integration
|
|
74
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
75
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
76
|
+
component.lifecycle.destroy = () => {
|
|
77
|
+
suffixElement.remove();
|
|
78
|
+
originalDestroy.call(component.lifecycle);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...component,
|
|
84
|
+
suffixTextElement: suffixElement,
|
|
85
|
+
|
|
86
|
+
setSuffixText(text: string) {
|
|
87
|
+
suffixElement.textContent = text;
|
|
88
|
+
return this;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
removeSuffixText() {
|
|
92
|
+
if (suffixElement.parentNode) {
|
|
93
|
+
suffixElement.remove();
|
|
94
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-suffix`);
|
|
95
|
+
this.suffixTextElement = null;
|
|
96
|
+
}
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/components/textfield/features/supporting-text.ts
|
|
2
|
+
|
|
3
|
+
import { BaseComponent, ElementComponent } from '../../../core/compose/component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for supporting text feature
|
|
7
|
+
*/
|
|
8
|
+
export interface SupportingTextConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Supporting text content
|
|
11
|
+
*/
|
|
12
|
+
supportingText?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether supporting text indicates an error
|
|
16
|
+
*/
|
|
17
|
+
error?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* CSS class prefix
|
|
21
|
+
*/
|
|
22
|
+
prefix?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Component name
|
|
26
|
+
*/
|
|
27
|
+
componentName?: string;
|
|
28
|
+
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Component with supporting text capabilities
|
|
34
|
+
*/
|
|
35
|
+
export interface SupportingTextComponent extends BaseComponent {
|
|
36
|
+
/**
|
|
37
|
+
* Supporting text element
|
|
38
|
+
*/
|
|
39
|
+
supportingTextElement: HTMLElement | null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sets supporting text content
|
|
43
|
+
* @param text - Text content
|
|
44
|
+
* @param isError - Whether text represents an error
|
|
45
|
+
* @returns Component instance for chaining
|
|
46
|
+
*/
|
|
47
|
+
setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Removes supporting text
|
|
51
|
+
* @returns Component instance for chaining
|
|
52
|
+
*/
|
|
53
|
+
removeSupportingText: () => SupportingTextComponent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Adds supporting text to a textfield component
|
|
58
|
+
* @param config - Configuration with supporting text settings
|
|
59
|
+
* @returns Function that enhances a component with supporting text
|
|
60
|
+
*/
|
|
61
|
+
export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
|
|
62
|
+
<C extends ElementComponent>(component: C): C & SupportingTextComponent => {
|
|
63
|
+
if (!config.supportingText) {
|
|
64
|
+
return component as any;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create supporting text element
|
|
68
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
69
|
+
const supportingElement = document.createElement('div');
|
|
70
|
+
supportingElement.className = `${PREFIX}-${config.componentName || 'textfield'}-helper`;
|
|
71
|
+
supportingElement.textContent = config.supportingText;
|
|
72
|
+
|
|
73
|
+
if (config.error) {
|
|
74
|
+
supportingElement.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`);
|
|
75
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--error`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add supporting text to the component
|
|
79
|
+
component.element.appendChild(supportingElement);
|
|
80
|
+
|
|
81
|
+
// Add lifecycle integration if available
|
|
82
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
83
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
84
|
+
component.lifecycle.destroy = () => {
|
|
85
|
+
supportingElement.remove();
|
|
86
|
+
originalDestroy.call(component.lifecycle);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...component,
|
|
92
|
+
supportingTextElement: supportingElement,
|
|
93
|
+
|
|
94
|
+
setSupportingText(text: string, isError = false) {
|
|
95
|
+
supportingElement.textContent = text;
|
|
96
|
+
|
|
97
|
+
// Handle error state
|
|
98
|
+
supportingElement.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}-helper--error`, isError);
|
|
99
|
+
component.element.classList.toggle(`${PREFIX}-${config.componentName || 'textfield'}--error`, isError);
|
|
100
|
+
|
|
101
|
+
return this;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
removeSupportingText() {
|
|
105
|
+
if (supportingElement.parentNode) {
|
|
106
|
+
supportingElement.remove();
|
|
107
|
+
this.supportingTextElement = null;
|
|
108
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--error`);
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/components/textfield/features/trailing-icon.ts
|
|
2
|
+
|
|
3
|
+
import { BaseComponent, ElementComponent } from '../../../core/compose/component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for trailing icon feature
|
|
7
|
+
*/
|
|
8
|
+
export interface TrailingIconConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Trailing icon HTML content
|
|
11
|
+
*/
|
|
12
|
+
trailingIcon?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CSS class prefix
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Component name
|
|
21
|
+
*/
|
|
22
|
+
componentName?: string;
|
|
23
|
+
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component with trailing icon capabilities
|
|
29
|
+
*/
|
|
30
|
+
export interface TrailingIconComponent extends BaseComponent {
|
|
31
|
+
/**
|
|
32
|
+
* Trailing icon element
|
|
33
|
+
*/
|
|
34
|
+
trailingIcon: HTMLElement | null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sets trailing icon content
|
|
38
|
+
* @param html - HTML content for the icon
|
|
39
|
+
* @returns Component instance for chaining
|
|
40
|
+
*/
|
|
41
|
+
setTrailingIcon: (html: string) => TrailingIconComponent;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Removes trailing icon
|
|
45
|
+
* @returns Component instance for chaining
|
|
46
|
+
*/
|
|
47
|
+
removeTrailingIcon: () => TrailingIconComponent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds trailing icon to a textfield component
|
|
52
|
+
* @param config - Configuration with trailing icon settings
|
|
53
|
+
* @returns Function that enhances a component with trailing icon
|
|
54
|
+
*/
|
|
55
|
+
export const withTrailingIcon = <T extends TrailingIconConfig>(config: T) =>
|
|
56
|
+
<C extends ElementComponent>(component: C): C & TrailingIconComponent => {
|
|
57
|
+
if (!config.trailingIcon) {
|
|
58
|
+
return component as any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create icon element
|
|
62
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
63
|
+
const iconElement = document.createElement('span');
|
|
64
|
+
iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-trailing-icon`;
|
|
65
|
+
iconElement.innerHTML = config.trailingIcon;
|
|
66
|
+
|
|
67
|
+
// Add trailing icon to the component
|
|
68
|
+
component.element.appendChild(iconElement);
|
|
69
|
+
|
|
70
|
+
// Add trailing-icon class to the component
|
|
71
|
+
component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
|
|
72
|
+
|
|
73
|
+
// When there's a trailing icon, adjust input padding
|
|
74
|
+
if (component.input) {
|
|
75
|
+
component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add lifecycle integration if available
|
|
79
|
+
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
80
|
+
const originalDestroy = component.lifecycle.destroy;
|
|
81
|
+
component.lifecycle.destroy = () => {
|
|
82
|
+
iconElement.remove();
|
|
83
|
+
originalDestroy.call(component.lifecycle);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...component,
|
|
89
|
+
trailingIcon: iconElement,
|
|
90
|
+
|
|
91
|
+
setTrailingIcon(html: string) {
|
|
92
|
+
iconElement.innerHTML = html;
|
|
93
|
+
return this;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
removeTrailingIcon() {
|
|
97
|
+
if (iconElement.parentNode) {
|
|
98
|
+
iconElement.remove();
|
|
99
|
+
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
|
|
100
|
+
if (component.input) {
|
|
101
|
+
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
|
|
102
|
+
}
|
|
103
|
+
this.trailingIcon = null;
|
|
104
|
+
}
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -13,7 +13,10 @@ import { withAPI } from './api';
|
|
|
13
13
|
import {
|
|
14
14
|
withLeadingIcon,
|
|
15
15
|
withTrailingIcon,
|
|
16
|
-
withSupportingText
|
|
16
|
+
withSupportingText,
|
|
17
|
+
withPrefixText,
|
|
18
|
+
withSuffixText,
|
|
19
|
+
withPlacement
|
|
17
20
|
} from './features';
|
|
18
21
|
import { TextfieldConfig, TextfieldComponent } from './types';
|
|
19
22
|
import {
|
|
@@ -24,26 +27,59 @@ import {
|
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Creates a new Textfield component
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* Textfields allow users to enter text into a UI. They typically appear in forms and dialogs.
|
|
32
|
+
* This implementation follows Material Design 3 guidelines for accessible, customizable textfields.
|
|
33
|
+
*
|
|
34
|
+
* @param {TextfieldConfig} config - Textfield configuration options
|
|
35
|
+
* @returns {TextfieldComponent} A fully configured textfield component instance
|
|
36
|
+
* @throws {Error} Throws an error if textfield creation fails
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Create a basic text field
|
|
40
|
+
* const textfield = createTextfield({
|
|
41
|
+
* label: 'Username',
|
|
42
|
+
* name: 'username'
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* document.querySelector('.form').appendChild(textfield.element);
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Create a text field with prefix and suffix
|
|
49
|
+
* const currencyField = createTextfield({
|
|
50
|
+
* label: 'Amount',
|
|
51
|
+
* type: 'number',
|
|
52
|
+
* prefixText: '$',
|
|
53
|
+
* suffixText: 'USD'
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // Add event listener
|
|
57
|
+
* currencyField.on('input', (e) => {
|
|
58
|
+
* console.log('Amount entered:', e.target.value);
|
|
59
|
+
* });
|
|
29
60
|
*/
|
|
30
61
|
const createTextfield = (config: TextfieldConfig = {}): TextfieldComponent => {
|
|
31
62
|
const baseConfig = createBaseConfig(config);
|
|
32
63
|
|
|
33
64
|
try {
|
|
65
|
+
// Build textfield through functional composition
|
|
66
|
+
// Each function in the pipe adds specific capabilities
|
|
34
67
|
const textfield = pipe(
|
|
35
|
-
createBase,
|
|
36
|
-
withEvents(),
|
|
37
|
-
withElement(getElementConfig(baseConfig)),
|
|
38
|
-
withVariant(baseConfig),
|
|
39
|
-
withTextInput(baseConfig),
|
|
40
|
-
withTextLabel(baseConfig),
|
|
41
|
-
withLeadingIcon(baseConfig),
|
|
42
|
-
withTrailingIcon(baseConfig),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
createBase, // Base component structure
|
|
69
|
+
withEvents(), // Event handling system
|
|
70
|
+
withElement(getElementConfig(baseConfig)), // Create DOM element
|
|
71
|
+
withVariant(baseConfig), // Apply variant styling (filled/outlined)
|
|
72
|
+
withTextInput(baseConfig), // Add input element
|
|
73
|
+
withTextLabel(baseConfig), // Add text label
|
|
74
|
+
withLeadingIcon(baseConfig), // Add leading icon (if specified)
|
|
75
|
+
withTrailingIcon(baseConfig), // Add trailing icon (if specified)
|
|
76
|
+
withPrefixText(baseConfig), // Add prefix text (if specified)
|
|
77
|
+
withSuffixText(baseConfig), // Add suffix text (if specified)
|
|
78
|
+
withSupportingText(baseConfig), // Add supporting/helper text (if specified)
|
|
79
|
+
withDisabled(baseConfig), // Add disabled state management
|
|
80
|
+
withLifecycle(), // Add lifecycle management
|
|
81
|
+
withPlacement(), // Add dynamic positioning for elements
|
|
82
|
+
comp => withAPI(getApiConfig(comp))(comp) // Add public API
|
|
47
83
|
)(baseConfig);
|
|
48
84
|
|
|
49
85
|
return textfield as TextfieldComponent;
|