mtrl 0.2.5 → 0.2.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/index.ts +18 -0
- package/package.json +1 -1
- package/src/components/badge/_styles.scss +123 -115
- package/src/components/badge/api.ts +57 -59
- package/src/components/badge/badge.ts +16 -2
- package/src/components/badge/config.ts +65 -11
- package/src/components/badge/constants.ts +22 -12
- package/src/components/badge/features.ts +44 -40
- package/src/components/badge/types.ts +42 -30
- package/src/components/bottom-app-bar/_styles.scss +103 -0
- package/src/components/bottom-app-bar/bottom-app-bar.ts +196 -0
- package/src/components/bottom-app-bar/config.ts +73 -0
- package/src/components/bottom-app-bar/index.ts +11 -0
- package/src/components/bottom-app-bar/types.ts +108 -0
- package/src/components/button/_styles.scss +0 -66
- package/src/components/button/api.ts +5 -0
- package/src/components/button/button.ts +0 -2
- package/src/components/button/config.ts +5 -0
- package/src/components/button/constants.ts +0 -6
- package/src/components/button/index.ts +2 -2
- package/src/components/button/types.ts +7 -7
- package/src/components/card/_styles.scss +67 -25
- package/src/components/card/api.ts +54 -3
- package/src/components/card/card.ts +25 -6
- package/src/components/card/config.ts +189 -22
- package/src/components/card/constants.ts +20 -19
- package/src/components/card/content.ts +299 -2
- package/src/components/card/features.ts +158 -4
- package/src/components/card/index.ts +31 -9
- package/src/components/card/types.ts +166 -15
- package/src/components/checkbox/_styles.scss +0 -2
- 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/datepicker/_styles.scss +358 -0
- package/src/components/datepicker/api.ts +272 -0
- package/src/components/datepicker/config.ts +144 -0
- package/src/components/datepicker/constants.ts +98 -0
- package/src/components/datepicker/datepicker.ts +346 -0
- package/src/components/datepicker/index.ts +9 -0
- package/src/components/datepicker/render.ts +452 -0
- package/src/components/datepicker/types.ts +268 -0
- package/src/components/datepicker/utils.ts +290 -0
- package/src/components/dialog/_styles.scss +174 -128
- package/src/components/dialog/api.ts +48 -13
- package/src/components/dialog/config.ts +9 -5
- package/src/components/dialog/dialog.ts +6 -3
- package/src/components/dialog/features.ts +290 -130
- package/src/components/dialog/types.ts +7 -4
- package/src/components/divider/_styles.scss +57 -0
- package/src/components/divider/config.ts +81 -0
- package/src/components/divider/divider.ts +37 -0
- package/src/components/divider/features.ts +207 -0
- package/src/components/divider/index.ts +5 -0
- package/src/components/divider/types.ts +55 -0
- package/src/components/extended-fab/_styles.scss +267 -0
- package/src/components/extended-fab/api.ts +141 -0
- package/src/components/extended-fab/config.ts +108 -0
- package/src/components/extended-fab/constants.ts +36 -0
- package/src/components/extended-fab/extended-fab.ts +125 -0
- package/src/components/extended-fab/index.ts +4 -0
- package/src/components/extended-fab/types.ts +287 -0
- package/src/components/fab/_styles.scss +225 -0
- package/src/components/fab/api.ts +97 -0
- package/src/components/fab/config.ts +94 -0
- package/src/components/fab/constants.ts +41 -0
- package/src/components/fab/fab.ts +67 -0
- package/src/components/fab/index.ts +4 -0
- package/src/components/fab/types.ts +234 -0
- package/src/components/navigation/_styles.scss +1 -0
- package/src/components/navigation/api.ts +78 -50
- package/src/components/navigation/features/items.ts +280 -0
- package/src/components/navigation/nav-item.ts +72 -23
- package/src/components/navigation/navigation.ts +54 -2
- package/src/components/navigation/types.ts +210 -188
- 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/search/_styles.scss +306 -0
- package/src/components/search/api.ts +203 -0
- package/src/components/search/config.ts +87 -0
- package/src/components/search/constants.ts +21 -0
- package/src/components/search/features/index.ts +4 -0
- package/src/components/search/features/search.ts +718 -0
- package/src/components/search/features/states.ts +165 -0
- package/src/components/search/features/structure.ts +198 -0
- package/src/components/search/index.ts +10 -0
- package/src/components/search/search.ts +52 -0
- package/src/components/search/types.ts +163 -0
- package/src/components/segmented-button/_styles.scss +117 -0
- package/src/components/segmented-button/config.ts +67 -0
- package/src/components/segmented-button/constants.ts +42 -0
- package/src/components/segmented-button/index.ts +4 -0
- package/src/components/segmented-button/segment.ts +155 -0
- package/src/components/segmented-button/segmented-button.ts +250 -0
- package/src/components/segmented-button/types.ts +219 -0
- package/src/components/slider/_styles.scss +221 -168
- package/src/components/slider/accessibility.md +59 -0
- package/src/components/slider/api.ts +41 -120
- package/src/components/slider/config.ts +51 -49
- package/src/components/slider/features/handlers.ts +495 -0
- package/src/components/slider/features/index.ts +1 -2
- package/src/components/slider/features/slider.ts +66 -84
- package/src/components/slider/features/states.ts +195 -0
- package/src/components/slider/features/structure.ts +141 -184
- package/src/components/slider/features/ui.ts +150 -201
- package/src/components/slider/index.ts +2 -11
- package/src/components/slider/slider.ts +9 -12
- package/src/components/slider/types.ts +39 -24
- package/src/components/switch/_styles.scss +0 -2
- package/src/components/tabs/_styles.scss +346 -154
- 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 +403 -0
- package/src/components/tabs/index.ts +60 -3
- package/src/components/tabs/indicator.ts +285 -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 +332 -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/components/timepicker/README.md +277 -0
- package/src/components/timepicker/_styles.scss +451 -0
- package/src/components/timepicker/api.ts +632 -0
- package/src/components/timepicker/clockdial.ts +482 -0
- package/src/components/timepicker/config.ts +130 -0
- package/src/components/timepicker/constants.ts +138 -0
- package/src/components/timepicker/index.ts +8 -0
- package/src/components/timepicker/render.ts +613 -0
- package/src/components/timepicker/timepicker.ts +117 -0
- package/src/components/timepicker/types.ts +336 -0
- package/src/components/timepicker/utils.ts +241 -0
- package/src/components/top-app-bar/_styles.scss +225 -0
- package/src/components/top-app-bar/config.ts +83 -0
- package/src/components/top-app-bar/index.ts +11 -0
- package/src/components/top-app-bar/top-app-bar.ts +316 -0
- package/src/components/top-app-bar/types.ts +140 -0
- package/src/core/build/_ripple.scss +6 -6
- package/src/core/build/ripple.ts +72 -95
- package/src/core/compose/component.ts +1 -1
- package/src/core/compose/features/badge.ts +79 -0
- package/src/core/compose/features/icon.ts +3 -1
- package/src/core/compose/features/index.ts +3 -1
- package/src/core/compose/features/ripple.ts +4 -1
- package/src/core/compose/features/textlabel.ts +26 -2
- package/src/core/dom/create.ts +5 -0
- package/src/index.ts +9 -0
- package/src/styles/abstract/_theme.scss +115 -3
- package/src/styles/themes/_autumn.scss +21 -0
- package/src/styles/themes/_base-theme.scss +61 -0
- package/src/styles/themes/_baseline.scss +58 -0
- package/src/styles/themes/_bluekhaki.scss +125 -0
- package/src/styles/themes/_brownbeige.scss +125 -0
- package/src/styles/themes/_browngreen.scss +125 -0
- package/src/styles/themes/_forest.scss +6 -0
- package/src/styles/themes/_greenbeige.scss +125 -0
- package/src/styles/themes/_material.scss +125 -0
- package/src/styles/themes/_ocean.scss +6 -0
- package/src/styles/themes/_sageivory.scss +125 -0
- package/src/styles/themes/_spring.scss +6 -0
- package/src/styles/themes/_summer.scss +5 -0
- package/src/styles/themes/_sunset.scss +5 -0
- package/src/styles/themes/_tealcaramel.scss +125 -0
- package/src/styles/themes/_winter.scss +6 -0
- package/src/components/card/actions.ts +0 -48
- package/src/components/card/header.ts +0 -88
- package/src/components/card/media.ts +0 -52
- package/src/components/navigation/features/items.js +0 -192
- package/src/components/slider/features/appearance.ts +0 -94
- package/src/components/slider/features/disabled.ts +0 -43
- package/src/components/slider/features/events.ts +0 -164
- package/src/components/slider/features/interactions.ts +0 -261
- package/src/components/slider/features/keyboard.ts +0 -112
- package/src/core/collection/adapters/mongodb.js +0 -232
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/components/tabs/state.ts
|
|
2
|
+
import { TabComponent } from './types';
|
|
3
|
+
import { TabIndicator } from './indicator';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* State manager for MD3 tab states
|
|
7
|
+
* Handles proper state transitions between tabs
|
|
8
|
+
*/
|
|
9
|
+
export interface TabsStateManager {
|
|
10
|
+
/**
|
|
11
|
+
* Activates a tab and handles state transitions
|
|
12
|
+
* @param tab - The tab to activate
|
|
13
|
+
* @param immediate - Whether to skip animation
|
|
14
|
+
*/
|
|
15
|
+
activateTab: (tab: TabComponent, immediate?: boolean) => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gets the currently active tab
|
|
19
|
+
*/
|
|
20
|
+
getActiveTab: () => TabComponent | null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cleans up event listeners
|
|
24
|
+
*/
|
|
25
|
+
destroy: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for creating a tabs state manager
|
|
30
|
+
*/
|
|
31
|
+
export interface TabsStateOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Initial tabs to manage
|
|
34
|
+
*/
|
|
35
|
+
tabs: TabComponent[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional callback when active tab changes
|
|
39
|
+
*/
|
|
40
|
+
onChange?: (data: { tab: TabComponent; value: string }) => void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optional indicator component
|
|
44
|
+
*/
|
|
45
|
+
indicator?: TabIndicator;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a state manager for MD3 tabs
|
|
50
|
+
* @param options - State manager options
|
|
51
|
+
* @returns A tabs state manager instance
|
|
52
|
+
*/
|
|
53
|
+
export const createTabsState = (options: TabsStateOptions): TabsStateManager => {
|
|
54
|
+
const { tabs = [], onChange, indicator } = options;
|
|
55
|
+
let activeTab: TabComponent | null = null;
|
|
56
|
+
|
|
57
|
+
// Find initial active tab if any
|
|
58
|
+
const initialActiveTab = tabs.find(tab => tab.isActive());
|
|
59
|
+
if (initialActiveTab) {
|
|
60
|
+
activeTab = initialActiveTab;
|
|
61
|
+
|
|
62
|
+
// Position indicator at initial active tab
|
|
63
|
+
if (indicator) {
|
|
64
|
+
// Delay initial positioning to ensure DOM is ready
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
indicator.moveToTab(initialActiveTab, true);
|
|
67
|
+
}, 50);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Handles ripple effect on tab activation
|
|
73
|
+
* @param tab - The tab to add ripple to
|
|
74
|
+
*/
|
|
75
|
+
const addRippleEffect = (tab: TabComponent): void => {
|
|
76
|
+
if (!tab.element) return;
|
|
77
|
+
|
|
78
|
+
const ripple = tab.element.querySelector(`.${tab.getClass('tab')}-ripple`);
|
|
79
|
+
if (!ripple) return;
|
|
80
|
+
|
|
81
|
+
// Create a new ripple element
|
|
82
|
+
const rippleElement = document.createElement('span');
|
|
83
|
+
rippleElement.className = 'ripple';
|
|
84
|
+
|
|
85
|
+
// Position the ripple in the center
|
|
86
|
+
rippleElement.style.width = '100%';
|
|
87
|
+
rippleElement.style.height = '100%';
|
|
88
|
+
rippleElement.style.left = '0';
|
|
89
|
+
rippleElement.style.top = '0';
|
|
90
|
+
|
|
91
|
+
// Add animation
|
|
92
|
+
rippleElement.style.animation = 'ripple-effect 0.4s cubic-bezier(0.4, 0, 0.2, 1)';
|
|
93
|
+
|
|
94
|
+
// Add to DOM
|
|
95
|
+
ripple.appendChild(rippleElement);
|
|
96
|
+
|
|
97
|
+
// Remove after animation completes
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
rippleElement.remove();
|
|
100
|
+
}, 400);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Activates a tab with proper state transitions
|
|
105
|
+
*/
|
|
106
|
+
const activateTab = (tab: TabComponent, immediate = false): void => {
|
|
107
|
+
if (!tab || (activeTab === tab)) return;
|
|
108
|
+
|
|
109
|
+
// First deactivate the current active tab
|
|
110
|
+
if (activeTab) {
|
|
111
|
+
activeTab.deactivate();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Activate the new tab
|
|
115
|
+
tab.activate();
|
|
116
|
+
activeTab = tab;
|
|
117
|
+
|
|
118
|
+
// Move indicator to this tab
|
|
119
|
+
if (indicator) {
|
|
120
|
+
// Small delay to ensure DOM updates before indicator positioning
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
indicator.moveToTab(tab, immediate);
|
|
123
|
+
}, 10);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add ripple effect unless immediate mode is on
|
|
127
|
+
if (!immediate) {
|
|
128
|
+
addRippleEffect(tab);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Trigger change callback
|
|
132
|
+
if (onChange) {
|
|
133
|
+
onChange({
|
|
134
|
+
tab,
|
|
135
|
+
value: tab.getValue()
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets the currently active tab
|
|
142
|
+
*/
|
|
143
|
+
const getActiveTab = (): TabComponent | null => {
|
|
144
|
+
return activeTab;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cleans up resources
|
|
149
|
+
*/
|
|
150
|
+
const destroy = (): void => {
|
|
151
|
+
// Clean up any event listeners or timers
|
|
152
|
+
activeTab = null;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
activateTab,
|
|
157
|
+
getActiveTab,
|
|
158
|
+
destroy
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Adds animation styles for ripple effects
|
|
164
|
+
* This is separate from indicator animations
|
|
165
|
+
*/
|
|
166
|
+
export const addTabStateStyles = (): void => {
|
|
167
|
+
// Only add once
|
|
168
|
+
if (document.getElementById('tab-state-styles')) return;
|
|
169
|
+
|
|
170
|
+
const style = document.createElement('style');
|
|
171
|
+
style.id = 'tab-state-styles';
|
|
172
|
+
style.textContent = `
|
|
173
|
+
@keyframes ripple-effect {
|
|
174
|
+
0% {
|
|
175
|
+
transform: scale(0);
|
|
176
|
+
opacity: 0.2;
|
|
177
|
+
}
|
|
178
|
+
100% {
|
|
179
|
+
transform: scale(1);
|
|
180
|
+
opacity: 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
document.head.appendChild(style);
|
|
186
|
+
};
|
|
@@ -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
|
+
};
|