@sneat/ui 0.1.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/eslint.config.js +7 -0
- package/ng-package.json +7 -0
- package/package.json +16 -0
- package/project.json +38 -0
- package/src/index.ts +3 -0
- package/src/lib/components/index.ts +2 -0
- package/src/lib/components/sneat-base-modal.component.ts +22 -0
- package/src/lib/components/sneat-base.component.spec.ts +71 -0
- package/src/lib/components/sneat-base.component.ts +78 -0
- package/src/lib/focus.ts +19 -0
- package/src/lib/selector/index.ts +6 -0
- package/src/lib/selector/multi-selector/index.ts +2 -0
- package/src/lib/selector/multi-selector/multi-selector.component.html +26 -0
- package/src/lib/selector/multi-selector/multi-selector.component.spec.ts +147 -0
- package/src/lib/selector/multi-selector/multi-selector.component.ts +79 -0
- package/src/lib/selector/multi-selector/multi-selector.service.spec.ts +91 -0
- package/src/lib/selector/multi-selector/multi-selector.service.ts +49 -0
- package/src/lib/selector/select-from-list/index.ts +1 -0
- package/src/lib/selector/select-from-list/select-from-list.component.html +210 -0
- package/src/lib/selector/select-from-list/select-from-list.component.spec.ts +297 -0
- package/src/lib/selector/select-from-list/select-from-list.component.ts +283 -0
- package/src/lib/selector/selector-base.component.ts +43 -0
- package/src/lib/selector/selector-base.service.ts +62 -0
- package/src/lib/selector/selector-interfaces.ts +28 -0
- package/src/lib/selector/selector-options.ts +18 -0
- package/src/test-setup.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +31 -0
- package/vite.config.mts +10 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
@if (value) {
|
|
2
|
+
<ion-item
|
|
3
|
+
[lines]="lastItemLines"
|
|
4
|
+
[class]="{ 'sneat-tiny-end-padding': !isReadonly }"
|
|
5
|
+
>
|
|
6
|
+
@if ($selectedItem()?.iconName) {
|
|
7
|
+
<ion-icon
|
|
8
|
+
slot="start"
|
|
9
|
+
[name]="$selectedItem()?.iconName"
|
|
10
|
+
[color]="
|
|
11
|
+
$selectedItem()?.iconColor || $selectedItem()?.labelColor || 'medium'
|
|
12
|
+
"
|
|
13
|
+
/>
|
|
14
|
+
}
|
|
15
|
+
<ion-select
|
|
16
|
+
#selectInput
|
|
17
|
+
interface="popover"
|
|
18
|
+
[label]="label"
|
|
19
|
+
[(ngModel)]="value"
|
|
20
|
+
(ionChange)="onSelectChanged()"
|
|
21
|
+
[disabled]="isReadonly || !!$isProcessing()"
|
|
22
|
+
>
|
|
23
|
+
@for (item of items; track item.id) {
|
|
24
|
+
<ion-select-option [value]="item.id">
|
|
25
|
+
{{ item.emoji }}
|
|
26
|
+
@if (item.shortTitle) {
|
|
27
|
+
{{ item.shortTitle }}
|
|
28
|
+
} @else {
|
|
29
|
+
{{ item.title }}
|
|
30
|
+
}
|
|
31
|
+
</ion-select-option>
|
|
32
|
+
}
|
|
33
|
+
<!-- <ion-select-option value="other">OTHER</ion-select-option>-->
|
|
34
|
+
</ion-select>
|
|
35
|
+
@if ($isProcessing()) {
|
|
36
|
+
<ion-spinner name="lines-small" color="medium" slot="end" />
|
|
37
|
+
} @else if (!isReadonly) {
|
|
38
|
+
<ion-buttons slot="end" class="ion-no-margin">
|
|
39
|
+
<ion-button color="medium" title="Deselect" (click)="deselect()">
|
|
40
|
+
<ion-icon name="close-outline" />
|
|
41
|
+
</ion-button>
|
|
42
|
+
</ion-buttons>
|
|
43
|
+
}
|
|
44
|
+
</ion-item>
|
|
45
|
+
} @else {
|
|
46
|
+
@if (isFilterable) {
|
|
47
|
+
<ion-item class="sneat-tiny-end-padding">
|
|
48
|
+
<ion-input
|
|
49
|
+
#filterInput
|
|
50
|
+
color="medium"
|
|
51
|
+
placeholder="filter"
|
|
52
|
+
[label]="filterLabel"
|
|
53
|
+
[value]="$filter()"
|
|
54
|
+
(ionChange)="onFilterChanged($event, 'ionChange')"
|
|
55
|
+
(ionInput)="onFilterChanged($event, 'ionInput')"
|
|
56
|
+
/>
|
|
57
|
+
@if ($filter()) {
|
|
58
|
+
<ion-buttons slot="end">
|
|
59
|
+
@if (canAdd && $hiddenCount()) {
|
|
60
|
+
<ion-button
|
|
61
|
+
title="Use this"
|
|
62
|
+
color="primary"
|
|
63
|
+
(click)="onAdd($event)"
|
|
64
|
+
>
|
|
65
|
+
<ion-icon name="add-outline" />
|
|
66
|
+
<ion-label>Add</ion-label>
|
|
67
|
+
</ion-button>
|
|
68
|
+
}
|
|
69
|
+
<ion-button (click)="clearFilter()" title="Clear filter">
|
|
70
|
+
<ion-icon name="close-outline" />
|
|
71
|
+
</ion-button>
|
|
72
|
+
</ion-buttons>
|
|
73
|
+
}
|
|
74
|
+
</ion-item>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@if (items && !items.length) {
|
|
78
|
+
<ion-item-divider>
|
|
79
|
+
<ion-label>No items yet.</ion-label>
|
|
80
|
+
</ion-item-divider>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@if (labelPlacement) {
|
|
84
|
+
<ion-radio-group [(ngModel)]="value" (ionChange)="onRadioChanged($event)">
|
|
85
|
+
<ion-list class="ion-no-padding" lines="full">
|
|
86
|
+
@if (listLabel === "divider") {
|
|
87
|
+
<ion-item [color]="listLabelColor">
|
|
88
|
+
<ion-label>{{ label }}</ion-label>
|
|
89
|
+
</ion-item>
|
|
90
|
+
}
|
|
91
|
+
@for (item of $displayItems(); track item.id) {
|
|
92
|
+
<ion-item
|
|
93
|
+
[lines]="item.description1 ? 'inset' : 'full'"
|
|
94
|
+
(click)="select(item)"
|
|
95
|
+
tappable="true"
|
|
96
|
+
>
|
|
97
|
+
@switch (selectMode) {
|
|
98
|
+
@case ("multiple") {
|
|
99
|
+
<ion-checkbox
|
|
100
|
+
slot="start"
|
|
101
|
+
[value]="item.id"
|
|
102
|
+
[checked]="false"
|
|
103
|
+
[disabled]="isDisabled"
|
|
104
|
+
(ionChange)="onCheckboxChange($event, item)"
|
|
105
|
+
/>
|
|
106
|
+
<ion-label>
|
|
107
|
+
<!-- TODO: duplicate code with the next case -->
|
|
108
|
+
{{ item.emoji }}
|
|
109
|
+
@if (item.shortTitle) {
|
|
110
|
+
@if (item.longTitle) {
|
|
111
|
+
{{ item.longTitle }} - {{ item.shortTitle }}
|
|
112
|
+
} @else {
|
|
113
|
+
{{ item.title }} - {{ item.shortTitle }}
|
|
114
|
+
}
|
|
115
|
+
} @else if (item.longTitle) {
|
|
116
|
+
{{ item.longTitle }}
|
|
117
|
+
} @else {
|
|
118
|
+
{{ item.title }}
|
|
119
|
+
}
|
|
120
|
+
</ion-label>
|
|
121
|
+
}
|
|
122
|
+
@case ("single") {
|
|
123
|
+
<ion-radio
|
|
124
|
+
[value]="item.id"
|
|
125
|
+
[labelPlacement]="labelPlacement"
|
|
126
|
+
[justify]="
|
|
127
|
+
justify ||
|
|
128
|
+
(!labelPlacement || labelPlacement === 'start'
|
|
129
|
+
? 'space-between'
|
|
130
|
+
: 'start')
|
|
131
|
+
"
|
|
132
|
+
>
|
|
133
|
+
{{ item.emoji }}
|
|
134
|
+
@if (item.shortTitle) {
|
|
135
|
+
@if (item.longTitle) {
|
|
136
|
+
{{ item.longTitle }} - {{ item.shortTitle }}
|
|
137
|
+
} @else {
|
|
138
|
+
{{ item.title }} - {{ item.shortTitle }}
|
|
139
|
+
}
|
|
140
|
+
} @else if (item.longTitle) {
|
|
141
|
+
{{ item.longTitle }}
|
|
142
|
+
} @else {
|
|
143
|
+
{{ item.title }}
|
|
144
|
+
}
|
|
145
|
+
</ion-radio>
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
</ion-item>
|
|
149
|
+
@if (item.description1) {
|
|
150
|
+
<ion-item-divider>
|
|
151
|
+
<ion-label>
|
|
152
|
+
{{ item.description1 }}
|
|
153
|
+
@if (item.description2) {
|
|
154
|
+
<i>{{ item.description2 }}</i>
|
|
155
|
+
}
|
|
156
|
+
</ion-label>
|
|
157
|
+
</ion-item-divider>
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
</ion-list>
|
|
161
|
+
</ion-radio-group>
|
|
162
|
+
} @else {
|
|
163
|
+
<ion-item-group>
|
|
164
|
+
@for (item of $displayItems(); track item.id; let last = $last) {
|
|
165
|
+
<ion-item
|
|
166
|
+
[lines]="item.description1 ? 'inset' : last ? lastItemLines : 'full'"
|
|
167
|
+
button
|
|
168
|
+
(click)="select(item)"
|
|
169
|
+
>
|
|
170
|
+
@if (item.iconName) {
|
|
171
|
+
<ion-icon
|
|
172
|
+
slot="start"
|
|
173
|
+
[name]="item.iconName"
|
|
174
|
+
[color]="item.iconColor || item.labelColor || 'medium'"
|
|
175
|
+
/>
|
|
176
|
+
}
|
|
177
|
+
@if (!labelPlacement) {
|
|
178
|
+
<ion-label [color]="item.labelColor">
|
|
179
|
+
<span class="ion-margin-end">{{ item.emoji }}</span>
|
|
180
|
+
{{ item.title }}
|
|
181
|
+
</ion-label>
|
|
182
|
+
}
|
|
183
|
+
</ion-item>
|
|
184
|
+
@if (item.description1) {
|
|
185
|
+
<ion-item [lines]="last ? lastItemLines : 'full'">
|
|
186
|
+
<ion-label color="medium">
|
|
187
|
+
{{ item.description1 }}
|
|
188
|
+
@if (item.description2) {
|
|
189
|
+
<i>{{ item.description2 }}</i>
|
|
190
|
+
}
|
|
191
|
+
</ion-label>
|
|
192
|
+
</ion-item>
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
</ion-item-group>
|
|
196
|
+
}
|
|
197
|
+
@if ($hiddenCount(); as hiddenCount) {
|
|
198
|
+
<ion-item-divider>
|
|
199
|
+
<ion-label color="medium"
|
|
200
|
+
>{{ hiddenCount }} out of {{ items?.length }} items are hidden by filter
|
|
201
|
+
</ion-label>
|
|
202
|
+
<ion-buttons slot="end">
|
|
203
|
+
<ion-button (click)="clearFilter()">
|
|
204
|
+
<ion-icon name="close-outline" slot="start" />
|
|
205
|
+
<ion-label>Clear filter</ion-label>
|
|
206
|
+
</ion-button>
|
|
207
|
+
</ion-buttons>
|
|
208
|
+
</ion-item-divider>
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
ComponentFixture,
|
|
4
|
+
fakeAsync,
|
|
5
|
+
TestBed,
|
|
6
|
+
tick,
|
|
7
|
+
waitForAsync,
|
|
8
|
+
} from '@angular/core/testing';
|
|
9
|
+
import { ErrorLogger } from '@sneat/core';
|
|
10
|
+
import { SelectFromListComponent } from './select-from-list.component';
|
|
11
|
+
import { of } from 'rxjs';
|
|
12
|
+
import { ISelectItem } from '../selector-interfaces';
|
|
13
|
+
|
|
14
|
+
describe('SelectFromListComponent', () => {
|
|
15
|
+
let component: SelectFromListComponent;
|
|
16
|
+
let fixture: ComponentFixture<SelectFromListComponent>;
|
|
17
|
+
let errorLoggerMock: ErrorLogger;
|
|
18
|
+
|
|
19
|
+
const mockItems: ISelectItem[] = [
|
|
20
|
+
{ id: '1', title: 'Apple' },
|
|
21
|
+
{ id: '2', title: 'Banana', longTitle: 'Yellow Banana' },
|
|
22
|
+
{ id: '3', title: 'Cherry' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
beforeEach(waitForAsync(async () => {
|
|
26
|
+
errorLoggerMock = {
|
|
27
|
+
logError: vi.fn(),
|
|
28
|
+
logErrorHandler: vi.fn(() => vi.fn()),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await TestBed.configureTestingModule({
|
|
32
|
+
imports: [SelectFromListComponent],
|
|
33
|
+
providers: [{ provide: ErrorLogger, useValue: errorLoggerMock }],
|
|
34
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
35
|
+
})
|
|
36
|
+
.overrideComponent(SelectFromListComponent, {
|
|
37
|
+
set: { imports: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] },
|
|
38
|
+
})
|
|
39
|
+
.compileComponents();
|
|
40
|
+
fixture = TestBed.createComponent(SelectFromListComponent);
|
|
41
|
+
component = fixture.componentInstance;
|
|
42
|
+
fixture.detectChanges();
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
it('should create', () => {
|
|
46
|
+
expect(component).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('ngOnChanges', () => {
|
|
50
|
+
it('should update items and apply filter when items change', () => {
|
|
51
|
+
component.items = mockItems;
|
|
52
|
+
component.ngOnChanges({
|
|
53
|
+
items: new SimpleChange(undefined, mockItems, true),
|
|
54
|
+
});
|
|
55
|
+
// @ts-expect-error accessing protected member
|
|
56
|
+
expect(component.$displayItems()).toEqual(mockItems);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should subscribe to items$ and update items when items$ changes', () => {
|
|
60
|
+
const items$ = of(mockItems);
|
|
61
|
+
component.items$ = items$;
|
|
62
|
+
component.ngOnChanges({
|
|
63
|
+
items$: new SimpleChange(undefined, items$, true),
|
|
64
|
+
});
|
|
65
|
+
// @ts-expect-error accessing protected member
|
|
66
|
+
expect(component.$displayItems()).toEqual(mockItems);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('filtering', () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
component.items = mockItems;
|
|
73
|
+
component.ngOnChanges({
|
|
74
|
+
items: new SimpleChange(undefined, mockItems, true),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should filter items by title', () => {
|
|
79
|
+
// @ts-expect-error accessing protected member
|
|
80
|
+
component.onFilterChanged(
|
|
81
|
+
{ detail: { value: 'app' } } as CustomEvent,
|
|
82
|
+
'ionInput',
|
|
83
|
+
);
|
|
84
|
+
// @ts-expect-error accessing protected member
|
|
85
|
+
expect(component.$displayItems()?.length).toBe(1);
|
|
86
|
+
// @ts-expect-error accessing protected member
|
|
87
|
+
expect(component.$displayItems()?.[0].id).toBe('1');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should filter items by longTitle', () => {
|
|
91
|
+
// @ts-expect-error accessing protected member
|
|
92
|
+
component.onFilterChanged(
|
|
93
|
+
{ detail: { value: 'yellow' } } as CustomEvent,
|
|
94
|
+
'ionInput',
|
|
95
|
+
);
|
|
96
|
+
// @ts-expect-error accessing protected member
|
|
97
|
+
expect(component.$displayItems()?.length).toBe(1);
|
|
98
|
+
// @ts-expect-error accessing protected member
|
|
99
|
+
expect(component.$displayItems()?.[0].id).toBe('2');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should use custom filter function if provided', () => {
|
|
103
|
+
component.filterItem = (item, filter) => item.id === filter;
|
|
104
|
+
// @ts-expect-error accessing protected member
|
|
105
|
+
component.onFilterChanged({ detail: { value: '3' } } as CustomEvent, 'ionInput');
|
|
106
|
+
// @ts-expect-error accessing protected member
|
|
107
|
+
expect(component.$displayItems()?.length).toBe(1);
|
|
108
|
+
// @ts-expect-error accessing protected member
|
|
109
|
+
expect(component.$displayItems()?.[0].id).toBe('3');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should clear filter', () => {
|
|
113
|
+
// @ts-expect-error accessing protected member
|
|
114
|
+
component.onFilterChanged(
|
|
115
|
+
{ detail: { value: 'app' } } as CustomEvent,
|
|
116
|
+
'ionInput',
|
|
117
|
+
);
|
|
118
|
+
// @ts-expect-error accessing protected member
|
|
119
|
+
component.clearFilter();
|
|
120
|
+
// @ts-expect-error accessing protected member
|
|
121
|
+
expect(component.$filter()).toBe('');
|
|
122
|
+
// @ts-expect-error accessing protected member
|
|
123
|
+
expect(component.$displayItems()?.length).toBe(3);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('sorting', () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
component.items = [
|
|
130
|
+
{ id: 'b', title: 'Banana' },
|
|
131
|
+
{ id: 'a', title: 'Apple' },
|
|
132
|
+
];
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should sort items by title', () => {
|
|
136
|
+
component.sortBy = 'title';
|
|
137
|
+
component.ngOnChanges({
|
|
138
|
+
items: new SimpleChange(undefined, component.items, true),
|
|
139
|
+
});
|
|
140
|
+
// @ts-expect-error accessing protected member
|
|
141
|
+
expect(component.$displayItems()?.[0].title).toBe('Apple');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should sort items by id', () => {
|
|
145
|
+
component.sortBy = 'id';
|
|
146
|
+
component.ngOnChanges({
|
|
147
|
+
items: new SimpleChange(undefined, component.items, true),
|
|
148
|
+
});
|
|
149
|
+
// @ts-expect-error accessing protected member
|
|
150
|
+
expect(component.$displayItems()?.[0].id).toBe('a');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('selection', () => {
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
component.items = mockItems;
|
|
157
|
+
component.ngOnChanges({
|
|
158
|
+
items: new SimpleChange(undefined, mockItems, true),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should select item in single mode', () => {
|
|
163
|
+
const onChange = vi.fn();
|
|
164
|
+
component.registerOnChange(onChange);
|
|
165
|
+
// @ts-expect-error accessing protected member
|
|
166
|
+
component.select(mockItems[0]);
|
|
167
|
+
// @ts-expect-error accessing protected member
|
|
168
|
+
expect(component.$selectedItem()).toEqual(mockItems[0]);
|
|
169
|
+
expect(onChange).toHaveBeenCalledWith('1');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should handle radio change in single mode', () => {
|
|
173
|
+
const onChange = vi.fn();
|
|
174
|
+
component.registerOnChange(onChange);
|
|
175
|
+
// @ts-expect-error accessing protected member
|
|
176
|
+
component.onRadioChanged({ detail: { value: '2' } } as CustomEvent);
|
|
177
|
+
expect(component.value).toBe('2');
|
|
178
|
+
expect(onChange).toHaveBeenCalledWith('2');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle select change', () => {
|
|
182
|
+
const onChange = vi.fn();
|
|
183
|
+
component.registerOnChange(onChange);
|
|
184
|
+
component.value = '3';
|
|
185
|
+
// @ts-expect-error accessing protected member
|
|
186
|
+
component.onSelectChanged({} as Event);
|
|
187
|
+
expect(onChange).toHaveBeenCalledWith('3');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should deselect item', () => {
|
|
191
|
+
const onChange = vi.fn();
|
|
192
|
+
component.registerOnChange(onChange);
|
|
193
|
+
// @ts-expect-error accessing protected member
|
|
194
|
+
component.deselect();
|
|
195
|
+
expect(component.value).toBe('');
|
|
196
|
+
expect(onChange).toHaveBeenCalledWith('');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('ControlValueAccessor', () => {
|
|
201
|
+
it('should write value', () => {
|
|
202
|
+
component.writeValue('test');
|
|
203
|
+
expect(component.value).toBe('test');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should register on change', () => {
|
|
207
|
+
const fn = vi.fn();
|
|
208
|
+
component.registerOnChange(fn);
|
|
209
|
+
// @ts-expect-error accessing protected member
|
|
210
|
+
component.onChange('test');
|
|
211
|
+
expect(fn).toHaveBeenCalledWith('test');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should register on touched', () => {
|
|
215
|
+
const fn = vi.fn();
|
|
216
|
+
component.registerOnTouched(fn);
|
|
217
|
+
component.onTouched();
|
|
218
|
+
expect(fn).toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should set disabled state', () => {
|
|
222
|
+
component.setDisabledState(true);
|
|
223
|
+
// @ts-expect-error accessing protected member
|
|
224
|
+
expect(component.isDisabled).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('focus', () => {
|
|
229
|
+
it('should call setFocus on filterInput after timeout', fakeAsync(() => {
|
|
230
|
+
const filterInputMock = {
|
|
231
|
+
setFocus: vi.fn(() => Promise.resolve()),
|
|
232
|
+
};
|
|
233
|
+
component.filterInput = filterInputMock as unknown;
|
|
234
|
+
component.focus();
|
|
235
|
+
tick(100);
|
|
236
|
+
expect(filterInputMock.setFocus).toHaveBeenCalled();
|
|
237
|
+
}));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('onAdd', () => {
|
|
241
|
+
it('should use current filter as value when onAdd is called', () => {
|
|
242
|
+
const onChange = vi.fn();
|
|
243
|
+
component.registerOnChange(onChange);
|
|
244
|
+
// @ts-expect-error accessing protected member
|
|
245
|
+
component.$filter.set('New Item');
|
|
246
|
+
const event = { preventDefault: vi.fn() };
|
|
247
|
+
// @ts-expect-error accessing protected member
|
|
248
|
+
component.onAdd(event as Event);
|
|
249
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
250
|
+
expect(component.value).toBe('New Item');
|
|
251
|
+
expect(onChange).toHaveBeenCalledWith('New Item');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('additional coverage', () => {
|
|
256
|
+
it('should ignore radio change if select mode is not single', () => {
|
|
257
|
+
component.selectMode = 'multiple';
|
|
258
|
+
component.value = '1';
|
|
259
|
+
// @ts-expect-error accessing protected member
|
|
260
|
+
component.onRadioChanged({ detail: { value: '2' } } as CustomEvent);
|
|
261
|
+
expect(component.value).toBe('1');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should handle checkbox change', () => {
|
|
265
|
+
const itemBefore = component.value;
|
|
266
|
+
// @ts-expect-error accessing protected member
|
|
267
|
+
component.onCheckboxChange({} as Event, mockItems[0]);
|
|
268
|
+
expect(component.value).toBe(itemBefore);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle onChange with an ID that does not exist in items', () => {
|
|
272
|
+
component.items = mockItems;
|
|
273
|
+
// @ts-expect-error accessing protected member
|
|
274
|
+
component.onChange('non-existent');
|
|
275
|
+
// @ts-expect-error accessing protected member
|
|
276
|
+
expect(component.$selectedItem()).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should ignore select if select mode is multiple', () => {
|
|
280
|
+
component.selectMode = 'multiple';
|
|
281
|
+
const onChange = vi.fn();
|
|
282
|
+
component.registerOnChange(onChange);
|
|
283
|
+
// @ts-expect-error accessing protected member
|
|
284
|
+
component.select(mockItems[0]);
|
|
285
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should clear filter when selecting an item', () => {
|
|
289
|
+
// @ts-expect-error accessing protected member
|
|
290
|
+
component.$filter.set('app');
|
|
291
|
+
// @ts-expect-error accessing protected member
|
|
292
|
+
component.select(mockItems[0]);
|
|
293
|
+
// @ts-expect-error accessing protected member
|
|
294
|
+
expect(component.$filter()).toBe('');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|