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.
- package/package.json +1 -1
- package/src/components/badge/_styles.scss +9 -9
- package/src/components/button/_styles.scss +0 -56
- package/src/components/button/button.ts +0 -2
- package/src/components/button/constants.ts +0 -6
- package/src/components/button/index.ts +2 -2
- package/src/components/button/types.ts +1 -7
- package/src/components/card/_styles.scss +67 -25
- package/src/components/card/api.ts +54 -3
- package/src/components/card/card.ts +33 -2
- package/src/components/card/config.ts +143 -21
- package/src/components/card/constants.ts +20 -19
- package/src/components/card/content.ts +299 -2
- package/src/components/card/features.ts +155 -4
- package/src/components/card/index.ts +31 -9
- package/src/components/card/types.ts +138 -15
- package/src/components/chip/chip.ts +1 -9
- package/src/components/chip/constants.ts +0 -10
- package/src/components/chip/index.ts +1 -1
- package/src/components/chip/types.ts +1 -4
- package/src/components/progress/_styles.scss +0 -65
- package/src/components/progress/config.ts +1 -2
- package/src/components/progress/constants.ts +0 -14
- package/src/components/progress/index.ts +1 -1
- package/src/components/progress/progress.ts +1 -4
- package/src/components/progress/types.ts +1 -4
- package/src/components/radios/_styles.scss +0 -45
- package/src/components/radios/api.ts +85 -60
- package/src/components/radios/config.ts +1 -2
- package/src/components/radios/constants.ts +0 -9
- package/src/components/radios/index.ts +1 -1
- package/src/components/radios/radio.ts +34 -11
- package/src/components/radios/radios.ts +2 -1
- package/src/components/radios/types.ts +1 -7
- package/src/components/slider/_styles.scss +149 -155
- package/src/components/slider/accessibility.md +59 -0
- package/src/components/slider/config.ts +4 -6
- package/src/components/slider/features/disabled.ts +41 -16
- package/src/components/slider/features/interactions.ts +153 -18
- package/src/components/slider/features/keyboard.ts +127 -6
- package/src/components/slider/features/structure.ts +32 -5
- package/src/components/slider/features/ui.ts +18 -8
- package/src/components/tabs/_styles.scss +285 -155
- package/src/components/tabs/api.ts +178 -400
- package/src/components/tabs/config.ts +46 -52
- package/src/components/tabs/constants.ts +85 -8
- package/src/components/tabs/features.ts +401 -0
- package/src/components/tabs/index.ts +60 -3
- package/src/components/tabs/indicator.ts +225 -0
- package/src/components/tabs/responsive.ts +144 -0
- package/src/components/tabs/scroll-indicators.ts +149 -0
- package/src/components/tabs/state.ts +186 -0
- package/src/components/tabs/tab-api.ts +258 -0
- package/src/components/tabs/tab.ts +255 -0
- package/src/components/tabs/tabs.ts +50 -31
- package/src/components/tabs/types.ts +324 -128
- package/src/components/tabs/utils.ts +107 -0
- package/src/components/textfield/_styles.scss +0 -98
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/constants.ts +0 -14
- package/src/components/textfield/index.ts +2 -2
- package/src/components/textfield/textfield.ts +0 -2
- package/src/components/textfield/types.ts +1 -4
- package/src/core/compose/component.ts +1 -1
- package/src/core/compose/features/badge.ts +79 -0
- package/src/core/compose/features/index.ts +3 -1
- package/src/styles/abstract/_theme.scss +106 -2
- package/src/components/card/actions.ts +0 -48
- package/src/components/card/header.ts +0 -88
- package/src/components/card/media.ts +0 -52
|
@@ -42,9 +42,107 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
42
42
|
triggerEvent = () => ({ defaultPrevented: false })
|
|
43
43
|
} = handlers;
|
|
44
44
|
|
|
45
|
-
|
|
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
|
-
console.log('handleThumbMouseDown', e)
|
|
48
146
|
// Verify component exists and check if disabled
|
|
49
147
|
if (!state.component || (state.component.disabled && state.component.disabled.isDisabled())) {
|
|
50
148
|
return;
|
|
@@ -53,6 +151,12 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
53
151
|
e.preventDefault();
|
|
54
152
|
e.stopPropagation();
|
|
55
153
|
|
|
154
|
+
// First hide any existing visible bubbles
|
|
155
|
+
hideAllBubbles();
|
|
156
|
+
|
|
157
|
+
// Clear any keyboard focus indicators globally
|
|
158
|
+
clearGlobalKeyboardFocus();
|
|
159
|
+
|
|
56
160
|
state.dragging = true;
|
|
57
161
|
state.activeThumb = isSecondThumb ? secondThumb : thumb;
|
|
58
162
|
state.activeBubble = isSecondThumb ? secondValueBubble : valueBubble;
|
|
@@ -60,10 +164,8 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
60
164
|
// Add dragging class to component element to style the thumb differently
|
|
61
165
|
state.component.element.classList.add(`${state.component.getClass('slider')}--dragging`);
|
|
62
166
|
|
|
63
|
-
// Show
|
|
64
|
-
|
|
65
|
-
showValueBubble(state.activeBubble, true);
|
|
66
|
-
}
|
|
167
|
+
// Show active bubble
|
|
168
|
+
showActiveBubble(state.activeBubble);
|
|
67
169
|
|
|
68
170
|
// Add global event listeners
|
|
69
171
|
document.addEventListener('mousemove', handleMouseMove);
|
|
@@ -79,6 +181,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
79
181
|
}
|
|
80
182
|
};
|
|
81
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Handle track mouse down with improved bubble handling
|
|
186
|
+
*/
|
|
82
187
|
const handleTrackMouseDown = (e) => {
|
|
83
188
|
// Verify component exists and check if disabled
|
|
84
189
|
if (!state.component || (state.component.disabled && state.component.disabled.isDisabled()) || !track) {
|
|
@@ -87,6 +192,12 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
87
192
|
|
|
88
193
|
e.preventDefault();
|
|
89
194
|
|
|
195
|
+
// Hide any existing visible bubbles
|
|
196
|
+
hideAllBubbles();
|
|
197
|
+
|
|
198
|
+
// Clear any keyboard focus indicators globally
|
|
199
|
+
clearGlobalKeyboardFocus();
|
|
200
|
+
|
|
90
201
|
// Determine which thumb to move based on click position
|
|
91
202
|
let isSecondThumb = false;
|
|
92
203
|
|
|
@@ -146,6 +257,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
146
257
|
handleThumbMouseDown(e, isSecondThumb);
|
|
147
258
|
};
|
|
148
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Handle mouse move with improved thumb and bubble switching
|
|
262
|
+
*/
|
|
149
263
|
const handleMouseMove = (e) => {
|
|
150
264
|
if (!state.dragging || !state.activeThumb) return;
|
|
151
265
|
|
|
@@ -174,30 +288,50 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
174
288
|
// For range slider, ensure thumbs don't cross
|
|
175
289
|
if (config.range && state.secondValue !== null) {
|
|
176
290
|
if (isSecondThumb) {
|
|
291
|
+
// Second thumb is active
|
|
292
|
+
|
|
177
293
|
// Don't allow second thumb to go below first thumb
|
|
178
|
-
if (newValue
|
|
294
|
+
if (newValue >= state.value) {
|
|
179
295
|
state.secondValue = newValue;
|
|
180
296
|
} else {
|
|
181
|
-
// 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
|
|
182
303
|
state.secondValue = state.value;
|
|
183
304
|
state.value = newValue;
|
|
184
305
|
|
|
185
|
-
// Swap active
|
|
306
|
+
// Swap active elements
|
|
186
307
|
state.activeThumb = thumb;
|
|
187
308
|
state.activeBubble = valueBubble;
|
|
309
|
+
|
|
310
|
+
// Show new active bubble
|
|
311
|
+
showActiveBubble(state.activeBubble);
|
|
188
312
|
}
|
|
189
313
|
} else {
|
|
314
|
+
// First thumb is active
|
|
315
|
+
|
|
190
316
|
// Don't allow first thumb to go above second thumb
|
|
191
|
-
if (newValue
|
|
317
|
+
if (newValue <= state.secondValue) {
|
|
192
318
|
state.value = newValue;
|
|
193
319
|
} else {
|
|
194
|
-
// 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
|
|
195
326
|
state.value = state.secondValue;
|
|
196
327
|
state.secondValue = newValue;
|
|
197
328
|
|
|
198
|
-
// Swap active
|
|
329
|
+
// Swap active elements
|
|
199
330
|
state.activeThumb = secondThumb;
|
|
200
331
|
state.activeBubble = secondValueBubble;
|
|
332
|
+
|
|
333
|
+
// Show new active bubble
|
|
334
|
+
showActiveBubble(state.activeBubble);
|
|
201
335
|
}
|
|
202
336
|
}
|
|
203
337
|
} else {
|
|
@@ -215,6 +349,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
215
349
|
}
|
|
216
350
|
};
|
|
217
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Handle mouse up with consistent bubble hiding
|
|
354
|
+
*/
|
|
218
355
|
const handleMouseUp = (e) => {
|
|
219
356
|
if (!state.dragging) return;
|
|
220
357
|
|
|
@@ -225,10 +362,9 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
225
362
|
// Remove dragging class from component element
|
|
226
363
|
state.component.element.classList.remove(`${state.component.getClass('slider')}--dragging`);
|
|
227
364
|
|
|
228
|
-
// Hide
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
365
|
+
// Hide bubble with delay
|
|
366
|
+
const currentBubble = state.activeBubble;
|
|
367
|
+
hideActiveBubble(currentBubble, 1000); // Hide after 1 second
|
|
232
368
|
|
|
233
369
|
// Remove global event listeners
|
|
234
370
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
@@ -236,9 +372,8 @@ export const createInteractionHandlers = (config: SliderConfig, state, handlers)
|
|
|
236
372
|
document.removeEventListener('touchmove', handleMouseMove);
|
|
237
373
|
document.removeEventListener('touchend', handleMouseUp);
|
|
238
374
|
|
|
239
|
-
// Reset active
|
|
375
|
+
// Reset active thumb
|
|
240
376
|
state.activeThumb = null;
|
|
241
|
-
state.activeBubble = null;
|
|
242
377
|
|
|
243
378
|
try {
|
|
244
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,41 +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':
|
|
115
|
+
case 'ArrowUp':
|
|
40
116
|
e.preventDefault();
|
|
41
|
-
newValue = Math.min(newValue +
|
|
117
|
+
newValue = Math.min(newValue + stepSize, state.max);
|
|
118
|
+
valueChanged = true;
|
|
42
119
|
break;
|
|
43
120
|
|
|
44
121
|
case 'ArrowLeft':
|
|
122
|
+
case 'ArrowDown':
|
|
45
123
|
e.preventDefault();
|
|
46
|
-
newValue = Math.max(newValue -
|
|
124
|
+
newValue = Math.max(newValue - stepSize, state.min);
|
|
125
|
+
valueChanged = true;
|
|
47
126
|
break;
|
|
48
127
|
|
|
49
128
|
case 'Home':
|
|
50
129
|
e.preventDefault();
|
|
51
130
|
newValue = state.min;
|
|
131
|
+
valueChanged = true;
|
|
52
132
|
break;
|
|
53
133
|
|
|
54
134
|
case 'End':
|
|
55
135
|
e.preventDefault();
|
|
56
136
|
newValue = state.max;
|
|
137
|
+
valueChanged = true;
|
|
57
138
|
break;
|
|
58
139
|
|
|
59
140
|
case 'PageUp':
|
|
60
141
|
e.preventDefault();
|
|
61
142
|
newValue = Math.min(newValue + (step * 10), state.max);
|
|
143
|
+
valueChanged = true;
|
|
62
144
|
break;
|
|
63
145
|
|
|
64
146
|
case 'PageDown':
|
|
65
147
|
e.preventDefault();
|
|
66
148
|
newValue = Math.max(newValue - (step * 10), state.min);
|
|
149
|
+
valueChanged = true;
|
|
67
150
|
break;
|
|
68
151
|
|
|
69
152
|
default:
|
|
70
153
|
return; // Exit if not a handled key
|
|
71
154
|
}
|
|
72
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
|
+
|
|
73
164
|
// Update the value
|
|
74
165
|
if (isSecondThumb) {
|
|
75
166
|
state.secondValue = newValue;
|
|
@@ -88,16 +179,46 @@ export const createKeyboardHandlers = (state, handlers) => {
|
|
|
88
179
|
const handleFocus = (e, isSecondThumb = false) => {
|
|
89
180
|
if (state.component.disabled && state.component.disabled.isDisabled()) return;
|
|
90
181
|
|
|
91
|
-
//
|
|
92
|
-
|
|
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;
|
|
93
202
|
|
|
94
203
|
// Trigger focus event
|
|
95
204
|
triggerEvent(SLIDER_EVENTS.FOCUS, e);
|
|
96
205
|
};
|
|
97
206
|
|
|
98
207
|
const handleBlur = (e, isSecondThumb = false) => {
|
|
99
|
-
//
|
|
100
|
-
|
|
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
|
+
}
|
|
101
222
|
|
|
102
223
|
// Trigger blur event
|
|
103
224
|
triggerEvent(SLIDER_EVENTS.BLUR, e);
|
|
@@ -3,7 +3,7 @@ import { SLIDER_COLORS, SLIDER_SIZES } from '../constants';
|
|
|
3
3
|
import { SliderConfig } from '../types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Creates the slider DOM structure following MD3 principles
|
|
6
|
+
* Creates the slider DOM structure following MD3 principles with improved accessibility
|
|
7
7
|
* @param config Slider configuration
|
|
8
8
|
* @returns Component enhancer with DOM structure
|
|
9
9
|
*/
|
|
@@ -15,6 +15,7 @@ export const withStructure = (config: SliderConfig) => component => {
|
|
|
15
15
|
const value = config.value !== undefined ? config.value : min;
|
|
16
16
|
const secondValue = config.secondValue !== undefined ? config.secondValue : null;
|
|
17
17
|
const isRangeSlider = config.range && secondValue !== null;
|
|
18
|
+
const isDisabled = config.disabled === true;
|
|
18
19
|
|
|
19
20
|
// Helper function to calculate percentage
|
|
20
21
|
const getPercentage = (val) => ((val - min) / range) * 100;
|
|
@@ -41,10 +42,19 @@ export const withStructure = (config: SliderConfig) => component => {
|
|
|
41
42
|
const valueBubble = createElement('slider-value');
|
|
42
43
|
valueBubble.textContent = formatter(value);
|
|
43
44
|
|
|
44
|
-
// Create thumb element
|
|
45
|
+
// Create thumb element with improved accessibility attributes
|
|
45
46
|
const thumb = createElement('slider-thumb');
|
|
46
|
-
thumb.setAttribute('tabindex', '0');
|
|
47
47
|
thumb.setAttribute('role', 'slider');
|
|
48
|
+
thumb.setAttribute('aria-valuemin', String(min));
|
|
49
|
+
thumb.setAttribute('aria-valuemax', String(max));
|
|
50
|
+
thumb.setAttribute('aria-valuenow', String(value));
|
|
51
|
+
thumb.setAttribute('aria-orientation', 'horizontal');
|
|
52
|
+
|
|
53
|
+
// Set tabindex based on disabled state
|
|
54
|
+
thumb.setAttribute('tabindex', isDisabled ? '-1' : '0');
|
|
55
|
+
if (isDisabled) {
|
|
56
|
+
thumb.setAttribute('aria-disabled', 'true');
|
|
57
|
+
}
|
|
48
58
|
|
|
49
59
|
// Set initial thumb position
|
|
50
60
|
thumb.style.left = `${valuePercent}%`;
|
|
@@ -60,8 +70,17 @@ export const withStructure = (config: SliderConfig) => component => {
|
|
|
60
70
|
|
|
61
71
|
if (isRangeSlider) {
|
|
62
72
|
secondThumb = createElement('slider-thumb');
|
|
63
|
-
secondThumb.setAttribute('tabindex', '0');
|
|
64
73
|
secondThumb.setAttribute('role', 'slider');
|
|
74
|
+
secondThumb.setAttribute('aria-valuemin', String(min));
|
|
75
|
+
secondThumb.setAttribute('aria-valuemax', String(max));
|
|
76
|
+
secondThumb.setAttribute('aria-valuenow', String(secondValue));
|
|
77
|
+
secondThumb.setAttribute('aria-orientation', 'horizontal');
|
|
78
|
+
|
|
79
|
+
// Set tabindex based on disabled state
|
|
80
|
+
secondThumb.setAttribute('tabindex', isDisabled ? '-1' : '0');
|
|
81
|
+
if (isDisabled) {
|
|
82
|
+
secondThumb.setAttribute('aria-disabled', 'true');
|
|
83
|
+
}
|
|
65
84
|
|
|
66
85
|
const secondPercent = getPercentage(secondValue);
|
|
67
86
|
secondThumb.style.left = `${secondPercent}%`;
|
|
@@ -80,6 +99,14 @@ export const withStructure = (config: SliderConfig) => component => {
|
|
|
80
99
|
|
|
81
100
|
// Add elements to the slider
|
|
82
101
|
component.element.classList.add(component.getClass('slider'));
|
|
102
|
+
|
|
103
|
+
// Accessibility enhancement: Container is not focusable
|
|
104
|
+
component.element.setAttribute('tabindex', '-1');
|
|
105
|
+
|
|
106
|
+
// Set container aria attributes
|
|
107
|
+
component.element.setAttribute('role', 'none');
|
|
108
|
+
component.element.setAttribute('aria-disabled', isDisabled ? 'true' : 'false');
|
|
109
|
+
|
|
83
110
|
component.element.appendChild(track);
|
|
84
111
|
component.element.appendChild(ticksContainer); // Add ticks container
|
|
85
112
|
component.element.appendChild(startDot);
|
|
@@ -217,7 +244,7 @@ export const withStructure = (config: SliderConfig) => component => {
|
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
// Apply disabled class if needed
|
|
220
|
-
if (
|
|
247
|
+
if (isDisabled) {
|
|
221
248
|
component.element.classList.add(`${baseClass}--disabled`);
|
|
222
249
|
}
|
|
223
250
|
}
|
|
@@ -180,15 +180,25 @@ export const createUiHelpers = (config: SliderConfig, state) => {
|
|
|
180
180
|
const adjustedLower = mapValueToVisualPercent(lowerPercent, edgeConstraint) + paddingPercent;
|
|
181
181
|
const adjustedHigher = mapValueToVisualPercent(higherPercent, edgeConstraint) - paddingPercent;
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
trackLength = Math.max(0, higherPercent - lowerPercent);
|
|
186
|
-
}
|
|
183
|
+
// Calculate the actual percentage difference between thumbs
|
|
184
|
+
const valueDiffPercent = Math.abs(higherPercent - lowerPercent);
|
|
187
185
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
// Define a threshold below which we'll hide the active track
|
|
187
|
+
// This threshold is based on the thumb width plus some margin
|
|
188
|
+
const hideThreshold = (thumbSize / trackSize) * 100;
|
|
189
|
+
|
|
190
|
+
if (valueDiffPercent <= hideThreshold) {
|
|
191
|
+
// Thumbs are too close together, hide the active track
|
|
192
|
+
activeTrack.style.display = 'none';
|
|
193
|
+
} else {
|
|
194
|
+
// Normal display of active track
|
|
195
|
+
let trackLength = Math.max(0, adjustedHigher - adjustedLower);
|
|
196
|
+
|
|
197
|
+
activeTrack.style.display = 'block';
|
|
198
|
+
activeTrack.style.width = `${trackLength}%`;
|
|
199
|
+
activeTrack.style.left = `${adjustedLower}%`;
|
|
200
|
+
activeTrack.style.height = '100%';
|
|
201
|
+
}
|
|
192
202
|
} else {
|
|
193
203
|
// Single thumb slider
|
|
194
204
|
const valuePercent = getPercentage(state.value);
|