wally-ui 1.14.1 → 1.16.0
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/dist/cli.js +0 -0
- package/package.json +1 -1
- package/playground/showcase/public/sitemap.xml +15 -0
- package/playground/showcase/src/app/app.routes.server.ts +8 -0
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +11 -2
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +13 -3
- package/playground/showcase/src/app/components/audio-waveform/audio-waveform.css +0 -0
- package/playground/showcase/src/app/components/audio-waveform/audio-waveform.html +41 -0
- package/playground/showcase/src/app/components/audio-waveform/audio-waveform.service.spec.ts +16 -0
- package/playground/showcase/src/app/components/audio-waveform/audio-waveform.service.ts +175 -0
- package/playground/showcase/src/app/components/audio-waveform/audio-waveform.spec.ts +23 -0
- package/playground/showcase/src/app/components/audio-waveform/audio-waveform.ts +64 -0
- package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.html +41 -0
- package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.spec.ts +228 -0
- package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.ts +217 -0
- package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.html +3 -0
- package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.spec.ts +56 -0
- package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.ts +11 -0
- package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.html +11 -0
- package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.spec.ts +57 -0
- package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.ts +11 -0
- package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.html +71 -0
- package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.spec.ts +468 -0
- package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.ts +90 -0
- package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.html +58 -0
- package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.spec.ts +173 -0
- package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.ts +37 -0
- package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.html +11 -0
- package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.spec.ts +166 -0
- package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.ts +36 -0
- package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.html +8 -0
- package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.spec.ts +137 -0
- package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.ts +30 -0
- package/playground/showcase/src/app/components/combobox/combobox.css +0 -0
- package/playground/showcase/src/app/components/combobox/combobox.html +3 -0
- package/playground/showcase/src/app/components/combobox/combobox.spec.ts +391 -0
- package/playground/showcase/src/app/components/combobox/combobox.ts +59 -0
- package/playground/showcase/src/app/components/combobox/lib/models/combobox.model.ts +13 -0
- package/playground/showcase/src/app/components/combobox/lib/service/combobox.service.spec.ts +530 -0
- package/playground/showcase/src/app/components/combobox/lib/service/combobox.service.ts +191 -0
- package/playground/showcase/src/app/components/combobox/lib/types/combobox-position.type.ts +1 -0
- package/playground/showcase/src/app/components/combobox/lib/types/combobox-trigger-mode.type.ts +1 -0
- package/playground/showcase/src/app/core/services/seo.service.ts +100 -0
- package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.css +1 -0
- package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.examples.ts +146 -0
- package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.html +576 -0
- package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.ts +124 -0
- package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.css +0 -0
- package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.html +383 -0
- package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.spec.ts +23 -0
- package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.ts +333 -0
- package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.examples.ts +226 -0
- package/playground/showcase/src/app/pages/documentation/components/components.html +27 -0
- package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +8 -0
- package/playground/showcase/src/app/pages/home/home.html +1 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { DebugElement } from '@angular/core';
|
|
3
|
+
import { By } from '@angular/platform-browser';
|
|
4
|
+
|
|
5
|
+
import { ComboboxInput } from './combobox-input';
|
|
6
|
+
import { ComboboxService } from '../lib/service/combobox.service';
|
|
7
|
+
import { ComboboxInterface } from '../lib/models/combobox.model';
|
|
8
|
+
|
|
9
|
+
describe('ComboboxInput', () => {
|
|
10
|
+
let component: ComboboxInput;
|
|
11
|
+
let fixture: ComponentFixture<ComboboxInput>;
|
|
12
|
+
let service: ComboboxService;
|
|
13
|
+
let inputElement: DebugElement;
|
|
14
|
+
|
|
15
|
+
const mockData: ComboboxInterface[] = [
|
|
16
|
+
{ value: 1, label: 'Apple' },
|
|
17
|
+
{ value: 2, label: 'Banana' },
|
|
18
|
+
{ value: 3, label: 'Orange' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
await TestBed.configureTestingModule({
|
|
23
|
+
imports: [ComboboxInput],
|
|
24
|
+
providers: [ComboboxService]
|
|
25
|
+
})
|
|
26
|
+
.compileComponents();
|
|
27
|
+
|
|
28
|
+
fixture = TestBed.createComponent(ComboboxInput);
|
|
29
|
+
component = fixture.componentInstance;
|
|
30
|
+
service = TestBed.inject(ComboboxService);
|
|
31
|
+
service.setData(mockData);
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
|
|
34
|
+
inputElement = fixture.debugElement.query(By.css('input'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should create', () => {
|
|
38
|
+
expect(component).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should inject ComboboxService', () => {
|
|
42
|
+
expect(service).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('Input Focus', () => {
|
|
46
|
+
it('should open combobox on focus', () => {
|
|
47
|
+
expect(service.isOpen()).toBe(false);
|
|
48
|
+
|
|
49
|
+
component.onInputFocus();
|
|
50
|
+
|
|
51
|
+
expect(service.isOpen()).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should trigger onInputFocus when input is focused', () => {
|
|
55
|
+
spyOn(component, 'onInputFocus');
|
|
56
|
+
|
|
57
|
+
inputElement.nativeElement.focus();
|
|
58
|
+
inputElement.triggerEventHandler('focus', {});
|
|
59
|
+
|
|
60
|
+
expect(component.onInputFocus).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('Input Click', () => {
|
|
65
|
+
it('should open combobox on click if closed', () => {
|
|
66
|
+
expect(service.isOpen()).toBe(false);
|
|
67
|
+
|
|
68
|
+
component.onInputClick();
|
|
69
|
+
|
|
70
|
+
expect(service.isOpen()).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should not close combobox if already open', () => {
|
|
74
|
+
service.open();
|
|
75
|
+
expect(service.isOpen()).toBe(true);
|
|
76
|
+
|
|
77
|
+
component.onInputClick();
|
|
78
|
+
|
|
79
|
+
expect(service.isOpen()).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should trigger onInputClick when input is clicked', () => {
|
|
83
|
+
spyOn(component, 'onInputClick');
|
|
84
|
+
|
|
85
|
+
inputElement.triggerEventHandler('click', {});
|
|
86
|
+
|
|
87
|
+
expect(component.onInputClick).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Input Change', () => {
|
|
92
|
+
it('should update search query on input', () => {
|
|
93
|
+
const event = { target: { value: 'app' } } as any;
|
|
94
|
+
|
|
95
|
+
component.onInput(event);
|
|
96
|
+
|
|
97
|
+
expect(service.searchQuery()).toBe('app');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should open combobox on input if closed', () => {
|
|
101
|
+
expect(service.isOpen()).toBe(false);
|
|
102
|
+
|
|
103
|
+
const event = { target: { value: 'test' } } as any;
|
|
104
|
+
component.onInput(event);
|
|
105
|
+
|
|
106
|
+
expect(service.isOpen()).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should keep combobox open on input', () => {
|
|
110
|
+
service.open();
|
|
111
|
+
|
|
112
|
+
const event = { target: { value: 'test' } } as any;
|
|
113
|
+
component.onInput(event);
|
|
114
|
+
|
|
115
|
+
expect(service.isOpen()).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should trigger onInput when typing', () => {
|
|
119
|
+
spyOn(component, 'onInput');
|
|
120
|
+
|
|
121
|
+
const event = new Event('input');
|
|
122
|
+
inputElement.nativeElement.dispatchEvent(event);
|
|
123
|
+
inputElement.triggerEventHandler('input', event);
|
|
124
|
+
|
|
125
|
+
expect(component.onInput).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Chip Removal (Multi-Select)', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
service.setMultiSelect(true);
|
|
132
|
+
service.selectItem(1);
|
|
133
|
+
service.selectItem(2);
|
|
134
|
+
fixture.detectChanges();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should remove chip when removeChip is called', () => {
|
|
138
|
+
expect(service.selectedValues()).toContain(1);
|
|
139
|
+
|
|
140
|
+
const event = new MouseEvent('click');
|
|
141
|
+
component.removeChip(1, event);
|
|
142
|
+
|
|
143
|
+
expect(service.selectedValues()).not.toContain(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should stop event propagation when removing chip', () => {
|
|
147
|
+
const event = new MouseEvent('click');
|
|
148
|
+
spyOn(event, 'stopPropagation');
|
|
149
|
+
|
|
150
|
+
component.removeChip(1, event);
|
|
151
|
+
|
|
152
|
+
expect(event.stopPropagation).toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should remove correct chip', () => {
|
|
156
|
+
component.removeChip(1, new MouseEvent('click'));
|
|
157
|
+
|
|
158
|
+
expect(service.selectedValues()).toEqual([2]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('Clear All', () => {
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
service.setMultiSelect(true);
|
|
165
|
+
service.selectItem(1);
|
|
166
|
+
service.selectItem(2);
|
|
167
|
+
service.setSearchQuery('test');
|
|
168
|
+
fixture.detectChanges();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should clear all selections', () => {
|
|
172
|
+
expect(service.selectedValues().length).toBe(2);
|
|
173
|
+
|
|
174
|
+
component.clearAll(inputElement.nativeElement);
|
|
175
|
+
|
|
176
|
+
expect(service.selectedValues()).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should clear search query', () => {
|
|
180
|
+
expect(service.searchQuery()).toBe('test');
|
|
181
|
+
|
|
182
|
+
component.clearAll(inputElement.nativeElement);
|
|
183
|
+
|
|
184
|
+
expect(service.searchQuery()).toBe('');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should focus input after clearing', () => {
|
|
188
|
+
const mockInput = jasmine.createSpyObj('HTMLInputElement', ['focus']);
|
|
189
|
+
component.clearAll(mockInput);
|
|
190
|
+
|
|
191
|
+
expect(mockInput.focus).toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Keyboard Navigation', () => {
|
|
196
|
+
describe('Arrow Down', () => {
|
|
197
|
+
it('should open combobox if closed', () => {
|
|
198
|
+
expect(service.isOpen()).toBe(false);
|
|
199
|
+
|
|
200
|
+
const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
|
|
201
|
+
spyOn(event, 'preventDefault');
|
|
202
|
+
component.onKeyDown(event);
|
|
203
|
+
|
|
204
|
+
expect(service.isOpen()).toBe(true);
|
|
205
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should focus first item if none focused', () => {
|
|
209
|
+
service.open();
|
|
210
|
+
expect(service.focusedIndex()).toBe(-1);
|
|
211
|
+
|
|
212
|
+
const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
|
|
213
|
+
component.onKeyDown(event);
|
|
214
|
+
|
|
215
|
+
expect(service.focusedIndex()).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should focus next item', () => {
|
|
219
|
+
service.open();
|
|
220
|
+
service.focusFirst();
|
|
221
|
+
|
|
222
|
+
const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
|
|
223
|
+
component.onKeyDown(event);
|
|
224
|
+
|
|
225
|
+
expect(service.focusedIndex()).toBe(1);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Arrow Up', () => {
|
|
230
|
+
it('should focus previous item', () => {
|
|
231
|
+
service.open();
|
|
232
|
+
service.focusLast();
|
|
233
|
+
|
|
234
|
+
const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
|
|
235
|
+
spyOn(event, 'preventDefault');
|
|
236
|
+
component.onKeyDown(event);
|
|
237
|
+
|
|
238
|
+
expect(service.focusedIndex()).toBe(1);
|
|
239
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should not do anything if combobox is closed', () => {
|
|
243
|
+
const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
|
|
244
|
+
component.onKeyDown(event);
|
|
245
|
+
|
|
246
|
+
expect(service.isOpen()).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('Enter', () => {
|
|
251
|
+
it('should select focused item', () => {
|
|
252
|
+
service.open();
|
|
253
|
+
service.focusFirst();
|
|
254
|
+
|
|
255
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
256
|
+
spyOn(event, 'preventDefault');
|
|
257
|
+
component.onKeyDown(event);
|
|
258
|
+
|
|
259
|
+
expect(service.selectedValues()).toContain(1);
|
|
260
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should not select if no item is focused', () => {
|
|
264
|
+
service.open();
|
|
265
|
+
|
|
266
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
267
|
+
component.onKeyDown(event);
|
|
268
|
+
|
|
269
|
+
expect(service.selectedValues()).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should not select if combobox is closed', () => {
|
|
273
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
274
|
+
component.onKeyDown(event);
|
|
275
|
+
|
|
276
|
+
expect(service.selectedValues()).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('Escape', () => {
|
|
281
|
+
it('should close combobox if open', () => {
|
|
282
|
+
service.open();
|
|
283
|
+
|
|
284
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
285
|
+
spyOn(event, 'preventDefault');
|
|
286
|
+
component.onKeyDown(event);
|
|
287
|
+
|
|
288
|
+
expect(service.isOpen()).toBe(false);
|
|
289
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should not prevent default if combobox is closed', () => {
|
|
293
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
294
|
+
const preventDefaultSpy = spyOn(event, 'preventDefault');
|
|
295
|
+
component.onKeyDown(event);
|
|
296
|
+
|
|
297
|
+
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('Backspace', () => {
|
|
302
|
+
it('should remove last chip if input is empty in multi-select', () => {
|
|
303
|
+
service.setMultiSelect(true);
|
|
304
|
+
service.selectItem(1);
|
|
305
|
+
service.selectItem(2);
|
|
306
|
+
service.selectItem(3);
|
|
307
|
+
|
|
308
|
+
const event = new KeyboardEvent('keydown', { key: 'Backspace' });
|
|
309
|
+
component.onKeyDown(event);
|
|
310
|
+
|
|
311
|
+
expect(service.selectedValues()).toEqual([1, 2]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should not remove chip if input has text', () => {
|
|
315
|
+
service.setMultiSelect(true);
|
|
316
|
+
service.selectItem(1);
|
|
317
|
+
service.selectItem(2);
|
|
318
|
+
service.setSearchQuery('test');
|
|
319
|
+
|
|
320
|
+
const event = new KeyboardEvent('keydown', { key: 'Backspace' });
|
|
321
|
+
component.onKeyDown(event);
|
|
322
|
+
|
|
323
|
+
expect(service.selectedValues()).toEqual([1, 2]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should not remove chip if no items selected', () => {
|
|
327
|
+
service.setMultiSelect(true);
|
|
328
|
+
|
|
329
|
+
const event = new KeyboardEvent('keydown', { key: 'Backspace' });
|
|
330
|
+
component.onKeyDown(event);
|
|
331
|
+
|
|
332
|
+
expect(service.selectedValues()).toEqual([]);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should handle HostListener keydown events', () => {
|
|
337
|
+
spyOn(component, 'onKeyDown');
|
|
338
|
+
|
|
339
|
+
const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
|
|
340
|
+
fixture.debugElement.nativeElement.dispatchEvent(event);
|
|
341
|
+
|
|
342
|
+
expect(component.onKeyDown).toHaveBeenCalledWith(event);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('Template Rendering', () => {
|
|
347
|
+
it('should display selected chips in multi-select mode', () => {
|
|
348
|
+
service.setMultiSelect(true);
|
|
349
|
+
service.selectItem(1);
|
|
350
|
+
service.selectItem(2);
|
|
351
|
+
fixture.detectChanges();
|
|
352
|
+
|
|
353
|
+
const chips = fixture.debugElement.queryAll(By.css('div[class*="bg-neutral-100"]'));
|
|
354
|
+
expect(chips.length).toBe(2);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should not display chips in single-select mode', () => {
|
|
358
|
+
service.setMultiSelect(false);
|
|
359
|
+
service.selectItem(1);
|
|
360
|
+
fixture.detectChanges();
|
|
361
|
+
|
|
362
|
+
const chips = fixture.debugElement.queryAll(By.css('div[class*="bg-neutral-100"]'));
|
|
363
|
+
expect(chips.length).toBe(0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should display clear button when items are selected', () => {
|
|
367
|
+
service.selectItem(1);
|
|
368
|
+
fixture.detectChanges();
|
|
369
|
+
|
|
370
|
+
const clearButton = fixture.debugElement.query(By.css('button[aria-label="Clear all"]'));
|
|
371
|
+
expect(clearButton).toBeTruthy();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should not display clear button when no items selected', () => {
|
|
375
|
+
fixture.detectChanges();
|
|
376
|
+
|
|
377
|
+
const clearButton = fixture.debugElement.query(By.css('button[aria-label="Clear all"]'));
|
|
378
|
+
expect(clearButton).toBeFalsy();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should display dropdown indicator', () => {
|
|
382
|
+
const indicator = fixture.debugElement.query(By.css('svg[class*="rotate-180"]'));
|
|
383
|
+
expect(indicator).toBeTruthy();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should rotate indicator when combobox is open', () => {
|
|
387
|
+
service.open();
|
|
388
|
+
fixture.detectChanges();
|
|
389
|
+
|
|
390
|
+
const indicator = fixture.debugElement.query(By.css('svg')).nativeElement;
|
|
391
|
+
expect(indicator.classList.contains('rotate-180')).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should display placeholder when no items selected', () => {
|
|
395
|
+
service.placeholder.set('Test placeholder');
|
|
396
|
+
fixture.detectChanges();
|
|
397
|
+
|
|
398
|
+
expect(inputElement.nativeElement.placeholder).toBe('Test placeholder');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should hide placeholder when items are selected', () => {
|
|
402
|
+
service.placeholder.set('Test placeholder');
|
|
403
|
+
service.selectItem(1);
|
|
404
|
+
fixture.detectChanges();
|
|
405
|
+
|
|
406
|
+
expect(inputElement.nativeElement.placeholder).toBe('');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should apply disabled state', () => {
|
|
410
|
+
service.disabled.set(true);
|
|
411
|
+
fixture.detectChanges();
|
|
412
|
+
|
|
413
|
+
expect(inputElement.nativeElement.disabled).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should apply opacity when disabled', () => {
|
|
417
|
+
service.disabled.set(true);
|
|
418
|
+
fixture.detectChanges();
|
|
419
|
+
|
|
420
|
+
const container = fixture.debugElement.query(By.css('div'));
|
|
421
|
+
expect(container.nativeElement.classList.contains('opacity-50')).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('Accessibility', () => {
|
|
426
|
+
it('should have correct role', () => {
|
|
427
|
+
expect(inputElement.nativeElement.getAttribute('role')).toBe('combobox');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should have aria-expanded attribute', () => {
|
|
431
|
+
expect(inputElement.nativeElement.hasAttribute('aria-expanded')).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should update aria-expanded when opened', () => {
|
|
435
|
+
expect(inputElement.nativeElement.getAttribute('aria-expanded')).toBe('false');
|
|
436
|
+
|
|
437
|
+
service.open();
|
|
438
|
+
fixture.detectChanges();
|
|
439
|
+
|
|
440
|
+
expect(inputElement.nativeElement.getAttribute('aria-expanded')).toBe('true');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should have aria-controls attribute', () => {
|
|
444
|
+
expect(inputElement.nativeElement.getAttribute('aria-controls')).toBe('combobox-listbox');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should have autocomplete off', () => {
|
|
448
|
+
expect(inputElement.nativeElement.getAttribute('autocomplete')).toBe('off');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should have aria-label on clear button', () => {
|
|
452
|
+
service.selectItem(1);
|
|
453
|
+
fixture.detectChanges();
|
|
454
|
+
|
|
455
|
+
const clearButton = fixture.debugElement.query(By.css('button[aria-label="Clear all"]'));
|
|
456
|
+
expect(clearButton.nativeElement.getAttribute('aria-label')).toBe('Clear all');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should have aria-label on chip remove buttons', () => {
|
|
460
|
+
service.setMultiSelect(true);
|
|
461
|
+
service.selectItem(1);
|
|
462
|
+
fixture.detectChanges();
|
|
463
|
+
|
|
464
|
+
const removeButton = fixture.debugElement.query(By.css('button[aria-label]'));
|
|
465
|
+
expect(removeButton.nativeElement.getAttribute('aria-label')).toContain('Remove');
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Component, HostListener, inject } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
import { ComboboxService } from '../lib/service/combobox.service';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'wally-combobox-input',
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
templateUrl: './combobox-input.html',
|
|
10
|
+
styleUrl: './combobox-input.css'
|
|
11
|
+
})
|
|
12
|
+
export class ComboboxInput {
|
|
13
|
+
comboboxService = inject(ComboboxService);
|
|
14
|
+
|
|
15
|
+
onInputFocus(): void {
|
|
16
|
+
this.comboboxService.open();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onInputClick(): void {
|
|
20
|
+
if (!this.comboboxService.isOpen()) {
|
|
21
|
+
this.comboboxService.open();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onInput(event: Event): void {
|
|
26
|
+
const input = event.target as HTMLInputElement;
|
|
27
|
+
this.comboboxService.setSearchQuery(input.value);
|
|
28
|
+
|
|
29
|
+
if (!this.comboboxService.isOpen()) {
|
|
30
|
+
this.comboboxService.open();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
removeChip(value: string | number, event: MouseEvent): void {
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
this.comboboxService.deselectItem(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clearAll(inputElement: HTMLInputElement): void {
|
|
40
|
+
this.comboboxService.clearSelection();
|
|
41
|
+
this.comboboxService.setSearchQuery('');
|
|
42
|
+
inputElement.focus();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@HostListener('keydown', ['$event'])
|
|
46
|
+
onKeyDown(event: KeyboardEvent): void {
|
|
47
|
+
switch (event.key) {
|
|
48
|
+
case 'ArrowDown':
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
if (!this.comboboxService.isOpen()) {
|
|
51
|
+
this.comboboxService.open();
|
|
52
|
+
}
|
|
53
|
+
if (this.comboboxService.focusedIndex() === -1) {
|
|
54
|
+
this.comboboxService.focusFirst();
|
|
55
|
+
} else {
|
|
56
|
+
this.comboboxService.focusNext();
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case 'ArrowUp':
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
if (this.comboboxService.isOpen()) {
|
|
63
|
+
this.comboboxService.focusPrevious();
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'Enter':
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
if (this.comboboxService.isOpen() && this.comboboxService.focusedIndex() >= 0) {
|
|
70
|
+
this.comboboxService.selectFocusedItem();
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'Escape':
|
|
75
|
+
if (this.comboboxService.isOpen()) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
this.comboboxService.close();
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'Backspace':
|
|
82
|
+
// Remove last chip if input is empty
|
|
83
|
+
if (!this.comboboxService.searchQuery() && this.comboboxService.selectedItems().length > 0) {
|
|
84
|
+
const items = this.comboboxService.selectedItems();
|
|
85
|
+
this.comboboxService.deselectItem(items[items.length - 1].value);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<div
|
|
2
|
+
(click)="handleClick($event)"
|
|
3
|
+
class="p-1"
|
|
4
|
+
role="option"
|
|
5
|
+
[attr.aria-selected]="selected()"
|
|
6
|
+
[attr.aria-disabled]="item().disabled"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="flex items-center gap-3 py-2 px-4 rounded-lg transition-all duration-150"
|
|
10
|
+
[ngClass]="{
|
|
11
|
+
'bg-neutral-100 dark:bg-neutral-800': focused() && !selected(),
|
|
12
|
+
'bg-blue-50 dark:bg-blue-900/20': selected(),
|
|
13
|
+
'hover:bg-neutral-100 dark:hover:bg-neutral-700/50 cursor-pointer': !item().disabled && !selected(),
|
|
14
|
+
'text-neutral-400 dark:text-neutral-600 pointer-events-none': item().disabled
|
|
15
|
+
}"
|
|
16
|
+
>
|
|
17
|
+
<!-- Checkbox (multi-select only) -->
|
|
18
|
+
@if (comboboxService.multiSelect()) {
|
|
19
|
+
<div class="flex-shrink-0">
|
|
20
|
+
<div
|
|
21
|
+
class="w-4 h-4 border-2 rounded transition-all"
|
|
22
|
+
[ngClass]="{
|
|
23
|
+
'border-blue-500 bg-blue-500': selected(),
|
|
24
|
+
'border-neutral-300 dark:border-neutral-600': !selected()
|
|
25
|
+
}"
|
|
26
|
+
>
|
|
27
|
+
@if (selected()) {
|
|
28
|
+
<svg class="w-full h-full text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
29
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
|
|
30
|
+
</svg>
|
|
31
|
+
}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
<!-- Item content -->
|
|
37
|
+
<div class="flex-1 min-w-0">
|
|
38
|
+
<div class="text-sm font-medium text-[#0a0a0a] dark:text-white">
|
|
39
|
+
{{ item().label }}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
@if (item().description) {
|
|
43
|
+
<div class="text-xs text-neutral-600 dark:text-neutral-400 mt-0.5">
|
|
44
|
+
{{ item().description }}
|
|
45
|
+
</div>
|
|
46
|
+
}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Selected indicator (single-select only) -->
|
|
50
|
+
@if (!comboboxService.multiSelect() && selected()) {
|
|
51
|
+
<div class="flex-shrink-0">
|
|
52
|
+
<svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
|
|
53
|
+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|