@dile/ui 3.4.5 → 3.5.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 (181) hide show
  1. package/components/accordion/accordion-item.js +3 -1
  2. package/components/accordion/accordion.js +3 -1
  3. package/components/app-drawer/app-drawer.js +3 -1
  4. package/components/avatar/avatar.js +3 -1
  5. package/components/avatar/avatar.spec.js +25 -0
  6. package/components/box-selector/box-selector-item.js +3 -1
  7. package/components/box-selector/box-selector-item.spec.js +27 -0
  8. package/components/box-selector/box-selector.js +3 -1
  9. package/components/box-selector/box-selector.spec.js +27 -0
  10. package/components/breadcrumbs/breadcrumbs-item.js +3 -1
  11. package/components/breadcrumbs/breadcrumbs-item.spec.js +28 -0
  12. package/components/breadcrumbs/breadcrumbs.js +3 -1
  13. package/components/breadcrumbs/breadcrumbs.spec.js +33 -0
  14. package/components/button/button-icon.js +3 -1
  15. package/components/button/button.js +3 -1
  16. package/components/card/card-slide.js +3 -1
  17. package/components/card/card-slide.spec.js +30 -0
  18. package/components/card/card.js +3 -1
  19. package/components/card/card.spec.js +26 -0
  20. package/components/checkbox/checkbox.js +3 -1
  21. package/components/chip/chip.js +3 -1
  22. package/components/chip/chip.spec.js +31 -0
  23. package/components/color-picker/color-picker.js +3 -1
  24. package/components/confirm/confirm.js +3 -1
  25. package/components/confirm/confirm.spec.js +32 -0
  26. package/components/confirm-text/confirm-text.js +3 -1
  27. package/components/confirm-text/confirm-text.spec.js +29 -0
  28. package/components/copy-markdown-url/copy-markdown-url.spec.js +27 -0
  29. package/components/copy-markdown-url/index.js +3 -1
  30. package/components/copy-text/__screenshots__/copy-text.spec.js/dile-copy-text-dispatches-dile-text-copied-with-the-content-on-copy---1.png +0 -0
  31. package/components/copy-text/copy-text.spec.js +38 -0
  32. package/components/copy-text/index.js +3 -1
  33. package/components/drop-file/drop-file.js +3 -1
  34. package/components/drop-file/drop-file.spec.js +33 -0
  35. package/components/drop-file/file-preview.js +3 -1
  36. package/components/drop-file/file-preview.spec.js +35 -0
  37. package/components/floating-feedback/floating-feedback.spec.js +30 -0
  38. package/components/floating-feedback/index.js +3 -1
  39. package/components/floating-menu/floating-menu-item.js +3 -1
  40. package/components/floating-menu/floating-menu-item.spec.js +27 -0
  41. package/components/floating-menu/floating-menu.js +3 -1
  42. package/components/floating-menu/floating-menu.spec.js +31 -0
  43. package/components/form-separator/form-separator.js +3 -1
  44. package/components/form-separator/form-separator.spec.js +28 -0
  45. package/components/hamburger/hamburger.js +3 -1
  46. package/components/hamburger/hamburger.spec.js +28 -0
  47. package/components/icon/icon.js +3 -1
  48. package/components/icon/icon.spec.js +31 -0
  49. package/components/icon-switch/icon-switch-group.js +3 -1
  50. package/components/icon-switch/icon-switch-group.spec.js +46 -0
  51. package/components/icon-switch/icon-switch.js +3 -1
  52. package/components/icon-switch/icon-switch.spec.js +35 -0
  53. package/components/info-box/info-box.js +3 -1
  54. package/components/info-box/info-box.spec.js +38 -0
  55. package/components/inline-feedback/inline-feedback.js +3 -1
  56. package/components/inline-feedback/inline-feedback.spec.js +46 -0
  57. package/components/input/__screenshots__/input-number-mask.spec.js/dile-input-number-mask-masks-digit-keydowns-following-the-given-pattern-1.png +0 -0
  58. package/components/input/input-contact.js +3 -1
  59. package/components/input/input-contact.spec.js +32 -0
  60. package/components/input/input-integer.js +3 -1
  61. package/components/input/input-integer.spec.js +31 -0
  62. package/components/input/input-message.js +3 -1
  63. package/components/input/input-message.spec.js +27 -0
  64. package/components/input/input-money.js +3 -1
  65. package/components/input/input-money.spec.js +30 -0
  66. package/components/input/input-number-mask.js +3 -1
  67. package/components/input/input-number-mask.spec.js +32 -0
  68. package/components/input/input-percentage.js +3 -1
  69. package/components/input/input-percentage.spec.js +31 -0
  70. package/components/input/input-search.js +3 -1
  71. package/components/input/input-search.spec.js +39 -0
  72. package/components/input/input.js +3 -1
  73. package/components/input/input.spec.js +47 -0
  74. package/components/light-mode-switch/light-mode-switch.js +3 -1
  75. package/components/light-mode-switch/light-mode-switch.spec.js +34 -0
  76. package/components/menu-hamburger/menu-hamburger.js +3 -1
  77. package/components/menu-hamburger/menu-hamburger.spec.js +34 -0
  78. package/components/menu-overlay/menu-overlay.js +3 -1
  79. package/components/menu-overlay/menu-overlay.spec.js +44 -0
  80. package/components/message/message.js +3 -1
  81. package/components/message/message.spec.js +42 -0
  82. package/components/modal/__screenshots__/modal-help.spec.js/dile-modal-help-opens-the-inner-modal-on-open---1.png +0 -0
  83. package/components/modal/modal-help.js +3 -1
  84. package/components/modal/modal-help.spec.js +32 -0
  85. package/components/modal/modal.js +3 -1
  86. package/components/modal/modal.spec.js +46 -0
  87. package/components/nav/nav.js +3 -1
  88. package/components/nav/nav.spec.js +33 -0
  89. package/components/number-picker/number-picker-element.js +3 -1
  90. package/components/number-picker/number-picker-element.spec.js +30 -0
  91. package/components/number-picker/number-picker.js +3 -1
  92. package/components/number-picker/number-picker.spec.js +30 -0
  93. package/components/order-switch/order-switch.js +3 -1
  94. package/components/order-switch/order-switch.spec.js +31 -0
  95. package/components/otp-input/otp-input.js +3 -1
  96. package/components/otp-input/otp-input.spec.js +41 -0
  97. package/components/pages/pages.js +3 -1
  98. package/components/pages/pages.spec.js +44 -0
  99. package/components/password/password.js +3 -1
  100. package/components/password/password.spec.js +35 -0
  101. package/components/progress-bar/progress-bar.js +3 -1
  102. package/components/progress-bar/progress-bar.spec.js +28 -0
  103. package/components/radio-group/radio-group.js +3 -1
  104. package/components/radio-group/radio-group.spec.js +44 -0
  105. package/components/radio-group/radio.js +3 -1
  106. package/components/radio-group/radio.spec.js +32 -0
  107. package/components/range/range.js +3 -1
  108. package/components/range/range.spec.js +36 -0
  109. package/components/rating/rating.js +3 -1
  110. package/components/rating/rating.spec.js +34 -0
  111. package/components/rating/star.js +3 -1
  112. package/components/rating/star.spec.js +30 -0
  113. package/components/rating-scale-question/rating-scale-option.js +3 -1
  114. package/components/rating-scale-question/rating-scale-option.spec.js +32 -0
  115. package/components/rating-scale-question/rating-scale-question.js +3 -1
  116. package/components/rating-scale-question/rating-scale-question.spec.js +35 -0
  117. 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
  118. 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
  119. 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
  120. 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
  121. 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
  122. 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
  123. package/components/select/__screenshots__/select-overlay.spec.js/dile-select-overlay-Rendering-throws-when-no-select-element-is-slotted-1.png +0 -0
  124. package/components/select/__screenshots__/select-overlay.spec.js/dile-select-overlay-hideTrigger-handleExternalKeydown---drives-navigation-without-a-visible-trigger-1.png +0 -0
  125. package/components/select/__screenshots__/zzdebug2.spec.js/debug-static-debug-1.png +0 -0
  126. package/components/select/__screenshots__/zzdebug3.spec.js/debug-rapid-close-reopen-reproduces-blur-then-quick-refocus-1.png +0 -0
  127. package/components/select/index.js +3 -1
  128. package/components/select/select-ajax-overlay.js +6 -0
  129. package/components/select/select-ajax-overlay.spec.js +389 -0
  130. package/components/select/select-ajax.js +3 -1
  131. package/components/select/select-boolean.js +3 -1
  132. package/components/select/select-overlay.js +5 -0
  133. package/components/select/select-overlay.spec.js +507 -0
  134. package/components/select/select.js +3 -1
  135. package/components/select/src/DileSelectAjaxOverlay.js +521 -0
  136. package/components/select/src/DileSelectOverlay.js +511 -0
  137. package/components/selector/selector-item.js +3 -1
  138. package/components/selector/selector-item.spec.js +40 -0
  139. package/components/selector/selector-overlay.js +3 -1
  140. package/components/selector/selector.js +3 -1
  141. package/components/selector/selector.spec.js +45 -0
  142. package/components/slide-menu/slide-menu.js +3 -1
  143. package/components/slide-menu/slide-menu.spec.js +38 -0
  144. package/components/slide-show/slide-show.js +3 -1
  145. package/components/slide-show/slide-show.spec.js +34 -0
  146. package/components/social-icon/social-icon.js +3 -1
  147. package/components/social-icon/social-icon.spec.js +26 -0
  148. package/components/spinner/spinner-bar.js +3 -1
  149. package/components/spinner/spinner-bar.spec.js +26 -0
  150. package/components/spinner/spinner-block.js +3 -1
  151. package/components/spinner/spinner-block.spec.js +21 -0
  152. package/components/spinner/spinner-horizontal.js +3 -1
  153. package/components/spinner/spinner-horizontal.spec.js +25 -0
  154. package/components/spinner/spinner-icon.js +3 -1
  155. package/components/spinner/spinner-icon.spec.js +25 -0
  156. package/components/spinner/spinner-modal.js +3 -1
  157. package/components/spinner/spinner-modal.spec.js +26 -0
  158. package/components/spinner/spinner.js +3 -1
  159. package/components/spinner/spinner.spec.js +26 -0
  160. package/components/switch/switch.js +3 -1
  161. package/components/switch/switch.spec.js +33 -0
  162. package/components/tabs/tab.js +3 -1
  163. package/components/tabs/tabs.js +3 -1
  164. package/components/tabs/tabs.spec.js +45 -0
  165. package/components/textarea/textarea.js +3 -1
  166. package/components/textarea/textarea.spec.js +39 -0
  167. package/components/time-picker/time-picker.js +3 -1
  168. package/components/time-picker/time-picker.spec.js +26 -0
  169. package/components/timer/timer.js +3 -1
  170. package/components/timer/timer.spec.js +31 -0
  171. package/components/toast/toast-item.js +3 -1
  172. package/components/toast/toast-persistent.js +3 -1
  173. package/components/toast/toast.js +3 -1
  174. package/components/tooltip/chip-tooltip.js +3 -1
  175. package/components/tooltip/chip-tooltip.spec.js +29 -0
  176. package/components/tooltip/tooltip.js +3 -1
  177. package/components/tooltip/tooltip.spec.js +32 -0
  178. package/components/web-visor/web-visor.js +3 -1
  179. package/components/web-visor/web-visor.spec.js +32 -0
  180. package/mixins/overlay/src/DileOverlay.js +9 -6
  181. package/package.json +3 -3
@@ -0,0 +1,389 @@
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
+
280
+ describe('Form Association', () => {
281
+ it('is form-associated and sets form value', async () => {
282
+ const el = await renderSelectAjaxOverlay(`
283
+ <dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name"></dile-select-ajax-overlay>
284
+ `);
285
+
286
+ expect(el.internals).toBeTruthy();
287
+
288
+ el.value = '5';
289
+ await el.updateComplete;
290
+ expect(el.internals.form).toBeDefined();
291
+ });
292
+ });
293
+
294
+ describe('Positioning', () => {
295
+ it('points the nested dile-select-overlay at the real search field for positioning (not its own hidden trigger)', async () => {
296
+ // Regression test: the nested popup's own trigger is a zero-height point sitting right
297
+ // after the search field, so flipping "above" it doesn't leave room for the field's real
298
+ // height and ends up covering it. The search field itself must be used as the anchor.
299
+ const el = await renderSelectAjaxOverlay(`
300
+ <dile-select-ajax-overlay endpoint="/api/countries" displayProperty="name"></dile-select-ajax-overlay>
301
+ `);
302
+ await el.select.updateComplete;
303
+
304
+ expect(el.select.trigger).toBe(el.shadowRoot.querySelector('.search-wrapper'));
305
+ });
306
+
307
+ it('does not override the trigger in static mode, which uses its own visible trigger', async () => {
308
+ mockFetch([{ id: 1, name: 'Spain' }]);
309
+ const el = await renderSelectAjaxOverlay(`
310
+ <dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
311
+ `);
312
+ await wait(50);
313
+ await el.updateComplete;
314
+ await el.select.updateComplete;
315
+
316
+ expect(el.select.trigger).toBe(el.select.shadowRoot.getElementById('trigger'));
317
+ });
318
+
319
+ it('rewires the trigger after clearing an initial value swaps in a fresh dile-select-overlay instance', async () => {
320
+ // Regression test: a component initialized with `value` starts on the read-only state, so
321
+ // the very first dile-select-overlay instance never survives (isSelected flips to true
322
+ // inside firstUpdated(), tearing it down before it settles). Clearing the selection later
323
+ // mounts a brand new instance that also needs the search-field wiring, not just the one
324
+ // that happened to exist at firstUpdated() time.
325
+ vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({
326
+ json: () => Promise.resolve({ data: { name: 'Spain' } }),
327
+ })));
328
+ const el = await renderSelectAjaxOverlay(`
329
+ <dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" resultDataProperty="data" value="22"></dile-select-ajax-overlay>
330
+ `);
331
+ await wait(50);
332
+ await el.updateComplete;
333
+ expect(el.isSelected).toBe(true);
334
+
335
+ el.shadowRoot.getElementById('result').dispatchEvent(new CustomEvent('dile-input-search-cleared', { bubbles: true }));
336
+ await el.updateComplete;
337
+ await el.select.updateComplete;
338
+
339
+ expect(el.select.trigger).toBe(el.shadowRoot.querySelector('.search-wrapper'));
340
+ });
341
+ });
342
+
343
+ describe('static mode', () => {
344
+ it('loads all data on init and renders it inline without requiring a search', async () => {
345
+ mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
346
+ const el = await renderSelectAjaxOverlay(`
347
+ <dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
348
+ `);
349
+ await wait(50);
350
+ await el.updateComplete;
351
+
352
+ expect(el.data.length).toBe(2);
353
+ expect(el.select).toBeTruthy();
354
+ });
355
+
356
+ it('does not auto-select the first result once data loads (no default placeholder option)', async () => {
357
+ // Regression test: without a default placeholder option, a native <select> defaults its
358
+ // value to the first real option. dile-select-overlay's mount-time value sync would then
359
+ // treat that as a genuine selection unless the nested component mounts before data (and
360
+ // its options) exist, exactly like the search flow does.
361
+ mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
362
+ const el = await renderSelectAjaxOverlay(`
363
+ <dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
364
+ `);
365
+ await wait(50);
366
+ await el.updateComplete;
367
+
368
+ expect(el.isSelected).toBe(false);
369
+ expect(el.value).toBeUndefined();
370
+ });
371
+
372
+ it('shows a visible, clickable trigger instead of hiding it (there is no separate search field to compose under)', async () => {
373
+ mockFetch([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
374
+ const el = await renderSelectAjaxOverlay(`
375
+ <dile-select-ajax-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" static></dile-select-ajax-overlay>
376
+ `);
377
+ await wait(50);
378
+ await el.updateComplete;
379
+
380
+ expect(el.select.hideTrigger).toBe(false);
381
+ const trigger = el.select.shadowRoot.getElementById('trigger');
382
+ expect(trigger.getAttribute('tabindex')).toBe('0');
383
+
384
+ trigger.click();
385
+ await wait(100);
386
+ expect(el.select.opened).toBe(true);
387
+ });
388
+ });
389
+ });
@@ -1,4 +1,6 @@
1
1
  import './select.js'
2
2
  import { DileSelectAjax } from "./src/DileSelectAjax.js";
3
3
 
4
- window.customElements.define("dile-select-ajax", DileSelectAjax);
4
+ if (!customElements.get("dile-select-ajax")) {
5
+ window.customElements.define("dile-select-ajax", DileSelectAjax);
6
+ }
@@ -1,3 +1,5 @@
1
1
  import { DileSelectBoolean } from "./src/DileSelectBoolean.js";
2
2
 
3
- window.customElements.define("dile-select-boolean", DileSelectBoolean);
3
+ if (!customElements.get("dile-select-boolean")) {
4
+ window.customElements.define("dile-select-boolean", DileSelectBoolean);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DileSelectOverlay } from "./src/DileSelectOverlay.js";
2
+
3
+ if (!customElements.get("dile-select-overlay")) {
4
+ window.customElements.define("dile-select-overlay", DileSelectOverlay);
5
+ }