mtrl 0.2.2 → 0.2.4
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/.typedocignore +11 -0
- package/DOCS.md +153 -0
- package/index.ts +18 -3
- package/package.json +7 -2
- package/src/components/badge/_styles.scss +174 -0
- package/src/components/badge/api.ts +292 -0
- package/src/components/badge/badge.ts +52 -0
- package/src/components/badge/config.ts +68 -0
- package/src/components/badge/constants.ts +30 -0
- package/src/components/badge/features.ts +185 -0
- package/src/components/badge/index.ts +4 -0
- package/src/components/badge/types.ts +105 -0
- package/src/components/button/types.ts +174 -29
- package/src/components/carousel/_styles.scss +645 -0
- package/src/components/carousel/api.ts +147 -0
- package/src/components/carousel/carousel.ts +178 -0
- package/src/components/carousel/config.ts +91 -0
- package/src/components/carousel/constants.ts +95 -0
- package/src/components/carousel/features/drag.ts +388 -0
- package/src/components/carousel/features/index.ts +8 -0
- package/src/components/carousel/features/slides.ts +682 -0
- package/src/components/carousel/index.ts +38 -0
- package/src/components/carousel/types.ts +327 -0
- package/src/components/dialog/_styles.scss +213 -0
- package/src/components/dialog/api.ts +283 -0
- package/src/components/dialog/config.ts +113 -0
- package/src/components/dialog/constants.ts +32 -0
- package/src/components/dialog/dialog.ts +56 -0
- package/src/components/dialog/features.ts +713 -0
- package/src/components/dialog/index.ts +15 -0
- package/src/components/dialog/types.ts +221 -0
- package/src/components/progress/_styles.scss +13 -1
- package/src/components/progress/api.ts +2 -2
- package/src/components/progress/progress.ts +2 -2
- package/src/components/progress/types.ts +3 -0
- package/src/components/radios/_styles.scss +232 -0
- package/src/components/radios/api.ts +100 -0
- package/src/components/radios/config.ts +60 -0
- package/src/components/radios/constants.ts +28 -0
- package/src/components/radios/index.ts +4 -0
- package/src/components/radios/radio.ts +269 -0
- package/src/components/radios/radios.ts +42 -0
- package/src/components/radios/types.ts +232 -0
- package/src/components/sheet/_styles.scss +236 -0
- package/src/components/sheet/api.ts +96 -0
- package/src/components/sheet/config.ts +66 -0
- package/src/components/sheet/constants.ts +20 -0
- package/src/components/sheet/features/content.ts +51 -0
- package/src/components/sheet/features/gestures.ts +177 -0
- package/src/components/sheet/features/index.ts +6 -0
- package/src/components/sheet/features/position.ts +42 -0
- package/src/components/sheet/features/state.ts +116 -0
- package/src/components/sheet/features/title.ts +86 -0
- package/src/components/sheet/index.ts +4 -0
- package/src/components/sheet/sheet.ts +57 -0
- package/src/components/sheet/types.ts +266 -0
- package/src/components/slider/_styles.scss +518 -0
- package/src/components/slider/api.ts +336 -0
- package/src/components/slider/config.ts +145 -0
- package/src/components/slider/constants.ts +28 -0
- package/src/components/slider/features/appearance.ts +140 -0
- package/src/components/slider/features/disabled.ts +43 -0
- package/src/components/slider/features/events.ts +164 -0
- package/src/components/slider/features/index.ts +5 -0
- package/src/components/slider/features/interactions.ts +256 -0
- package/src/components/slider/features/keyboard.ts +114 -0
- package/src/components/slider/features/slider.ts +336 -0
- package/src/components/slider/features/structure.ts +264 -0
- package/src/components/slider/features/ui.ts +518 -0
- package/src/components/slider/index.ts +9 -0
- package/src/components/slider/slider.ts +58 -0
- package/src/components/slider/types.ts +166 -0
- package/src/components/tabs/_styles.scss +224 -0
- package/src/components/tabs/api.ts +443 -0
- package/src/components/tabs/config.ts +80 -0
- package/src/components/tabs/constants.ts +12 -0
- package/src/components/tabs/index.ts +4 -0
- package/src/components/tabs/tabs.ts +52 -0
- package/src/components/tabs/types.ts +247 -0
- package/src/components/textfield/_styles.scss +97 -4
- package/src/components/tooltip/_styles.scss +241 -0
- package/src/components/tooltip/api.ts +411 -0
- package/src/components/tooltip/config.ts +78 -0
- package/src/components/tooltip/constants.ts +27 -0
- package/src/components/tooltip/index.ts +4 -0
- package/src/components/tooltip/tooltip.ts +60 -0
- package/src/components/tooltip/types.ts +178 -0
- package/src/core/build/_ripple.scss +79 -0
- package/src/core/build/constants.ts +48 -0
- package/src/core/build/icon.ts +137 -0
- package/src/core/build/ripple.ts +216 -0
- package/src/core/build/text.ts +91 -0
- package/src/index.ts +9 -1
- package/src/styles/abstract/_variables.scss +24 -12
- package/tsconfig.json +22 -0
- package/typedoc.json +28 -0
- package/typedoc.simple.json +14 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// src/components/tabs/api.ts
|
|
2
|
+
import { TabsComponent, TabItem, TabChangeEventData } from './types';
|
|
3
|
+
import { ANIMATION_DURATION } from './constants';
|
|
4
|
+
|
|
5
|
+
interface ApiOptions {
|
|
6
|
+
disabled: {
|
|
7
|
+
enable: () => void;
|
|
8
|
+
disable: () => void;
|
|
9
|
+
isDisabled: () => boolean;
|
|
10
|
+
};
|
|
11
|
+
lifecycle: {
|
|
12
|
+
destroy: () => void;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ComponentWithElements {
|
|
17
|
+
element: HTMLElement;
|
|
18
|
+
getClass: (name: string) => string;
|
|
19
|
+
events: {
|
|
20
|
+
emit: (name: string, data?: any) => void;
|
|
21
|
+
on: (name: string, handler: Function) => any;
|
|
22
|
+
off: (name: string, handler: Function) => any;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates DOM elements for the tabs component
|
|
28
|
+
* @param component - Base component with element and class getter
|
|
29
|
+
* @returns Component with tabs-specific elements
|
|
30
|
+
*/
|
|
31
|
+
const setupElements = (component: ComponentWithElements) => {
|
|
32
|
+
const baseClass = component.getClass('tabs');
|
|
33
|
+
|
|
34
|
+
// Create tabs list container
|
|
35
|
+
const tabsListElement = document.createElement('div');
|
|
36
|
+
tabsListElement.className = `${baseClass}__list`;
|
|
37
|
+
tabsListElement.setAttribute('role', 'none');
|
|
38
|
+
|
|
39
|
+
// Create tabs indicator
|
|
40
|
+
const indicatorElement = document.createElement('span');
|
|
41
|
+
indicatorElement.className = `${baseClass}__indicator`;
|
|
42
|
+
|
|
43
|
+
// Append elements to container
|
|
44
|
+
component.element.appendChild(tabsListElement);
|
|
45
|
+
component.element.appendChild(indicatorElement);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...component,
|
|
49
|
+
tabsListElement,
|
|
50
|
+
indicatorElement
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enhances a tabs component with API methods
|
|
56
|
+
* @param {ApiOptions} options - API configuration options
|
|
57
|
+
* @returns {Function} Higher-order function that adds API methods to component
|
|
58
|
+
* @internal This is an internal utility for the Tabs component
|
|
59
|
+
*/
|
|
60
|
+
export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
|
|
61
|
+
(component: ReturnType<typeof setupElements>): TabsComponent => {
|
|
62
|
+
// Set up internal state
|
|
63
|
+
let items: TabItem[] = [];
|
|
64
|
+
let activeIndex = -1;
|
|
65
|
+
|
|
66
|
+
// Set up the component with DOM elements
|
|
67
|
+
const enhancedComponent = setupElements(component);
|
|
68
|
+
|
|
69
|
+
// Function to create a tab element
|
|
70
|
+
const createTabElement = (item: TabItem, index: number) => {
|
|
71
|
+
const baseClass = component.getClass('tabs');
|
|
72
|
+
const tabElement = document.createElement('button');
|
|
73
|
+
|
|
74
|
+
tabElement.className = `${baseClass}__tab`;
|
|
75
|
+
tabElement.setAttribute('role', 'tab');
|
|
76
|
+
tabElement.setAttribute('type', 'button');
|
|
77
|
+
tabElement.setAttribute('data-tab-id', item.id);
|
|
78
|
+
tabElement.setAttribute('data-tab-index', index.toString());
|
|
79
|
+
tabElement.setAttribute('aria-selected', 'false');
|
|
80
|
+
|
|
81
|
+
if (item.disabled) {
|
|
82
|
+
tabElement.disabled = true;
|
|
83
|
+
tabElement.setAttribute('aria-disabled', 'true');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create content container
|
|
87
|
+
const contentElement = document.createElement('div');
|
|
88
|
+
contentElement.className = `${baseClass}__tab-content`;
|
|
89
|
+
|
|
90
|
+
// Add icon if provided
|
|
91
|
+
if (item.icon) {
|
|
92
|
+
const iconElement = document.createElement('div');
|
|
93
|
+
iconElement.className = `${baseClass}__tab-icon`;
|
|
94
|
+
iconElement.innerHTML = item.icon;
|
|
95
|
+
contentElement.appendChild(iconElement);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add label
|
|
99
|
+
const labelElement = document.createElement('div');
|
|
100
|
+
labelElement.className = `${baseClass}__tab-label`;
|
|
101
|
+
labelElement.textContent = item.label;
|
|
102
|
+
contentElement.appendChild(labelElement);
|
|
103
|
+
|
|
104
|
+
tabElement.appendChild(contentElement);
|
|
105
|
+
|
|
106
|
+
// Add click event
|
|
107
|
+
tabElement.addEventListener('click', () => {
|
|
108
|
+
if (!tabElement.disabled && !disabled.isDisabled()) {
|
|
109
|
+
const clickedIndex = parseInt(tabElement.getAttribute('data-tab-index') || '0', 10);
|
|
110
|
+
api.setActiveTab(clickedIndex);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return tabElement;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Function to update the indicator position
|
|
118
|
+
const updateIndicator = (animate = true) => {
|
|
119
|
+
if (activeIndex < 0 || !items.length) {
|
|
120
|
+
// Hide indicator if no active tab
|
|
121
|
+
enhancedComponent.indicatorElement.style.transform = 'translateX(-100%)';
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Find the active tab element
|
|
126
|
+
const tabElement = enhancedComponent.tabsListElement.querySelector(
|
|
127
|
+
`[data-tab-index="${activeIndex}"]`
|
|
128
|
+
) as HTMLElement;
|
|
129
|
+
|
|
130
|
+
if (!tabElement) return;
|
|
131
|
+
|
|
132
|
+
// Calculate position
|
|
133
|
+
const tabRect = tabElement.getBoundingClientRect();
|
|
134
|
+
const listRect = enhancedComponent.tabsListElement.getBoundingClientRect();
|
|
135
|
+
|
|
136
|
+
const left = tabElement.offsetLeft;
|
|
137
|
+
const width = tabRect.width;
|
|
138
|
+
|
|
139
|
+
// Update indicator style
|
|
140
|
+
enhancedComponent.indicatorElement.style.transition = animate ?
|
|
141
|
+
`transform ${ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)` : 'none';
|
|
142
|
+
enhancedComponent.indicatorElement.style.transform = `translateX(${left}px)`;
|
|
143
|
+
enhancedComponent.indicatorElement.style.width = `${width}px`;
|
|
144
|
+
|
|
145
|
+
// Scroll into view if needed
|
|
146
|
+
if (enhancedComponent.element.classList.contains(`${component.getClass('tabs')}--scrollable`)) {
|
|
147
|
+
const scrollLeft = enhancedComponent.tabsListElement.scrollLeft;
|
|
148
|
+
const listWidth = listRect.width;
|
|
149
|
+
|
|
150
|
+
if (left < scrollLeft) {
|
|
151
|
+
enhancedComponent.tabsListElement.scrollTo({
|
|
152
|
+
left: left,
|
|
153
|
+
behavior: animate ? 'smooth' : 'auto'
|
|
154
|
+
});
|
|
155
|
+
} else if (left + width > scrollLeft + listWidth) {
|
|
156
|
+
enhancedComponent.tabsListElement.scrollTo({
|
|
157
|
+
left: left + width - listWidth,
|
|
158
|
+
behavior: animate ? 'smooth' : 'auto'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Function to update tab elements' states
|
|
165
|
+
const updateTabStates = () => {
|
|
166
|
+
// Update aria-selected for all tabs
|
|
167
|
+
const tabElements = enhancedComponent.tabsListElement.querySelectorAll('[role="tab"]');
|
|
168
|
+
|
|
169
|
+
tabElements.forEach((tab: Element) => {
|
|
170
|
+
const index = parseInt(tab.getAttribute('data-tab-index') || '-1', 10);
|
|
171
|
+
tab.setAttribute('aria-selected', index === activeIndex ? 'true' : 'false');
|
|
172
|
+
|
|
173
|
+
if (index === activeIndex) {
|
|
174
|
+
tab.classList.add(`${component.getClass('tabs')}__tab--active`);
|
|
175
|
+
} else {
|
|
176
|
+
tab.classList.remove(`${component.getClass('tabs')}__tab--active`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Create the API object
|
|
182
|
+
const api: TabsComponent = {
|
|
183
|
+
...enhancedComponent as any,
|
|
184
|
+
element: enhancedComponent.element,
|
|
185
|
+
tabsListElement: enhancedComponent.tabsListElement,
|
|
186
|
+
indicatorElement: enhancedComponent.indicatorElement,
|
|
187
|
+
disabled,
|
|
188
|
+
lifecycle,
|
|
189
|
+
|
|
190
|
+
getClass: component.getClass,
|
|
191
|
+
|
|
192
|
+
enable() {
|
|
193
|
+
disabled.enable();
|
|
194
|
+
this.element.removeAttribute('aria-disabled');
|
|
195
|
+
return this;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
disable() {
|
|
199
|
+
disabled.disable();
|
|
200
|
+
this.element.setAttribute('aria-disabled', 'true');
|
|
201
|
+
return this;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
getItems() {
|
|
205
|
+
return [...items];
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
setItems(newItems) {
|
|
209
|
+
// Clear existing tabs
|
|
210
|
+
while (this.tabsListElement.firstChild) {
|
|
211
|
+
this.tabsListElement.removeChild(this.tabsListElement.firstChild);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Store items and create elements
|
|
215
|
+
items = [...newItems];
|
|
216
|
+
|
|
217
|
+
// Create tab elements
|
|
218
|
+
items.forEach((item, index) => {
|
|
219
|
+
const tabElement = createTabElement(item, index);
|
|
220
|
+
this.tabsListElement.appendChild(tabElement);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Reset active tab if needed
|
|
224
|
+
if (activeIndex >= items.length) {
|
|
225
|
+
activeIndex = items.length > 0 ? 0 : -1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Update UI
|
|
229
|
+
updateTabStates();
|
|
230
|
+
updateIndicator(false);
|
|
231
|
+
|
|
232
|
+
return this;
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
addTab(item, index) {
|
|
236
|
+
const newItems = [...items];
|
|
237
|
+
|
|
238
|
+
if (index !== undefined && index >= 0 && index <= items.length) {
|
|
239
|
+
// Insert at specific position
|
|
240
|
+
newItems.splice(index, 0, item);
|
|
241
|
+
|
|
242
|
+
// Adjust active index if needed
|
|
243
|
+
if (activeIndex >= index) {
|
|
244
|
+
activeIndex++;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Append to end
|
|
248
|
+
newItems.push(item);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.setItems(newItems);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
removeTab(idOrIndex) {
|
|
255
|
+
if (items.length === 0) return this;
|
|
256
|
+
|
|
257
|
+
let index = -1;
|
|
258
|
+
|
|
259
|
+
if (typeof idOrIndex === 'number') {
|
|
260
|
+
index = idOrIndex;
|
|
261
|
+
} else {
|
|
262
|
+
// Find by ID
|
|
263
|
+
index = items.findIndex(item => item.id === idOrIndex);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (index < 0 || index >= items.length) return this;
|
|
267
|
+
|
|
268
|
+
const newItems = items.filter((_, i) => i !== index);
|
|
269
|
+
|
|
270
|
+
// Handle active index adjustment
|
|
271
|
+
let newActiveIndex = activeIndex;
|
|
272
|
+
|
|
273
|
+
if (activeIndex === index) {
|
|
274
|
+
// Removed active tab, select a new one
|
|
275
|
+
if (newItems.length > 0) {
|
|
276
|
+
newActiveIndex = Math.min(activeIndex, newItems.length - 1);
|
|
277
|
+
} else {
|
|
278
|
+
newActiveIndex = -1;
|
|
279
|
+
}
|
|
280
|
+
} else if (activeIndex > index) {
|
|
281
|
+
// Active tab is after removed tab, adjust index
|
|
282
|
+
newActiveIndex--;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update items
|
|
286
|
+
items = newItems;
|
|
287
|
+
|
|
288
|
+
// Rebuild the tabs
|
|
289
|
+
this.setItems(newItems);
|
|
290
|
+
|
|
291
|
+
// Set the correct active tab
|
|
292
|
+
if (newActiveIndex >= 0) {
|
|
293
|
+
this.setActiveTab(newActiveIndex);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return this;
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
getActiveTab() {
|
|
300
|
+
return activeIndex >= 0 && activeIndex < items.length ? items[activeIndex] : null;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
getActiveIndex() {
|
|
304
|
+
return activeIndex;
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
setActiveTab(index) {
|
|
308
|
+
if (
|
|
309
|
+
index < 0 ||
|
|
310
|
+
index >= items.length ||
|
|
311
|
+
items[index].disabled ||
|
|
312
|
+
disabled.isDisabled() ||
|
|
313
|
+
index === activeIndex
|
|
314
|
+
) {
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const previousIndex = activeIndex;
|
|
319
|
+
const previousTab = this.getActiveTab();
|
|
320
|
+
|
|
321
|
+
activeIndex = index;
|
|
322
|
+
const currentTab = items[index];
|
|
323
|
+
|
|
324
|
+
// Update DOM
|
|
325
|
+
updateTabStates();
|
|
326
|
+
updateIndicator();
|
|
327
|
+
|
|
328
|
+
// Emit change event
|
|
329
|
+
component.events.emit('change', {
|
|
330
|
+
index,
|
|
331
|
+
tab: currentTab,
|
|
332
|
+
previousIndex,
|
|
333
|
+
previousTab
|
|
334
|
+
} as TabChangeEventData);
|
|
335
|
+
|
|
336
|
+
return this;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
setActiveTabById(id) {
|
|
340
|
+
const index = items.findIndex(item => item.id === id);
|
|
341
|
+
if (index >= 0) {
|
|
342
|
+
this.setActiveTab(index);
|
|
343
|
+
}
|
|
344
|
+
return this;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
destroy() {
|
|
348
|
+
// Clean up event listeners
|
|
349
|
+
const tabElements = enhancedComponent.tabsListElement.querySelectorAll('[role="tab"]');
|
|
350
|
+
tabElements.forEach(tab => {
|
|
351
|
+
tab.replaceWith(tab.cloneNode(true));
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Call lifecycle destroy
|
|
355
|
+
lifecycle.destroy();
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
on(event, handler) {
|
|
359
|
+
component.events.on(event, handler);
|
|
360
|
+
return this;
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
off(event, handler) {
|
|
364
|
+
component.events.off(event, handler);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Set up keyboard navigation
|
|
370
|
+
enhancedComponent.element.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
371
|
+
if (disabled.isDisabled() || !items.length) return;
|
|
372
|
+
|
|
373
|
+
const key = e.key;
|
|
374
|
+
let newIndex = activeIndex;
|
|
375
|
+
|
|
376
|
+
switch (key) {
|
|
377
|
+
case 'ArrowRight':
|
|
378
|
+
case 'ArrowDown':
|
|
379
|
+
// Move to next non-disabled tab
|
|
380
|
+
for (let i = 1; i <= items.length; i++) {
|
|
381
|
+
const index = (activeIndex + i) % items.length;
|
|
382
|
+
if (!items[index].disabled) {
|
|
383
|
+
newIndex = index;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
|
|
389
|
+
case 'ArrowLeft':
|
|
390
|
+
case 'ArrowUp':
|
|
391
|
+
// Move to previous non-disabled tab
|
|
392
|
+
for (let i = 1; i <= items.length; i++) {
|
|
393
|
+
const index = (activeIndex - i + items.length) % items.length;
|
|
394
|
+
if (!items[index].disabled) {
|
|
395
|
+
newIndex = index;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
|
|
401
|
+
case 'Home':
|
|
402
|
+
// Move to first non-disabled tab
|
|
403
|
+
for (let i = 0; i < items.length; i++) {
|
|
404
|
+
if (!items[i].disabled) {
|
|
405
|
+
newIndex = i;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'End':
|
|
412
|
+
// Move to last non-disabled tab
|
|
413
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
414
|
+
if (!items[i].disabled) {
|
|
415
|
+
newIndex = i;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
|
|
421
|
+
default:
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (newIndex !== activeIndex) {
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
api.setActiveTab(newIndex);
|
|
428
|
+
|
|
429
|
+
// Focus the tab
|
|
430
|
+
const tabElement = enhancedComponent.tabsListElement.querySelector(
|
|
431
|
+
`[data-tab-index="${newIndex}"]`
|
|
432
|
+
) as HTMLElement;
|
|
433
|
+
|
|
434
|
+
if (tabElement) {
|
|
435
|
+
tabElement.focus();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return api;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export default withAPI;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/components/tabs/config.ts
|
|
2
|
+
import {
|
|
3
|
+
createComponentConfig,
|
|
4
|
+
createElementConfig
|
|
5
|
+
} from '../../core/config/component-config';
|
|
6
|
+
import { TabsConfig } from './types';
|
|
7
|
+
import { TABS_VARIANTS } from './constants';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default configuration for the Tabs component
|
|
11
|
+
*/
|
|
12
|
+
export const defaultConfig: TabsConfig = {
|
|
13
|
+
variant: TABS_VARIANTS.PRIMARY,
|
|
14
|
+
showIndicator: true,
|
|
15
|
+
animated: true,
|
|
16
|
+
scrollable: true,
|
|
17
|
+
activeIndex: 0
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates the base configuration for Tabs component
|
|
22
|
+
* @param {TabsConfig} config - User provided configuration
|
|
23
|
+
* @returns {TabsConfig} Complete configuration with defaults applied
|
|
24
|
+
*/
|
|
25
|
+
export const createBaseConfig = (config: TabsConfig = {}): TabsConfig =>
|
|
26
|
+
createComponentConfig(defaultConfig, config, 'tabs') as TabsConfig;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generates element configuration for the Tabs component
|
|
30
|
+
* @param {TabsConfig} config - Tabs configuration
|
|
31
|
+
* @returns {Object} Element configuration object for withElement
|
|
32
|
+
*/
|
|
33
|
+
export const getElementConfig = (config: TabsConfig) => {
|
|
34
|
+
// Create the attributes object
|
|
35
|
+
const attrs: Record<string, any> = {
|
|
36
|
+
role: 'tablist'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Only add disabled attribute if it's explicitly true
|
|
40
|
+
if (config.disabled === true) {
|
|
41
|
+
attrs['aria-disabled'] = 'true';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const extraClasses: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (config.scrollable) {
|
|
47
|
+
extraClasses.push('--scrollable');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (config.animated) {
|
|
51
|
+
extraClasses.push('--animated');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return createElementConfig(config, {
|
|
55
|
+
tag: 'div',
|
|
56
|
+
attrs,
|
|
57
|
+
className: config.class,
|
|
58
|
+
extraClasses,
|
|
59
|
+
forwardEvents: {
|
|
60
|
+
keydown: true
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates API configuration for the Tabs component
|
|
67
|
+
* @param {Object} comp - Component with disabled and lifecycle features
|
|
68
|
+
* @returns {Object} API configuration object
|
|
69
|
+
*/
|
|
70
|
+
export const getApiConfig = (comp) => ({
|
|
71
|
+
disabled: {
|
|
72
|
+
enable: () => comp.disabled.enable(),
|
|
73
|
+
disable: () => comp.disabled.disable()
|
|
74
|
+
},
|
|
75
|
+
lifecycle: {
|
|
76
|
+
destroy: () => comp.lifecycle.destroy()
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export default defaultConfig;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/components/tabs/constants.ts
|
|
2
|
+
|
|
3
|
+
export const TABS_VARIANTS = {
|
|
4
|
+
PRIMARY: 'primary',
|
|
5
|
+
SECONDARY: 'secondary',
|
|
6
|
+
SEGMENTED: 'segmented',
|
|
7
|
+
NEUTRAL: 'neutral'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ANIMATION_DURATION = 250
|
|
11
|
+
export const DEFAULT_TAB_MIN_WIDTH = 90
|
|
12
|
+
export const DEFAULT_TAB_MAX_WIDTH = 360
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/components/tabs/tabs.ts
|
|
2
|
+
import { PREFIX } from '../../core/config';
|
|
3
|
+
import { pipe } from '../../core/compose';
|
|
4
|
+
import { createBase, withElement } from '../../core/compose/component';
|
|
5
|
+
import {
|
|
6
|
+
withEvents,
|
|
7
|
+
withVariant,
|
|
8
|
+
withDisabled,
|
|
9
|
+
withLifecycle
|
|
10
|
+
} from '../../core/compose/features';
|
|
11
|
+
import { withAPI } from './api';
|
|
12
|
+
import { TabsConfig } from './types';
|
|
13
|
+
import { TABS_VARIANTS } from './constants';
|
|
14
|
+
import { createBaseConfig, getElementConfig, getApiConfig } from './config';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new Tabs component
|
|
18
|
+
* @param {TabsConfig} config - Tabs configuration object
|
|
19
|
+
* @returns {TabsComponent} Tabs component instance
|
|
20
|
+
*/
|
|
21
|
+
const createTabs = (config: TabsConfig = {}) => {
|
|
22
|
+
const baseConfig = createBaseConfig(config);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const tabs = pipe(
|
|
26
|
+
createBase,
|
|
27
|
+
withEvents(),
|
|
28
|
+
withElement(getElementConfig(baseConfig)),
|
|
29
|
+
withVariant(baseConfig),
|
|
30
|
+
withDisabled(baseConfig),
|
|
31
|
+
withLifecycle(),
|
|
32
|
+
comp => withAPI(getApiConfig(comp))(comp)
|
|
33
|
+
)(baseConfig);
|
|
34
|
+
|
|
35
|
+
// Initialize tabs
|
|
36
|
+
if (baseConfig.items && baseConfig.items.length > 0) {
|
|
37
|
+
tabs.setItems(baseConfig.items);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set active tab if specified
|
|
41
|
+
if (baseConfig.activeIndex !== undefined) {
|
|
42
|
+
tabs.setActiveTab(baseConfig.activeIndex);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return tabs;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Tabs creation error:', error);
|
|
48
|
+
throw new Error(`Failed to create tabs: ${(error as Error).message}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default createTabs;
|