mtrl 0.3.1 → 0.3.2

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 (159) hide show
  1. package/.env +15 -0
  2. package/CONTRIBUTING.md +8 -8
  3. package/DOCS.md +3 -3
  4. package/README.md +43 -20
  5. package/TESTING.md +128 -18
  6. package/dist/index.js +14865 -0
  7. package/git-user-stats.js +545 -0
  8. package/index.ts +9 -67
  9. package/package.json +8 -3
  10. package/src/components/badge/api.ts +15 -1
  11. package/src/components/badge/badge.ts +43 -4
  12. package/src/components/badge/config.ts +40 -8
  13. package/src/components/badge/index.ts +64 -3
  14. package/src/components/badge/types.ts +175 -33
  15. package/src/components/button/api.ts +63 -1
  16. package/src/components/button/button.ts +39 -3
  17. package/src/components/button/config.ts +21 -4
  18. package/src/components/button/index.ts +26 -1
  19. package/src/components/button/types.ts +7 -1
  20. package/src/components/card/api.ts +78 -9
  21. package/src/components/card/card.ts +58 -3
  22. package/src/components/card/config.ts +41 -11
  23. package/src/components/card/features.ts +39 -12
  24. package/src/components/card/index.ts +84 -19
  25. package/src/components/card/types.ts +218 -29
  26. package/src/components/carousel/carousel.ts +92 -28
  27. package/src/components/carousel/constants.ts +107 -21
  28. package/src/components/carousel/index.ts +31 -13
  29. package/src/components/checkbox/checkbox.ts +83 -16
  30. package/src/components/checkbox/index.ts +43 -1
  31. package/src/components/checkbox/types.ts +219 -32
  32. package/src/components/chips/api.ts +194 -0
  33. package/src/components/{chip → chips/chip}/api.ts +42 -2
  34. package/src/components/chips/chip/chip.ts +131 -0
  35. package/src/components/{chip → chips/chip}/config.ts +3 -3
  36. package/src/components/chips/chip/index.ts +3 -0
  37. package/src/components/chips/chips.md +481 -0
  38. package/src/components/chips/chips.ts +75 -0
  39. package/src/components/chips/config.ts +109 -0
  40. package/src/components/chips/constants.ts +61 -0
  41. package/src/components/chips/features/chip-items.ts +33 -0
  42. package/src/components/chips/features/container.ts +77 -0
  43. package/src/components/chips/features/controller.ts +448 -0
  44. package/src/components/chips/features/index.ts +5 -0
  45. package/src/components/chips/features/label.ts +108 -0
  46. package/src/components/chips/index.ts +11 -0
  47. package/src/components/chips/schema.ts +61 -0
  48. package/src/components/{chip → chips}/types.ts +203 -92
  49. package/src/components/dialog/dialog.ts +99 -16
  50. package/src/components/dialog/index.ts +97 -1
  51. package/src/components/dialog/types.ts +375 -69
  52. package/src/components/divider/config.ts +90 -6
  53. package/src/components/divider/divider.ts +32 -2
  54. package/src/components/divider/features.ts +26 -0
  55. package/src/components/divider/index.ts +30 -0
  56. package/src/components/divider/types.ts +86 -9
  57. package/src/components/extended-fab/api.ts +53 -1
  58. package/src/components/extended-fab/config.ts +29 -1
  59. package/src/components/extended-fab/extended-fab.ts +28 -0
  60. package/src/components/extended-fab/index.ts +36 -0
  61. package/src/components/extended-fab/types.ts +458 -13
  62. package/src/components/fab/api.ts +42 -2
  63. package/src/components/fab/config.ts +29 -1
  64. package/src/components/fab/fab.ts +16 -2
  65. package/src/components/fab/index.ts +35 -0
  66. package/src/components/fab/types.ts +374 -10
  67. package/src/components/list/api.ts +12 -2
  68. package/src/components/list/config.ts +21 -0
  69. package/src/components/list/features.ts +6 -0
  70. package/src/components/list/index.ts +56 -1
  71. package/src/components/list/list-item.ts +46 -2
  72. package/src/components/list/list.ts +73 -2
  73. package/src/components/list/types.ts +172 -0
  74. package/src/components/list/utils.ts +26 -2
  75. package/src/components/menu/api.ts +217 -20
  76. package/src/components/menu/config.ts +27 -0
  77. package/src/components/menu/features/visibility.ts +55 -6
  78. package/src/components/menu/index.ts +64 -0
  79. package/src/components/menu/menu-item.ts +46 -3
  80. package/src/components/menu/menu.ts +77 -1
  81. package/src/components/menu/types.ts +404 -39
  82. package/src/components/sheet/config.ts +1 -2
  83. package/src/components/sheet/features/gestures.ts +1 -1
  84. package/src/components/sheet/features/position.ts +1 -2
  85. package/src/components/sheet/features/state.ts +1 -1
  86. package/src/components/sheet/index.ts +10 -2
  87. package/src/components/sheet/sheet.ts +1 -2
  88. package/src/components/sheet/types.ts +29 -1
  89. package/src/components/slider/api.ts +1 -1
  90. package/src/components/slider/config.ts +1 -1
  91. package/src/components/slider/features/controller.ts +1 -1
  92. package/src/components/slider/features/handlers.ts +1 -1
  93. package/src/components/slider/features/states.ts +1 -1
  94. package/src/components/slider/index.ts +12 -5
  95. package/src/components/slider/schema.ts +1 -1
  96. package/src/components/slider/types.ts +31 -0
  97. package/src/components/tabs/tab-api.ts +1 -1
  98. package/src/components/tabs/types.ts +1 -1
  99. package/src/components/tooltip/api.ts +6 -2
  100. package/src/components/tooltip/config.ts +9 -28
  101. package/src/components/tooltip/index.ts +10 -1
  102. package/src/components/tooltip/types.ts +38 -3
  103. package/src/index.ts +129 -31
  104. package/src/styles/abstract/_mixins.scss +23 -9
  105. package/src/styles/abstract/_variables.scss +14 -4
  106. package/src/styles/components/_card.scss +1 -1
  107. package/src/styles/components/_chip.scss +323 -113
  108. package/src/styles/components/_tabs.scss +1 -1
  109. package/CLAUDE.md +0 -33
  110. package/src/components/checkbox/constants.ts +0 -37
  111. package/src/components/chip/chip-set.ts +0 -225
  112. package/src/components/chip/chip.ts +0 -118
  113. package/src/components/chip/constants.ts +0 -28
  114. package/src/components/chip/index.ts +0 -12
  115. package/src/components/list/constants.ts +0 -116
  116. package/src/components/sheet/constants.ts +0 -20
  117. package/src/components/slider/constants.ts +0 -32
  118. package/src/components/tooltip/constants.ts +0 -27
  119. package/test/components/badge.test.ts +0 -545
  120. package/test/components/bottom-app-bar.test.ts +0 -303
  121. package/test/components/button.test.ts +0 -233
  122. package/test/components/card.test.ts +0 -560
  123. package/test/components/carousel.test.ts +0 -951
  124. package/test/components/checkbox.test.ts +0 -462
  125. package/test/components/chip.test.ts +0 -692
  126. package/test/components/datepicker.test.ts +0 -1124
  127. package/test/components/dialog.test.ts +0 -990
  128. package/test/components/divider.test.ts +0 -412
  129. package/test/components/extended-fab.test.ts +0 -672
  130. package/test/components/fab.test.ts +0 -561
  131. package/test/components/list.test.ts +0 -365
  132. package/test/components/menu.test.ts +0 -718
  133. package/test/components/navigation.test.ts +0 -186
  134. package/test/components/progress.test.ts +0 -567
  135. package/test/components/radios.test.ts +0 -699
  136. package/test/components/search.test.ts +0 -1135
  137. package/test/components/segmented-button.test.ts +0 -732
  138. package/test/components/sheet.test.ts +0 -641
  139. package/test/components/slider.test.ts +0 -1220
  140. package/test/components/snackbar.test.ts +0 -461
  141. package/test/components/switch.test.ts +0 -452
  142. package/test/components/tabs.test.ts +0 -1369
  143. package/test/components/textfield.test.ts +0 -400
  144. package/test/components/timepicker.test.ts +0 -592
  145. package/test/components/tooltip.test.ts +0 -630
  146. package/test/components/top-app-bar.test.ts +0 -566
  147. package/test/core/dom.attributes.test.ts +0 -148
  148. package/test/core/dom.classes.test.ts +0 -152
  149. package/test/core/dom.events.test.ts +0 -243
  150. package/test/core/emitter.test.ts +0 -141
  151. package/test/core/ripple.test.ts +0 -99
  152. package/test/core/state.store.test.ts +0 -189
  153. package/test/core/utils.normalize.test.ts +0 -61
  154. package/test/core/utils.object.test.ts +0 -120
  155. package/test/setup.js +0 -371
  156. package/test/setup.ts +0 -451
  157. package/tsconfig.json +0 -22
  158. package/typedoc.json +0 -28
  159. package/typedoc.simple.json +0 -14
@@ -1,1135 +0,0 @@
1
- // test/components/search.test.ts
2
- import { describe, test, expect } from 'bun:test';
3
- import {
4
- type SearchComponent,
5
- type SearchConfig,
6
- type NavVariant,
7
- type SearchEventType,
8
- type SearchEvent
9
- } from '../../src/components/search/types';
10
-
11
- // Constants for search variants
12
- const SEARCH_VARIANTS = {
13
- RAIL: 'rail',
14
- DRAWER: 'drawer',
15
- BAR: 'bar',
16
- MODAL: 'modal',
17
- STANDARD: 'standard'
18
- } as const;
19
-
20
- // Constants for search events
21
- const SEARCH_EVENTS = {
22
- FOCUS: 'focus',
23
- BLUR: 'blur',
24
- INPUT: 'input',
25
- SUBMIT: 'submit',
26
- CLEAR: 'clear',
27
- ICON_CLICK: 'iconClick'
28
- } as const;
29
-
30
- // Mock search implementation
31
- const createMockSearch = (config: SearchConfig = {}): SearchComponent => {
32
- // Create main container element
33
- const element = document.createElement('div');
34
- element.className = 'mtrl-search';
35
-
36
- // Default settings
37
- const settings = {
38
- variant: config.variant || SEARCH_VARIANTS.STANDARD,
39
- disabled: config.disabled || false,
40
- placeholder: config.placeholder || 'Search',
41
- value: config.value || '',
42
- leadingIcon: config.leadingIcon !== undefined ? config.leadingIcon : '<svg>search</svg>',
43
- trailingIcon: config.trailingIcon || '',
44
- trailingIcon2: config.trailingIcon2 || '',
45
- avatar: config.avatar || '',
46
- showClearButton: config.showClearButton !== undefined ? config.showClearButton : true,
47
- suggestions: config.suggestions || [],
48
- showDividers: config.showDividers || false,
49
- expanded: false,
50
- hasFocus: false
51
- };
52
-
53
- // Apply variant class
54
- element.classList.add(`mtrl-search--${settings.variant}`);
55
-
56
- // Apply disabled state
57
- if (settings.disabled) {
58
- element.classList.add('mtrl-search--disabled');
59
- }
60
-
61
- // Apply additional classes
62
- if (config.class) {
63
- const classes = config.class.split(' ');
64
- classes.forEach(className => element.classList.add(className));
65
- }
66
-
67
- // Apply width styles
68
- if (config.fullWidth) {
69
- element.classList.add('mtrl-search--full-width');
70
- element.style.width = '100%';
71
- } else {
72
- if (config.maxWidth) {
73
- element.style.maxWidth = `${config.maxWidth}px`;
74
- }
75
-
76
- if (config.minWidth) {
77
- element.style.minWidth = `${config.minWidth}px`;
78
- }
79
- }
80
-
81
- // Create search input container
82
- const inputContainer = document.createElement('div');
83
- inputContainer.className = 'mtrl-search__container';
84
-
85
- // Create leading icon if provided
86
- if (settings.leadingIcon && settings.leadingIcon !== 'none') {
87
- const leadingIconElement = document.createElement('div');
88
- leadingIconElement.className = 'mtrl-search__leading-icon';
89
- leadingIconElement.innerHTML = settings.leadingIcon;
90
- inputContainer.appendChild(leadingIconElement);
91
- }
92
-
93
- // Create search input
94
- const input = document.createElement('input');
95
- input.type = 'text';
96
- input.className = 'mtrl-search__input';
97
- input.placeholder = settings.placeholder;
98
- input.value = settings.value;
99
- input.disabled = settings.disabled;
100
- inputContainer.appendChild(input);
101
-
102
- // Create clear button if enabled
103
- let clearButton: HTMLElement | null = null;
104
- if (settings.showClearButton) {
105
- clearButton = document.createElement('button');
106
- clearButton.className = 'mtrl-search__clear';
107
- clearButton.type = 'button';
108
- clearButton.innerHTML = '<svg>clear</svg>';
109
- clearButton.style.display = settings.value ? 'block' : 'none';
110
- inputContainer.appendChild(clearButton);
111
- }
112
-
113
- // Create avatar if provided
114
- if (settings.avatar) {
115
- const avatarElement = document.createElement('div');
116
- avatarElement.className = 'mtrl-search__avatar';
117
- avatarElement.innerHTML = settings.avatar;
118
- inputContainer.appendChild(avatarElement);
119
- }
120
-
121
- // Create trailing icons if provided
122
- if (settings.trailingIcon) {
123
- const trailingIconElement = document.createElement('div');
124
- trailingIconElement.className = 'mtrl-search__trailing-icon';
125
- trailingIconElement.innerHTML = settings.trailingIcon;
126
- inputContainer.appendChild(trailingIconElement);
127
- }
128
-
129
- if (settings.trailingIcon2) {
130
- const trailingIcon2Element = document.createElement('div');
131
- trailingIcon2Element.className = 'mtrl-search__trailing-icon';
132
- trailingIcon2Element.innerHTML = settings.trailingIcon2;
133
- inputContainer.appendChild(trailingIcon2Element);
134
- }
135
-
136
- element.appendChild(inputContainer);
137
-
138
- // Create suggestions container if suggestions provided
139
- let suggestionsContainer: HTMLElement | null = null;
140
- if (settings.suggestions.length > 0) {
141
- suggestionsContainer = document.createElement('div');
142
- suggestionsContainer.className = 'mtrl-search__suggestions';
143
- suggestionsContainer.style.display = 'none';
144
-
145
- const renderSuggestions = () => {
146
- if (!suggestionsContainer) return;
147
-
148
- suggestionsContainer.innerHTML = '';
149
-
150
- settings.suggestions.forEach((suggestion, index) => {
151
- const suggestionElement = document.createElement('div');
152
- suggestionElement.className = 'mtrl-search__suggestion';
153
-
154
- if (typeof suggestion === 'string') {
155
- suggestionElement.textContent = suggestion;
156
- suggestionElement.setAttribute('data-value', suggestion);
157
- } else {
158
- if (suggestion.icon) {
159
- const iconElement = document.createElement('span');
160
- iconElement.className = 'mtrl-search__suggestion-icon';
161
- iconElement.innerHTML = suggestion.icon;
162
- suggestionElement.appendChild(iconElement);
163
- }
164
-
165
- const textElement = document.createElement('span');
166
- textElement.className = 'mtrl-search__suggestion-text';
167
- textElement.textContent = suggestion.text;
168
- suggestionElement.appendChild(textElement);
169
-
170
- suggestionElement.setAttribute('data-value', suggestion.value || suggestion.text);
171
- }
172
-
173
- // Add divider if enabled and not the last item
174
- if (settings.showDividers && index < settings.suggestions.length - 1) {
175
- const divider = document.createElement('div');
176
- divider.className = 'mtrl-search__suggestion-divider';
177
- suggestionsContainer.appendChild(suggestionElement);
178
- suggestionsContainer.appendChild(divider);
179
- } else {
180
- suggestionsContainer.appendChild(suggestionElement);
181
- }
182
-
183
- // Handle suggestion click
184
- suggestionElement.addEventListener('click', () => {
185
- const value = suggestionElement.getAttribute('data-value') || '';
186
- search.setValue(value, true);
187
- search.submit();
188
- search.showSuggestions(false);
189
- });
190
- });
191
- };
192
-
193
- renderSuggestions();
194
- element.appendChild(suggestionsContainer);
195
- }
196
-
197
- // Track event handlers
198
- const eventHandlers: Record<string, Function[]> = {};
199
-
200
- // Emit an event
201
- const emit = (event: SearchEventType, originalEvent?: Event | null): boolean => {
202
- let defaultPrevented = false;
203
-
204
- const eventData: SearchEvent = {
205
- search,
206
- value: input.value,
207
- originalEvent: originalEvent || null,
208
- preventDefault: () => {
209
- defaultPrevented = true;
210
- },
211
- defaultPrevented: false
212
- };
213
-
214
- // Call handlers from config.on
215
- if (config.on && config.on[event]) {
216
- config.on[event]!(eventData);
217
- }
218
-
219
- // Call registered event handlers
220
- if (eventHandlers[event]) {
221
- eventHandlers[event].forEach(handler => handler(eventData));
222
- }
223
-
224
- // Direct callback handlers
225
- if (event === SEARCH_EVENTS.SUBMIT && config.onSubmit) {
226
- config.onSubmit(input.value);
227
- } else if (event === SEARCH_EVENTS.INPUT && config.onInput) {
228
- config.onInput(input.value);
229
- } else if (event === SEARCH_EVENTS.CLEAR && config.onClear) {
230
- config.onClear();
231
- }
232
-
233
- eventData.defaultPrevented = defaultPrevented;
234
- return defaultPrevented;
235
- };
236
-
237
- // Set up event handlers
238
- input.addEventListener('focus', (e) => {
239
- if (!settings.disabled) {
240
- settings.hasFocus = true;
241
- element.classList.add('mtrl-search--focused');
242
-
243
- if (suggestionsContainer && settings.suggestions.length > 0) {
244
- suggestionsContainer.style.display = 'block';
245
- }
246
-
247
- emit(SEARCH_EVENTS.FOCUS, e);
248
- }
249
- });
250
-
251
- input.addEventListener('blur', (e) => {
252
- settings.hasFocus = false;
253
- element.classList.remove('mtrl-search--focused');
254
-
255
- // Delay hiding suggestions to allow for clicks
256
- setTimeout(() => {
257
- if (suggestionsContainer && !settings.hasFocus) {
258
- suggestionsContainer.style.display = 'none';
259
- }
260
- }, 200);
261
-
262
- emit(SEARCH_EVENTS.BLUR, e);
263
- });
264
-
265
- input.addEventListener('input', (e) => {
266
- if (!settings.disabled) {
267
- if (clearButton) {
268
- clearButton.style.display = input.value ? 'block' : 'none';
269
- }
270
-
271
- emit(SEARCH_EVENTS.INPUT, e);
272
- }
273
- });
274
-
275
- input.addEventListener('keydown', (e) => {
276
- if (e.key === 'Enter' && !settings.disabled) {
277
- search.submit();
278
- }
279
- });
280
-
281
- // Clear button handler
282
- if (clearButton) {
283
- clearButton.addEventListener('click', (e) => {
284
- if (!settings.disabled) {
285
- search.clear();
286
- emit(SEARCH_EVENTS.CLEAR, e);
287
- }
288
- });
289
- }
290
-
291
- // Create the search component
292
- const search: SearchComponent = {
293
- element,
294
-
295
- setValue: (value: string, triggerEvent: boolean = false) => {
296
- input.value = value;
297
-
298
- if (clearButton) {
299
- clearButton.style.display = value ? 'block' : 'none';
300
- }
301
-
302
- if (triggerEvent) {
303
- emit(SEARCH_EVENTS.INPUT);
304
- }
305
-
306
- return search;
307
- },
308
-
309
- getValue: () => input.value,
310
-
311
- setPlaceholder: (text: string) => {
312
- settings.placeholder = text;
313
- input.placeholder = text;
314
- return search;
315
- },
316
-
317
- getPlaceholder: () => settings.placeholder,
318
-
319
- setLeadingIcon: (iconHtml: string) => {
320
- const existingIcon = element.querySelector('.mtrl-search__leading-icon');
321
-
322
- if (iconHtml === 'none') {
323
- if (existingIcon) {
324
- existingIcon.remove();
325
- }
326
- } else {
327
- if (existingIcon) {
328
- existingIcon.innerHTML = iconHtml;
329
- } else {
330
- const leadingIconElement = document.createElement('div');
331
- leadingIconElement.className = 'mtrl-search__leading-icon';
332
- leadingIconElement.innerHTML = iconHtml;
333
- inputContainer.insertBefore(leadingIconElement, inputContainer.firstChild);
334
- }
335
- }
336
-
337
- settings.leadingIcon = iconHtml;
338
- return search;
339
- },
340
-
341
- setTrailingIcon: (iconHtml: string) => {
342
- const existingIcon = element.querySelector('.mtrl-search__trailing-icon');
343
-
344
- if (iconHtml === 'none') {
345
- if (existingIcon) {
346
- existingIcon.remove();
347
- }
348
- } else {
349
- if (existingIcon) {
350
- existingIcon.innerHTML = iconHtml;
351
- } else {
352
- const trailingIconElement = document.createElement('div');
353
- trailingIconElement.className = 'mtrl-search__trailing-icon';
354
- trailingIconElement.innerHTML = iconHtml;
355
- inputContainer.appendChild(trailingIconElement);
356
- }
357
- }
358
-
359
- settings.trailingIcon = iconHtml;
360
- return search;
361
- },
362
-
363
- setTrailingIcon2: (iconHtml: string) => {
364
- const existingIcons = element.querySelectorAll('.mtrl-search__trailing-icon');
365
- const existingIcon2 = existingIcons.length > 1 ? existingIcons[1] : null;
366
-
367
- if (iconHtml === 'none') {
368
- if (existingIcon2) {
369
- existingIcon2.remove();
370
- }
371
- } else {
372
- if (existingIcon2) {
373
- existingIcon2.innerHTML = iconHtml;
374
- } else {
375
- const trailingIcon2Element = document.createElement('div');
376
- trailingIcon2Element.className = 'mtrl-search__trailing-icon';
377
- trailingIcon2Element.innerHTML = iconHtml;
378
- inputContainer.appendChild(trailingIcon2Element);
379
- }
380
- }
381
-
382
- settings.trailingIcon2 = iconHtml;
383
- return search;
384
- },
385
-
386
- setAvatar: (avatarHtml: string) => {
387
- const existingAvatar = element.querySelector('.mtrl-search__avatar');
388
-
389
- if (avatarHtml === 'none') {
390
- if (existingAvatar) {
391
- existingAvatar.remove();
392
- }
393
- } else {
394
- if (existingAvatar) {
395
- existingAvatar.innerHTML = avatarHtml;
396
- } else {
397
- const avatarElement = document.createElement('div');
398
- avatarElement.className = 'mtrl-search__avatar';
399
- avatarElement.innerHTML = avatarHtml;
400
- inputContainer.appendChild(avatarElement);
401
- }
402
- }
403
-
404
- settings.avatar = avatarHtml;
405
- return search;
406
- },
407
-
408
- showClearButton: (show: boolean) => {
409
- settings.showClearButton = show;
410
-
411
- if (show) {
412
- if (!clearButton) {
413
- clearButton = document.createElement('button');
414
- clearButton.className = 'mtrl-search__clear';
415
- clearButton.type = 'button';
416
- clearButton.innerHTML = '<svg>clear</svg>';
417
-
418
- clearButton.addEventListener('click', (e) => {
419
- if (!settings.disabled) {
420
- search.clear();
421
- emit(SEARCH_EVENTS.CLEAR, e);
422
- }
423
- });
424
-
425
- inputContainer.appendChild(clearButton);
426
- }
427
-
428
- clearButton.style.display = input.value ? 'block' : 'none';
429
- } else {
430
- if (clearButton) {
431
- clearButton.remove();
432
- clearButton = null;
433
- }
434
- }
435
-
436
- return search;
437
- },
438
-
439
- setSuggestions: (suggestions: string[] | Array<{text: string, value?: string, icon?: string}>) => {
440
- settings.suggestions = suggestions;
441
-
442
- if (suggestions.length > 0) {
443
- if (!suggestionsContainer) {
444
- suggestionsContainer = document.createElement('div');
445
- suggestionsContainer.className = 'mtrl-search__suggestions';
446
- suggestionsContainer.style.display = settings.hasFocus ? 'block' : 'none';
447
- element.appendChild(suggestionsContainer);
448
- }
449
-
450
- suggestionsContainer.innerHTML = '';
451
-
452
- suggestions.forEach((suggestion, index) => {
453
- const suggestionElement = document.createElement('div');
454
- suggestionElement.className = 'mtrl-search__suggestion';
455
-
456
- if (typeof suggestion === 'string') {
457
- suggestionElement.textContent = suggestion;
458
- suggestionElement.setAttribute('data-value', suggestion);
459
- } else {
460
- if (suggestion.icon) {
461
- const iconElement = document.createElement('span');
462
- iconElement.className = 'mtrl-search__suggestion-icon';
463
- iconElement.innerHTML = suggestion.icon;
464
- suggestionElement.appendChild(iconElement);
465
- }
466
-
467
- const textElement = document.createElement('span');
468
- textElement.className = 'mtrl-search__suggestion-text';
469
- textElement.textContent = suggestion.text;
470
- suggestionElement.appendChild(textElement);
471
-
472
- suggestionElement.setAttribute('data-value', suggestion.value || suggestion.text);
473
- }
474
-
475
- // Add divider if enabled and not the last item
476
- if (settings.showDividers && index < suggestions.length - 1) {
477
- const divider = document.createElement('div');
478
- divider.className = 'mtrl-search__suggestion-divider';
479
- suggestionsContainer.appendChild(suggestionElement);
480
- suggestionsContainer.appendChild(divider);
481
- } else {
482
- suggestionsContainer.appendChild(suggestionElement);
483
- }
484
-
485
- // Handle suggestion click
486
- suggestionElement.addEventListener('click', () => {
487
- const value = suggestionElement.getAttribute('data-value') || '';
488
- search.setValue(value, true);
489
- search.submit();
490
- search.showSuggestions(false);
491
- });
492
- });
493
- } else if (suggestionsContainer) {
494
- suggestionsContainer.remove();
495
- suggestionsContainer = null;
496
- }
497
-
498
- return search;
499
- },
500
-
501
- showSuggestions: (show: boolean) => {
502
- if (suggestionsContainer) {
503
- suggestionsContainer.style.display = show ? 'block' : 'none';
504
- }
505
-
506
- return search;
507
- },
508
-
509
- focus: () => {
510
- if (!settings.disabled) {
511
- input.focus();
512
- }
513
-
514
- return search;
515
- },
516
-
517
- blur: () => {
518
- input.blur();
519
- return search;
520
- },
521
-
522
- expand: () => {
523
- if (settings.variant === SEARCH_VARIANTS.BAR && !settings.expanded) {
524
- settings.expanded = true;
525
- element.classList.add('mtrl-search--expanded');
526
- input.focus();
527
- }
528
-
529
- return search;
530
- },
531
-
532
- collapse: () => {
533
- if (settings.variant === SEARCH_VARIANTS.BAR && settings.expanded) {
534
- settings.expanded = false;
535
- element.classList.remove('mtrl-search--expanded');
536
- input.blur();
537
- }
538
-
539
- return search;
540
- },
541
-
542
- clear: () => {
543
- input.value = '';
544
-
545
- if (clearButton) {
546
- clearButton.style.display = 'none';
547
- }
548
-
549
- emit(SEARCH_EVENTS.CLEAR);
550
-
551
- return search;
552
- },
553
-
554
- submit: () => {
555
- emit(SEARCH_EVENTS.SUBMIT);
556
- return search;
557
- },
558
-
559
- enable: () => {
560
- settings.disabled = false;
561
- input.disabled = false;
562
- element.classList.remove('mtrl-search--disabled');
563
- return search;
564
- },
565
-
566
- disable: () => {
567
- settings.disabled = true;
568
- input.disabled = true;
569
- element.classList.add('mtrl-search--disabled');
570
- return search;
571
- },
572
-
573
- isDisabled: () => settings.disabled,
574
-
575
- on: (event: SearchEventType, handler: (event: SearchEvent) => void) => {
576
- if (!eventHandlers[event]) {
577
- eventHandlers[event] = [];
578
- }
579
-
580
- eventHandlers[event].push(handler);
581
- return search;
582
- },
583
-
584
- off: (event: SearchEventType, handler: (event: SearchEvent) => void) => {
585
- if (eventHandlers[event]) {
586
- eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
587
- }
588
-
589
- return search;
590
- },
591
-
592
- destroy: () => {
593
- // Remove element from DOM if it has a parent
594
- if (element.parentNode) {
595
- element.parentNode.removeChild(element);
596
- }
597
-
598
- // Clear event handlers
599
- for (const event in eventHandlers) {
600
- eventHandlers[event] = [];
601
- }
602
- }
603
- };
604
-
605
- return search;
606
- };
607
-
608
- describe('Search Component', () => {
609
- test('should create a search component', () => {
610
- const search = createMockSearch({
611
- placeholder: 'Search items'
612
- });
613
-
614
- expect(search.element).toBeDefined();
615
- expect(search.element.tagName).toBe('DIV');
616
- expect(search.element.className).toContain('mtrl-search');
617
-
618
- const input = search.element.querySelector('input');
619
- expect(input).toBeDefined();
620
- expect(input?.className).toContain('mtrl-search__input');
621
- expect(input?.placeholder).toBe('Search items');
622
- });
623
-
624
- test('should apply variant classes', () => {
625
- const variants: NavVariant[] = [
626
- SEARCH_VARIANTS.STANDARD,
627
- SEARCH_VARIANTS.BAR,
628
- SEARCH_VARIANTS.DRAWER,
629
- SEARCH_VARIANTS.MODAL,
630
- SEARCH_VARIANTS.RAIL
631
- ];
632
-
633
- variants.forEach(variant => {
634
- const search = createMockSearch({ variant });
635
- expect(search.element.className).toContain(`mtrl-search--${variant}`);
636
- });
637
- });
638
-
639
- test('should apply disabled state', () => {
640
- const search = createMockSearch({
641
- disabled: true
642
- });
643
-
644
- expect(search.element.className).toContain('mtrl-search--disabled');
645
-
646
- const input = search.element.querySelector('input');
647
- expect(input?.disabled).toBe(true);
648
-
649
- expect(search.isDisabled()).toBe(true);
650
- });
651
-
652
- test('should render leading icon by default', () => {
653
- const search = createMockSearch();
654
-
655
- const leadingIcon = search.element.querySelector('.mtrl-search__leading-icon');
656
- expect(leadingIcon).toBeDefined();
657
- expect(leadingIcon?.innerHTML).toBe('<svg>search</svg>');
658
- });
659
-
660
- test('should support no leading icon with "none" value', () => {
661
- const search = createMockSearch({
662
- leadingIcon: 'none'
663
- });
664
-
665
- const leadingIcon = search.element.querySelector('.mtrl-search__leading-icon');
666
- expect(leadingIcon).toBeNull();
667
- });
668
-
669
- test('should render trailing icons when provided', () => {
670
- const search = createMockSearch({
671
- trailingIcon: '<svg>filter</svg>',
672
- trailingIcon2: '<svg>more</svg>'
673
- });
674
-
675
- const trailingIcons = search.element.querySelectorAll('.mtrl-search__trailing-icon');
676
- expect(trailingIcons.length).toBe(2);
677
- expect(trailingIcons[0].innerHTML).toBe('<svg>filter</svg>');
678
- expect(trailingIcons[1].innerHTML).toBe('<svg>more</svg>');
679
- });
680
-
681
- test('should render avatar when provided', () => {
682
- const avatarHtml = '<img src="avatar.jpg" alt="User">';
683
- const search = createMockSearch({
684
- avatar: avatarHtml
685
- });
686
-
687
- const avatar = search.element.querySelector('.mtrl-search__avatar');
688
- expect(avatar).toBeDefined();
689
- expect(avatar?.innerHTML).toBe(avatarHtml);
690
- });
691
-
692
- test('should show clear button by default when value is provided', () => {
693
- const search = createMockSearch({
694
- value: 'test query'
695
- });
696
-
697
- const clearButton = search.element.querySelector('.mtrl-search__clear');
698
- expect(clearButton).toBeDefined();
699
- expect(clearButton?.style.display).not.toBe('none');
700
- });
701
-
702
- test('should hide clear button when value is empty', () => {
703
- const search = createMockSearch({
704
- value: ''
705
- });
706
-
707
- const clearButton = search.element.querySelector('.mtrl-search__clear');
708
- expect(clearButton).toBeDefined();
709
- expect(clearButton?.style.display).toBe('none');
710
- });
711
-
712
- test('should not render clear button when showClearButton is false', () => {
713
- const search = createMockSearch({
714
- showClearButton: false,
715
- value: 'test'
716
- });
717
-
718
- const clearButton = search.element.querySelector('.mtrl-search__clear');
719
- expect(clearButton).toBeNull();
720
- });
721
-
722
- test('should set and get value', () => {
723
- const search = createMockSearch();
724
-
725
- expect(search.getValue()).toBe('');
726
-
727
- search.setValue('test query');
728
-
729
- expect(search.getValue()).toBe('test query');
730
-
731
- const input = search.element.querySelector('input');
732
- expect(input?.value).toBe('test query');
733
- });
734
-
735
- test('should set and get placeholder', () => {
736
- const search = createMockSearch({
737
- placeholder: 'Original placeholder'
738
- });
739
-
740
- expect(search.getPlaceholder()).toBe('Original placeholder');
741
-
742
- search.setPlaceholder('New placeholder');
743
-
744
- expect(search.getPlaceholder()).toBe('New placeholder');
745
-
746
- const input = search.element.querySelector('input');
747
- expect(input?.placeholder).toBe('New placeholder');
748
- });
749
-
750
- test('should change leading icon', () => {
751
- const search = createMockSearch();
752
-
753
- const initialIcon = search.element.querySelector('.mtrl-search__leading-icon');
754
- expect(initialIcon?.innerHTML).toBe('<svg>search</svg>');
755
-
756
- search.setLeadingIcon('<svg>new-icon</svg>');
757
-
758
- const updatedIcon = search.element.querySelector('.mtrl-search__leading-icon');
759
- expect(updatedIcon?.innerHTML).toBe('<svg>new-icon</svg>');
760
- });
761
-
762
- test('should remove leading icon', () => {
763
- const search = createMockSearch();
764
-
765
- const initialIcon = search.element.querySelector('.mtrl-search__leading-icon');
766
- expect(initialIcon).not.toBeNull();
767
-
768
- search.setLeadingIcon('none');
769
-
770
- const updatedIcon = search.element.querySelector('.mtrl-search__leading-icon');
771
- expect(updatedIcon).toBeNull();
772
- });
773
-
774
- test('should change trailing icon', () => {
775
- const search = createMockSearch({
776
- trailingIcon: '<svg>menu</svg>'
777
- });
778
-
779
- const initialIcon = search.element.querySelector('.mtrl-search__trailing-icon');
780
- expect(initialIcon?.innerHTML).toBe('<svg>menu</svg>');
781
-
782
- search.setTrailingIcon('<svg>new-icon</svg>');
783
-
784
- const updatedIcon = search.element.querySelector('.mtrl-search__trailing-icon');
785
- expect(updatedIcon?.innerHTML).toBe('<svg>new-icon</svg>');
786
- });
787
-
788
- test('should change second trailing icon', () => {
789
- const search = createMockSearch({
790
- trailingIcon: '<svg>filter</svg>',
791
- trailingIcon2: '<svg>menu</svg>'
792
- });
793
-
794
- const trailingIcons = search.element.querySelectorAll('.mtrl-search__trailing-icon');
795
- expect(trailingIcons[1].innerHTML).toBe('<svg>menu</svg>');
796
-
797
- search.setTrailingIcon2('<svg>new-icon</svg>');
798
-
799
- const updatedIcons = search.element.querySelectorAll('.mtrl-search__trailing-icon');
800
- expect(updatedIcons[1].innerHTML).toBe('<svg>new-icon</svg>');
801
- });
802
-
803
- test('should change avatar', () => {
804
- const search = createMockSearch({
805
- avatar: '<img src="user1.jpg">'
806
- });
807
-
808
- const initialAvatar = search.element.querySelector('.mtrl-search__avatar');
809
- expect(initialAvatar?.innerHTML).toBe('<img src="user1.jpg">');
810
-
811
- search.setAvatar('<img src="user2.jpg">');
812
-
813
- const updatedAvatar = search.element.querySelector('.mtrl-search__avatar');
814
- expect(updatedAvatar?.innerHTML).toBe('<img src="user2.jpg">');
815
- });
816
-
817
- test('should render string suggestions', () => {
818
- const suggestions = ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'];
819
-
820
- const search = createMockSearch({
821
- suggestions
822
- });
823
-
824
- const suggestionsContainer = search.element.querySelector('.mtrl-search__suggestions');
825
- expect(suggestionsContainer).toBeDefined();
826
- expect(suggestionsContainer?.style.display).toBe('none');
827
-
828
- const suggestionElements = search.element.querySelectorAll('.mtrl-search__suggestion');
829
- expect(suggestionElements.length).toBe(3);
830
-
831
- expect(suggestionElements[0].textContent).toBe('Suggestion 1');
832
- expect(suggestionElements[1].textContent).toBe('Suggestion 2');
833
- expect(suggestionElements[2].textContent).toBe('Suggestion 3');
834
- });
835
-
836
- test('should render object suggestions with icons', () => {
837
- const suggestions = [
838
- { text: 'Suggestion 1', icon: '<svg>icon1</svg>' },
839
- { text: 'Suggestion 2', icon: '<svg>icon2</svg>' },
840
- { text: 'Suggestion 3', value: 'custom-value', icon: '<svg>icon3</svg>' }
841
- ];
842
-
843
- const search = createMockSearch({
844
- suggestions
845
- });
846
-
847
- const suggestionElements = search.element.querySelectorAll('.mtrl-search__suggestion');
848
- expect(suggestionElements.length).toBe(3);
849
-
850
- // Check for icons
851
- const icons = search.element.querySelectorAll('.mtrl-search__suggestion-icon');
852
- expect(icons.length).toBe(3);
853
-
854
- // Check text content
855
- const textElements = search.element.querySelectorAll('.mtrl-search__suggestion-text');
856
- expect(textElements.length).toBe(3);
857
- expect(textElements[0].textContent).toBe('Suggestion 1');
858
- expect(textElements[1].textContent).toBe('Suggestion 2');
859
- expect(textElements[2].textContent).toBe('Suggestion 3');
860
-
861
- // Check data-value attributes
862
- expect(suggestionElements[0].getAttribute('data-value')).toBe('Suggestion 1');
863
- expect(suggestionElements[1].getAttribute('data-value')).toBe('Suggestion 2');
864
- expect(suggestionElements[2].getAttribute('data-value')).toBe('custom-value');
865
- });
866
-
867
- test('should add dividers between suggestions when enabled', () => {
868
- const suggestions = ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'];
869
-
870
- const search = createMockSearch({
871
- suggestions,
872
- showDividers: true
873
- });
874
-
875
- const dividers = search.element.querySelectorAll('.mtrl-search__suggestion-divider');
876
- // There should be dividers between items (n-1 dividers)
877
- expect(dividers.length).toBe(2);
878
- });
879
-
880
- test('should update suggestions', () => {
881
- const search = createMockSearch();
882
-
883
- const newSuggestions = ['New 1', 'New 2', 'New 3'];
884
- search.setSuggestions(newSuggestions);
885
-
886
- const suggestionsContainer = search.element.querySelector('.mtrl-search__suggestions');
887
- expect(suggestionsContainer).toBeDefined();
888
-
889
- const suggestionElements = search.element.querySelectorAll('.mtrl-search__suggestion');
890
- expect(suggestionElements.length).toBe(3);
891
-
892
- expect(suggestionElements[0].textContent).toBe('New 1');
893
- expect(suggestionElements[1].textContent).toBe('New 2');
894
- expect(suggestionElements[2].textContent).toBe('New 3');
895
- });
896
-
897
- test('should show and hide suggestions', () => {
898
- const search = createMockSearch({
899
- suggestions: ['Suggestion 1', 'Suggestion 2']
900
- });
901
-
902
- const suggestionsContainer = search.element.querySelector('.mtrl-search__suggestions');
903
- expect(suggestionsContainer?.style.display).toBe('none');
904
-
905
- search.showSuggestions(true);
906
- expect(suggestionsContainer?.style.display).toBe('block');
907
-
908
- search.showSuggestions(false);
909
- expect(suggestionsContainer?.style.display).toBe('none');
910
- });
911
-
912
- test('should clear input value', () => {
913
- const search = createMockSearch({
914
- value: 'test query'
915
- });
916
-
917
- expect(search.getValue()).toBe('test query');
918
-
919
- search.clear();
920
-
921
- expect(search.getValue()).toBe('');
922
-
923
- const clearButton = search.element.querySelector('.mtrl-search__clear');
924
- expect(clearButton?.style.display).toBe('none');
925
- });
926
-
927
- test('should enable and disable search', () => {
928
- const search = createMockSearch();
929
-
930
- expect(search.isDisabled()).toBe(false);
931
-
932
- search.disable();
933
-
934
- expect(search.isDisabled()).toBe(true);
935
- expect(search.element.className).toContain('mtrl-search--disabled');
936
-
937
- const input = search.element.querySelector('input');
938
- expect(input?.disabled).toBe(true);
939
-
940
- search.enable();
941
-
942
- expect(search.isDisabled()).toBe(false);
943
- expect(search.element.className).not.toContain('mtrl-search--disabled');
944
- expect(input?.disabled).toBe(false);
945
- });
946
-
947
- test('should expand and collapse search bar', () => {
948
- const search = createMockSearch({
949
- variant: SEARCH_VARIANTS.BAR
950
- });
951
-
952
- expect(search.element.className).not.toContain('mtrl-search--expanded');
953
-
954
- search.expand();
955
-
956
- expect(search.element.className).toContain('mtrl-search--expanded');
957
-
958
- search.collapse();
959
-
960
- expect(search.element.className).not.toContain('mtrl-search--expanded');
961
- });
962
-
963
- test('should emit change events', () => {
964
- const search = createMockSearch();
965
-
966
- let inputEventFired = false;
967
- let eventValue = '';
968
-
969
- search.on(SEARCH_EVENTS.INPUT, (event) => {
970
- inputEventFired = true;
971
- eventValue = event.value;
972
- });
973
-
974
- search.setValue('test', true);
975
-
976
- expect(inputEventFired).toBe(true);
977
- expect(eventValue).toBe('test');
978
- });
979
-
980
- test('should emit submit events', () => {
981
- const search = createMockSearch({
982
- value: 'query'
983
- });
984
-
985
- let submitEventFired = false;
986
- let eventValue = '';
987
-
988
- search.on(SEARCH_EVENTS.SUBMIT, (event) => {
989
- submitEventFired = true;
990
- eventValue = event.value;
991
- });
992
-
993
- search.submit();
994
-
995
- expect(submitEventFired).toBe(true);
996
- expect(eventValue).toBe('query');
997
- });
998
-
999
- test('should emit clear events', () => {
1000
- const search = createMockSearch({
1001
- value: 'query'
1002
- });
1003
-
1004
- let clearEventFired = false;
1005
-
1006
- search.on(SEARCH_EVENTS.CLEAR, () => {
1007
- clearEventFired = true;
1008
- });
1009
-
1010
- search.clear();
1011
-
1012
- expect(clearEventFired).toBe(true);
1013
- expect(search.getValue()).toBe('');
1014
- });
1015
-
1016
- test('should call onSubmit callback', () => {
1017
- let callbackFired = false;
1018
- let callbackValue = '';
1019
-
1020
- const search = createMockSearch({
1021
- value: 'test',
1022
- onSubmit: (value) => {
1023
- callbackFired = true;
1024
- callbackValue = value;
1025
- }
1026
- });
1027
-
1028
- search.submit();
1029
-
1030
- expect(callbackFired).toBe(true);
1031
- expect(callbackValue).toBe('test');
1032
- });
1033
-
1034
- test('should call onInput callback', () => {
1035
- let callbackFired = false;
1036
- let callbackValue = '';
1037
-
1038
- const search = createMockSearch({
1039
- onInput: (value) => {
1040
- callbackFired = true;
1041
- callbackValue = value;
1042
- }
1043
- });
1044
-
1045
- search.setValue('input test', true);
1046
-
1047
- expect(callbackFired).toBe(true);
1048
- expect(callbackValue).toBe('input test');
1049
- });
1050
-
1051
- test('should call onClear callback', () => {
1052
- let callbackFired = false;
1053
-
1054
- const search = createMockSearch({
1055
- value: 'test',
1056
- onClear: () => {
1057
- callbackFired = true;
1058
- }
1059
- });
1060
-
1061
- search.clear();
1062
-
1063
- expect(callbackFired).toBe(true);
1064
- });
1065
-
1066
- test('should apply full width', () => {
1067
- const search = createMockSearch({
1068
- fullWidth: true
1069
- });
1070
-
1071
- expect(search.element.className).toContain('mtrl-search--full-width');
1072
- expect(search.element.style.width).toBe('100%');
1073
- });
1074
-
1075
- test('should apply min and max width', () => {
1076
- const search = createMockSearch({
1077
- minWidth: 200,
1078
- maxWidth: 400
1079
- });
1080
-
1081
- expect(search.element.style.minWidth).toBe('200px');
1082
- expect(search.element.style.maxWidth).toBe('400px');
1083
- });
1084
-
1085
- test('should toggle clear button visibility', () => {
1086
- const search = createMockSearch({
1087
- value: 'test'
1088
- });
1089
-
1090
- // Clear button should be visible with value
1091
- const initialClearButton = search.element.querySelector('.mtrl-search__clear');
1092
- expect(initialClearButton).not.toBeNull();
1093
-
1094
- // Hide clear button
1095
- search.showClearButton(false);
1096
- let clearButton = search.element.querySelector('.mtrl-search__clear');
1097
- expect(clearButton).toBeNull();
1098
-
1099
- // Show clear button again
1100
- search.showClearButton(true);
1101
- clearButton = search.element.querySelector('.mtrl-search__clear');
1102
- expect(clearButton).not.toBeNull();
1103
- });
1104
-
1105
- test('should remove event listeners', () => {
1106
- const search = createMockSearch();
1107
-
1108
- let eventCount = 0;
1109
-
1110
- const handler = () => {
1111
- eventCount++;
1112
- };
1113
-
1114
- search.on(SEARCH_EVENTS.INPUT, handler);
1115
-
1116
- search.setValue('test', true);
1117
- expect(eventCount).toBe(1);
1118
-
1119
- search.off(SEARCH_EVENTS.INPUT, handler);
1120
-
1121
- search.setValue('another', true);
1122
- expect(eventCount).toBe(1); // Count should not increase
1123
- });
1124
-
1125
- test('should be properly destroyed', () => {
1126
- const search = createMockSearch();
1127
- document.body.appendChild(search.element);
1128
-
1129
- expect(document.body.contains(search.element)).toBe(true);
1130
-
1131
- search.destroy();
1132
-
1133
- expect(document.body.contains(search.element)).toBe(false);
1134
- });
1135
- });