mtrl 0.2.5 → 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.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/components/badge/_styles.scss +9 -9
  3. package/src/components/button/_styles.scss +0 -56
  4. package/src/components/button/button.ts +0 -2
  5. package/src/components/button/constants.ts +0 -6
  6. package/src/components/button/index.ts +2 -2
  7. package/src/components/button/types.ts +1 -7
  8. package/src/components/card/_styles.scss +67 -25
  9. package/src/components/card/api.ts +54 -3
  10. package/src/components/card/card.ts +33 -2
  11. package/src/components/card/config.ts +143 -21
  12. package/src/components/card/constants.ts +20 -19
  13. package/src/components/card/content.ts +299 -2
  14. package/src/components/card/features.ts +155 -4
  15. package/src/components/card/index.ts +31 -9
  16. package/src/components/card/types.ts +138 -15
  17. package/src/components/chip/chip.ts +1 -9
  18. package/src/components/chip/constants.ts +0 -10
  19. package/src/components/chip/index.ts +1 -1
  20. package/src/components/chip/types.ts +1 -4
  21. package/src/components/progress/_styles.scss +0 -65
  22. package/src/components/progress/config.ts +1 -2
  23. package/src/components/progress/constants.ts +0 -14
  24. package/src/components/progress/index.ts +1 -1
  25. package/src/components/progress/progress.ts +1 -4
  26. package/src/components/progress/types.ts +1 -4
  27. package/src/components/radios/_styles.scss +0 -45
  28. package/src/components/radios/api.ts +85 -60
  29. package/src/components/radios/config.ts +1 -2
  30. package/src/components/radios/constants.ts +0 -9
  31. package/src/components/radios/index.ts +1 -1
  32. package/src/components/radios/radio.ts +34 -11
  33. package/src/components/radios/radios.ts +2 -1
  34. package/src/components/radios/types.ts +1 -7
  35. package/src/components/slider/_styles.scss +149 -155
  36. package/src/components/slider/accessibility.md +59 -0
  37. package/src/components/slider/config.ts +4 -6
  38. package/src/components/slider/features/disabled.ts +41 -16
  39. package/src/components/slider/features/interactions.ts +153 -18
  40. package/src/components/slider/features/keyboard.ts +127 -6
  41. package/src/components/slider/features/structure.ts +32 -5
  42. package/src/components/slider/features/ui.ts +18 -8
  43. package/src/components/tabs/_styles.scss +285 -155
  44. package/src/components/tabs/api.ts +178 -400
  45. package/src/components/tabs/config.ts +46 -52
  46. package/src/components/tabs/constants.ts +85 -8
  47. package/src/components/tabs/features.ts +401 -0
  48. package/src/components/tabs/index.ts +60 -3
  49. package/src/components/tabs/indicator.ts +225 -0
  50. package/src/components/tabs/responsive.ts +144 -0
  51. package/src/components/tabs/scroll-indicators.ts +149 -0
  52. package/src/components/tabs/state.ts +186 -0
  53. package/src/components/tabs/tab-api.ts +258 -0
  54. package/src/components/tabs/tab.ts +255 -0
  55. package/src/components/tabs/tabs.ts +50 -31
  56. package/src/components/tabs/types.ts +324 -128
  57. package/src/components/tabs/utils.ts +107 -0
  58. package/src/components/textfield/_styles.scss +0 -98
  59. package/src/components/textfield/config.ts +2 -3
  60. package/src/components/textfield/constants.ts +0 -14
  61. package/src/components/textfield/index.ts +2 -2
  62. package/src/components/textfield/textfield.ts +0 -2
  63. package/src/components/textfield/types.ts +1 -4
  64. package/src/core/compose/component.ts +1 -1
  65. package/src/core/compose/features/badge.ts +79 -0
  66. package/src/core/compose/features/index.ts +3 -1
  67. package/src/styles/abstract/_theme.scss +106 -2
  68. package/src/components/card/actions.ts +0 -48
  69. package/src/components/card/header.ts +0 -88
  70. package/src/components/card/media.ts +0 -52
@@ -1,443 +1,221 @@
1
1
  // src/components/tabs/api.ts
2
- import { TabsComponent, TabItem, TabChangeEventData } from './types';
3
- import { ANIMATION_DURATION } from './constants';
2
+ import { TabsComponent, TabComponent, TabConfig } from './types';
3
+ import { createTab } from './tab';
4
4
 
5
+ /**
6
+ * API options for a Tabs component
7
+ */
5
8
  interface ApiOptions {
6
- disabled: {
7
- enable: () => void;
8
- disable: () => void;
9
- isDisabled: () => boolean;
10
- };
9
+ /** The component's lifecycle API */
11
10
  lifecycle: {
12
11
  destroy: () => void;
13
12
  };
14
13
  }
15
14
 
15
+ /**
16
+ * Component with required elements and methods
17
+ */
16
18
  interface ComponentWithElements {
19
+ /** The DOM element */
17
20
  element: HTMLElement;
21
+ /** Array of tab components */
22
+ tabs: TabComponent[];
23
+ /** Container for tabs */
24
+ tabsContainer: HTMLElement;
25
+ /** Tab click handler */
26
+ handleTabClick: (event: Event, tab: TabComponent) => void;
27
+ /** Scroll container (optional) */
28
+ scrollContainer?: HTMLElement;
29
+ /** Class name helper */
18
30
  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
- };
31
+ /** Event subscription (optional) */
32
+ on?: (event: string, handler: Function) => any;
33
+ /** Event unsubscription (optional) */
34
+ off?: (event: string, handler: Function) => any;
35
+ /** Event emission (optional) */
36
+ emit?: (event: string, data: any) => any;
37
+ /** Component configuration */
38
+ config: Record<string, any>;
24
39
  }
25
40
 
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
41
  /**
55
42
  * Enhances a tabs component with API methods
56
43
  * @param {ApiOptions} options - API configuration options
57
44
  * @returns {Function} Higher-order function that adds API methods to component
58
- * @internal This is an internal utility for the Tabs component
59
45
  */
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;
46
+ export const withAPI = ({ lifecycle }: ApiOptions) =>
47
+ (component: ComponentWithElements): TabsComponent => ({
48
+ ...component as any,
49
+ element: component.element,
65
50
 
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');
51
+ /**
52
+ * Creates and adds a new tab
53
+ */
54
+ addTab(config: TabConfig) {
55
+ // Create a merged config that inherits from tabs component
56
+ const mergedConfig = {
57
+ ...config,
58
+ prefix: component.config.prefix,
59
+ variant: config.variant || component.config.variant
60
+ };
61
+
62
+ // Ensure value is set if not provided
63
+ if (mergedConfig.value === undefined) {
64
+ mergedConfig.value = ''; // Default empty value
84
65
  }
85
66
 
86
- // Create content container
87
- const contentElement = document.createElement('div');
88
- contentElement.className = `${baseClass}__tab-content`;
67
+ // Create the tab
68
+ const tab = createTab(mergedConfig);
89
69
 
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
- }
70
+ // Add to internal tabs array
71
+ component.tabs.push(tab);
97
72
 
98
- // Add label
99
- const labelElement = document.createElement('div');
100
- labelElement.className = `${baseClass}__tab-label`;
101
- labelElement.textContent = item.label;
102
- contentElement.appendChild(labelElement);
73
+ // Add to DOM
74
+ const targetContainer = component.tabsContainer;
75
+ targetContainer.appendChild(tab.element);
103
76
 
104
- tabElement.appendChild(contentElement);
77
+ // Add click handler with robust event handling
78
+ if (tab.on && typeof tab.on === 'function') {
79
+ tab.on('click', (event) => component.handleTabClick(event, tab));
80
+ }
105
81
 
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
- }
82
+ // Add direct DOM event handler as a fallback
83
+ tab.element.addEventListener('click', (event) => {
84
+ component.handleTabClick(event, tab);
112
85
  });
113
86
 
114
- return tabElement;
115
- };
87
+ return tab;
88
+ },
116
89
 
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;
90
+ /**
91
+ * Adds a pre-created tab
92
+ */
93
+ add(tab: TabComponent) {
94
+ component.tabs.push(tab);
95
+
96
+ // Add tab to DOM
97
+ const targetContainer = component.tabsContainer;
98
+ targetContainer.appendChild(tab.element);
99
+
100
+ // Add click handler via API and direct DOM event
101
+ if (tab.on && typeof tab.on === 'function') {
102
+ tab.on('click', (event) => component.handleTabClick(event, tab));
123
103
  }
124
104
 
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`;
105
+ tab.element.addEventListener('click', (event) => {
106
+ component.handleTabClick(event, tab);
107
+ });
144
108
 
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
- };
109
+ return this;
110
+ },
163
111
 
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
- };
112
+ /**
113
+ * Gets all tabs
114
+ */
115
+ getTabs() {
116
+ return [...component.tabs];
117
+ },
180
118
 
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));
119
+ /**
120
+ * Gets the active tab
121
+ */
122
+ getActiveTab() {
123
+ return component.tabs.find(tab => tab.isActive()) || null;
124
+ },
125
+
126
+ /**
127
+ * Sets a tab as active
128
+ */
129
+ setActiveTab(tabOrValue: TabComponent | string) {
130
+ const targetTab = typeof tabOrValue === 'string'
131
+ ? component.tabs.find(tab => tab.getValue() === tabOrValue)
132
+ : tabOrValue;
133
+
134
+ if (!targetTab) return this;
135
+
136
+ // Deactivate all tabs first
137
+ component.tabs.forEach(tab => tab.deactivate());
138
+
139
+ // Activate the target tab
140
+ targetTab.activate();
141
+
142
+ // Emit change event
143
+ if (component.emit) {
144
+ component.emit('change', {
145
+ tab: targetTab,
146
+ value: targetTab.getValue()
352
147
  });
353
-
354
- // Call lifecycle destroy
355
- lifecycle.destroy();
356
- },
148
+ }
357
149
 
358
- on(event, handler) {
359
- component.events.on(event, handler);
360
- return this;
361
- },
150
+ return this;
151
+ },
152
+
153
+ /**
154
+ * Removes a tab
155
+ */
156
+ removeTab(tabOrValue: TabComponent | string) {
157
+ const targetTab = typeof tabOrValue === 'string'
158
+ ? component.tabs.find(tab => tab.getValue() === tabOrValue)
159
+ : tabOrValue;
160
+
161
+ if (!targetTab) return this;
162
+
163
+ // Remove from array
164
+ const index = component.tabs.indexOf(targetTab);
165
+ if (index !== -1) {
166
+ component.tabs.splice(index, 1);
167
+ }
362
168
 
363
- off(event, handler) {
364
- component.events.off(event, handler);
365
- return this;
169
+ // Clean up tab and remove from DOM
170
+ if (targetTab.element.parentNode) {
171
+ targetTab.element.parentNode.removeChild(targetTab.element);
366
172
  }
367
- };
368
-
369
- // Set up keyboard navigation
370
- enhancedComponent.element.addEventListener('keydown', (e: KeyboardEvent) => {
371
- if (disabled.isDisabled() || !items.length) return;
372
173
 
373
- const key = e.key;
374
- let newIndex = activeIndex;
174
+ targetTab.destroy();
375
175
 
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;
176
+ return this;
177
+ },
178
+
179
+ /**
180
+ * Adds an event listener
181
+ */
182
+ on(event: string, handler: Function) {
183
+ if (component.on) {
184
+ component.on(event, handler);
423
185
  }
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
- }
186
+ return this;
187
+ },
188
+
189
+ /**
190
+ * Removes an event listener
191
+ */
192
+ off(event: string, handler: Function) {
193
+ if (component.off) {
194
+ component.off(event, handler);
437
195
  }
438
- });
196
+ return this;
197
+ },
439
198
 
440
- return api;
441
- };
199
+ /**
200
+ * Destroys the tabs component
201
+ */
202
+ destroy() {
203
+ // Clean up all tabs first
204
+ component.tabs.forEach(tab => tab.destroy());
205
+ component.tabs.length = 0;
206
+
207
+ // Then destroy container
208
+ lifecycle.destroy();
209
+ }
210
+ });
442
211
 
443
- export default withAPI;
212
+ /**
213
+ * Creates API configuration for the Tabs component
214
+ * @param {Object} comp - Component with lifecycle feature
215
+ * @returns {Object} API configuration object
216
+ */
217
+ export const getApiConfig = (comp) => ({
218
+ lifecycle: {
219
+ destroy: () => comp.lifecycle.destroy()
220
+ }
221
+ });