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.
Files changed (62) hide show
  1. package/dist/cli.js +0 -0
  2. package/package.json +1 -1
  3. package/playground/showcase/public/sitemap.xml +15 -0
  4. package/playground/showcase/src/app/app.routes.server.ts +8 -0
  5. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +11 -2
  6. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +13 -3
  7. package/playground/showcase/src/app/components/audio-waveform/audio-waveform.css +0 -0
  8. package/playground/showcase/src/app/components/audio-waveform/audio-waveform.html +41 -0
  9. package/playground/showcase/src/app/components/audio-waveform/audio-waveform.service.spec.ts +16 -0
  10. package/playground/showcase/src/app/components/audio-waveform/audio-waveform.service.ts +175 -0
  11. package/playground/showcase/src/app/components/audio-waveform/audio-waveform.spec.ts +23 -0
  12. package/playground/showcase/src/app/components/audio-waveform/audio-waveform.ts +64 -0
  13. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.css +0 -0
  14. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.html +41 -0
  15. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.spec.ts +228 -0
  16. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.ts +217 -0
  17. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.css +0 -0
  18. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.html +3 -0
  19. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.spec.ts +56 -0
  20. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.ts +11 -0
  21. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.css +0 -0
  22. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.html +11 -0
  23. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.spec.ts +57 -0
  24. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.ts +11 -0
  25. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.css +0 -0
  26. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.html +71 -0
  27. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.spec.ts +468 -0
  28. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.ts +90 -0
  29. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.css +0 -0
  30. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.html +58 -0
  31. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.spec.ts +173 -0
  32. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.ts +37 -0
  33. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.css +0 -0
  34. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.html +11 -0
  35. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.spec.ts +166 -0
  36. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.ts +36 -0
  37. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.css +0 -0
  38. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.html +8 -0
  39. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.spec.ts +137 -0
  40. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.ts +30 -0
  41. package/playground/showcase/src/app/components/combobox/combobox.css +0 -0
  42. package/playground/showcase/src/app/components/combobox/combobox.html +3 -0
  43. package/playground/showcase/src/app/components/combobox/combobox.spec.ts +391 -0
  44. package/playground/showcase/src/app/components/combobox/combobox.ts +59 -0
  45. package/playground/showcase/src/app/components/combobox/lib/models/combobox.model.ts +13 -0
  46. package/playground/showcase/src/app/components/combobox/lib/service/combobox.service.spec.ts +530 -0
  47. package/playground/showcase/src/app/components/combobox/lib/service/combobox.service.ts +191 -0
  48. package/playground/showcase/src/app/components/combobox/lib/types/combobox-position.type.ts +1 -0
  49. package/playground/showcase/src/app/components/combobox/lib/types/combobox-trigger-mode.type.ts +1 -0
  50. package/playground/showcase/src/app/core/services/seo.service.ts +100 -0
  51. package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.css +1 -0
  52. package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.examples.ts +146 -0
  53. package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.html +576 -0
  54. package/playground/showcase/src/app/pages/documentation/components/audio-waveform-docs/audio-waveform-docs.ts +124 -0
  55. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.css +0 -0
  56. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.html +383 -0
  57. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.spec.ts +23 -0
  58. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.ts +333 -0
  59. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.examples.ts +226 -0
  60. package/playground/showcase/src/app/pages/documentation/components/components.html +27 -0
  61. package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +8 -0
  62. package/playground/showcase/src/app/pages/home/home.html +1 -1
@@ -0,0 +1,173 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { ComponentRef } from '@angular/core';
3
+
4
+ import { ComboboxItem } from './combobox-item';
5
+ import { ComboboxService } from '../lib/service/combobox.service';
6
+ import { ComboboxInterface } from '../lib/models/combobox.model';
7
+
8
+ describe('ComboboxItem', () => {
9
+ let component: ComboboxItem;
10
+ let fixture: ComponentFixture<ComboboxItem>;
11
+ let componentRef: ComponentRef<ComboboxItem>;
12
+ let service: ComboboxService;
13
+
14
+ const mockItem: ComboboxInterface = { value: 1, label: 'Apple', description: 'A red fruit' };
15
+ const mockData: ComboboxInterface[] = [
16
+ mockItem,
17
+ { value: 2, label: 'Banana' },
18
+ { value: 3, label: 'Orange' }
19
+ ];
20
+
21
+ beforeEach(async () => {
22
+ await TestBed.configureTestingModule({
23
+ imports: [ComboboxItem],
24
+ providers: [ComboboxService]
25
+ })
26
+ .compileComponents();
27
+
28
+ fixture = TestBed.createComponent(ComboboxItem);
29
+ component = fixture.componentInstance;
30
+ componentRef = fixture.componentRef;
31
+ service = TestBed.inject(ComboboxService);
32
+
33
+ componentRef.setInput('item', mockItem);
34
+ service.setData(mockData);
35
+ fixture.detectChanges();
36
+ });
37
+
38
+ it('should create', () => {
39
+ expect(component).toBeTruthy();
40
+ });
41
+
42
+ it('should inject ComboboxService', () => {
43
+ expect(service).toBeTruthy();
44
+ });
45
+
46
+ describe('Input: item', () => {
47
+ it('should be required', () => {
48
+ expect(component.item()).toEqual(mockItem);
49
+ });
50
+
51
+ it('should update when changed', () => {
52
+ const newItem: ComboboxInterface = { value: 4, label: 'Mango' };
53
+ componentRef.setInput('item', newItem);
54
+ fixture.detectChanges();
55
+
56
+ expect(component.item()).toEqual(newItem);
57
+ });
58
+ });
59
+
60
+ describe('Input: focused', () => {
61
+ it('should default to false', () => {
62
+ expect(component.focused()).toBe(false);
63
+ });
64
+
65
+ it('should accept true', () => {
66
+ componentRef.setInput('focused', true);
67
+ fixture.detectChanges();
68
+
69
+ expect(component.focused()).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe('Input: selected', () => {
74
+ it('should default to false', () => {
75
+ expect(component.selected()).toBe(false);
76
+ });
77
+
78
+ it('should accept true', () => {
79
+ componentRef.setInput('selected', true);
80
+ fixture.detectChanges();
81
+
82
+ expect(component.selected()).toBe(true);
83
+ });
84
+ });
85
+
86
+ describe('Click Handling', () => {
87
+ it('should select item on click', () => {
88
+ const event = new MouseEvent('click');
89
+ spyOn(event, 'stopPropagation');
90
+
91
+ component.handleClick(event);
92
+
93
+ expect(service.selectedValues()).toContain(1);
94
+ expect(event.stopPropagation).toHaveBeenCalled();
95
+ });
96
+
97
+ it('should not select disabled item', () => {
98
+ const disabledItem: ComboboxInterface = { value: 5, label: 'Disabled', disabled: true };
99
+ componentRef.setInput('item', disabledItem);
100
+ fixture.detectChanges();
101
+
102
+ const event = new MouseEvent('click');
103
+ component.handleClick(event);
104
+
105
+ expect(service.selectedValues()).not.toContain(5);
106
+ });
107
+
108
+ it('should stop event propagation', () => {
109
+ const event = new MouseEvent('click');
110
+ spyOn(event, 'stopPropagation');
111
+
112
+ component.handleClick(event);
113
+
114
+ expect(event.stopPropagation).toHaveBeenCalled();
115
+ });
116
+ });
117
+
118
+ describe('Mouse Enter Handling', () => {
119
+ it('should focus item on mouse enter', () => {
120
+ component.onMouseEnter();
121
+
122
+ expect(service.focusedIndex()).toBe(0);
123
+ });
124
+
125
+ it('should not focus disabled item', () => {
126
+ const disabledItem: ComboboxInterface = { value: 5, label: 'Disabled', disabled: true };
127
+ componentRef.setInput('item', disabledItem);
128
+ fixture.detectChanges();
129
+
130
+ component.onMouseEnter();
131
+
132
+ expect(service.focusedIndex()).toBe(-1);
133
+ });
134
+
135
+ it('should find correct index in filtered data', () => {
136
+ service.setData([
137
+ { value: 1, label: 'Apple' },
138
+ { value: 2, label: 'Banana' },
139
+ { value: 3, label: 'Apricot' }
140
+ ]);
141
+
142
+ service.setSearchQuery('ban');
143
+ fixture.detectChanges();
144
+
145
+ const bananaItem: ComboboxInterface = { value: 2, label: 'Banana' };
146
+ componentRef.setInput('item', bananaItem);
147
+ fixture.detectChanges();
148
+
149
+ component.onMouseEnter();
150
+
151
+ expect(service.focusedIndex()).toBe(0); // First in filtered results
152
+ });
153
+
154
+ it('should not focus if item not found in filtered data', () => {
155
+ service.setSearchQuery('xyz'); // No matches
156
+
157
+ component.onMouseEnter();
158
+
159
+ expect(service.focusedIndex()).toBe(-1);
160
+ });
161
+ });
162
+
163
+ describe('HostListener', () => {
164
+ it('should handle mouseenter event via HostListener', () => {
165
+ spyOn(component, 'onMouseEnter');
166
+
167
+ const event = new MouseEvent('mouseenter');
168
+ fixture.nativeElement.dispatchEvent(event);
169
+
170
+ expect(component.onMouseEnter).toHaveBeenCalled();
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,37 @@
1
+ import { Component, HostListener, inject, input } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ import { ComboboxService } from '../lib/service/combobox.service';
5
+ import { ComboboxInterface } from '../lib/models/combobox.model';
6
+
7
+ @Component({
8
+ selector: 'wally-combobox-item',
9
+ imports: [CommonModule],
10
+ templateUrl: './combobox-item.html',
11
+ styleUrl: './combobox-item.css'
12
+ })
13
+ export class ComboboxItem {
14
+ comboboxService = inject(ComboboxService);
15
+
16
+ item = input.required<ComboboxInterface>();
17
+ focused = input<boolean>(false);
18
+ selected = input<boolean>(false);
19
+
20
+ handleClick(event: MouseEvent): void {
21
+ if (this.item().disabled) return;
22
+
23
+ event.stopPropagation();
24
+ this.comboboxService.selectItem(this.item().value);
25
+ }
26
+
27
+ @HostListener('mouseenter')
28
+ onMouseEnter(): void {
29
+ if (this.item().disabled) return;
30
+
31
+ const items = this.comboboxService.filteredData();
32
+ const index = items.findIndex(i => i.value === this.item().value);
33
+ if (index >= 0) {
34
+ this.comboboxService.focusedIndex.set(index);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ <div class="p-2 border-b border-neutral-200 dark:border-neutral-700">
2
+ <input
3
+ type="text"
4
+ class="w-full px-3 py-2 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-[#0a0a0a] dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 transition-all duration-200"
5
+ [placeholder]="comboboxService.placeholder()"
6
+ [value]="comboboxService.searchQuery()"
7
+ (input)="onInput($event)"
8
+ (keydown)="onKeyDown($event)"
9
+ #searchInput
10
+ />
11
+ </div>
@@ -0,0 +1,166 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { ComboboxSearch } from './combobox-search';
4
+ import { ComboboxService } from '../lib/service/combobox.service';
5
+
6
+ describe('ComboboxSearch', () => {
7
+ let component: ComboboxSearch;
8
+ let fixture: ComponentFixture<ComboboxSearch>;
9
+ let service: ComboboxService;
10
+
11
+ const mockData = [
12
+ { value: 1, label: 'Apple' },
13
+ { value: 2, label: 'Banana' },
14
+ { value: 3, label: 'Orange' }
15
+ ];
16
+
17
+ beforeEach(async () => {
18
+ await TestBed.configureTestingModule({
19
+ imports: [ComboboxSearch],
20
+ providers: [ComboboxService]
21
+ })
22
+ .compileComponents();
23
+
24
+ fixture = TestBed.createComponent(ComboboxSearch);
25
+ component = fixture.componentInstance;
26
+ service = TestBed.inject(ComboboxService);
27
+ service.setData(mockData);
28
+ fixture.detectChanges();
29
+ });
30
+
31
+ it('should create', () => {
32
+ expect(component).toBeTruthy();
33
+ });
34
+
35
+ it('should inject ComboboxService', () => {
36
+ expect(service).toBeTruthy();
37
+ });
38
+
39
+ describe('Input Handling', () => {
40
+ it('should update search query on input', () => {
41
+ const event = { target: { value: 'apple' } } as any;
42
+
43
+ component.onInput(event);
44
+
45
+ expect(service.searchQuery()).toBe('apple');
46
+ });
47
+
48
+ it('should handle empty input', () => {
49
+ service.setSearchQuery('test');
50
+
51
+ const event = { target: { value: '' } } as any;
52
+ component.onInput(event);
53
+
54
+ expect(service.searchQuery()).toBe('');
55
+ });
56
+
57
+ it('should update search query multiple times', () => {
58
+ const event1 = { target: { value: 'a' } } as any;
59
+ component.onInput(event1);
60
+ expect(service.searchQuery()).toBe('a');
61
+
62
+ const event2 = { target: { value: 'ap' } } as any;
63
+ component.onInput(event2);
64
+ expect(service.searchQuery()).toBe('ap');
65
+
66
+ const event3 = { target: { value: 'app' } } as any;
67
+ component.onInput(event3);
68
+ expect(service.searchQuery()).toBe('app');
69
+ });
70
+ });
71
+
72
+ describe('Keyboard Handling', () => {
73
+ describe('Arrow Down', () => {
74
+ it('should focus next item', () => {
75
+ service.focusFirst();
76
+
77
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
78
+ spyOn(event, 'preventDefault');
79
+ component.onKeyDown(event);
80
+
81
+ expect(service.focusedIndex()).toBe(1);
82
+ expect(event.preventDefault).toHaveBeenCalled();
83
+ });
84
+
85
+ it('should prevent default behavior', () => {
86
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
87
+ spyOn(event, 'preventDefault');
88
+
89
+ component.onKeyDown(event);
90
+
91
+ expect(event.preventDefault).toHaveBeenCalled();
92
+ });
93
+ });
94
+
95
+ describe('Arrow Up', () => {
96
+ it('should focus previous item', () => {
97
+ service.focusLast();
98
+
99
+ const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
100
+ spyOn(event, 'preventDefault');
101
+ component.onKeyDown(event);
102
+
103
+ expect(service.focusedIndex()).toBe(1);
104
+ expect(event.preventDefault).toHaveBeenCalled();
105
+ });
106
+
107
+ it('should prevent default behavior', () => {
108
+ const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
109
+ spyOn(event, 'preventDefault');
110
+
111
+ component.onKeyDown(event);
112
+
113
+ expect(event.preventDefault).toHaveBeenCalled();
114
+ });
115
+ });
116
+
117
+ describe('Enter', () => {
118
+ it('should select focused item', () => {
119
+ service.focusFirst();
120
+
121
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
122
+ spyOn(event, 'preventDefault');
123
+ component.onKeyDown(event);
124
+
125
+ expect(service.selectedValues()).toContain(1);
126
+ expect(event.preventDefault).toHaveBeenCalled();
127
+ });
128
+
129
+ it('should prevent default behavior', () => {
130
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
131
+ spyOn(event, 'preventDefault');
132
+
133
+ component.onKeyDown(event);
134
+
135
+ expect(event.preventDefault).toHaveBeenCalled();
136
+ });
137
+
138
+ it('should not select if no item is focused', () => {
139
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
140
+ component.onKeyDown(event);
141
+
142
+ expect(service.selectedValues()).toEqual([]);
143
+ });
144
+ });
145
+
146
+ describe('Other Keys', () => {
147
+ it('should not prevent default for other keys', () => {
148
+ const event = new KeyboardEvent('keydown', { key: 'a' });
149
+ const preventDefaultSpy = spyOn(event, 'preventDefault');
150
+
151
+ component.onKeyDown(event);
152
+
153
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it('should not affect service for other keys', () => {
157
+ const initialIndex = service.focusedIndex();
158
+
159
+ const event = new KeyboardEvent('keydown', { key: 'Backspace' });
160
+ component.onKeyDown(event);
161
+
162
+ expect(service.focusedIndex()).toBe(initialIndex);
163
+ });
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,36 @@
1
+ import { Component, inject } from '@angular/core';
2
+
3
+ import { ComboboxService } from '../lib/service/combobox.service';
4
+
5
+ @Component({
6
+ selector: 'wally-combobox-search',
7
+ imports: [],
8
+ templateUrl: './combobox-search.html',
9
+ styleUrl: './combobox-search.css'
10
+ })
11
+ export class ComboboxSearch {
12
+ comboboxService = inject(ComboboxService);
13
+
14
+ onInput(event: Event): void {
15
+ const input = event.target as HTMLInputElement;
16
+ this.comboboxService.setSearchQuery(input.value);
17
+ }
18
+
19
+ onKeyDown(event: KeyboardEvent): void {
20
+ // Prevent input from losing focus on arrow keys
21
+ if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
22
+ event.preventDefault();
23
+
24
+ if (event.key === 'ArrowDown') {
25
+ this.comboboxService.focusNext();
26
+ } else {
27
+ this.comboboxService.focusPrevious();
28
+ }
29
+ }
30
+
31
+ if (event.key === 'Enter') {
32
+ event.preventDefault();
33
+ this.comboboxService.selectFocusedItem();
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ <div
2
+ class="w-full"
3
+ [attr.tabindex]="comboboxService.triggerMode() === 'custom' ? 0 : undefined"
4
+ [attr.role]="comboboxService.triggerMode() === 'custom' ? 'button' : undefined"
5
+ [attr.aria-expanded]="comboboxService.triggerMode() === 'custom' ? comboboxService.isOpen() : undefined"
6
+ >
7
+ <ng-content></ng-content>
8
+ </div>
@@ -0,0 +1,137 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { ComboboxTrigger } from './combobox-trigger';
4
+ import { ComboboxService } from '../lib/service/combobox.service';
5
+
6
+ describe('ComboboxTrigger', () => {
7
+ let component: ComboboxTrigger;
8
+ let fixture: ComponentFixture<ComboboxTrigger>;
9
+ let service: ComboboxService;
10
+
11
+ beforeEach(async () => {
12
+ await TestBed.configureTestingModule({
13
+ imports: [ComboboxTrigger],
14
+ providers: [ComboboxService]
15
+ })
16
+ .compileComponents();
17
+
18
+ fixture = TestBed.createComponent(ComboboxTrigger);
19
+ component = fixture.componentInstance;
20
+ service = TestBed.inject(ComboboxService);
21
+ fixture.detectChanges();
22
+ });
23
+
24
+ it('should create', () => {
25
+ expect(component).toBeTruthy();
26
+ });
27
+
28
+ it('should inject ComboboxService', () => {
29
+ expect(service).toBeTruthy();
30
+ });
31
+
32
+ describe('Click Handling', () => {
33
+ it('should toggle combobox on click in custom mode', () => {
34
+ service.setTriggerMode('custom');
35
+ expect(service.isOpen()).toBe(false);
36
+
37
+ component.onClick();
38
+
39
+ expect(service.isOpen()).toBe(true);
40
+ });
41
+
42
+ it('should toggle from open to closed on click', () => {
43
+ service.setTriggerMode('custom');
44
+ service.open();
45
+
46
+ component.onClick();
47
+
48
+ expect(service.isOpen()).toBe(false);
49
+ });
50
+
51
+ it('should not toggle in input mode', () => {
52
+ service.setTriggerMode('input');
53
+ expect(service.isOpen()).toBe(false);
54
+
55
+ component.onClick();
56
+
57
+ expect(service.isOpen()).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe('Keyboard Handling', () => {
62
+ it('should toggle on Enter key in custom mode', () => {
63
+ service.setTriggerMode('custom');
64
+ expect(service.isOpen()).toBe(false);
65
+
66
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
67
+ spyOn(event, 'preventDefault');
68
+ component.onKeyDown(event);
69
+
70
+ expect(service.isOpen()).toBe(true);
71
+ expect(event.preventDefault).toHaveBeenCalled();
72
+ });
73
+
74
+ it('should toggle on Space key in custom mode', () => {
75
+ service.setTriggerMode('custom');
76
+ expect(service.isOpen()).toBe(false);
77
+
78
+ const event = new KeyboardEvent('keydown', { key: ' ' });
79
+ spyOn(event, 'preventDefault');
80
+ component.onKeyDown(event);
81
+
82
+ expect(service.isOpen()).toBe(true);
83
+ expect(event.preventDefault).toHaveBeenCalled();
84
+ });
85
+
86
+ it('should not toggle on other keys in custom mode', () => {
87
+ service.setTriggerMode('custom');
88
+
89
+ const event = new KeyboardEvent('keydown', { key: 'a' });
90
+ component.onKeyDown(event);
91
+
92
+ expect(service.isOpen()).toBe(false);
93
+ });
94
+
95
+ it('should not toggle on Enter in input mode', () => {
96
+ service.setTriggerMode('input');
97
+
98
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
99
+ const preventDefaultSpy = spyOn(event, 'preventDefault');
100
+ component.onKeyDown(event);
101
+
102
+ expect(service.isOpen()).toBe(false);
103
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it('should not toggle on Space in input mode', () => {
107
+ service.setTriggerMode('input');
108
+
109
+ const event = new KeyboardEvent('keydown', { key: ' ' });
110
+ const preventDefaultSpy = spyOn(event, 'preventDefault');
111
+ component.onKeyDown(event);
112
+
113
+ expect(service.isOpen()).toBe(false);
114
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
115
+ });
116
+ });
117
+
118
+ describe('HostListener', () => {
119
+ it('should handle click event via HostListener', () => {
120
+ spyOn(component, 'onClick');
121
+ service.setTriggerMode('custom');
122
+
123
+ fixture.nativeElement.click();
124
+
125
+ expect(component.onClick).toHaveBeenCalled();
126
+ });
127
+
128
+ it('should handle keydown event via HostListener', () => {
129
+ spyOn(component, 'onKeyDown');
130
+
131
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
132
+ fixture.nativeElement.dispatchEvent(event);
133
+
134
+ expect(component.onKeyDown).toHaveBeenCalledWith(event);
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,30 @@
1
+ import { Component, HostListener, inject } from '@angular/core';
2
+
3
+ import { ComboboxService } from '../lib/service/combobox.service';
4
+
5
+ @Component({
6
+ selector: 'wally-combobox-trigger',
7
+ imports: [],
8
+ templateUrl: './combobox-trigger.html',
9
+ styleUrl: './combobox-trigger.css'
10
+ })
11
+ export class ComboboxTrigger {
12
+ comboboxService = inject(ComboboxService);
13
+
14
+ @HostListener('click')
15
+ onClick(): void {
16
+ if (this.comboboxService.triggerMode() === 'custom') {
17
+ this.comboboxService.toggle();
18
+ }
19
+ }
20
+
21
+ @HostListener('keydown', ['$event'])
22
+ onKeyDown(event: KeyboardEvent): void {
23
+ if (this.comboboxService.triggerMode() === 'custom') {
24
+ if (event.key === 'Enter' || event.key === ' ') {
25
+ event.preventDefault();
26
+ this.comboboxService.toggle();
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,3 @@
1
+ <div class="relative">
2
+ <ng-content></ng-content>
3
+ </div>