0
# Feedback Components
1
2
User feedback components including progress indicators, spinners, snackbars, and tooltips for communicating system state and user actions. These components provide visual feedback and temporary notifications to enhance user experience.
3
4
## Capabilities
5
6
### Material Snackbar
7
8
Temporary notification component that appears at the bottom of the screen with optional actions.
9
10
```javascript { .api }
11
/**
12
* Material Design snackbar component
13
* CSS Class: mdl-js-snackbar
14
* Widget: true
15
*/
16
interface MaterialSnackbar {
17
/**
18
* Show snackbar with configuration
19
* @param data - Configuration object for the snackbar
20
*/
21
showSnackbar(data: SnackbarData): void;
22
}
23
24
/**
25
* Configuration object for snackbar display
26
*/
27
interface SnackbarData {
28
/** Text message to display (required) */
29
message: string;
30
31
/** Optional action button text */
32
actionText?: string;
33
34
/** Optional action click handler function */
35
actionHandler?: () => void;
36
37
/** Optional timeout in milliseconds (default: 2750) */
38
timeout?: number;
39
}
40
```
41
42
**HTML Structure:**
43
44
```html
45
<!-- Basic snackbar container -->
46
<div id="demo-snackbar-example" class="mdl-js-snackbar mdl-snackbar">
47
<div class="mdl-snackbar__text"></div>
48
<button class="mdl-snackbar__action" type="button"></button>
49
</div>
50
```
51
52
**Usage Examples:**
53
54
```javascript
55
// Access snackbar instance
56
const snackbarContainer = document.querySelector('#demo-snackbar-example');
57
const snackbar = snackbarContainer.MaterialSnackbar;
58
59
// Simple message
60
snackbar.showSnackbar({
61
message: 'Hello World!'
62
});
63
64
// Message with action
65
snackbar.showSnackbar({
66
message: 'Email sent',
67
actionText: 'Undo',
68
actionHandler: function() {
69
console.log('Undo clicked');
70
undoEmailSend();
71
}
72
});
73
74
// Custom timeout
75
snackbar.showSnackbar({
76
message: 'This will disappear quickly',
77
timeout: 1000
78
});
79
80
// Queue multiple messages
81
function showMultipleMessages() {
82
snackbar.showSnackbar({
83
message: 'First message',
84
timeout: 2000
85
});
86
87
setTimeout(() => {
88
snackbar.showSnackbar({
89
message: 'Second message',
90
timeout: 2000
91
});
92
}, 2500);
93
}
94
95
// Common usage patterns
96
function showSuccessMessage(message) {
97
snackbar.showSnackbar({
98
message: message,
99
timeout: 3000
100
});
101
}
102
103
function showUndoableAction(message, undoCallback) {
104
snackbar.showSnackbar({
105
message: message,
106
actionText: 'Undo',
107
actionHandler: undoCallback,
108
timeout: 5000 // Longer timeout for actionable messages
109
});
110
}
111
112
// Example usage in forms
113
document.querySelector('#save-button').addEventListener('click', async () => {
114
try {
115
await saveData();
116
showSuccessMessage('Data saved successfully');
117
} catch (error) {
118
snackbar.showSnackbar({
119
message: 'Failed to save data',
120
actionText: 'Retry',
121
actionHandler: () => {
122
document.querySelector('#save-button').click();
123
}
124
});
125
}
126
});
127
```
128
129
### Material Progress
130
131
Linear progress indicator for showing task completion or loading states.
132
133
```javascript { .api }
134
/**
135
* Material Design progress component
136
* CSS Class: mdl-js-progress
137
* Widget: true
138
*/
139
interface MaterialProgress {
140
/**
141
* Set progress value
142
* @param value - Progress value between 0 and 100
143
*/
144
setProgress(value: number): void;
145
146
/**
147
* Set buffer value for buffered progress
148
* @param value - Buffer value between 0 and 100
149
*/
150
setBuffer(value: number): void;
151
}
152
153
// Note: MaterialProgress automatically creates internal DOM structure
154
// with .mdl-progress__progressbar, .mdl-progress__bufferbar, and
155
// .mdl-progress__auxbar elements for rendering the progress visualization
156
```
157
158
**HTML Structure:**
159
160
```html
161
<!-- Basic progress bar -->
162
<div id="p1" class="mdl-progress mdl-js-progress"></div>
163
164
<!-- Progress bar with initial value -->
165
<div id="p2" class="mdl-progress mdl-js-progress"
166
data-upgraded="true" style="width: 250px;"></div>
167
168
<!-- Indeterminate progress (for unknown duration) -->
169
<div id="p3" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>
170
```
171
172
**Usage Examples:**
173
174
```javascript
175
// Access progress instance
176
const progressBar = document.querySelector('#p1').MaterialProgress;
177
178
// Set progress value
179
progressBar.setProgress(65);
180
181
// Animate progress
182
function animateProgress(targetValue) {
183
let currentValue = 0;
184
const increment = targetValue / 100;
185
186
const interval = setInterval(() => {
187
currentValue += increment;
188
progressBar.setProgress(Math.min(currentValue, targetValue));
189
190
if (currentValue >= targetValue) {
191
clearInterval(interval);
192
}
193
}, 10);
194
}
195
196
// Usage with file upload
197
function handleFileUpload(file) {
198
const progressBar = document.querySelector('#upload-progress').MaterialProgress;
199
200
const xhr = new XMLHttpRequest();
201
202
xhr.upload.addEventListener('progress', (event) => {
203
if (event.lengthComputable) {
204
const percentComplete = (event.loaded / event.total) * 100;
205
progressBar.setProgress(percentComplete);
206
}
207
});
208
209
xhr.addEventListener('load', () => {
210
progressBar.setProgress(100);
211
showSuccessMessage('Upload complete');
212
});
213
214
// Upload file
215
const formData = new FormData();
216
formData.append('file', file);
217
xhr.open('POST', '/upload');
218
xhr.send(formData);
219
}
220
221
// Buffered progress (for streaming content)
222
function updateBufferedProgress(loaded, buffered, total) {
223
const progressBar = document.querySelector('#stream-progress').MaterialProgress;
224
225
const loadedPercent = (loaded / total) * 100;
226
const bufferedPercent = (buffered / total) * 100;
227
228
progressBar.setProgress(loadedPercent);
229
progressBar.setBuffer(bufferedPercent);
230
}
231
```
232
233
### Material Spinner
234
235
Circular loading spinner for indicating ongoing operations.
236
237
```javascript { .api }
238
/**
239
* Material Design spinner component
240
* CSS Class: mdl-js-spinner
241
* Widget: true
242
*/
243
interface MaterialSpinner {
244
/** Start the spinner animation */
245
start(): void;
246
247
/** Stop the spinner animation */
248
stop(): void;
249
250
/**
251
* Create a spinner layer with specified index
252
* @param index - Index of the layer to be created (1-4)
253
*/
254
createLayer(index: number): void;
255
}
256
```
257
258
**HTML Structure:**
259
260
```html
261
<!-- Basic spinner -->
262
<div class="mdl-spinner mdl-js-spinner"></div>
263
264
<!-- Active spinner -->
265
<div class="mdl-spinner mdl-js-spinner is-active"></div>
266
267
<!-- Single color spinner -->
268
<div class="mdl-spinner mdl-js-spinner mdl-spinner--single-color"></div>
269
```
270
271
**Usage Examples:**
272
273
```javascript
274
// Access spinner instance
275
const spinner = document.querySelector('.mdl-js-spinner').MaterialSpinner;
276
277
// Start/stop spinner
278
spinner.start();
279
spinner.stop();
280
281
// Usage with async operations
282
async function performAsyncOperation() {
283
const spinner = document.querySelector('#loading-spinner').MaterialSpinner;
284
285
spinner.start();
286
287
try {
288
const result = await fetchData();
289
return result;
290
} finally {
291
spinner.stop();
292
}
293
}
294
295
// Button with loading state
296
function setupLoadingButton(buttonSelector, spinnerSelector) {
297
const button = document.querySelector(buttonSelector);
298
const spinner = document.querySelector(spinnerSelector).MaterialSpinner;
299
300
button.addEventListener('click', async () => {
301
button.disabled = true;
302
spinner.start();
303
304
try {
305
await performLongOperation();
306
showSuccessMessage('Operation completed');
307
} catch (error) {
308
showErrorMessage('Operation failed');
309
} finally {
310
spinner.stop();
311
button.disabled = false;
312
}
313
});
314
}
315
```
316
317
### Material Tooltip
318
319
Contextual information component that appears on hover or focus.
320
321
```javascript { .api }
322
/**
323
* Material Design tooltip component
324
* CSS Class: mdl-js-tooltip
325
* Widget: false
326
*/
327
interface MaterialTooltip {
328
// No public methods - behavior is entirely automatic
329
// Tooltips show on hover/focus and hide on mouse leave/blur
330
}
331
```
332
333
**HTML Structure:**
334
335
```html
336
<!-- Basic tooltip -->
337
<div id="tt1" class="icon material-icons">add</div>
338
<div class="mdl-tooltip" for="tt1">Follow</div>
339
340
<!-- Large tooltip -->
341
<div id="tt2" class="icon material-icons">print</div>
342
<div class="mdl-tooltip mdl-tooltip--large" for="tt2">
343
344
</div>
345
346
<!-- Left-aligned tooltip -->
347
<div id="tt3" class="icon material-icons">cloud_upload</div>
348
<div class="mdl-tooltip mdl-tooltip--left" for="tt3">Upload</div>
349
350
<!-- Right-aligned tooltip -->
351
<div id="tt4" class="icon material-icons">cloud_download</div>
352
<div class="mdl-tooltip mdl-tooltip--right" for="tt4">Download</div>
353
354
<!-- Top-aligned tooltip -->
355
<div id="tt5" class="icon material-icons">favorite</div>
356
<div class="mdl-tooltip mdl-tooltip--top" for="tt5">Favorite</div>
357
358
<!-- Bottom-aligned tooltip -->
359
<div id="tt6" class="icon material-icons">delete</div>
360
<div class="mdl-tooltip mdl-tooltip--bottom" for="tt6">Delete</div>
361
```
362
363
**Usage Examples:**
364
365
```javascript
366
// Tooltips work automatically, but you can create them dynamically
367
function addTooltip(elementId, text, position = '') {
368
const element = document.getElementById(elementId);
369
if (!element) return;
370
371
// Create tooltip element
372
const tooltip = document.createElement('div');
373
tooltip.className = `mdl-tooltip ${position}`;
374
tooltip.setAttribute('for', elementId);
375
tooltip.textContent = text;
376
377
// Insert tooltip after the target element
378
element.parentNode.insertBefore(tooltip, element.nextSibling);
379
380
// Upgrade tooltip
381
componentHandler.upgradeElement(tooltip);
382
}
383
384
// Dynamic tooltip management
385
function updateTooltip(elementId, newText) {
386
const tooltip = document.querySelector(`[for="${elementId}"]`);
387
if (tooltip) {
388
tooltip.textContent = newText;
389
}
390
}
391
392
// Conditional tooltips
393
function setupConditionalTooltips() {
394
document.addEventListener('mouseenter', (event) => {
395
if (event.target.matches('[data-dynamic-tooltip]')) {
396
const tooltipText = getTooltipText(event.target);
397
if (tooltipText) {
398
showDynamicTooltip(event.target, tooltipText);
399
}
400
}
401
});
402
}
403
404
function getTooltipText(element) {
405
// Return different tooltip text based on element state
406
if (element.disabled) {
407
return 'This action is currently disabled';
408
} else if (element.classList.contains('loading')) {
409
return 'Please wait...';
410
} else {
411
return element.dataset.dynamicTooltip;
412
}
413
}
414
```
415
416
## Feedback Constants
417
418
```javascript { .api }
419
/**
420
* Material Snackbar constants
421
*/
422
interface SnackbarConstants {
423
/** Default animation length in milliseconds */
424
ANIMATION_LENGTH: 250;
425
426
/** Default timeout in milliseconds */
427
DEFAULT_TIMEOUT: 2750;
428
}
429
430
/**
431
* Material Progress constants
432
*/
433
interface ProgressConstants {
434
/** CSS class for indeterminate progress */
435
INDETERMINATE_CLASS: 'mdl-progress__indeterminate';
436
}
437
```
438
439
## Feedback Patterns
440
441
### Notification Queue System
442
443
```javascript
444
// Queue system for managing multiple notifications
445
class NotificationManager {
446
constructor(snackbarElement) {
447
this.snackbar = snackbarElement.MaterialSnackbar;
448
this.queue = [];
449
this.isShowing = false;
450
}
451
452
show(data) {
453
this.queue.push(data);
454
this.processQueue();
455
}
456
457
processQueue() {
458
if (this.isShowing || this.queue.length === 0) {
459
return;
460
}
461
462
const nextNotification = this.queue.shift();
463
this.isShowing = true;
464
465
// Add completion callback
466
const originalHandler = nextNotification.actionHandler;
467
const timeout = nextNotification.timeout || 2750;
468
469
nextNotification.actionHandler = () => {
470
if (originalHandler) originalHandler();
471
this.onNotificationComplete();
472
};
473
474
this.snackbar.showSnackbar(nextNotification);
475
476
// Auto-complete after timeout
477
setTimeout(() => {
478
this.onNotificationComplete();
479
}, timeout + 500); // Add buffer for animation
480
}
481
482
onNotificationComplete() {
483
this.isShowing = false;
484
setTimeout(() => this.processQueue(), 300);
485
}
486
}
487
488
// Usage
489
const notificationManager = new NotificationManager(
490
document.querySelector('#notification-snackbar')
491
);
492
493
// Show notifications that will be queued
494
notificationManager.show({ message: 'First notification' });
495
notificationManager.show({ message: 'Second notification' });
496
notificationManager.show({ message: 'Third notification' });
497
```
498
499
### Progress Tracking
500
501
```javascript
502
// Multi-step progress tracking
503
class ProgressTracker {
504
constructor(progressElement) {
505
this.progress = progressElement.MaterialProgress;
506
this.steps = [];
507
this.currentStep = 0;
508
}
509
510
addStep(name, weight = 1) {
511
this.steps.push({ name, weight, completed: false });
512
return this;
513
}
514
515
completeStep(stepIndex) {
516
if (stepIndex < this.steps.length) {
517
this.steps[stepIndex].completed = true;
518
this.updateProgress();
519
}
520
}
521
522
completeCurrentStep() {
523
this.completeStep(this.currentStep);
524
this.currentStep++;
525
}
526
527
updateProgress() {
528
const totalWeight = this.steps.reduce((sum, step) => sum + step.weight, 0);
529
const completedWeight = this.steps
530
.filter(step => step.completed)
531
.reduce((sum, step) => sum + step.weight, 0);
532
533
const percentage = (completedWeight / totalWeight) * 100;
534
this.progress.setProgress(percentage);
535
}
536
537
reset() {
538
this.steps.forEach(step => step.completed = false);
539
this.currentStep = 0;
540
this.progress.setProgress(0);
541
}
542
}
543
544
// Usage
545
const tracker = new ProgressTracker(document.querySelector('#multi-step-progress'))
546
.addStep('Initialize', 1)
547
.addStep('Load Data', 2)
548
.addStep('Process Data', 3)
549
.addStep('Save Results', 1);
550
551
async function performMultiStepOperation() {
552
tracker.reset();
553
554
// Step 1
555
await initialize();
556
tracker.completeCurrentStep();
557
558
// Step 2
559
await loadData();
560
tracker.completeCurrentStep();
561
562
// Step 3
563
await processData();
564
tracker.completeCurrentStep();
565
566
// Step 4
567
await saveResults();
568
tracker.completeCurrentStep();
569
}
570
```
571
572
### Loading State Management
573
574
```javascript
575
// Centralized loading state management
576
class LoadingStateManager {
577
constructor() {
578
this.loadingStates = new Map();
579
this.globalSpinner = document.querySelector('#global-spinner')?.MaterialSpinner;
580
}
581
582
setLoading(key, isLoading, options = {}) {
583
if (isLoading) {
584
this.loadingStates.set(key, options);
585
} else {
586
this.loadingStates.delete(key);
587
}
588
589
this.updateGlobalState();
590
this.updateSpecificElements(key, isLoading, options);
591
}
592
593
updateGlobalState() {
594
const hasAnyLoading = this.loadingStates.size > 0;
595
596
if (this.globalSpinner) {
597
if (hasAnyLoading) {
598
this.globalSpinner.start();
599
} else {
600
this.globalSpinner.stop();
601
}
602
}
603
604
// Update body class for global loading styles
605
document.body.classList.toggle('is-loading', hasAnyLoading);
606
}
607
608
updateSpecificElements(key, isLoading, options) {
609
const { spinner, progress, button } = options;
610
611
if (spinner) {
612
const spinnerElement = document.querySelector(spinner);
613
if (spinnerElement?.MaterialSpinner) {
614
if (isLoading) {
615
spinnerElement.MaterialSpinner.start();
616
} else {
617
spinnerElement.MaterialSpinner.stop();
618
}
619
}
620
}
621
622
if (button) {
623
const buttonElement = document.querySelector(button);
624
if (buttonElement) {
625
buttonElement.disabled = isLoading;
626
if (buttonElement.MaterialButton) {
627
if (isLoading) {
628
buttonElement.MaterialButton.disable();
629
} else {
630
buttonElement.MaterialButton.enable();
631
}
632
}
633
}
634
}
635
636
if (progress && typeof progress === 'object') {
637
const progressElement = document.querySelector(progress.selector);
638
if (progressElement?.MaterialProgress) {
639
if (!isLoading) {
640
progressElement.MaterialProgress.setProgress(100);
641
setTimeout(() => {
642
progressElement.MaterialProgress.setProgress(0);
643
}, 500);
644
}
645
}
646
}
647
}
648
649
isLoading(key) {
650
return this.loadingStates.has(key);
651
}
652
653
hasAnyLoading() {
654
return this.loadingStates.size > 0;
655
}
656
}
657
658
// Global loading manager
659
const loadingManager = new LoadingStateManager();
660
661
// Usage
662
async function saveUserData() {
663
loadingManager.setLoading('save-user', true, {
664
spinner: '#save-spinner',
665
button: '#save-button',
666
progress: { selector: '#save-progress' }
667
});
668
669
try {
670
await api.saveUser(userData);
671
notificationManager.show({ message: 'User saved successfully' });
672
} catch (error) {
673
notificationManager.show({
674
message: 'Failed to save user',
675
actionText: 'Retry',
676
actionHandler: () => saveUserData()
677
});
678
} finally {
679
loadingManager.setLoading('save-user', false);
680
}
681
}
682
```