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.
- package/.env +15 -0
- package/CONTRIBUTING.md +8 -8
- package/DOCS.md +3 -3
- package/README.md +43 -20
- package/TESTING.md +128 -18
- package/dist/index.js +14865 -0
- package/git-user-stats.js +545 -0
- package/index.ts +9 -67
- package/package.json +8 -3
- package/src/components/badge/api.ts +15 -1
- package/src/components/badge/badge.ts +43 -4
- package/src/components/badge/config.ts +40 -8
- package/src/components/badge/index.ts +64 -3
- package/src/components/badge/types.ts +175 -33
- package/src/components/button/api.ts +63 -1
- package/src/components/button/button.ts +39 -3
- package/src/components/button/config.ts +21 -4
- package/src/components/button/index.ts +26 -1
- package/src/components/button/types.ts +7 -1
- package/src/components/card/api.ts +78 -9
- package/src/components/card/card.ts +58 -3
- package/src/components/card/config.ts +41 -11
- package/src/components/card/features.ts +39 -12
- package/src/components/card/index.ts +84 -19
- package/src/components/card/types.ts +218 -29
- package/src/components/carousel/carousel.ts +92 -28
- package/src/components/carousel/constants.ts +107 -21
- package/src/components/carousel/index.ts +31 -13
- package/src/components/checkbox/checkbox.ts +83 -16
- package/src/components/checkbox/index.ts +43 -1
- package/src/components/checkbox/types.ts +219 -32
- package/src/components/chips/api.ts +194 -0
- package/src/components/{chip → chips/chip}/api.ts +42 -2
- package/src/components/chips/chip/chip.ts +131 -0
- package/src/components/{chip → chips/chip}/config.ts +3 -3
- package/src/components/chips/chip/index.ts +3 -0
- package/src/components/chips/chips.md +481 -0
- package/src/components/chips/chips.ts +75 -0
- package/src/components/chips/config.ts +109 -0
- package/src/components/chips/constants.ts +61 -0
- package/src/components/chips/features/chip-items.ts +33 -0
- package/src/components/chips/features/container.ts +77 -0
- package/src/components/chips/features/controller.ts +448 -0
- package/src/components/chips/features/index.ts +5 -0
- package/src/components/chips/features/label.ts +108 -0
- package/src/components/chips/index.ts +11 -0
- package/src/components/chips/schema.ts +61 -0
- package/src/components/{chip → chips}/types.ts +203 -92
- package/src/components/dialog/dialog.ts +99 -16
- package/src/components/dialog/index.ts +97 -1
- package/src/components/dialog/types.ts +375 -69
- package/src/components/divider/config.ts +90 -6
- package/src/components/divider/divider.ts +32 -2
- package/src/components/divider/features.ts +26 -0
- package/src/components/divider/index.ts +30 -0
- package/src/components/divider/types.ts +86 -9
- package/src/components/extended-fab/api.ts +53 -1
- package/src/components/extended-fab/config.ts +29 -1
- package/src/components/extended-fab/extended-fab.ts +28 -0
- package/src/components/extended-fab/index.ts +36 -0
- package/src/components/extended-fab/types.ts +458 -13
- package/src/components/fab/api.ts +42 -2
- package/src/components/fab/config.ts +29 -1
- package/src/components/fab/fab.ts +16 -2
- package/src/components/fab/index.ts +35 -0
- package/src/components/fab/types.ts +374 -10
- package/src/components/list/api.ts +12 -2
- package/src/components/list/config.ts +21 -0
- package/src/components/list/features.ts +6 -0
- package/src/components/list/index.ts +56 -1
- package/src/components/list/list-item.ts +46 -2
- package/src/components/list/list.ts +73 -2
- package/src/components/list/types.ts +172 -0
- package/src/components/list/utils.ts +26 -2
- package/src/components/menu/api.ts +217 -20
- package/src/components/menu/config.ts +27 -0
- package/src/components/menu/features/visibility.ts +55 -6
- package/src/components/menu/index.ts +64 -0
- package/src/components/menu/menu-item.ts +46 -3
- package/src/components/menu/menu.ts +77 -1
- package/src/components/menu/types.ts +404 -39
- package/src/components/sheet/config.ts +1 -2
- package/src/components/sheet/features/gestures.ts +1 -1
- package/src/components/sheet/features/position.ts +1 -2
- package/src/components/sheet/features/state.ts +1 -1
- package/src/components/sheet/index.ts +10 -2
- package/src/components/sheet/sheet.ts +1 -2
- package/src/components/sheet/types.ts +29 -1
- package/src/components/slider/api.ts +1 -1
- package/src/components/slider/config.ts +1 -1
- package/src/components/slider/features/controller.ts +1 -1
- package/src/components/slider/features/handlers.ts +1 -1
- package/src/components/slider/features/states.ts +1 -1
- package/src/components/slider/index.ts +12 -5
- package/src/components/slider/schema.ts +1 -1
- package/src/components/slider/types.ts +31 -0
- package/src/components/tabs/tab-api.ts +1 -1
- package/src/components/tabs/types.ts +1 -1
- package/src/components/tooltip/api.ts +6 -2
- package/src/components/tooltip/config.ts +9 -28
- package/src/components/tooltip/index.ts +10 -1
- package/src/components/tooltip/types.ts +38 -3
- package/src/index.ts +129 -31
- package/src/styles/abstract/_mixins.scss +23 -9
- package/src/styles/abstract/_variables.scss +14 -4
- package/src/styles/components/_card.scss +1 -1
- package/src/styles/components/_chip.scss +323 -113
- package/src/styles/components/_tabs.scss +1 -1
- package/CLAUDE.md +0 -33
- package/src/components/checkbox/constants.ts +0 -37
- package/src/components/chip/chip-set.ts +0 -225
- package/src/components/chip/chip.ts +0 -118
- package/src/components/chip/constants.ts +0 -28
- package/src/components/chip/index.ts +0 -12
- package/src/components/list/constants.ts +0 -116
- package/src/components/sheet/constants.ts +0 -20
- package/src/components/slider/constants.ts +0 -32
- package/src/components/tooltip/constants.ts +0 -27
- package/test/components/badge.test.ts +0 -545
- package/test/components/bottom-app-bar.test.ts +0 -303
- package/test/components/button.test.ts +0 -233
- package/test/components/card.test.ts +0 -560
- package/test/components/carousel.test.ts +0 -951
- package/test/components/checkbox.test.ts +0 -462
- package/test/components/chip.test.ts +0 -692
- package/test/components/datepicker.test.ts +0 -1124
- package/test/components/dialog.test.ts +0 -990
- package/test/components/divider.test.ts +0 -412
- package/test/components/extended-fab.test.ts +0 -672
- package/test/components/fab.test.ts +0 -561
- package/test/components/list.test.ts +0 -365
- package/test/components/menu.test.ts +0 -718
- package/test/components/navigation.test.ts +0 -186
- package/test/components/progress.test.ts +0 -567
- package/test/components/radios.test.ts +0 -699
- package/test/components/search.test.ts +0 -1135
- package/test/components/segmented-button.test.ts +0 -732
- package/test/components/sheet.test.ts +0 -641
- package/test/components/slider.test.ts +0 -1220
- package/test/components/snackbar.test.ts +0 -461
- package/test/components/switch.test.ts +0 -452
- package/test/components/tabs.test.ts +0 -1369
- package/test/components/textfield.test.ts +0 -400
- package/test/components/timepicker.test.ts +0 -592
- package/test/components/tooltip.test.ts +0 -630
- package/test/components/top-app-bar.test.ts +0 -566
- package/test/core/dom.attributes.test.ts +0 -148
- package/test/core/dom.classes.test.ts +0 -152
- package/test/core/dom.events.test.ts +0 -243
- package/test/core/emitter.test.ts +0 -141
- package/test/core/ripple.test.ts +0 -99
- package/test/core/state.store.test.ts +0 -189
- package/test/core/utils.normalize.test.ts +0 -61
- package/test/core/utils.object.test.ts +0 -120
- package/test/setup.js +0 -371
- package/test/setup.ts +0 -451
- package/tsconfig.json +0 -22
- package/typedoc.json +0 -28
- 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
|
-
});
|