mtrl 0.2.9 → 0.3.1

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 (99) hide show
  1. package/CLAUDE.md +33 -0
  2. package/package.json +3 -1
  3. package/src/components/button/button.ts +34 -5
  4. package/src/components/navigation/index.ts +4 -1
  5. package/src/components/navigation/system/core.ts +302 -0
  6. package/src/components/navigation/system/events.ts +240 -0
  7. package/src/components/navigation/system/index.ts +184 -0
  8. package/src/components/navigation/system/mobile.ts +278 -0
  9. package/src/components/navigation/system/state.ts +77 -0
  10. package/src/components/navigation/system/types.ts +364 -0
  11. package/src/components/navigation/types.ts +33 -0
  12. package/src/components/slider/config.ts +2 -2
  13. package/src/components/slider/features/controller.ts +1 -25
  14. package/src/components/slider/features/handlers.ts +0 -1
  15. package/src/components/slider/features/range.ts +7 -7
  16. package/src/components/slider/{structure.ts → schema.ts} +2 -13
  17. package/src/components/slider/slider.ts +3 -2
  18. package/src/components/snackbar/index.ts +7 -1
  19. package/src/components/snackbar/types.ts +25 -0
  20. package/src/components/switch/api.ts +16 -0
  21. package/src/components/switch/config.ts +1 -18
  22. package/src/components/switch/features.ts +198 -0
  23. package/src/components/switch/index.ts +6 -1
  24. package/src/components/switch/switch.ts +3 -3
  25. package/src/components/switch/types.ts +27 -2
  26. package/src/components/textfield/index.ts +7 -1
  27. package/src/components/textfield/types.ts +36 -0
  28. package/src/core/composition/features/dom.ts +26 -14
  29. package/src/core/composition/features/icon.ts +18 -18
  30. package/src/core/composition/features/index.ts +3 -2
  31. package/src/core/composition/features/label.ts +16 -17
  32. package/src/core/composition/features/layout.ts +47 -0
  33. package/src/core/composition/index.ts +4 -4
  34. package/src/core/layout/README.md +350 -0
  35. package/src/core/layout/array.ts +181 -0
  36. package/src/core/layout/create.ts +55 -0
  37. package/src/core/layout/index.ts +26 -0
  38. package/src/core/layout/object.ts +124 -0
  39. package/src/core/layout/processor.ts +58 -0
  40. package/src/core/layout/result.ts +85 -0
  41. package/src/core/layout/types.ts +125 -0
  42. package/src/core/layout/utils.ts +136 -0
  43. package/src/styles/abstract/_variables.scss +28 -0
  44. package/src/styles/components/_switch.scss +133 -69
  45. package/src/styles/components/_textfield.scss +9 -16
  46. package/test/components/badge.test.ts +545 -0
  47. package/test/components/bottom-app-bar.test.ts +303 -0
  48. package/test/components/button.test.ts +233 -0
  49. package/test/components/card.test.ts +560 -0
  50. package/test/components/carousel.test.ts +951 -0
  51. package/test/components/checkbox.test.ts +462 -0
  52. package/test/components/chip.test.ts +692 -0
  53. package/test/components/datepicker.test.ts +1124 -0
  54. package/test/components/dialog.test.ts +990 -0
  55. package/test/components/divider.test.ts +412 -0
  56. package/test/components/extended-fab.test.ts +672 -0
  57. package/test/components/fab.test.ts +561 -0
  58. package/test/components/list.test.ts +365 -0
  59. package/test/components/menu.test.ts +718 -0
  60. package/test/components/navigation.test.ts +186 -0
  61. package/test/components/progress.test.ts +567 -0
  62. package/test/components/radios.test.ts +699 -0
  63. package/test/components/search.test.ts +1135 -0
  64. package/test/components/segmented-button.test.ts +732 -0
  65. package/test/components/sheet.test.ts +641 -0
  66. package/test/components/slider.test.ts +1220 -0
  67. package/test/components/snackbar.test.ts +461 -0
  68. package/test/components/switch.test.ts +452 -0
  69. package/test/components/tabs.test.ts +1369 -0
  70. package/test/components/textfield.test.ts +400 -0
  71. package/test/components/timepicker.test.ts +592 -0
  72. package/test/components/tooltip.test.ts +630 -0
  73. package/test/components/top-app-bar.test.ts +566 -0
  74. package/test/core/dom.attributes.test.ts +148 -0
  75. package/test/core/dom.classes.test.ts +152 -0
  76. package/test/core/dom.events.test.ts +243 -0
  77. package/test/core/emitter.test.ts +141 -0
  78. package/test/core/ripple.test.ts +99 -0
  79. package/test/core/state.store.test.ts +189 -0
  80. package/test/core/utils.normalize.test.ts +61 -0
  81. package/test/core/utils.object.test.ts +120 -0
  82. package/test/setup.ts +451 -0
  83. package/tsconfig.json +2 -2
  84. package/src/components/navigation/system-types.ts +0 -124
  85. package/src/components/navigation/system.ts +0 -776
  86. package/src/components/snackbar/constants.ts +0 -26
  87. package/src/core/composition/features/structure.ts +0 -22
  88. package/src/core/layout/index.js +0 -95
  89. package/src/core/structure.ts +0 -288
  90. package/test/components/button.test.js +0 -170
  91. package/test/components/checkbox.test.js +0 -238
  92. package/test/components/list.test.js +0 -105
  93. package/test/components/menu.test.js +0 -385
  94. package/test/components/navigation.test.js +0 -227
  95. package/test/components/snackbar.test.js +0 -234
  96. package/test/components/switch.test.js +0 -186
  97. package/test/components/textfield.test.js +0 -314
  98. package/test/core/emitter.test.js +0 -141
  99. package/test/core/ripple.test.js +0 -66
@@ -0,0 +1,630 @@
1
+ // test/components/tooltip.test.ts
2
+ import { describe, test, expect, mock, beforeAll, afterAll } from 'bun:test';
3
+ import { JSDOM } from 'jsdom';
4
+
5
+ // Setup jsdom environment
6
+ let dom: JSDOM;
7
+ let window: Window;
8
+ let document: Document;
9
+ let originalGlobalDocument: any;
10
+ let originalGlobalWindow: any;
11
+
12
+ beforeAll(() => {
13
+ // Create a new JSDOM instance
14
+ dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
15
+ url: 'http://localhost/',
16
+ pretendToBeVisual: true
17
+ });
18
+
19
+ // Get window and document from jsdom
20
+ window = dom.window;
21
+ document = window.document;
22
+
23
+ // Store original globals
24
+ originalGlobalDocument = global.document;
25
+ originalGlobalWindow = global.window;
26
+
27
+ // Set globals to use jsdom
28
+ global.document = document;
29
+ global.window = window;
30
+ global.Element = window.Element;
31
+ global.HTMLElement = window.HTMLElement;
32
+ global.HTMLButtonElement = window.HTMLButtonElement;
33
+ global.Event = window.Event;
34
+ });
35
+
36
+ afterAll(() => {
37
+ // Restore original globals
38
+ global.document = originalGlobalDocument;
39
+ global.window = originalGlobalWindow;
40
+
41
+ // Clean up jsdom
42
+ window.close();
43
+ });
44
+
45
+ // Import only the types we need from the component
46
+ import type { TooltipComponent, TooltipConfig } from '../../src/components/tooltip/types';
47
+
48
+ // Define constants to avoid circular dependencies
49
+ const TOOLTIP_POSITIONS = {
50
+ TOP: 'top',
51
+ RIGHT: 'right',
52
+ BOTTOM: 'bottom',
53
+ LEFT: 'left',
54
+ TOP_START: 'top-start',
55
+ TOP_END: 'top-end',
56
+ RIGHT_START: 'right-start',
57
+ RIGHT_END: 'right-end',
58
+ BOTTOM_START: 'bottom-start',
59
+ BOTTOM_END: 'bottom-end',
60
+ LEFT_START: 'left-start',
61
+ LEFT_END: 'left-end'
62
+ };
63
+
64
+ const TOOLTIP_VARIANTS = {
65
+ DEFAULT: 'default',
66
+ RICH: 'rich',
67
+ PLAIN: 'plain'
68
+ };
69
+
70
+ // Create mock tooltip component
71
+ const createMockTooltip = (config: TooltipConfig = {}): TooltipComponent => {
72
+ // Create tooltip element
73
+ const element = document.createElement('div');
74
+ element.className = `mtrl-tooltip mtrl-tooltip--${config.position || TOOLTIP_POSITIONS.BOTTOM} mtrl-tooltip--${config.variant || TOOLTIP_VARIANTS.DEFAULT}`;
75
+ element.setAttribute('role', 'tooltip');
76
+ element.setAttribute('aria-hidden', 'true');
77
+
78
+ // Create arrow element
79
+ const arrowElement = document.createElement('div');
80
+ arrowElement.className = 'mtrl-tooltip__arrow';
81
+ element.appendChild(arrowElement);
82
+
83
+ // Set text content if provided
84
+ if (config.text) {
85
+ const textNode = document.createTextNode(config.text);
86
+ element.insertBefore(textNode, arrowElement);
87
+ }
88
+
89
+ // Generate unique ID
90
+ const id = `mtrl-tooltip-${Math.random().toString(36).substring(2, 9)}`;
91
+ element.id = id;
92
+
93
+ // Add z-index if provided
94
+ if (config.zIndex) {
95
+ element.style.zIndex = config.zIndex.toString();
96
+ }
97
+
98
+ // Add to body
99
+ document.body.appendChild(element);
100
+
101
+ // Set up internal state
102
+ let targetElement: HTMLElement | null = config.target || null;
103
+ let positionValue = config.position || TOOLTIP_POSITIONS.BOTTOM;
104
+ let isVisible = !!config.visible;
105
+ let showDelay = config.showDelay || 300;
106
+ let hideDelay = config.hideDelay || 100;
107
+ let showOnFocus = config.showOnFocus !== undefined ? config.showOnFocus : true;
108
+ let showOnHover = config.showOnHover !== undefined ? config.showOnHover : true;
109
+
110
+ // Set initial visibility
111
+ if (isVisible) {
112
+ element.setAttribute('aria-hidden', 'false');
113
+ element.classList.add('mtrl-tooltip--visible');
114
+ }
115
+
116
+ // Set target if provided
117
+ if (targetElement) {
118
+ targetElement.setAttribute('aria-describedby', id);
119
+ }
120
+
121
+ // Event handlers
122
+ let mouseEnterHandler: ((e: Event) => void) | null = null;
123
+ let mouseLeaveHandler: ((e: Event) => void) | null = null;
124
+ let focusHandler: ((e: Event) => void) | null = null;
125
+ let blurHandler: ((e: Event) => void) | null = null;
126
+
127
+ // Timers
128
+ let showTimer: number | null = null;
129
+ let hideTimer: number | null = null;
130
+
131
+ // Helper to update position
132
+ const updatePositionImpl = () => {
133
+ if (!targetElement) return;
134
+
135
+ // Simulate position calculation (in real component this would use actual positioning)
136
+ const targetRect = {
137
+ top: 100,
138
+ left: 100,
139
+ width: 100,
140
+ height: 50,
141
+ right: 200,
142
+ bottom: 150
143
+ };
144
+
145
+ // Simple positioning logic
146
+ let top = 0;
147
+ let left = 0;
148
+
149
+ // Position based on position value
150
+ switch (positionValue) {
151
+ case TOOLTIP_POSITIONS.TOP:
152
+ top = targetRect.top - 10;
153
+ left = targetRect.left + targetRect.width / 2;
154
+ break;
155
+ case TOOLTIP_POSITIONS.RIGHT:
156
+ top = targetRect.top + targetRect.height / 2;
157
+ left = targetRect.right + 10;
158
+ break;
159
+ case TOOLTIP_POSITIONS.BOTTOM:
160
+ top = targetRect.bottom + 10;
161
+ left = targetRect.left + targetRect.width / 2;
162
+ break;
163
+ case TOOLTIP_POSITIONS.LEFT:
164
+ top = targetRect.top + targetRect.height / 2;
165
+ left = targetRect.left - 10;
166
+ break;
167
+ default:
168
+ top = targetRect.bottom + 10;
169
+ left = targetRect.left + targetRect.width / 2;
170
+ }
171
+
172
+ element.style.top = `${top}px`;
173
+ element.style.left = `${left}px`;
174
+
175
+ // Update arrow position
176
+ arrowElement.className = `mtrl-tooltip__arrow mtrl-tooltip__arrow--${positionValue}`;
177
+ };
178
+
179
+ // Setup target event handlers
180
+ const setupTargetEvents = (target: HTMLElement) => {
181
+ // Create handlers
182
+ mouseEnterHandler = () => tooltipApi.show();
183
+ mouseLeaveHandler = () => tooltipApi.hide();
184
+ focusHandler = () => tooltipApi.show();
185
+ blurHandler = () => tooltipApi.hide();
186
+
187
+ // Attach events
188
+ if (showOnHover) {
189
+ target.addEventListener('mouseenter', mouseEnterHandler);
190
+ target.addEventListener('mouseleave', mouseLeaveHandler);
191
+ }
192
+
193
+ if (showOnFocus) {
194
+ target.addEventListener('focus', focusHandler);
195
+ target.addEventListener('blur', blurHandler);
196
+ }
197
+ };
198
+
199
+ // Remove target event handlers
200
+ const removeTargetEvents = (target: HTMLElement) => {
201
+ if (mouseEnterHandler) {
202
+ target.removeEventListener('mouseenter', mouseEnterHandler);
203
+ }
204
+
205
+ if (mouseLeaveHandler) {
206
+ target.removeEventListener('mouseleave', mouseLeaveHandler);
207
+ }
208
+
209
+ if (focusHandler) {
210
+ target.removeEventListener('focus', focusHandler);
211
+ }
212
+
213
+ if (blurHandler) {
214
+ target.removeEventListener('blur', blurHandler);
215
+ }
216
+
217
+ mouseEnterHandler = null;
218
+ mouseLeaveHandler = null;
219
+ focusHandler = null;
220
+ blurHandler = null;
221
+ };
222
+
223
+ // Set up initial target events
224
+ if (targetElement) {
225
+ setupTargetEvents(targetElement);
226
+ }
227
+
228
+ // Create the tooltip API
229
+ const tooltipApi: TooltipComponent = {
230
+ element,
231
+ target: targetElement,
232
+
233
+ lifecycle: {
234
+ destroy: () => {}
235
+ },
236
+
237
+ getClass: (name: string) => `mtrl-${name}`,
238
+
239
+ setText(text: string) {
240
+ // Clear existing content (except arrow)
241
+ while (element.firstChild !== arrowElement) {
242
+ element.removeChild(element.firstChild);
243
+ }
244
+
245
+ // Add new text
246
+ const textNode = document.createTextNode(text);
247
+ element.insertBefore(textNode, arrowElement);
248
+
249
+ // Update position if visible
250
+ if (isVisible) {
251
+ this.updatePosition();
252
+ }
253
+
254
+ return this;
255
+ },
256
+
257
+ getText() {
258
+ // Clone element without arrow
259
+ const clone = element.cloneNode(true) as HTMLElement;
260
+ const arrowClone = clone.querySelector('.mtrl-tooltip__arrow');
261
+ if (arrowClone) {
262
+ arrowClone.remove();
263
+ }
264
+ return clone.textContent || '';
265
+ },
266
+
267
+ setPosition(position: string) {
268
+ // Update position value
269
+ positionValue = position;
270
+
271
+ // Remove existing position classes
272
+ Object.values(TOOLTIP_POSITIONS).forEach(p => {
273
+ element.classList.remove(`mtrl-tooltip--${p}`);
274
+ });
275
+
276
+ // Add new position class
277
+ element.classList.add(`mtrl-tooltip--${position}`);
278
+
279
+ // Update position if visible
280
+ if (isVisible) {
281
+ this.updatePosition();
282
+ }
283
+
284
+ return this;
285
+ },
286
+
287
+ getPosition() {
288
+ return positionValue;
289
+ },
290
+
291
+ setTarget(target: HTMLElement) {
292
+ // Remove events from old target
293
+ if (targetElement) {
294
+ removeTargetEvents(targetElement);
295
+ targetElement.removeAttribute('aria-describedby');
296
+ }
297
+
298
+ // Set new target
299
+ targetElement = target;
300
+ this.target = target;
301
+
302
+ // Set aria-describedby
303
+ target.setAttribute('aria-describedby', element.id);
304
+
305
+ // Add events to new target
306
+ setupTargetEvents(target);
307
+
308
+ // Update position if visible
309
+ if (isVisible) {
310
+ this.updatePosition();
311
+ }
312
+
313
+ return this;
314
+ },
315
+
316
+ show(immediate = false) {
317
+ // Clear timers
318
+ if (showTimer !== null) {
319
+ window.clearTimeout(showTimer);
320
+ showTimer = null;
321
+ }
322
+
323
+ if (hideTimer !== null) {
324
+ window.clearTimeout(hideTimer);
325
+ hideTimer = null;
326
+ }
327
+
328
+ const showTooltip = () => {
329
+ if (!targetElement) return this;
330
+
331
+ // Show tooltip
332
+ element.setAttribute('aria-hidden', 'false');
333
+ element.classList.add('mtrl-tooltip--visible');
334
+ isVisible = true;
335
+
336
+ // Update position
337
+ this.updatePosition();
338
+ };
339
+
340
+ if (immediate) {
341
+ showTooltip();
342
+ } else {
343
+ showTimer = window.setTimeout(showTooltip, showDelay);
344
+ }
345
+
346
+ return this;
347
+ },
348
+
349
+ hide(immediate = false) {
350
+ // Clear timers
351
+ if (hideTimer !== null) {
352
+ window.clearTimeout(hideTimer);
353
+ hideTimer = null;
354
+ }
355
+
356
+ if (showTimer !== null) {
357
+ window.clearTimeout(showTimer);
358
+ showTimer = null;
359
+ }
360
+
361
+ const hideTooltip = () => {
362
+ // Hide tooltip
363
+ element.setAttribute('aria-hidden', 'true');
364
+ element.classList.remove('mtrl-tooltip--visible');
365
+ isVisible = false;
366
+ };
367
+
368
+ if (immediate) {
369
+ hideTooltip();
370
+ } else {
371
+ hideTimer = window.setTimeout(hideTooltip, hideDelay);
372
+ }
373
+
374
+ return this;
375
+ },
376
+
377
+ isVisible() {
378
+ return isVisible;
379
+ },
380
+
381
+ updatePosition() {
382
+ updatePositionImpl();
383
+ return this;
384
+ },
385
+
386
+ destroy() {
387
+ // Clear timers
388
+ if (showTimer !== null) {
389
+ window.clearTimeout(showTimer);
390
+ showTimer = null;
391
+ }
392
+
393
+ if (hideTimer !== null) {
394
+ window.clearTimeout(hideTimer);
395
+ hideTimer = null;
396
+ }
397
+
398
+ // Remove target events
399
+ if (targetElement) {
400
+ removeTargetEvents(targetElement);
401
+ targetElement.removeAttribute('aria-describedby');
402
+ }
403
+
404
+ // Remove from DOM
405
+ if (element.parentNode) {
406
+ element.parentNode.removeChild(element);
407
+ }
408
+ }
409
+ };
410
+
411
+ return tooltipApi;
412
+ };
413
+
414
+ describe('Tooltip Component', () => {
415
+ test('should create a tooltip element', () => {
416
+ const tooltip = createMockTooltip();
417
+
418
+ expect(tooltip.element).toBeDefined();
419
+ expect(tooltip.element.tagName).toBe('DIV');
420
+ expect(tooltip.element.className).toContain('mtrl-tooltip');
421
+ expect(tooltip.element.getAttribute('role')).toBe('tooltip');
422
+ expect(tooltip.element.getAttribute('aria-hidden')).toBe('true');
423
+
424
+ // Clean up
425
+ tooltip.destroy();
426
+ });
427
+
428
+ test('should initialize with text content', () => {
429
+ const tooltipText = 'Delete item';
430
+ const tooltip = createMockTooltip({
431
+ text: tooltipText
432
+ });
433
+
434
+ expect(tooltip.getText()).toBe(tooltipText);
435
+
436
+ // Clean up
437
+ tooltip.destroy();
438
+ });
439
+
440
+ test('should initialize with target element', () => {
441
+ const targetButton = document.createElement('button');
442
+ document.body.appendChild(targetButton);
443
+
444
+ const tooltip = createMockTooltip({
445
+ target: targetButton
446
+ });
447
+
448
+ expect(tooltip.target).toBe(targetButton);
449
+ expect(targetButton.getAttribute('aria-describedby')).toBe(tooltip.element.id);
450
+
451
+ // Clean up
452
+ tooltip.destroy();
453
+ document.body.removeChild(targetButton);
454
+ });
455
+
456
+ test('should initialize with position', () => {
457
+ const position = TOOLTIP_POSITIONS.TOP;
458
+ const tooltip = createMockTooltip({
459
+ position
460
+ });
461
+
462
+ expect(tooltip.getPosition()).toBe(position);
463
+ expect(tooltip.element.className).toContain(`mtrl-tooltip--${position}`);
464
+
465
+ // Clean up
466
+ tooltip.destroy();
467
+ });
468
+
469
+ test('should initialize with visibility', () => {
470
+ const tooltip = createMockTooltip({
471
+ visible: true
472
+ });
473
+
474
+ expect(tooltip.isVisible()).toBe(true);
475
+ expect(tooltip.element.getAttribute('aria-hidden')).toBe('false');
476
+ expect(tooltip.element.className).toContain('mtrl-tooltip--visible');
477
+
478
+ // Clean up
479
+ tooltip.destroy();
480
+ });
481
+
482
+ test('should set and get text content', () => {
483
+ const tooltip = createMockTooltip();
484
+ const initialText = 'Initial tooltip';
485
+ const updatedText = 'Updated tooltip';
486
+
487
+ // Set initial text
488
+ tooltip.setText(initialText);
489
+ expect(tooltip.getText()).toBe(initialText);
490
+
491
+ // Update text
492
+ tooltip.setText(updatedText);
493
+ expect(tooltip.getText()).toBe(updatedText);
494
+
495
+ // Clean up
496
+ tooltip.destroy();
497
+ });
498
+
499
+ test('should set and get position', () => {
500
+ const tooltip = createMockTooltip({
501
+ position: TOOLTIP_POSITIONS.BOTTOM
502
+ });
503
+
504
+ // Initial position
505
+ expect(tooltip.getPosition()).toBe(TOOLTIP_POSITIONS.BOTTOM);
506
+
507
+ // Change position
508
+ tooltip.setPosition(TOOLTIP_POSITIONS.RIGHT);
509
+ expect(tooltip.getPosition()).toBe(TOOLTIP_POSITIONS.RIGHT);
510
+ expect(tooltip.element.className).toContain(`mtrl-tooltip--${TOOLTIP_POSITIONS.RIGHT}`);
511
+ expect(tooltip.element.className).not.toContain(`mtrl-tooltip--${TOOLTIP_POSITIONS.BOTTOM}`);
512
+
513
+ // Clean up
514
+ tooltip.destroy();
515
+ });
516
+
517
+ test('should change target element', () => {
518
+ const initialTarget = document.createElement('button');
519
+ initialTarget.textContent = 'Initial';
520
+ document.body.appendChild(initialTarget);
521
+
522
+ const newTarget = document.createElement('button');
523
+ newTarget.textContent = 'New';
524
+ document.body.appendChild(newTarget);
525
+
526
+ const tooltip = createMockTooltip({
527
+ target: initialTarget
528
+ });
529
+
530
+ // Initial target
531
+ expect(tooltip.target).toBe(initialTarget);
532
+ expect(initialTarget.getAttribute('aria-describedby')).toBe(tooltip.element.id);
533
+
534
+ // Change target
535
+ tooltip.setTarget(newTarget);
536
+ expect(tooltip.target).toBe(newTarget);
537
+ expect(newTarget.getAttribute('aria-describedby')).toBe(tooltip.element.id);
538
+ expect(initialTarget.getAttribute('aria-describedby')).toBeNull();
539
+
540
+ // Clean up
541
+ tooltip.destroy();
542
+ document.body.removeChild(initialTarget);
543
+ document.body.removeChild(newTarget);
544
+ });
545
+
546
+ test('should show and hide tooltip', () => {
547
+ const targetButton = document.createElement('button');
548
+ document.body.appendChild(targetButton);
549
+
550
+ const tooltip = createMockTooltip({
551
+ target: targetButton
552
+ });
553
+
554
+ // Initially hidden
555
+ expect(tooltip.isVisible()).toBe(false);
556
+
557
+ // Show immediately
558
+ tooltip.show(true);
559
+ expect(tooltip.isVisible()).toBe(true);
560
+ expect(tooltip.element.getAttribute('aria-hidden')).toBe('false');
561
+ expect(tooltip.element.className).toContain('mtrl-tooltip--visible');
562
+
563
+ // Hide immediately
564
+ tooltip.hide(true);
565
+ expect(tooltip.isVisible()).toBe(false);
566
+ expect(tooltip.element.getAttribute('aria-hidden')).toBe('true');
567
+ expect(tooltip.element.className).not.toContain('mtrl-tooltip--visible');
568
+
569
+ // Clean up
570
+ document.body.removeChild(targetButton);
571
+ tooltip.destroy();
572
+ });
573
+
574
+ test('should update position', () => {
575
+ const targetButton = document.createElement('button');
576
+ document.body.appendChild(targetButton);
577
+
578
+ const tooltip = createMockTooltip({
579
+ target: targetButton,
580
+ position: TOOLTIP_POSITIONS.BOTTOM
581
+ });
582
+
583
+ // Initial position (this is simplified in our mock)
584
+ const initialTop = tooltip.element.style.top;
585
+ const initialLeft = tooltip.element.style.left;
586
+
587
+ // Change position
588
+ tooltip.setPosition(TOOLTIP_POSITIONS.TOP);
589
+ tooltip.updatePosition();
590
+
591
+ // The exact values aren't important for the test, just that they updated
592
+ expect(tooltip.element.style.top).toBeDefined();
593
+ expect(tooltip.element.style.left).toBeDefined();
594
+
595
+ // Check arrow position updated
596
+ const arrowElement = tooltip.element.querySelector('.mtrl-tooltip__arrow') as HTMLElement;
597
+ expect(arrowElement.className).toContain(`mtrl-tooltip__arrow--${TOOLTIP_POSITIONS.TOP}`);
598
+
599
+ // Clean up
600
+ tooltip.destroy();
601
+ document.body.removeChild(targetButton);
602
+ });
603
+
604
+ test('should clean up resources when destroyed', () => {
605
+ const targetButton = document.createElement('button');
606
+ document.body.appendChild(targetButton);
607
+
608
+ const tooltip = createMockTooltip({
609
+ target: targetButton
610
+ });
611
+
612
+ // Tooltip should be in the document
613
+ expect(document.body.contains(tooltip.element)).toBe(true);
614
+
615
+ // Target should have aria-describedby
616
+ expect(targetButton.getAttribute('aria-describedby')).toBe(tooltip.element.id);
617
+
618
+ // Destroy the tooltip
619
+ tooltip.destroy();
620
+
621
+ // Tooltip should be removed from the document
622
+ expect(document.body.contains(tooltip.element)).toBe(false);
623
+
624
+ // Target should no longer have aria-describedby
625
+ expect(targetButton.getAttribute('aria-describedby')).toBeNull();
626
+
627
+ // Clean up
628
+ document.body.removeChild(targetButton);
629
+ });
630
+ });