mtrl 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/components/badge/_styles.scss +9 -9
- package/src/components/button/_styles.scss +0 -56
- package/src/components/button/button.ts +0 -2
- package/src/components/button/constants.ts +0 -6
- package/src/components/button/index.ts +2 -2
- package/src/components/button/types.ts +1 -7
- package/src/components/card/_styles.scss +67 -25
- package/src/components/card/api.ts +54 -3
- package/src/components/card/card.ts +33 -2
- package/src/components/card/config.ts +143 -21
- package/src/components/card/constants.ts +20 -19
- package/src/components/card/content.ts +299 -2
- package/src/components/card/features.ts +155 -4
- package/src/components/card/index.ts +31 -9
- package/src/components/card/types.ts +138 -15
- package/src/components/chip/chip.ts +1 -9
- package/src/components/chip/constants.ts +0 -10
- package/src/components/chip/index.ts +1 -1
- package/src/components/chip/types.ts +1 -4
- package/src/components/progress/_styles.scss +0 -65
- package/src/components/progress/config.ts +1 -2
- package/src/components/progress/constants.ts +0 -14
- package/src/components/progress/index.ts +1 -1
- package/src/components/progress/progress.ts +1 -4
- package/src/components/progress/types.ts +1 -4
- package/src/components/radios/_styles.scss +0 -45
- package/src/components/radios/api.ts +85 -60
- package/src/components/radios/config.ts +1 -2
- package/src/components/radios/constants.ts +0 -9
- package/src/components/radios/index.ts +1 -1
- package/src/components/radios/radio.ts +34 -11
- package/src/components/radios/radios.ts +2 -1
- package/src/components/radios/types.ts +1 -7
- package/src/components/slider/_styles.scss +193 -281
- package/src/components/slider/accessibility.md +59 -0
- package/src/components/slider/api.ts +36 -101
- package/src/components/slider/config.ts +29 -78
- package/src/components/slider/constants.ts +12 -8
- package/src/components/slider/features/appearance.ts +1 -47
- package/src/components/slider/features/disabled.ts +41 -16
- package/src/components/slider/features/interactions.ts +166 -26
- package/src/components/slider/features/keyboard.ts +125 -6
- package/src/components/slider/features/structure.ts +182 -195
- package/src/components/slider/features/ui.ts +234 -303
- package/src/components/slider/index.ts +11 -1
- package/src/components/slider/slider.ts +1 -1
- package/src/components/slider/types.ts +10 -25
- package/src/components/tabs/_styles.scss +285 -155
- package/src/components/tabs/api.ts +178 -400
- package/src/components/tabs/config.ts +46 -52
- package/src/components/tabs/constants.ts +85 -8
- package/src/components/tabs/features.ts +401 -0
- package/src/components/tabs/index.ts +60 -3
- package/src/components/tabs/indicator.ts +225 -0
- package/src/components/tabs/responsive.ts +144 -0
- package/src/components/tabs/scroll-indicators.ts +149 -0
- package/src/components/tabs/state.ts +186 -0
- package/src/components/tabs/tab-api.ts +258 -0
- package/src/components/tabs/tab.ts +255 -0
- package/src/components/tabs/tabs.ts +50 -31
- package/src/components/tabs/types.ts +324 -128
- package/src/components/tabs/utils.ts +107 -0
- package/src/components/textfield/_styles.scss +0 -98
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/constants.ts +0 -14
- package/src/components/textfield/index.ts +2 -2
- package/src/components/textfield/textfield.ts +0 -2
- package/src/components/textfield/types.ts +1 -4
- package/src/core/compose/component.ts +1 -1
- package/src/core/compose/features/badge.ts +79 -0
- package/src/core/compose/features/index.ts +3 -1
- package/src/styles/abstract/_theme.scss +106 -2
- package/src/components/card/actions.ts +0 -48
- package/src/components/card/header.ts +0 -88
- package/src/components/card/media.ts +0 -52
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// src/components/tabs/tab-api.ts
|
|
2
|
+
import { TabComponent } from './types';
|
|
3
|
+
import { TAB_STATES, TAB_LAYOUT } from './constants';
|
|
4
|
+
import { BadgeComponent } from '../badge/types';
|
|
5
|
+
import createBadge from '../badge';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* API options for a Tab component
|
|
9
|
+
*/
|
|
10
|
+
interface ApiOptions {
|
|
11
|
+
/** The button component's disabled API */
|
|
12
|
+
disabled: {
|
|
13
|
+
enable: () => void;
|
|
14
|
+
disable: () => void;
|
|
15
|
+
isDisabled?: () => boolean;
|
|
16
|
+
};
|
|
17
|
+
/** The component's lifecycle API */
|
|
18
|
+
lifecycle: {
|
|
19
|
+
destroy: () => void;
|
|
20
|
+
};
|
|
21
|
+
/** The button component (optional) */
|
|
22
|
+
button?: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Component with required elements and methods
|
|
27
|
+
*/
|
|
28
|
+
interface ComponentWithElements {
|
|
29
|
+
/** The DOM element */
|
|
30
|
+
element: HTMLElement;
|
|
31
|
+
/** The button component (optional) */
|
|
32
|
+
button?: any;
|
|
33
|
+
/** The badge component (optional) */
|
|
34
|
+
badge?: BadgeComponent;
|
|
35
|
+
/** Class name helper */
|
|
36
|
+
getClass: (name: string) => string;
|
|
37
|
+
/** Component configuration */
|
|
38
|
+
config: Record<string, any>;
|
|
39
|
+
/** Event emitter (optional) */
|
|
40
|
+
emit?: (event: string, data: any) => any;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Enhances a tab component with API methods
|
|
45
|
+
* @param {ApiOptions} options - API configuration options
|
|
46
|
+
* @returns {Function} Higher-order function that adds API methods to component
|
|
47
|
+
*/
|
|
48
|
+
export const withTabAPI = ({ disabled, lifecycle, button }: ApiOptions) =>
|
|
49
|
+
(component: ComponentWithElements): TabComponent => {
|
|
50
|
+
// Use the button component as a delegate for some methods
|
|
51
|
+
const buttonComponent = button || component.button;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...component as any,
|
|
55
|
+
element: component.element,
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gets the tab's value
|
|
59
|
+
*/
|
|
60
|
+
getValue: () => {
|
|
61
|
+
if (buttonComponent && typeof buttonComponent.getValue === 'function') {
|
|
62
|
+
return buttonComponent.getValue();
|
|
63
|
+
}
|
|
64
|
+
const value = component.element.getAttribute('data-value');
|
|
65
|
+
return value !== null ? value : '';
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets the tab's value
|
|
70
|
+
*/
|
|
71
|
+
setValue(value: string) {
|
|
72
|
+
const safeValue = value || '';
|
|
73
|
+
if (buttonComponent && typeof buttonComponent.setValue === 'function') {
|
|
74
|
+
buttonComponent.setValue(safeValue);
|
|
75
|
+
} else {
|
|
76
|
+
component.element.setAttribute('data-value', safeValue);
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Activates the tab
|
|
83
|
+
*/
|
|
84
|
+
activate() {
|
|
85
|
+
component.element.classList.add(`${component.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
86
|
+
component.element.setAttribute('aria-selected', 'true');
|
|
87
|
+
return this;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deactivates the tab
|
|
92
|
+
*/
|
|
93
|
+
deactivate() {
|
|
94
|
+
component.element.classList.remove(`${component.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
95
|
+
component.element.setAttribute('aria-selected', 'false');
|
|
96
|
+
return this;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks if the tab is active
|
|
101
|
+
*/
|
|
102
|
+
isActive() {
|
|
103
|
+
return component.element.classList.contains(`${component.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Enables the tab
|
|
108
|
+
*/
|
|
109
|
+
enable() {
|
|
110
|
+
if (disabled && typeof disabled.enable === 'function') {
|
|
111
|
+
disabled.enable();
|
|
112
|
+
}
|
|
113
|
+
return this;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Disables the tab
|
|
118
|
+
*/
|
|
119
|
+
disable() {
|
|
120
|
+
if (disabled && typeof disabled.disable === 'function') {
|
|
121
|
+
disabled.disable();
|
|
122
|
+
}
|
|
123
|
+
return this;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sets the tab's text content
|
|
128
|
+
*/
|
|
129
|
+
setText(content: string) {
|
|
130
|
+
if (buttonComponent && typeof buttonComponent.setText === 'function') {
|
|
131
|
+
buttonComponent.setText(content);
|
|
132
|
+
this.updateLayoutStyle();
|
|
133
|
+
}
|
|
134
|
+
return this;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Gets the tab's text content
|
|
139
|
+
*/
|
|
140
|
+
getText() {
|
|
141
|
+
return buttonComponent && typeof buttonComponent.getText === 'function'
|
|
142
|
+
? buttonComponent.getText()
|
|
143
|
+
: '';
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Sets the tab's icon
|
|
148
|
+
*/
|
|
149
|
+
setIcon(icon: string) {
|
|
150
|
+
if (buttonComponent && typeof buttonComponent.setIcon === 'function') {
|
|
151
|
+
buttonComponent.setIcon(icon);
|
|
152
|
+
this.updateLayoutStyle();
|
|
153
|
+
}
|
|
154
|
+
return this;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gets the tab's icon
|
|
159
|
+
*/
|
|
160
|
+
getIcon() {
|
|
161
|
+
return buttonComponent && typeof buttonComponent.getIcon === 'function'
|
|
162
|
+
? buttonComponent.getIcon()
|
|
163
|
+
: '';
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sets the tab's badge
|
|
168
|
+
*/
|
|
169
|
+
setBadge(content: string | number) {
|
|
170
|
+
if (!component.badge) {
|
|
171
|
+
// Create badge on demand if it doesn't exist
|
|
172
|
+
const badgeConfig = {
|
|
173
|
+
content,
|
|
174
|
+
standalone: false,
|
|
175
|
+
target: component.element,
|
|
176
|
+
prefix: component.config.prefix
|
|
177
|
+
};
|
|
178
|
+
component.badge = createBadge(badgeConfig);
|
|
179
|
+
} else {
|
|
180
|
+
component.badge.setContent(content);
|
|
181
|
+
component.badge.show();
|
|
182
|
+
}
|
|
183
|
+
return this;
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets the tab's badge content
|
|
188
|
+
*/
|
|
189
|
+
getBadge() {
|
|
190
|
+
return component.badge ? component.badge.getContent() : '';
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Shows the tab's badge
|
|
195
|
+
*/
|
|
196
|
+
showBadge() {
|
|
197
|
+
if (component.badge) {
|
|
198
|
+
component.badge.show();
|
|
199
|
+
}
|
|
200
|
+
return this;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Hides the tab's badge
|
|
205
|
+
*/
|
|
206
|
+
hideBadge() {
|
|
207
|
+
if (component.badge) {
|
|
208
|
+
component.badge.hide();
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Gets the badge component
|
|
215
|
+
*/
|
|
216
|
+
getBadgeComponent() {
|
|
217
|
+
return component.badge;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Destroys the tab
|
|
222
|
+
*/
|
|
223
|
+
destroy() {
|
|
224
|
+
if (component.badge) {
|
|
225
|
+
component.badge.destroy();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (lifecycle && typeof lifecycle.destroy === 'function') {
|
|
229
|
+
lifecycle.destroy();
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Updates tab layout classes based on content
|
|
235
|
+
*/
|
|
236
|
+
updateLayoutStyle() {
|
|
237
|
+
const hasText = !!this.getText();
|
|
238
|
+
const hasIcon = !!this.getIcon();
|
|
239
|
+
let layoutClass = '';
|
|
240
|
+
|
|
241
|
+
if (hasText && hasIcon) {
|
|
242
|
+
layoutClass = TAB_LAYOUT.ICON_AND_TEXT;
|
|
243
|
+
} else if (hasIcon) {
|
|
244
|
+
layoutClass = TAB_LAYOUT.ICON_ONLY;
|
|
245
|
+
} else {
|
|
246
|
+
layoutClass = TAB_LAYOUT.TEXT_ONLY;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Remove all existing layout classes
|
|
250
|
+
Object.values(TAB_LAYOUT).forEach(layout => {
|
|
251
|
+
component.element.classList.remove(`${component.getClass('tab')}--${layout}`);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Add the appropriate layout class
|
|
255
|
+
component.element.classList.add(`${component.getClass('tab')}--${layoutClass}`);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// src/components/tabs/tab.ts
|
|
2
|
+
import { pipe } from '../../core/compose';
|
|
3
|
+
import { createBase } from '../../core/compose/component';
|
|
4
|
+
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
5
|
+
import { TabConfig, TabComponent } from './types';
|
|
6
|
+
import { TAB_STATES, TAB_LAYOUT } from './constants';
|
|
7
|
+
import { createTabConfig } from './config';
|
|
8
|
+
import createButton from '../button';
|
|
9
|
+
import createBadge from '../badge';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new Tab component following MD3 guidelines
|
|
13
|
+
* @param {TabConfig} config - Tab configuration object
|
|
14
|
+
* @returns {TabComponent} Tab component instance
|
|
15
|
+
*/
|
|
16
|
+
export const createTab = (config: TabConfig = {}): TabComponent => {
|
|
17
|
+
const baseConfig = createTabConfig(config);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Create base component with events and lifecycle
|
|
21
|
+
const baseComponent = pipe(
|
|
22
|
+
createBase,
|
|
23
|
+
withEvents(),
|
|
24
|
+
withLifecycle()
|
|
25
|
+
)(baseConfig);
|
|
26
|
+
|
|
27
|
+
// Create a button for the tab
|
|
28
|
+
const button = createButton({
|
|
29
|
+
text: baseConfig.text,
|
|
30
|
+
icon: baseConfig.icon,
|
|
31
|
+
iconSize: baseConfig.iconSize,
|
|
32
|
+
disabled: baseConfig.disabled,
|
|
33
|
+
ripple: baseConfig.ripple !== false, // Enable ripple by default
|
|
34
|
+
rippleConfig: {
|
|
35
|
+
duration: 400,
|
|
36
|
+
timing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
37
|
+
opacity: ['0.2', '0'],
|
|
38
|
+
...(baseConfig.rippleConfig || {})
|
|
39
|
+
},
|
|
40
|
+
value: baseConfig.value,
|
|
41
|
+
prefix: baseConfig.prefix,
|
|
42
|
+
variant: 'text', // MD3 tabs use text button style
|
|
43
|
+
class: `${baseConfig.prefix}-tab`
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Use the button element as our element
|
|
47
|
+
baseComponent.element = button.element;
|
|
48
|
+
|
|
49
|
+
// Set up tab accessibility attributes
|
|
50
|
+
baseComponent.element.setAttribute('role', 'tab');
|
|
51
|
+
baseComponent.element.setAttribute('aria-selected',
|
|
52
|
+
baseConfig.state === TAB_STATES.ACTIVE ? 'true' : 'false');
|
|
53
|
+
|
|
54
|
+
// For better accessibility
|
|
55
|
+
if (baseConfig.value) {
|
|
56
|
+
baseComponent.element.setAttribute('id', `tab-${baseConfig.value}`);
|
|
57
|
+
baseComponent.element.setAttribute('aria-controls', `tabpanel-${baseConfig.value}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add active state if specified in config
|
|
61
|
+
if (baseConfig.state === TAB_STATES.ACTIVE) {
|
|
62
|
+
baseComponent.element.classList.add(`${baseComponent.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Forward button events to our component
|
|
66
|
+
button.on('click', (event) => {
|
|
67
|
+
if (baseComponent.emit) {
|
|
68
|
+
baseComponent.emit('click', event);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Create the tab component with enhanced API
|
|
73
|
+
const tab: TabComponent = {
|
|
74
|
+
...baseComponent,
|
|
75
|
+
button,
|
|
76
|
+
element: button.element,
|
|
77
|
+
|
|
78
|
+
// Badge support
|
|
79
|
+
badge: null,
|
|
80
|
+
|
|
81
|
+
// Tab state methods
|
|
82
|
+
getValue() {
|
|
83
|
+
return button.getValue();
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
setValue(value) {
|
|
87
|
+
const safeValue = value || '';
|
|
88
|
+
button.setValue(safeValue);
|
|
89
|
+
|
|
90
|
+
// Update accessibility attributes
|
|
91
|
+
this.element.setAttribute('id', `tab-${safeValue}`);
|
|
92
|
+
this.element.setAttribute('aria-controls', `tabpanel-${safeValue}`);
|
|
93
|
+
|
|
94
|
+
return this;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
activate() {
|
|
98
|
+
this.element.classList.add(`${this.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
99
|
+
this.element.setAttribute('aria-selected', 'true');
|
|
100
|
+
|
|
101
|
+
// Dispatch event for screen readers
|
|
102
|
+
const event = new CustomEvent('tab:activated', {
|
|
103
|
+
bubbles: true,
|
|
104
|
+
detail: { value: this.getValue() }
|
|
105
|
+
});
|
|
106
|
+
this.element.dispatchEvent(event);
|
|
107
|
+
|
|
108
|
+
return this;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
deactivate() {
|
|
112
|
+
this.element.classList.remove(`${this.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
113
|
+
this.element.setAttribute('aria-selected', 'false');
|
|
114
|
+
return this;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
isActive() {
|
|
118
|
+
return this.element.classList.contains(`${this.getClass('tab')}--${TAB_STATES.ACTIVE}`);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
enable() {
|
|
122
|
+
button.enable();
|
|
123
|
+
this.element.removeAttribute('aria-disabled');
|
|
124
|
+
return this;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
disable() {
|
|
128
|
+
button.disable();
|
|
129
|
+
this.element.setAttribute('aria-disabled', 'true');
|
|
130
|
+
return this;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
setText(content) {
|
|
134
|
+
button.setText(content);
|
|
135
|
+
this.updateLayoutStyle();
|
|
136
|
+
return this;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
getText() {
|
|
140
|
+
return button.getText();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
setIcon(icon) {
|
|
144
|
+
button.setIcon(icon);
|
|
145
|
+
this.updateLayoutStyle();
|
|
146
|
+
return this;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
getIcon() {
|
|
150
|
+
return button.getIcon();
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Badge methods
|
|
154
|
+
setBadge(content) {
|
|
155
|
+
if (!this.badge) {
|
|
156
|
+
const badgeConfig = {
|
|
157
|
+
content,
|
|
158
|
+
standalone: false,
|
|
159
|
+
target: this.element,
|
|
160
|
+
prefix: baseConfig.prefix,
|
|
161
|
+
...(baseConfig.badgeConfig || {})
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
this.badge = createBadge(badgeConfig);
|
|
165
|
+
} else {
|
|
166
|
+
this.badge.setContent(content);
|
|
167
|
+
this.badge.show();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add badge presence attribute for potential styling
|
|
171
|
+
this.element.setAttribute('data-has-badge', 'true');
|
|
172
|
+
|
|
173
|
+
return this;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
getBadge() {
|
|
177
|
+
return this.badge ? this.badge.getContent() : '';
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
showBadge() {
|
|
181
|
+
if (this.badge) {
|
|
182
|
+
this.badge.show();
|
|
183
|
+
this.element.setAttribute('data-has-badge', 'true');
|
|
184
|
+
}
|
|
185
|
+
return this;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
hideBadge() {
|
|
189
|
+
if (this.badge) {
|
|
190
|
+
this.badge.hide();
|
|
191
|
+
this.element.setAttribute('data-has-badge', 'false');
|
|
192
|
+
}
|
|
193
|
+
return this;
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
getBadgeComponent() {
|
|
197
|
+
return this.badge;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
destroy() {
|
|
201
|
+
if (this.badge) {
|
|
202
|
+
this.badge.destroy();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (button.destroy) {
|
|
206
|
+
button.destroy();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
baseComponent.lifecycle.destroy();
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
updateLayoutStyle() {
|
|
213
|
+
const hasText = !!this.getText();
|
|
214
|
+
const hasIcon = !!this.getIcon();
|
|
215
|
+
let layoutClass = '';
|
|
216
|
+
|
|
217
|
+
if (hasText && hasIcon) {
|
|
218
|
+
layoutClass = TAB_LAYOUT.ICON_AND_TEXT;
|
|
219
|
+
} else if (hasIcon) {
|
|
220
|
+
layoutClass = TAB_LAYOUT.ICON_ONLY;
|
|
221
|
+
} else {
|
|
222
|
+
layoutClass = TAB_LAYOUT.TEXT_ONLY;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Remove all existing layout classes
|
|
226
|
+
Object.values(TAB_LAYOUT).forEach(layout => {
|
|
227
|
+
this.element.classList.remove(`${this.getClass('tab')}--${layout}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Add the appropriate layout class
|
|
231
|
+
this.element.classList.add(`${this.getClass('tab')}--${layoutClass}`);
|
|
232
|
+
|
|
233
|
+
// Set appropriate aria-label when icon-only
|
|
234
|
+
if (layoutClass === TAB_LAYOUT.ICON_ONLY && hasText) {
|
|
235
|
+
this.element.setAttribute('aria-label', this.getText());
|
|
236
|
+
} else {
|
|
237
|
+
this.element.removeAttribute('aria-label');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Add badge if specified in config
|
|
243
|
+
if (baseConfig.badge !== undefined) {
|
|
244
|
+
tab.setBadge(baseConfig.badge);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Initialize layout style based on content
|
|
248
|
+
tab.updateLayoutStyle();
|
|
249
|
+
|
|
250
|
+
return tab;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('Tab creation error:', error);
|
|
253
|
+
throw new Error(`Failed to create tab: ${(error as Error).message}`);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
@@ -1,48 +1,67 @@
|
|
|
1
1
|
// src/components/tabs/tabs.ts
|
|
2
|
-
import { PREFIX } from '../../core/config';
|
|
3
2
|
import { pipe } from '../../core/compose';
|
|
4
|
-
import { createBase
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
3
|
+
import { createBase } from '../../core/compose/component';
|
|
4
|
+
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
5
|
+
import { withAPI, getApiConfig } from './api';
|
|
6
|
+
import {
|
|
7
|
+
withTabsManagement,
|
|
8
|
+
withScrollable,
|
|
9
|
+
withDivider,
|
|
10
|
+
withIndicator
|
|
11
|
+
} from './features';
|
|
12
|
+
import { createTabsConfig, getTabsElementConfig } from './config';
|
|
13
|
+
import { TabsConfig, TabsComponent } from './types';
|
|
14
|
+
import { addTabStateStyles } from './state';
|
|
15
|
+
import { setupKeyboardNavigation } from './utils';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Creates a new Tabs component
|
|
18
|
+
* Creates a new Tabs component following MD3 guidelines
|
|
18
19
|
* @param {TabsConfig} config - Tabs configuration object
|
|
19
20
|
* @returns {TabsComponent} Tabs component instance
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Create basic tabs with three items
|
|
24
|
+
* const tabs = createTabs({
|
|
25
|
+
* tabs: [
|
|
26
|
+
* { text: 'Home', value: 'home', state: 'active' },
|
|
27
|
+
* { text: 'Products', value: 'products' },
|
|
28
|
+
* { text: 'About', value: 'about' }
|
|
29
|
+
* ]
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Add tabs to DOM
|
|
33
|
+
* document.body.appendChild(tabs.element);
|
|
34
|
+
*
|
|
35
|
+
* // Listen for tab changes
|
|
36
|
+
* tabs.on('change', (e) => {
|
|
37
|
+
* console.log(`Active tab: ${e.value}`);
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
20
40
|
*/
|
|
21
|
-
const createTabs = (config: TabsConfig = {}) => {
|
|
22
|
-
const baseConfig =
|
|
41
|
+
const createTabs = (config: TabsConfig = {}): TabsComponent => {
|
|
42
|
+
const baseConfig = createTabsConfig(config);
|
|
43
|
+
|
|
44
|
+
// Add ripple styles for state transitions
|
|
45
|
+
addTabStateStyles();
|
|
23
46
|
|
|
24
47
|
try {
|
|
25
|
-
|
|
48
|
+
// Build the tabs component with all features
|
|
49
|
+
const component = pipe(
|
|
26
50
|
createBase,
|
|
27
51
|
withEvents(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
getTabsElementConfig(baseConfig),
|
|
53
|
+
withScrollable(baseConfig),
|
|
54
|
+
withTabsManagement(baseConfig),
|
|
55
|
+
withDivider(baseConfig),
|
|
56
|
+
withIndicator(baseConfig), // Add indicator feature
|
|
31
57
|
withLifecycle(),
|
|
32
58
|
comp => withAPI(getApiConfig(comp))(comp)
|
|
33
59
|
)(baseConfig);
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Set active tab if specified
|
|
41
|
-
if (baseConfig.activeIndex !== undefined) {
|
|
42
|
-
tabs.setActiveTab(baseConfig.activeIndex);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return tabs;
|
|
60
|
+
|
|
61
|
+
// Set up keyboard navigation
|
|
62
|
+
setupKeyboardNavigation(component);
|
|
63
|
+
|
|
64
|
+
return component;
|
|
46
65
|
} catch (error) {
|
|
47
66
|
console.error('Tabs creation error:', error);
|
|
48
67
|
throw new Error(`Failed to create tabs: ${(error as Error).message}`);
|