wally-ui 1.15.0 → 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 (47) hide show
  1. package/dist/cli.js +0 -0
  2. package/package.json +1 -1
  3. package/playground/showcase/src/app/app.routes.server.ts +4 -0
  4. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.css +0 -0
  5. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.html +41 -0
  6. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.spec.ts +228 -0
  7. package/playground/showcase/src/app/components/combobox/combobox-content/combobox-content.ts +217 -0
  8. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.css +0 -0
  9. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.html +3 -0
  10. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.spec.ts +56 -0
  11. package/playground/showcase/src/app/components/combobox/combobox-empty/combobox-empty.ts +11 -0
  12. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.css +0 -0
  13. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.html +11 -0
  14. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.spec.ts +57 -0
  15. package/playground/showcase/src/app/components/combobox/combobox-group/combobox-group.ts +11 -0
  16. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.css +0 -0
  17. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.html +71 -0
  18. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.spec.ts +468 -0
  19. package/playground/showcase/src/app/components/combobox/combobox-input/combobox-input.ts +90 -0
  20. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.css +0 -0
  21. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.html +58 -0
  22. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.spec.ts +173 -0
  23. package/playground/showcase/src/app/components/combobox/combobox-item/combobox-item.ts +37 -0
  24. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.css +0 -0
  25. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.html +11 -0
  26. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.spec.ts +166 -0
  27. package/playground/showcase/src/app/components/combobox/combobox-search/combobox-search.ts +36 -0
  28. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.css +0 -0
  29. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.html +8 -0
  30. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.spec.ts +137 -0
  31. package/playground/showcase/src/app/components/combobox/combobox-trigger/combobox-trigger.ts +30 -0
  32. package/playground/showcase/src/app/components/combobox/combobox.css +0 -0
  33. package/playground/showcase/src/app/components/combobox/combobox.html +3 -0
  34. package/playground/showcase/src/app/components/combobox/combobox.spec.ts +391 -0
  35. package/playground/showcase/src/app/components/combobox/combobox.ts +59 -0
  36. package/playground/showcase/src/app/components/combobox/lib/models/combobox.model.ts +13 -0
  37. package/playground/showcase/src/app/components/combobox/lib/service/combobox.service.spec.ts +530 -0
  38. package/playground/showcase/src/app/components/combobox/lib/service/combobox.service.ts +191 -0
  39. package/playground/showcase/src/app/components/combobox/lib/types/combobox-position.type.ts +1 -0
  40. package/playground/showcase/src/app/components/combobox/lib/types/combobox-trigger-mode.type.ts +1 -0
  41. package/playground/showcase/src/app/core/services/seo.service.ts +100 -0
  42. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.css +0 -0
  43. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.html +383 -0
  44. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.spec.ts +23 -0
  45. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.component.ts +333 -0
  46. package/playground/showcase/src/app/pages/documentation/components/combobox-docs/combobox-docs.examples.ts +226 -0
  47. package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
package/dist/cli.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wally-ui",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "About Where’s Wally? Right here — bringing you ready-to-use Angular components with Wally-UI. Stop searching, start building.",
5
5
  "bin": {
6
6
  "wally": "dist/cli.js"
@@ -41,6 +41,10 @@ export const serverRoutes: ServerRoute[] = [
41
41
  path: 'documentation/components/audio-waveform',
42
42
  renderMode: RenderMode.Prerender,
43
43
  },
44
+ {
45
+ path: 'documentation/components/combobox',
46
+ renderMode: RenderMode.Prerender,
47
+ },
44
48
  {
45
49
  path: 'documentation/chat-sdk',
46
50
  renderMode: RenderMode.Prerender,
@@ -0,0 +1,41 @@
1
+ @if (comboboxService.isOpen()) {
2
+ <div
3
+ [class]="'absolute bg-white dark:bg-[#1b1b1b] dark:border-neutral-700 rounded-xl shadow-2xl border border-neutral-300 w-full z-50 transition-all duration-200 ease-out ' + positionClasses()"
4
+ id="combobox-listbox"
5
+ role="listbox"
6
+ [attr.aria-multiselectable]="comboboxService.multiSelect()"
7
+ >
8
+ <!-- Search (só no custom trigger mode) -->
9
+ @if (comboboxService.triggerMode() === 'custom') {
10
+ <wally-combobox-search></wally-combobox-search>
11
+ }
12
+
13
+ <div class="p-1">
14
+ @if (comboboxService.filteredData().length === 0) {
15
+ <wally-combobox-empty></wally-combobox-empty>
16
+ } @else if (comboboxService.groupedData(); as groups) {
17
+ <!-- Itens agrupados -->
18
+ @for (group of groups; track group.label; let groupIdx = $index) {
19
+ <wally-combobox-group [label]="group.label">
20
+ @for (item of group.items; track item.value; let itemIdx = $index) {
21
+ <wally-combobox-item
22
+ [item]="item"
23
+ [focused]="comboboxService.focusedIndex() === getGlobalIndex(groupIdx, itemIdx)"
24
+ [selected]="comboboxService.isSelected(item.value)"
25
+ ></wally-combobox-item>
26
+ }
27
+ </wally-combobox-group>
28
+ }
29
+ } @else {
30
+ <!-- Itens normais -->
31
+ @for (item of comboboxService.filteredData(); track item.value; let idx = $index) {
32
+ <wally-combobox-item
33
+ [item]="item"
34
+ [focused]="comboboxService.focusedIndex() === idx"
35
+ [selected]="comboboxService.isSelected(item.value)"
36
+ ></wally-combobox-item>
37
+ }
38
+ }
39
+ </div>
40
+ </div>
41
+ }
@@ -0,0 +1,228 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { ComponentRef } from '@angular/core';
3
+
4
+ import { ComboboxContent } from './combobox-content';
5
+ import { ComboboxService } from '../lib/service/combobox.service';
6
+ import { ComboboxPosition } from '../lib/types/combobox-position.type';
7
+
8
+ describe('ComboboxContent', () => {
9
+ let component: ComboboxContent;
10
+ let fixture: ComponentFixture<ComboboxContent>;
11
+ let componentRef: ComponentRef<ComboboxContent>;
12
+ let service: ComboboxService;
13
+
14
+ beforeEach(async () => {
15
+ await TestBed.configureTestingModule({
16
+ imports: [ComboboxContent],
17
+ providers: [ComboboxService]
18
+ })
19
+ .compileComponents();
20
+
21
+ fixture = TestBed.createComponent(ComboboxContent);
22
+ component = fixture.componentInstance;
23
+ componentRef = fixture.componentRef;
24
+ service = TestBed.inject(ComboboxService);
25
+ fixture.detectChanges();
26
+ });
27
+
28
+ it('should create', () => {
29
+ expect(component).toBeTruthy();
30
+ });
31
+
32
+ it('should inject ComboboxService', () => {
33
+ expect(service).toBeTruthy();
34
+ });
35
+
36
+ describe('Input: position', () => {
37
+ it('should default to bottom position', () => {
38
+ expect(component.position()).toBe('bottom');
39
+ });
40
+
41
+ it('should accept top position', () => {
42
+ componentRef.setInput('position', 'top');
43
+ fixture.detectChanges();
44
+
45
+ expect(component.position()).toBe('top');
46
+ });
47
+
48
+ it('should accept all valid positions', () => {
49
+ const positions: ComboboxPosition[] = ['top', 'bottom', 'left', 'right'];
50
+
51
+ positions.forEach(pos => {
52
+ componentRef.setInput('position', pos);
53
+ fixture.detectChanges();
54
+ expect(component.position()).toBe(pos);
55
+ });
56
+ });
57
+ });
58
+
59
+ describe('Position Calculation', () => {
60
+ it('should initialize with bottom calculated position', () => {
61
+ expect(component.calculatedPosition()).toBe('bottom');
62
+ });
63
+
64
+ it('should generate correct position classes for bottom', () => {
65
+ component.calculatedPosition.set('bottom');
66
+ const classes = component.positionClasses();
67
+
68
+ expect(classes).toContain('top-full');
69
+ expect(classes).toContain('mt-2');
70
+ expect(classes).toContain('left-0');
71
+ });
72
+
73
+ it('should generate correct position classes for top', () => {
74
+ component.calculatedPosition.set('top');
75
+ const classes = component.positionClasses();
76
+
77
+ expect(classes).toContain('bottom-full');
78
+ expect(classes).toContain('mb-2');
79
+ expect(classes).toContain('left-0');
80
+ });
81
+
82
+ it('should generate correct position classes for right', () => {
83
+ component.calculatedPosition.set('right');
84
+ const classes = component.positionClasses();
85
+
86
+ expect(classes).toContain('left-full');
87
+ expect(classes).toContain('ml-2');
88
+ expect(classes).toContain('top-0');
89
+ });
90
+
91
+ it('should generate correct position classes for left', () => {
92
+ component.calculatedPosition.set('left');
93
+ const classes = component.positionClasses();
94
+
95
+ expect(classes).toContain('right-full');
96
+ expect(classes).toContain('mr-2');
97
+ expect(classes).toContain('top-0');
98
+ });
99
+
100
+ it('should include scroll classes', () => {
101
+ const classes = component.positionClasses();
102
+
103
+ expect(classes).toContain('max-h-96');
104
+ expect(classes).toContain('overflow-y-auto');
105
+ });
106
+ });
107
+
108
+ describe('Global Index Calculation', () => {
109
+ it('should return item index when no grouping', () => {
110
+ const index = component.getGlobalIndex(0, 5);
111
+ expect(index).toBe(5);
112
+ });
113
+
114
+ it('should calculate global index for grouped items', () => {
115
+ service.setData([
116
+ { value: 1, label: 'JS', group: 'Frontend' },
117
+ { value: 2, label: 'TS', group: 'Frontend' },
118
+ { value: 3, label: 'Python', group: 'Backend' },
119
+ { value: 4, label: 'Java', group: 'Backend' }
120
+ ]);
121
+ service.setGroupBy('group');
122
+
123
+ // First item in second group (Backend)
124
+ const index = component.getGlobalIndex(1, 0);
125
+ expect(index).toBe(2); // After 2 Frontend items
126
+ });
127
+
128
+ it('should handle multiple groups correctly', () => {
129
+ service.setData([
130
+ { value: 1, label: 'Item1', group: 'A' },
131
+ { value: 2, label: 'Item2', group: 'A' },
132
+ { value: 3, label: 'Item3', group: 'B' },
133
+ { value: 4, label: 'Item4', group: 'C' },
134
+ { value: 5, label: 'Item5', group: 'C' }
135
+ ]);
136
+ service.setGroupBy('group');
137
+
138
+ expect(component.getGlobalIndex(0, 0)).toBe(0); // First item in Group A
139
+ expect(component.getGlobalIndex(1, 0)).toBe(2); // First item in Group B
140
+ expect(component.getGlobalIndex(2, 1)).toBe(4); // Second item in Group C
141
+ });
142
+ });
143
+
144
+ describe('Window Resize', () => {
145
+ it('should recalculate position on resize when open', () => {
146
+ spyOn<any>(component, 'calculateBestPosition').and.returnValue('top');
147
+ service.open();
148
+
149
+ component.onResize();
150
+
151
+ expect(component['calculateBestPosition']).toHaveBeenCalled();
152
+ expect(component.calculatedPosition()).toBe('top');
153
+ });
154
+
155
+ it('should not recalculate position on resize when closed', () => {
156
+ spyOn<any>(component, 'calculateBestPosition');
157
+
158
+ component.onResize();
159
+
160
+ expect(component['calculateBestPosition']).not.toHaveBeenCalled();
161
+ });
162
+ });
163
+
164
+ describe('Escape Key', () => {
165
+ it('should close combobox on escape when open', () => {
166
+ service.open();
167
+ expect(service.isOpen()).toBe(true);
168
+
169
+ component.onEscape();
170
+
171
+ expect(service.isOpen()).toBe(false);
172
+ });
173
+
174
+ it('should not error on escape when closed', () => {
175
+ expect(service.isOpen()).toBe(false);
176
+
177
+ expect(() => component.onEscape()).not.toThrow();
178
+ });
179
+ });
180
+
181
+ describe('Click Outside', () => {
182
+ it('should close when clicking outside', () => {
183
+ service.open();
184
+
185
+ const event = new MouseEvent('mousedown');
186
+ Object.defineProperty(event, 'target', { value: document.body, enumerable: true });
187
+
188
+ component.onDocumentClick(event);
189
+
190
+ expect(service.isOpen()).toBe(false);
191
+ });
192
+
193
+ it('should not close when clicking inside content', () => {
194
+ service.open();
195
+
196
+ const event = new MouseEvent('mousedown');
197
+ Object.defineProperty(event, 'target', { value: fixture.nativeElement, enumerable: true });
198
+
199
+ component.onDocumentClick(event);
200
+
201
+ expect(service.isOpen()).toBe(true);
202
+ });
203
+
204
+ it('should not process click when combobox is closed', () => {
205
+ const closeSpy = spyOn(service, 'close');
206
+
207
+ const event = new MouseEvent('mousedown');
208
+ Object.defineProperty(event, 'target', { value: document.body, enumerable: true });
209
+
210
+ component.onDocumentClick(event);
211
+
212
+ expect(closeSpy).not.toHaveBeenCalled();
213
+ });
214
+ });
215
+
216
+ describe('Position Recalculation on Open', () => {
217
+ it('should recalculate position when opening', (done) => {
218
+ spyOn<any>(component, 'calculateBestPosition').and.returnValue('top');
219
+
220
+ service.open();
221
+
222
+ setTimeout(() => {
223
+ expect(component['calculateBestPosition']).toHaveBeenCalled();
224
+ done();
225
+ }, 10);
226
+ });
227
+ });
228
+ });
@@ -0,0 +1,217 @@
1
+ import { Component, computed, effect, ElementRef, HostListener, inject, input, signal } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ import { ComboboxService } from '../lib/service/combobox.service';
5
+ import { ComboboxPosition } from '../lib/types/combobox-position.type';
6
+ import { ComboboxItem } from '../combobox-item/combobox-item';
7
+ import { ComboboxEmpty } from '../combobox-empty/combobox-empty';
8
+ import { ComboboxGroup } from '../combobox-group/combobox-group';
9
+ import { ComboboxSearch } from '../combobox-search/combobox-search';
10
+
11
+ @Component({
12
+ selector: 'wally-combobox-content',
13
+ imports: [
14
+ CommonModule,
15
+ ComboboxItem,
16
+ ComboboxEmpty,
17
+ ComboboxGroup,
18
+ ComboboxSearch
19
+ ],
20
+ templateUrl: './combobox-content.html',
21
+ styleUrl: './combobox-content.css'
22
+ })
23
+ export class ComboboxContent {
24
+ comboboxService = inject(ComboboxService);
25
+ private elementRef = inject(ElementRef);
26
+
27
+ position = input<ComboboxPosition>('bottom');
28
+
29
+ calculatedPosition = signal<ComboboxPosition>('bottom');
30
+
31
+ positionClasses = computed(() => {
32
+ const position = this.calculatedPosition();
33
+ const scrollClasses = 'max-h-96 overflow-y-auto';
34
+
35
+ const positionMap = {
36
+ bottom: 'top-full mt-2 left-0',
37
+ top: 'bottom-full mb-2 left-0',
38
+ right: 'left-full ml-2 top-0',
39
+ left: 'right-full mr-2 top-0'
40
+ };
41
+
42
+ return `${scrollClasses} ${positionMap[position]}`;
43
+ });
44
+
45
+ constructor() {
46
+ // Recalcular ao abrir
47
+ effect(() => {
48
+ if (this.comboboxService.isOpen()) {
49
+ setTimeout(() => {
50
+ const bestPosition = this.calculateBestPosition();
51
+ this.calculatedPosition.set(bestPosition);
52
+ }, 0);
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Recalculates position on window resize to maintain optimal placement
59
+ */
60
+ @HostListener('window:resize')
61
+ onResize(): void {
62
+ if (this.comboboxService.isOpen()) {
63
+ const bestPosition = this.calculateBestPosition();
64
+ this.calculatedPosition.set(bestPosition);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Close on Escape key
70
+ */
71
+ @HostListener('document:keydown.escape')
72
+ onEscape(): void {
73
+ if (this.comboboxService.isOpen()) {
74
+ this.comboboxService.close();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Close when clicking outside
80
+ */
81
+ @HostListener('document:mousedown', ['$event'])
82
+ onDocumentClick(event: MouseEvent): void {
83
+ if (!this.comboboxService.isOpen()) return;
84
+
85
+ const contentElement = this.elementRef.nativeElement;
86
+ const triggerElement = contentElement.parentElement;
87
+
88
+ const clickedInside = contentElement.contains(event.target as Node);
89
+ const clickedTrigger = triggerElement?.contains(event.target as Node);
90
+
91
+ if (!clickedInside && !clickedTrigger) {
92
+ this.comboboxService.close();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Measures available space around the trigger element in all directions.
98
+ * Pattern from dropdown-menu-content.ts
99
+ */
100
+ private measureAvailableSpace(): {
101
+ triggerRect: DOMRect;
102
+ spaceAbove: number;
103
+ spaceBelow: number;
104
+ spaceLeft: number;
105
+ spaceRight: number;
106
+ } | null {
107
+ const triggerElement = this.elementRef.nativeElement.parentElement;
108
+
109
+ if (!triggerElement) return null;
110
+
111
+ const triggerRect = triggerElement.getBoundingClientRect();
112
+ const viewportWidth = window.innerWidth;
113
+ const viewportHeight = window.innerHeight;
114
+
115
+ return {
116
+ triggerRect,
117
+ spaceAbove: triggerRect.top,
118
+ spaceBelow: viewportHeight - triggerRect.bottom,
119
+ spaceLeft: triggerRect.left,
120
+ spaceRight: viewportWidth - triggerRect.right
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Calculates the best position for the combobox based on available viewport space.
126
+ * Falls back to alternative positions if preferred position doesn't fit.
127
+ * If no position has enough space, chooses the direction with the most available space.
128
+ */
129
+ private calculateBestPosition(): ComboboxPosition {
130
+ const space = this.measureAvailableSpace();
131
+
132
+ if (!space) return this.position();
133
+
134
+ const menuDimensions = this.getMenuDimensions();
135
+ const MENU_MIN_HEIGHT = menuDimensions.height + 20;
136
+ const MENU_MIN_WIDTH = menuDimensions.width + 20;
137
+
138
+ const preferred = this.position();
139
+
140
+ // Try preferred position first, then fallbacks
141
+ switch (preferred) {
142
+ case 'bottom':
143
+ if (space.spaceBelow >= MENU_MIN_HEIGHT) return 'bottom';
144
+ if (space.spaceAbove >= MENU_MIN_HEIGHT) return 'top';
145
+ if (space.spaceRight >= MENU_MIN_WIDTH) return 'right';
146
+ if (space.spaceLeft >= MENU_MIN_WIDTH) return 'left';
147
+ break;
148
+
149
+ case 'top':
150
+ if (space.spaceAbove >= MENU_MIN_HEIGHT) return 'top';
151
+ if (space.spaceBelow >= MENU_MIN_HEIGHT) return 'bottom';
152
+ if (space.spaceRight >= MENU_MIN_WIDTH) return 'right';
153
+ if (space.spaceLeft >= MENU_MIN_WIDTH) return 'left';
154
+ break;
155
+
156
+ case 'right':
157
+ if (space.spaceRight >= MENU_MIN_WIDTH) return 'right';
158
+ if (space.spaceLeft >= MENU_MIN_WIDTH) return 'left';
159
+ if (space.spaceBelow >= MENU_MIN_HEIGHT) return 'bottom';
160
+ if (space.spaceAbove >= MENU_MIN_HEIGHT) return 'top';
161
+ break;
162
+
163
+ case 'left':
164
+ if (space.spaceLeft >= MENU_MIN_WIDTH) return 'left';
165
+ if (space.spaceRight >= MENU_MIN_WIDTH) return 'right';
166
+ if (space.spaceBelow >= MENU_MIN_HEIGHT) return 'bottom';
167
+ if (space.spaceAbove >= MENU_MIN_HEIGHT) return 'top';
168
+ break;
169
+ }
170
+
171
+ // If no position has enough space, choose the one with the most space
172
+ const maxVerticalSpace = Math.max(space.spaceAbove, space.spaceBelow);
173
+ const maxHorizontalSpace = Math.max(space.spaceLeft, space.spaceRight);
174
+
175
+ // Prefer vertical positions (bottom/top) over horizontal
176
+ if (maxVerticalSpace >= maxHorizontalSpace) {
177
+ return space.spaceBelow >= space.spaceAbove ? 'bottom' : 'top';
178
+ } else {
179
+ return space.spaceRight >= space.spaceLeft ? 'right' : 'left';
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Gets actual rendered dimensions of the dropdown menu
185
+ */
186
+ private getMenuDimensions(): { height: number; width: number } {
187
+ const menuElement = this.elementRef.nativeElement.querySelector('[role="listbox"]');
188
+
189
+ if (!menuElement) {
190
+ return {
191
+ height: 384, // max-h-96 = 384px
192
+ width: 288 // min-w-72 = 288px
193
+ };
194
+ }
195
+
196
+ const rect = menuElement.getBoundingClientRect();
197
+
198
+ return {
199
+ height: rect.height || 384,
200
+ width: rect.width || 288
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Calculates global index for grouped items
206
+ */
207
+ getGlobalIndex(groupIndex: number, itemIndex: number): number {
208
+ const groups = this.comboboxService.groupedData();
209
+ if (!groups) return itemIndex;
210
+
211
+ let globalIndex = 0;
212
+ for (let i = 0; i < groupIndex; i++) {
213
+ globalIndex += groups[i].items.length;
214
+ }
215
+ return globalIndex + itemIndex;
216
+ }
217
+ }
@@ -0,0 +1,3 @@
1
+ <div class="py-6 text-center text-sm text-neutral-500 dark:text-neutral-400">
2
+ {{ message() }}
3
+ </div>
@@ -0,0 +1,56 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { ComponentRef } from '@angular/core';
3
+
4
+ import { ComboboxEmpty } from './combobox-empty';
5
+
6
+ describe('ComboboxEmpty', () => {
7
+ let component: ComboboxEmpty;
8
+ let fixture: ComponentFixture<ComboboxEmpty>;
9
+ let componentRef: ComponentRef<ComboboxEmpty>;
10
+
11
+ beforeEach(async () => {
12
+ await TestBed.configureTestingModule({
13
+ imports: [ComboboxEmpty]
14
+ })
15
+ .compileComponents();
16
+
17
+ fixture = TestBed.createComponent(ComboboxEmpty);
18
+ component = fixture.componentInstance;
19
+ componentRef = fixture.componentRef;
20
+ fixture.detectChanges();
21
+ });
22
+
23
+ it('should create', () => {
24
+ expect(component).toBeTruthy();
25
+ });
26
+
27
+ describe('Input: message', () => {
28
+ it('should have default message', () => {
29
+ expect(component.message()).toBe('No results found');
30
+ });
31
+
32
+ it('should accept custom message', () => {
33
+ componentRef.setInput('message', 'No items to display');
34
+ fixture.detectChanges();
35
+
36
+ expect(component.message()).toBe('No items to display');
37
+ });
38
+
39
+ it('should update when changed', () => {
40
+ componentRef.setInput('message', 'Empty list');
41
+ fixture.detectChanges();
42
+ expect(component.message()).toBe('Empty list');
43
+
44
+ componentRef.setInput('message', 'Nothing here');
45
+ fixture.detectChanges();
46
+ expect(component.message()).toBe('Nothing here');
47
+ });
48
+
49
+ it('should accept empty string', () => {
50
+ componentRef.setInput('message', '');
51
+ fixture.detectChanges();
52
+
53
+ expect(component.message()).toBe('');
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,11 @@
1
+ import { Component, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-combobox-empty',
5
+ imports: [],
6
+ templateUrl: './combobox-empty.html',
7
+ styleUrl: './combobox-empty.css'
8
+ })
9
+ export class ComboboxEmpty {
10
+ message = input<string>('No results found');
11
+ }
@@ -0,0 +1,11 @@
1
+ <div class="pt-2 first:pt-0">
2
+ <!-- Group header -->
3
+ <div class="px-3 py-2 text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
4
+ {{ label() }}
5
+ </div>
6
+
7
+ <!-- Group items -->
8
+ <div class="space-y-0.5">
9
+ <ng-content></ng-content>
10
+ </div>
11
+ </div>
@@ -0,0 +1,57 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { ComponentRef } from '@angular/core';
3
+
4
+ import { ComboboxGroup } from './combobox-group';
5
+
6
+ describe('ComboboxGroup', () => {
7
+ let component: ComboboxGroup;
8
+ let fixture: ComponentFixture<ComboboxGroup>;
9
+ let componentRef: ComponentRef<ComboboxGroup>;
10
+
11
+ beforeEach(async () => {
12
+ await TestBed.configureTestingModule({
13
+ imports: [ComboboxGroup]
14
+ })
15
+ .compileComponents();
16
+
17
+ fixture = TestBed.createComponent(ComboboxGroup);
18
+ component = fixture.componentInstance;
19
+ componentRef = fixture.componentRef;
20
+ });
21
+
22
+ it('should create', () => {
23
+ componentRef.setInput('label', 'Test Group');
24
+ fixture.detectChanges();
25
+
26
+ expect(component).toBeTruthy();
27
+ });
28
+
29
+ describe('Input: label', () => {
30
+ it('should be required', () => {
31
+ componentRef.setInput('label', 'Frontend');
32
+ fixture.detectChanges();
33
+
34
+ expect(component.label()).toBe('Frontend');
35
+ });
36
+
37
+ it('should update when changed', () => {
38
+ componentRef.setInput('label', 'Backend');
39
+ fixture.detectChanges();
40
+ expect(component.label()).toBe('Backend');
41
+
42
+ componentRef.setInput('label', 'DevOps');
43
+ fixture.detectChanges();
44
+ expect(component.label()).toBe('DevOps');
45
+ });
46
+
47
+ it('should accept any string value', () => {
48
+ const testLabels = ['Group 1', 'Category A', 'Section X', ''];
49
+
50
+ testLabels.forEach(label => {
51
+ componentRef.setInput('label', label);
52
+ fixture.detectChanges();
53
+ expect(component.label()).toBe(label);
54
+ });
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,11 @@
1
+ import { Component, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-combobox-group',
5
+ imports: [],
6
+ templateUrl: './combobox-group.html',
7
+ styleUrl: './combobox-group.css'
8
+ })
9
+ export class ComboboxGroup {
10
+ label = input.required<string>();
11
+ }