or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

component-management.mddata-display-components.mdfeedback-components.mdform-components.mdindex.mdlayout-components.mdnavigation-components.mdvisual-effects.md

feedback-components.mddocs/

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

Print

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

```