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.
Files changed (76) hide show
  1. package/package.json +6 -3
  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 +193 -281
  36. package/src/components/slider/accessibility.md +59 -0
  37. package/src/components/slider/api.ts +36 -101
  38. package/src/components/slider/config.ts +29 -78
  39. package/src/components/slider/constants.ts +12 -8
  40. package/src/components/slider/features/appearance.ts +1 -47
  41. package/src/components/slider/features/disabled.ts +41 -16
  42. package/src/components/slider/features/interactions.ts +166 -26
  43. package/src/components/slider/features/keyboard.ts +125 -6
  44. package/src/components/slider/features/structure.ts +182 -195
  45. package/src/components/slider/features/ui.ts +234 -303
  46. package/src/components/slider/index.ts +11 -1
  47. package/src/components/slider/slider.ts +1 -1
  48. package/src/components/slider/types.ts +10 -25
  49. package/src/components/tabs/_styles.scss +285 -155
  50. package/src/components/tabs/api.ts +178 -400
  51. package/src/components/tabs/config.ts +46 -52
  52. package/src/components/tabs/constants.ts +85 -8
  53. package/src/components/tabs/features.ts +401 -0
  54. package/src/components/tabs/index.ts +60 -3
  55. package/src/components/tabs/indicator.ts +225 -0
  56. package/src/components/tabs/responsive.ts +144 -0
  57. package/src/components/tabs/scroll-indicators.ts +149 -0
  58. package/src/components/tabs/state.ts +186 -0
  59. package/src/components/tabs/tab-api.ts +258 -0
  60. package/src/components/tabs/tab.ts +255 -0
  61. package/src/components/tabs/tabs.ts +50 -31
  62. package/src/components/tabs/types.ts +324 -128
  63. package/src/components/tabs/utils.ts +107 -0
  64. package/src/components/textfield/_styles.scss +0 -98
  65. package/src/components/textfield/config.ts +2 -3
  66. package/src/components/textfield/constants.ts +0 -14
  67. package/src/components/textfield/index.ts +2 -2
  68. package/src/components/textfield/textfield.ts +0 -2
  69. package/src/components/textfield/types.ts +1 -4
  70. package/src/core/compose/component.ts +1 -1
  71. package/src/core/compose/features/badge.ts +79 -0
  72. package/src/core/compose/features/index.ts +3 -1
  73. package/src/styles/abstract/_theme.scss +106 -2
  74. package/src/components/card/actions.ts +0 -48
  75. package/src/components/card/header.ts +0 -88
  76. package/src/components/card/media.ts +0 -52
@@ -1,5 +1,5 @@
1
1
  // src/components/slider/features/interactions.ts
2
- import { SLIDER_EVENTS, SLIDER_ORIENTATIONS } from '../constants';
2
+ import { SLIDER_EVENTS } from '../constants';
3
3
  import { SliderConfig, SliderEvent } from '../types';
4
4
 
5
5
  /**
@@ -42,7 +42,106 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
42
42
  triggerEvent = () => ({ defaultPrevented: false })
43
43
  } = handlers;
44
44
 
45
- // Event handlers
45
+ /**
46
+ * Clear any existing bubble hide timers
47
+ */
48
+ const clearBubbleHideTimer = () => {
49
+ if (state.valueHideTimer) {
50
+ clearTimeout(state.valueHideTimer);
51
+ state.valueHideTimer = null;
52
+ }
53
+ };
54
+
55
+ /**
56
+ * Hide all bubbles immediately
57
+ */
58
+ const hideAllBubbles = () => {
59
+ // Clear any pending hide timers
60
+ clearBubbleHideTimer();
61
+
62
+ // Hide both bubbles immediately
63
+ if (valueBubble) {
64
+ showValueBubble(valueBubble, false);
65
+ }
66
+ if (secondValueBubble) {
67
+ showValueBubble(secondValueBubble, false);
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Clear keyboard focus indicators across all sliders in the document
73
+ * Not just for this instance, but for any slider thumb
74
+ */
75
+ const clearGlobalKeyboardFocus = () => {
76
+ // First clear local focus indicators
77
+ if (thumb) {
78
+ thumb.classList.remove(`${state.component.getClass('slider-thumb')}--focused`);
79
+ }
80
+
81
+ if (secondThumb) {
82
+ secondThumb.classList.remove(`${state.component.getClass('slider-thumb')}--focused`);
83
+ }
84
+
85
+ // Now look for all slider thumbs in the document with the focused class
86
+ // This covers cases where we switch between sliders
87
+ try {
88
+ const focusClass = state.component.getClass('slider-thumb--focused');
89
+ const allFocusedThumbs = document.querySelectorAll(`.${focusClass}`);
90
+
91
+ // Remove focus class from all thumbs
92
+ allFocusedThumbs.forEach(element => {
93
+ element.classList.remove(focusClass);
94
+ });
95
+
96
+ // Also blur the active element if it's a thumb
97
+ if (document.activeElement &&
98
+ document.activeElement.classList.contains(state.component.getClass('slider-thumb'))) {
99
+ (document.activeElement as HTMLElement).blur();
100
+ }
101
+ } catch (error) {
102
+ console.warn('Error clearing global keyboard focus:', error);
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Show the active bubble with consistent behavior
108
+ * @param bubble Bubble element to show
109
+ */
110
+ const showActiveBubble = (bubble) => {
111
+ // First hide all bubbles
112
+ hideAllBubbles();
113
+
114
+ // Then show the active bubble if allowed by config
115
+ if (bubble && config.showValue) {
116
+ showValueBubble(bubble, true);
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Hide the active bubble with optional delay
122
+ * @param bubble Bubble element to hide
123
+ * @param delay Delay in milliseconds before hiding
124
+ */
125
+ const hideActiveBubble = (bubble, delay = 0) => {
126
+ // Clear any existing timers first
127
+ clearBubbleHideTimer();
128
+
129
+ if (!bubble || !config.showValue) return;
130
+
131
+ if (delay > 0) {
132
+ // Set delayed hide
133
+ state.valueHideTimer = setTimeout(() => {
134
+ showValueBubble(bubble, false);
135
+ }, delay);
136
+ } else {
137
+ // Hide immediately
138
+ showValueBubble(bubble, false);
139
+ }
140
+ };
141
+
142
+ /**
143
+ * Handle thumb mouse down with improved bubble handling
144
+ */
46
145
  const handleThumbMouseDown = (e, isSecondThumb = false) => {
47
146
  // Verify component exists and check if disabled
48
147
  if (!state.component || (state.component.disabled && state.component.disabled.isDisabled())) {
@@ -52,14 +151,21 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
52
151
  e.preventDefault();
53
152
  e.stopPropagation();
54
153
 
154
+ // First hide any existing visible bubbles
155
+ hideAllBubbles();
156
+
157
+ // Clear any keyboard focus indicators globally
158
+ clearGlobalKeyboardFocus();
159
+
55
160
  state.dragging = true;
56
161
  state.activeThumb = isSecondThumb ? secondThumb : thumb;
57
162
  state.activeBubble = isSecondThumb ? secondValueBubble : valueBubble;
58
163
 
59
- // Show value bubble if it exists
60
- if (state.activeBubble) {
61
- showValueBubble(state.activeBubble, true);
62
- }
164
+ // Add dragging class to component element to style the thumb differently
165
+ state.component.element.classList.add(`${state.component.getClass('slider')}--dragging`);
166
+
167
+ // Show active bubble
168
+ showActiveBubble(state.activeBubble);
63
169
 
64
170
  // Add global event listeners
65
171
  document.addEventListener('mousemove', handleMouseMove);
@@ -75,6 +181,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
75
181
  }
76
182
  };
77
183
 
184
+ /**
185
+ * Handle track mouse down with improved bubble handling
186
+ */
78
187
  const handleTrackMouseDown = (e) => {
79
188
  // Verify component exists and check if disabled
80
189
  if (!state.component || (state.component.disabled && state.component.disabled.isDisabled()) || !track) {
@@ -83,21 +192,26 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
83
192
 
84
193
  e.preventDefault();
85
194
 
195
+ // Hide any existing visible bubbles
196
+ hideAllBubbles();
197
+
198
+ // Clear any keyboard focus indicators globally
199
+ clearGlobalKeyboardFocus();
200
+
86
201
  // Determine which thumb to move based on click position
87
202
  let isSecondThumb = false;
88
203
 
89
204
  try {
90
205
  // Get track rect for calculating position
91
206
  const trackRect = track.getBoundingClientRect();
92
- const isVertical = config.orientation === SLIDER_ORIENTATIONS.VERTICAL;
93
207
 
94
208
  // Get position from mouse or touch event
95
209
  const position = e.type.includes('touch')
96
- ? isVertical ? e.touches[0].clientY : e.touches[0].clientX
97
- : isVertical ? e.clientY : e.clientX;
210
+ ? e.touches[0].clientX
211
+ : e.clientX;
98
212
 
99
213
  // Calculate value at click position
100
- let newValue = getValueFromPosition(position, isVertical);
214
+ let newValue = getValueFromPosition(position);
101
215
 
102
216
  // Round to step if needed
103
217
  if (config.snapToSteps && state.step > 0) {
@@ -143,6 +257,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
143
257
  handleThumbMouseDown(e, isSecondThumb);
144
258
  };
145
259
 
260
+ /**
261
+ * Handle mouse move with improved thumb and bubble switching
262
+ */
146
263
  const handleMouseMove = (e) => {
147
264
  if (!state.dragging || !state.activeThumb) return;
148
265
 
@@ -150,13 +267,12 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
150
267
 
151
268
  try {
152
269
  // Get position
153
- const isVertical = config.orientation === SLIDER_ORIENTATIONS.VERTICAL;
154
270
  const position = e.type.includes('touch')
155
- ? isVertical ? e.touches[0].clientY : e.touches[0].clientX
156
- : isVertical ? e.clientY : e.clientX;
271
+ ? e.touches[0].clientX
272
+ : e.clientX;
157
273
 
158
274
  // Calculate new value
159
- let newValue = getValueFromPosition(position, isVertical);
275
+ let newValue = getValueFromPosition(position);
160
276
 
161
277
  // Round to step if needed
162
278
  if (config.snapToSteps && state.step > 0) {
@@ -172,30 +288,50 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
172
288
  // For range slider, ensure thumbs don't cross
173
289
  if (config.range && state.secondValue !== null) {
174
290
  if (isSecondThumb) {
291
+ // Second thumb is active
292
+
175
293
  // Don't allow second thumb to go below first thumb
176
- if (newValue < state.value) {
294
+ if (newValue >= state.value) {
177
295
  state.secondValue = newValue;
178
296
  } else {
179
- // Thumbs are crossed, swap them
297
+ // Thumbs are crossed, need to swap them
298
+
299
+ // First hide current bubble
300
+ hideActiveBubble(state.activeBubble, 0);
301
+
302
+ // Swap values
180
303
  state.secondValue = state.value;
181
304
  state.value = newValue;
182
305
 
183
- // Swap active thumb and bubble
306
+ // Swap active elements
184
307
  state.activeThumb = thumb;
185
308
  state.activeBubble = valueBubble;
309
+
310
+ // Show new active bubble
311
+ showActiveBubble(state.activeBubble);
186
312
  }
187
313
  } else {
314
+ // First thumb is active
315
+
188
316
  // Don't allow first thumb to go above second thumb
189
- if (newValue > state.secondValue) {
317
+ if (newValue <= state.secondValue) {
190
318
  state.value = newValue;
191
319
  } else {
192
- // Thumbs are crossed, swap them
320
+ // Thumbs are crossed, need to swap them
321
+
322
+ // First hide current bubble
323
+ hideActiveBubble(state.activeBubble, 0);
324
+
325
+ // Swap values
193
326
  state.value = state.secondValue;
194
327
  state.secondValue = newValue;
195
328
 
196
- // Swap active thumb and bubble
329
+ // Swap active elements
197
330
  state.activeThumb = secondThumb;
198
331
  state.activeBubble = secondValueBubble;
332
+
333
+ // Show new active bubble
334
+ showActiveBubble(state.activeBubble);
199
335
  }
200
336
  }
201
337
  } else {
@@ -213,6 +349,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
213
349
  }
214
350
  };
215
351
 
352
+ /**
353
+ * Handle mouse up with consistent bubble hiding
354
+ */
216
355
  const handleMouseUp = (e) => {
217
356
  if (!state.dragging) return;
218
357
 
@@ -220,10 +359,12 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
220
359
 
221
360
  state.dragging = false;
222
361
 
223
- // Hide value bubble
224
- if (state.activeBubble) {
225
- showValueBubble(state.activeBubble, false);
226
- }
362
+ // Remove dragging class from component element
363
+ state.component.element.classList.remove(`${state.component.getClass('slider')}--dragging`);
364
+
365
+ // Hide bubble with delay
366
+ const currentBubble = state.activeBubble;
367
+ hideActiveBubble(currentBubble, 1000); // Hide after 1 second
227
368
 
228
369
  // Remove global event listeners
229
370
  document.removeEventListener('mousemove', handleMouseMove);
@@ -231,9 +372,8 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
231
372
  document.removeEventListener('touchmove', handleMouseMove);
232
373
  document.removeEventListener('touchend', handleMouseUp);
233
374
 
234
- // Reset active elements
375
+ // Reset active thumb
235
376
  state.activeThumb = null;
236
- state.activeBubble = null;
237
377
 
238
378
  try {
239
379
  // Trigger change event (only when done dragging)
@@ -21,12 +21,73 @@ export const createKeyboardHandlers = (state, handlers) => {
21
21
  triggerEvent
22
22
  } = handlers;
23
23
 
24
+ // Last focused thumb tracker to handle tab sequences properly
25
+ let lastFocusedThumb = null;
26
+
27
+ /**
28
+ * Clear any existing bubble hide timers
29
+ */
30
+ const clearBubbleHideTimer = () => {
31
+ if (state.valueHideTimer) {
32
+ clearTimeout(state.valueHideTimer);
33
+ state.valueHideTimer = null;
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Hide all bubbles immediately
39
+ */
40
+ const hideAllBubbles = () => {
41
+ // Clear any pending hide timers
42
+ clearBubbleHideTimer();
43
+
44
+ // Hide both bubbles immediately
45
+ if (valueBubble) {
46
+ showValueBubble(valueBubble, false);
47
+ }
48
+ if (secondValueBubble) {
49
+ showValueBubble(secondValueBubble, false);
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Shows a bubble element with consistent behavior
55
+ */
56
+ const showBubble = (bubble) => {
57
+ // First hide all bubbles
58
+ hideAllBubbles();
59
+
60
+ // Then show the active bubble
61
+ if (bubble) {
62
+ showValueBubble(bubble, true);
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Hides a bubble element with optional delay
68
+ */
69
+ const hideBubble = (bubble, delay = 0) => {
70
+ // Clear any existing timers
71
+ clearBubbleHideTimer();
72
+
73
+ if (!bubble) return;
74
+
75
+ if (delay > 0) {
76
+ state.valueHideTimer = setTimeout(() => {
77
+ showValueBubble(bubble, false);
78
+ }, delay);
79
+ } else {
80
+ showValueBubble(bubble, false);
81
+ }
82
+ };
83
+
24
84
  // Event handlers
25
85
  const handleKeyDown = (e, isSecondThumb = false) => {
26
86
  if (state.component.disabled && state.component.disabled.isDisabled()) return;
27
87
 
28
88
  const step = state.step || 1;
29
89
  let newValue;
90
+ let stepSize = step;
30
91
 
31
92
  // Determine which value to modify
32
93
  if (isSecondThumb) {
@@ -35,43 +96,71 @@ export const createKeyboardHandlers = (state, handlers) => {
35
96
  newValue = state.value;
36
97
  }
37
98
 
99
+ // Handle tab key specifically for range sliders
100
+ if (e.key === 'Tab') {
101
+ // Let the browser handle the tab navigation
102
+ // We'll deal with showing/hiding bubbles in the focus/blur handlers
103
+ return;
104
+ }
105
+
106
+ // Determine step size based on modifier keys
107
+ if (e.shiftKey) {
108
+ stepSize = step * 10; // Large step when Shift is pressed
109
+ }
110
+
111
+ let valueChanged = false;
112
+
38
113
  switch (e.key) {
39
114
  case 'ArrowRight':
40
115
  case 'ArrowUp':
41
116
  e.preventDefault();
42
- newValue = Math.min(newValue + step, state.max);
117
+ newValue = Math.min(newValue + stepSize, state.max);
118
+ valueChanged = true;
43
119
  break;
44
120
 
45
121
  case 'ArrowLeft':
46
122
  case 'ArrowDown':
47
123
  e.preventDefault();
48
- newValue = Math.max(newValue - step, state.min);
124
+ newValue = Math.max(newValue - stepSize, state.min);
125
+ valueChanged = true;
49
126
  break;
50
127
 
51
128
  case 'Home':
52
129
  e.preventDefault();
53
130
  newValue = state.min;
131
+ valueChanged = true;
54
132
  break;
55
133
 
56
134
  case 'End':
57
135
  e.preventDefault();
58
136
  newValue = state.max;
137
+ valueChanged = true;
59
138
  break;
60
139
 
61
140
  case 'PageUp':
62
141
  e.preventDefault();
63
142
  newValue = Math.min(newValue + (step * 10), state.max);
143
+ valueChanged = true;
64
144
  break;
65
145
 
66
146
  case 'PageDown':
67
147
  e.preventDefault();
68
148
  newValue = Math.max(newValue - (step * 10), state.min);
149
+ valueChanged = true;
69
150
  break;
70
151
 
71
152
  default:
72
153
  return; // Exit if not a handled key
73
154
  }
74
155
 
156
+ if (!valueChanged) return;
157
+
158
+ // Update active bubble reference
159
+ state.activeBubble = isSecondThumb ? secondValueBubble : valueBubble;
160
+
161
+ // Show value bubble during keyboard interaction
162
+ showBubble(state.activeBubble);
163
+
75
164
  // Update the value
76
165
  if (isSecondThumb) {
77
166
  state.secondValue = newValue;
@@ -90,16 +179,46 @@ export const createKeyboardHandlers = (state, handlers) => {
90
179
  const handleFocus = (e, isSecondThumb = false) => {
91
180
  if (state.component.disabled && state.component.disabled.isDisabled()) return;
92
181
 
93
- // Show value bubble
94
- showValueBubble(isSecondThumb ? secondValueBubble : valueBubble, true);
182
+ // Track the currently focused thumb for tab sequence handling
183
+ const currentThumb = isSecondThumb ? secondThumb : state.component.structure.thumb;
184
+
185
+ // If we're tabbing between thumbs, hide the previous bubble immediately
186
+ if (lastFocusedThumb && lastFocusedThumb !== currentThumb) {
187
+ hideAllBubbles();
188
+ }
189
+
190
+ // Update the last focused thumb
191
+ lastFocusedThumb = currentThumb;
192
+
193
+ // Add a class to indicate keyboard focus
194
+ currentThumb.classList.add(`${state.component.getClass('slider-thumb')}--focused`);
195
+
196
+ // Show value bubble on focus
197
+ const bubble = isSecondThumb ? secondValueBubble : valueBubble;
198
+ showBubble(bubble);
199
+
200
+ // Update active bubble reference
201
+ state.activeBubble = bubble;
95
202
 
96
203
  // Trigger focus event
97
204
  triggerEvent(SLIDER_EVENTS.FOCUS, e);
98
205
  };
99
206
 
100
207
  const handleBlur = (e, isSecondThumb = false) => {
101
- // Hide value bubble
102
- showValueBubble(isSecondThumb ? secondValueBubble : valueBubble, false);
208
+ // Remove keyboard focus class
209
+ const thumb = isSecondThumb ? secondThumb : state.component.structure.thumb;
210
+ thumb.classList.remove(`${state.component.getClass('slider-thumb')}--focused`);
211
+
212
+ // Only hide the bubble if we're not tabbing to another thumb
213
+ // This check prevents the bubble from flickering when tabbing between thumbs
214
+ const relatedTarget = e.relatedTarget;
215
+ const otherThumb = isSecondThumb ? state.component.structure.thumb : secondThumb;
216
+
217
+ if (!relatedTarget || relatedTarget !== otherThumb) {
218
+ // We're not tabbing to the other thumb, so we can hide the bubble
219
+ const bubble = isSecondThumb ? secondValueBubble : valueBubble;
220
+ hideBubble(bubble, 200);
221
+ }
103
222
 
104
223
  // Trigger blur event
105
224
  triggerEvent(SLIDER_EVENTS.BLUR, e);