@dile/ui 3.4.6 → 3.5.1
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/components/select/__screenshots__/select-ajax-overlay.spec.js/dile-select-ajax-overlay-Selecting-a-result-clear---does-not-re-select-the-same-item--regression--infinite-add-loop-under-addOnSelect--1.png +0 -0
- package/components/select/__screenshots__/select-ajax-overlay.spec.js/dile-select-ajax-overlay-Selecting-a-result-clearing-the-selection-returns-to-the-search-field-1.png +0 -0
- package/components/select/__screenshots__/select-ajax-overlay.spec.js/dile-select-ajax-overlay-Selecting-a-result-selecting-an-option-sets-value-selectedText--closes-the-popup-and-fires-events-1.png +0 -0
- package/components/select/__screenshots__/select-ajax-overlay.spec.js/dile-select-ajax-overlay-Selecting-a-result-shows-the-read-only-field-with-the-clear-icon-after-selecting-1.png +0 -0
- package/components/select/__screenshots__/select-ajax-overlay.spec.js/dile-select-ajax-overlay-static-mode-loads-all-data-on-init-and-renders-it-inline-without-requiring-a-search-1.png +0 -0
- package/components/select/__screenshots__/select-ajax-overlay.spec.js/dile-select-ajax-overlay-static-mode-shows-a-visible--clickable-trigger-instead-of-hiding-it--there-is-no-separate-search-field-to-compose-under--1.png +0 -0
- package/components/select/__screenshots__/select-overlay.spec.js/dile-select-overlay-Positioning-keeps-the-same-moveTop-tightened-gap-when-flipping-above-the-trigger-as-when-sitting-below-it-1.png +0 -0
- package/components/select/__screenshots__/select-overlay.spec.js/dile-select-overlay-Rendering-throws-when-no-select-element-is-slotted-1.png +0 -0
- package/components/select/__screenshots__/select-overlay.spec.js/dile-select-overlay-hideTrigger-handleExternalKeydown---drives-navigation-without-a-visible-trigger-1.png +0 -0
- package/components/select/__screenshots__/zzdebug2.spec.js/debug-static-debug-1.png +0 -0
- package/components/select/__screenshots__/zzdebug3.spec.js/debug-rapid-close-reopen-reproduces-blur-then-quick-refocus-1.png +0 -0
- package/components/select/index.js +3 -1
- package/components/select/select-ajax-overlay.js +6 -0
- package/components/select/select-ajax-overlay.spec.js +422 -0
- package/components/select/select-overlay.js +5 -0
- package/components/select/select-overlay.spec.js +507 -0
- package/components/select/src/DileSelectAjaxOverlay.js +531 -0
- package/components/select/src/DileSelectOverlay.js +511 -0
- package/mixins/overlay/src/DileOverlay.js +9 -6
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { DileSelect } from './src/DileSelect.js';
|
|
2
2
|
export { DileSelectAjax } from './src/DileSelectAjax.js';
|
|
3
|
-
export { DileSelectBoolean } from './src/DileSelectBoolean.js';
|
|
3
|
+
export { DileSelectBoolean } from './src/DileSelectBoolean.js';
|
|
4
|
+
export { DileSelectOverlay } from './src/DileSelectOverlay.js';
|
|
5
|
+
export { DileSelectAjaxOverlay } from './src/DileSelectAjaxOverlay.js';
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import './select-ajax-overlay.js';
|
|
3
|
+
|
|
4
|
+
describe('dile-select-ajax-overlay', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
vi.unstubAllGlobals();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function wait(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mockFetch(data) {
|
|
15
|
+
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({
|
|
16
|
+
json: () => Promise.resolve(data),
|
|
17
|
+
})));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function renderSelectAjaxOverlay(html) {
|
|
21
|
+
document.body.innerHTML = html;
|
|
22
|
+
const el = document.body.querySelector('dile-select-ajax-overlay');
|
|
23
|
+
await el.updateComplete;
|
|
24
|
+
return el;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Clicking a result sets the value on the *nested* dile-select-overlay, which fires its
|
|
28
|
+
// own element-changed a microtask later; the outer component reacts to that in doSelected,
|
|
29
|
+
// which triggers a *second* update/dispatch. Chain both updateComplete promises rather than
|
|
30
|
+
// relying on a single await, which can resolve before the follow-up reaction settles.
|
|
31
|
+
async function selectOption(el, index) {
|
|
32
|
+
const options = el.select.shadowRoot.querySelectorAll('[role="option"]');
|
|
33
|
+
options[index].click();
|
|
34
|
+
await el.select.updateComplete;
|
|
35
|
+
await el.updateComplete;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function typeKeyword(el, keyword) {
|
|
39
|
+
const input = el.shadowRoot.getElementById('search');
|
|
40
|
+
input.value = keyword;
|
|
41
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
42
|
+
await wait(el.delay + 50);
|
|
43
|
+
await el.updateComplete;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('Rendering', () => {
|
|
47
|
+
it('renders the search input when nothing is selected', async () => {
|
|
48
|
+
const el = await renderSelectAjaxOverlay(`
|
|
49
|
+
<dile-select-ajax-overlay endpoint="/api/countries" displayProperty="name"></dile-select-ajax-overlay>
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
const input = el.shadowRoot.getElementById('search');
|
|
53
|
+
expect(input).toBeTruthy();
|
|
54
|
+
expect(input.getAttribute('role')).toBe('combobox');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders a label associated with the search input', async () => {
|
|
58
|
+
const el = await renderSelectAjaxOverlay(`
|
|
59
|
+
<dile-select-ajax-overlay endpoint="/api/countries" displayProperty="name" label="Country"></dile-select-ajax-overlay>
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
const label = el.shadowRoot.querySelector('label');
|
|
63
|
+
expect(label.textContent).toBe('Country');
|
|
64
|
+
expect(label.getAttribute('for')).toBe('search');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps an initial errored/message state on mount even with hideErrorOnInput set', async () => {
|
|
68
|
+
// Regression test: the nested dile-select-overlay fires its own element-changed while
|
|
69
|
+
// syncing to its placeholder option on mount, which must not be treated as a real
|
|
70
|
+
// selection that clears the error via hideErrorOnInput.
|
|
71
|
+
const el = await renderSelectAjaxOverlay(`
|
|
72
|
+
<dile-select-ajax-overlay endpoint="/api/countries" displayProperty="name" errored message="Required" hideErrorOnInput></dile-select-ajax-overlay>
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
expect(el.errored).toBe(true);
|
|
76
|
+
expect(el.message).toBe('Required');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not let a click on the search field bubble to document (avoids racing with the global click-outside-closes handler)', async () => {
|
|
80
|
+
// Regression test: clicking to refocus the field opens the nested dile-select-overlay via
|
|
81
|
+
// onSearchFocus(), but that same click event then continues bubbling to document, where
|
|
82
|
+
// DileCloseDocumentClick's global handler closes every registered overlay instance,
|
|
83
|
+
// including the one we just opened. Stopping propagation here keeps that click from ever
|
|
84
|
+
// reaching document in the first place.
|
|
85
|
+
const el = await renderSelectAjaxOverlay(`
|
|
86
|
+
<dile-select-ajax-overlay endpoint="/api/countries" displayProperty="name"></dile-select-ajax-overlay>
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
let reachedDocument = false;
|
|
90
|
+
document.addEventListener('click', () => { reachedDocument = true; });
|
|
91
|
+
|
|
92
|
+
el.shadowRoot.querySelector('.search-wrapper').dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
|
93
|
+
|
|
94
|
+
expect(reachedDocument).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Searching (no extra click required)', () => {
|
|
99
|
+
it('opens the nested popup as soon as results load, without an extra click', async () => {
|
|
100
|
+
mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
|
|
101
|
+
const el = await renderSelectAjaxOverlay(`
|
|
102
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
await typeKeyword(el, 'sp');
|
|
106
|
+
|
|
107
|
+
expect(el.data.length).toBe(2);
|
|
108
|
+
expect(el.select.opened).toBe(true);
|
|
109
|
+
const options = el.select.shadowRoot.querySelectorAll('[role="option"]');
|
|
110
|
+
expect(options.length).toBe(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('shows the empty message when there are no results', async () => {
|
|
114
|
+
mockFetch([]);
|
|
115
|
+
const el = await renderSelectAjaxOverlay(`
|
|
116
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
await typeKeyword(el, 'zz');
|
|
120
|
+
|
|
121
|
+
expect(el.shadowRoot.querySelector('.status').textContent.trim()).toBe(el.emptyMessage);
|
|
122
|
+
expect(el.select.opened).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('shows loading, not the empty-results message, during the debounce delay right after typing', async () => {
|
|
126
|
+
// Regression test: loading must flip to true as soon as the user types, not only once
|
|
127
|
+
// the debounced request actually starts, otherwise the empty-results message flashes
|
|
128
|
+
// for the length of the debounce delay before any request has actually come back.
|
|
129
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
130
|
+
const el = await renderSelectAjaxOverlay(`
|
|
131
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="50"></dile-select-ajax-overlay>
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
const input = el.shadowRoot.getElementById('search');
|
|
135
|
+
input.value = 'sp';
|
|
136
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
137
|
+
await el.updateComplete;
|
|
138
|
+
|
|
139
|
+
expect(el.loading).toBe(true);
|
|
140
|
+
const status = el.shadowRoot.querySelector('.status');
|
|
141
|
+
expect(status.querySelector('dile-spinner-horizontal')).toBeTruthy();
|
|
142
|
+
expect(status.textContent.trim()).not.toBe(el.emptyMessage);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('shows a spinner icon in the search field while loading, replacing the search/clear icon', async () => {
|
|
146
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
147
|
+
const el = await renderSelectAjaxOverlay(`
|
|
148
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
149
|
+
`);
|
|
150
|
+
|
|
151
|
+
const input = el.shadowRoot.getElementById('search');
|
|
152
|
+
input.value = 'sp';
|
|
153
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
154
|
+
await el.updateComplete;
|
|
155
|
+
|
|
156
|
+
expect(el.shadowRoot.querySelector('dile-spinner-icon')).toBeTruthy();
|
|
157
|
+
expect(el.shadowRoot.querySelector('dile-icon')).toBeNull();
|
|
158
|
+
|
|
159
|
+
await wait(50);
|
|
160
|
+
await el.updateComplete;
|
|
161
|
+
|
|
162
|
+
expect(el.loading).toBe(false);
|
|
163
|
+
expect(el.shadowRoot.querySelector('dile-spinner-icon')).toBeNull();
|
|
164
|
+
expect(el.shadowRoot.querySelector('dile-icon')).toBeTruthy();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('shows the spinner icon again on a second search even though results are already displayed', async () => {
|
|
168
|
+
// Regression test: the popup's own status spinner only shows while there is no data yet,
|
|
169
|
+
// so a second search (which keeps the previous results visible while it's in flight) had
|
|
170
|
+
// no loading indication at all. The field's own icon must show it regardless of whether
|
|
171
|
+
// there is already data on screen.
|
|
172
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
173
|
+
const el = await renderSelectAjaxOverlay(`
|
|
174
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
175
|
+
`);
|
|
176
|
+
|
|
177
|
+
await typeKeyword(el, 'sp');
|
|
178
|
+
expect(el.data.length).toBe(1);
|
|
179
|
+
|
|
180
|
+
mockFetch([{ id: 2, name: 'France' }]);
|
|
181
|
+
const input = el.shadowRoot.getElementById('search');
|
|
182
|
+
input.value = 'fra';
|
|
183
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
184
|
+
await el.updateComplete;
|
|
185
|
+
|
|
186
|
+
expect(el.data.length).toBe(1); // previous results still showing while the new search is in flight
|
|
187
|
+
expect(el.shadowRoot.querySelector('dile-spinner-icon')).toBeTruthy();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('shows the ajax error message on a failed request', async () => {
|
|
191
|
+
vi.stubGlobal('fetch', vi.fn(() => Promise.reject(new Error('network error'))));
|
|
192
|
+
const el = await renderSelectAjaxOverlay(`
|
|
193
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
194
|
+
`);
|
|
195
|
+
|
|
196
|
+
await typeKeyword(el, 'sp');
|
|
197
|
+
|
|
198
|
+
expect(el.ajaxError).toBe(true);
|
|
199
|
+
expect(el.shadowRoot.querySelector('.status').textContent.trim()).toBe(el.ajaxErrorMessage);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('Selecting a result', () => {
|
|
204
|
+
it('selecting an option sets value/selectedText, closes the popup and fires events', async () => {
|
|
205
|
+
mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
|
|
206
|
+
const el = await renderSelectAjaxOverlay(`
|
|
207
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
await typeKeyword(el, 'sp');
|
|
211
|
+
|
|
212
|
+
let changedDetail = null;
|
|
213
|
+
let selectedTextDetail = null;
|
|
214
|
+
el.addEventListener('element-changed', (e) => { changedDetail = e.detail; });
|
|
215
|
+
el.addEventListener('dile-select-ajax-selected-text-changed', (e) => { selectedTextDetail = e.detail; });
|
|
216
|
+
|
|
217
|
+
// options[0] is "Spain" (no default placeholder option in this component)
|
|
218
|
+
await selectOption(el, 0);
|
|
219
|
+
|
|
220
|
+
expect(el.value).toBe('1');
|
|
221
|
+
expect(el.selectedText).toBe('Spain');
|
|
222
|
+
expect(el.isSelected).toBe(true);
|
|
223
|
+
expect(changedDetail.value).toBe('1');
|
|
224
|
+
expect(selectedTextDetail.selectedText).toBe('Spain');
|
|
225
|
+
// Selecting swaps to the read-only state, unmounting the nested dile-select-overlay entirely.
|
|
226
|
+
expect(el.select).toBeNull();
|
|
227
|
+
expect(el.opened).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('keyboard flow (ArrowDown + Enter) forwarded to the nested dile-select-overlay selects a result', async () => {
|
|
231
|
+
mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
|
|
232
|
+
const el = await renderSelectAjaxOverlay(`
|
|
233
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
await typeKeyword(el, 'sp');
|
|
237
|
+
|
|
238
|
+
const input = el.shadowRoot.getElementById('search');
|
|
239
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
240
|
+
await wait(50);
|
|
241
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
242
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
243
|
+
await el.updateComplete;
|
|
244
|
+
|
|
245
|
+
expect(el.isSelected).toBe(true);
|
|
246
|
+
expect(el.value).toBeTruthy();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('shows the read-only field with the clear icon after selecting', async () => {
|
|
250
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
251
|
+
const el = await renderSelectAjaxOverlay(`
|
|
252
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
253
|
+
`);
|
|
254
|
+
|
|
255
|
+
await typeKeyword(el, 'sp');
|
|
256
|
+
await selectOption(el, 0);
|
|
257
|
+
|
|
258
|
+
expect(el.shadowRoot.getElementById('result')).toBeTruthy();
|
|
259
|
+
expect(el.shadowRoot.getElementById('search')).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('clearing the selection returns to the search field', async () => {
|
|
263
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
264
|
+
const el = await renderSelectAjaxOverlay(`
|
|
265
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
266
|
+
`);
|
|
267
|
+
|
|
268
|
+
await typeKeyword(el, 'sp');
|
|
269
|
+
await selectOption(el, 0);
|
|
270
|
+
|
|
271
|
+
el.shadowRoot.getElementById('result').dispatchEvent(new CustomEvent('dile-input-search-cleared', { bubbles: true }));
|
|
272
|
+
await el.updateComplete;
|
|
273
|
+
|
|
274
|
+
expect(el.isSelected).toBe(false);
|
|
275
|
+
expect(el.value).toBeUndefined();
|
|
276
|
+
expect(el.shadowRoot.getElementById('search')).toBeTruthy();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('clear() does not re-select the same item (regression: infinite add loop under addOnSelect)', async () => {
|
|
280
|
+
// Regression test: unlike onClearSelected() (triggered by the user's own "x" click), the
|
|
281
|
+
// public clear() API left `data`/`keyword` untouched. Flipping isSelected back to false
|
|
282
|
+
// remounts a brand new nested dile-select-overlay, whose firstUpdated() syncs its value to
|
|
283
|
+
// whatever the native <select> currently holds. With no placeholder option and the stale
|
|
284
|
+
// data still in place, that was the just-cleared item's own option — auto-selecting it and
|
|
285
|
+
// re-firing element-changed for the same value, which under dile-many-relation's
|
|
286
|
+
// addOnSelect re-added the same item forever until the API started rate-limiting.
|
|
287
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
288
|
+
const el = await renderSelectAjaxOverlay(`
|
|
289
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-select-ajax-overlay>
|
|
290
|
+
`);
|
|
291
|
+
|
|
292
|
+
await typeKeyword(el, 'sp');
|
|
293
|
+
await selectOption(el, 0);
|
|
294
|
+
expect(el.value).toBe('1');
|
|
295
|
+
|
|
296
|
+
const changedValues = [];
|
|
297
|
+
el.addEventListener('element-changed', (e) => { changedValues.push(e.detail.value); });
|
|
298
|
+
|
|
299
|
+
el.clear();
|
|
300
|
+
await el.updateComplete;
|
|
301
|
+
await wait(20);
|
|
302
|
+
await el.updateComplete;
|
|
303
|
+
|
|
304
|
+
expect(el.isSelected).toBe(false);
|
|
305
|
+
expect(el.value).toBeUndefined();
|
|
306
|
+
expect(el.data).toEqual([]);
|
|
307
|
+
// Exactly one event: the clear itself. A second one carrying the old value ('1') back
|
|
308
|
+
// would mean the freshly remounted select auto-picked the stale option again.
|
|
309
|
+
expect(changedValues).toEqual([undefined]);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Form Association', () => {
|
|
314
|
+
it('is form-associated and sets form value', async () => {
|
|
315
|
+
const el = await renderSelectAjaxOverlay(`
|
|
316
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name"></dile-select-ajax-overlay>
|
|
317
|
+
`);
|
|
318
|
+
|
|
319
|
+
expect(el.internals).toBeTruthy();
|
|
320
|
+
|
|
321
|
+
el.value = '5';
|
|
322
|
+
await el.updateComplete;
|
|
323
|
+
expect(el.internals.form).toBeDefined();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('Positioning', () => {
|
|
328
|
+
it('points the nested dile-select-overlay at the real search field for positioning (not its own hidden trigger)', async () => {
|
|
329
|
+
// Regression test: the nested popup's own trigger is a zero-height point sitting right
|
|
330
|
+
// after the search field, so flipping "above" it doesn't leave room for the field's real
|
|
331
|
+
// height and ends up covering it. The search field itself must be used as the anchor.
|
|
332
|
+
const el = await renderSelectAjaxOverlay(`
|
|
333
|
+
<dile-select-ajax-overlay endpoint="/api/countries" displayProperty="name"></dile-select-ajax-overlay>
|
|
334
|
+
`);
|
|
335
|
+
await el.select.updateComplete;
|
|
336
|
+
|
|
337
|
+
expect(el.select.trigger).toBe(el.shadowRoot.querySelector('.search-wrapper'));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('does not override the trigger in static mode, which uses its own visible trigger', async () => {
|
|
341
|
+
mockFetch([{ id: 1, name: 'Spain' }]);
|
|
342
|
+
const el = await renderSelectAjaxOverlay(`
|
|
343
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
|
|
344
|
+
`);
|
|
345
|
+
await wait(50);
|
|
346
|
+
await el.updateComplete;
|
|
347
|
+
await el.select.updateComplete;
|
|
348
|
+
|
|
349
|
+
expect(el.select.trigger).toBe(el.select.shadowRoot.getElementById('trigger'));
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('rewires the trigger after clearing an initial value swaps in a fresh dile-select-overlay instance', async () => {
|
|
353
|
+
// Regression test: a component initialized with `value` starts on the read-only state, so
|
|
354
|
+
// the very first dile-select-overlay instance never survives (isSelected flips to true
|
|
355
|
+
// inside firstUpdated(), tearing it down before it settles). Clearing the selection later
|
|
356
|
+
// mounts a brand new instance that also needs the search-field wiring, not just the one
|
|
357
|
+
// that happened to exist at firstUpdated() time.
|
|
358
|
+
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({
|
|
359
|
+
json: () => Promise.resolve({ data: { name: 'Spain' } }),
|
|
360
|
+
})));
|
|
361
|
+
const el = await renderSelectAjaxOverlay(`
|
|
362
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" resultDataProperty="data" value="22"></dile-select-ajax-overlay>
|
|
363
|
+
`);
|
|
364
|
+
await wait(50);
|
|
365
|
+
await el.updateComplete;
|
|
366
|
+
expect(el.isSelected).toBe(true);
|
|
367
|
+
|
|
368
|
+
el.shadowRoot.getElementById('result').dispatchEvent(new CustomEvent('dile-input-search-cleared', { bubbles: true }));
|
|
369
|
+
await el.updateComplete;
|
|
370
|
+
await el.select.updateComplete;
|
|
371
|
+
|
|
372
|
+
expect(el.select.trigger).toBe(el.shadowRoot.querySelector('.search-wrapper'));
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('static mode', () => {
|
|
377
|
+
it('loads all data on init and renders it inline without requiring a search', async () => {
|
|
378
|
+
mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
|
|
379
|
+
const el = await renderSelectAjaxOverlay(`
|
|
380
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
|
|
381
|
+
`);
|
|
382
|
+
await wait(50);
|
|
383
|
+
await el.updateComplete;
|
|
384
|
+
|
|
385
|
+
expect(el.data.length).toBe(2);
|
|
386
|
+
expect(el.select).toBeTruthy();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('does not auto-select the first result once data loads (no default placeholder option)', async () => {
|
|
390
|
+
// Regression test: without a default placeholder option, a native <select> defaults its
|
|
391
|
+
// value to the first real option. dile-select-overlay's mount-time value sync would then
|
|
392
|
+
// treat that as a genuine selection unless the nested component mounts before data (and
|
|
393
|
+
// its options) exist, exactly like the search flow does.
|
|
394
|
+
mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
|
|
395
|
+
const el = await renderSelectAjaxOverlay(`
|
|
396
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
|
|
397
|
+
`);
|
|
398
|
+
await wait(50);
|
|
399
|
+
await el.updateComplete;
|
|
400
|
+
|
|
401
|
+
expect(el.isSelected).toBe(false);
|
|
402
|
+
expect(el.value).toBeUndefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('shows a visible, clickable trigger instead of hiding it (there is no separate search field to compose under)', async () => {
|
|
406
|
+
mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
|
|
407
|
+
const el = await renderSelectAjaxOverlay(`
|
|
408
|
+
<dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
|
|
409
|
+
`);
|
|
410
|
+
await wait(50);
|
|
411
|
+
await el.updateComplete;
|
|
412
|
+
|
|
413
|
+
expect(el.select.hideTrigger).toBe(false);
|
|
414
|
+
const trigger = el.select.shadowRoot.getElementById('trigger');
|
|
415
|
+
expect(trigger.getAttribute('tabindex')).toBe('0');
|
|
416
|
+
|
|
417
|
+
trigger.click();
|
|
418
|
+
await wait(100);
|
|
419
|
+
expect(el.select.opened).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|