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.
- package/CLAUDE.md +33 -0
- package/package.json +3 -1
- package/src/components/button/button.ts +34 -5
- package/src/components/navigation/index.ts +4 -1
- package/src/components/navigation/system/core.ts +302 -0
- package/src/components/navigation/system/events.ts +240 -0
- package/src/components/navigation/system/index.ts +184 -0
- package/src/components/navigation/system/mobile.ts +278 -0
- package/src/components/navigation/system/state.ts +77 -0
- package/src/components/navigation/system/types.ts +364 -0
- package/src/components/navigation/types.ts +33 -0
- package/src/components/slider/config.ts +2 -2
- package/src/components/slider/features/controller.ts +1 -25
- package/src/components/slider/features/handlers.ts +0 -1
- package/src/components/slider/features/range.ts +7 -7
- package/src/components/slider/{structure.ts → schema.ts} +2 -13
- package/src/components/slider/slider.ts +3 -2
- package/src/components/snackbar/index.ts +7 -1
- package/src/components/snackbar/types.ts +25 -0
- package/src/components/switch/api.ts +16 -0
- package/src/components/switch/config.ts +1 -18
- package/src/components/switch/features.ts +198 -0
- package/src/components/switch/index.ts +6 -1
- package/src/components/switch/switch.ts +3 -3
- package/src/components/switch/types.ts +27 -2
- package/src/components/textfield/index.ts +7 -1
- package/src/components/textfield/types.ts +36 -0
- package/src/core/composition/features/dom.ts +26 -14
- package/src/core/composition/features/icon.ts +18 -18
- package/src/core/composition/features/index.ts +3 -2
- package/src/core/composition/features/label.ts +16 -17
- package/src/core/composition/features/layout.ts +47 -0
- package/src/core/composition/index.ts +4 -4
- package/src/core/layout/README.md +350 -0
- package/src/core/layout/array.ts +181 -0
- package/src/core/layout/create.ts +55 -0
- package/src/core/layout/index.ts +26 -0
- package/src/core/layout/object.ts +124 -0
- package/src/core/layout/processor.ts +58 -0
- package/src/core/layout/result.ts +85 -0
- package/src/core/layout/types.ts +125 -0
- package/src/core/layout/utils.ts +136 -0
- package/src/styles/abstract/_variables.scss +28 -0
- package/src/styles/components/_switch.scss +133 -69
- package/src/styles/components/_textfield.scss +9 -16
- package/test/components/badge.test.ts +545 -0
- package/test/components/bottom-app-bar.test.ts +303 -0
- package/test/components/button.test.ts +233 -0
- package/test/components/card.test.ts +560 -0
- package/test/components/carousel.test.ts +951 -0
- package/test/components/checkbox.test.ts +462 -0
- package/test/components/chip.test.ts +692 -0
- package/test/components/datepicker.test.ts +1124 -0
- package/test/components/dialog.test.ts +990 -0
- package/test/components/divider.test.ts +412 -0
- package/test/components/extended-fab.test.ts +672 -0
- package/test/components/fab.test.ts +561 -0
- package/test/components/list.test.ts +365 -0
- package/test/components/menu.test.ts +718 -0
- package/test/components/navigation.test.ts +186 -0
- package/test/components/progress.test.ts +567 -0
- package/test/components/radios.test.ts +699 -0
- package/test/components/search.test.ts +1135 -0
- package/test/components/segmented-button.test.ts +732 -0
- package/test/components/sheet.test.ts +641 -0
- package/test/components/slider.test.ts +1220 -0
- package/test/components/snackbar.test.ts +461 -0
- package/test/components/switch.test.ts +452 -0
- package/test/components/tabs.test.ts +1369 -0
- package/test/components/textfield.test.ts +400 -0
- package/test/components/timepicker.test.ts +592 -0
- package/test/components/tooltip.test.ts +630 -0
- package/test/components/top-app-bar.test.ts +566 -0
- package/test/core/dom.attributes.test.ts +148 -0
- package/test/core/dom.classes.test.ts +152 -0
- package/test/core/dom.events.test.ts +243 -0
- package/test/core/emitter.test.ts +141 -0
- package/test/core/ripple.test.ts +99 -0
- package/test/core/state.store.test.ts +189 -0
- package/test/core/utils.normalize.test.ts +61 -0
- package/test/core/utils.object.test.ts +120 -0
- package/test/setup.ts +451 -0
- package/tsconfig.json +2 -2
- package/src/components/navigation/system-types.ts +0 -124
- package/src/components/navigation/system.ts +0 -776
- package/src/components/snackbar/constants.ts +0 -26
- package/src/core/composition/features/structure.ts +0 -22
- package/src/core/layout/index.js +0 -95
- package/src/core/structure.ts +0 -288
- package/test/components/button.test.js +0 -170
- package/test/components/checkbox.test.js +0 -238
- package/test/components/list.test.js +0 -105
- package/test/components/menu.test.js +0 -385
- package/test/components/navigation.test.js +0 -227
- package/test/components/snackbar.test.js +0 -234
- package/test/components/switch.test.js +0 -186
- package/test/components/textfield.test.js +0 -314
- package/test/core/emitter.test.js +0 -141
- 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
|
+
});
|