@dile/ui 3.14.0 → 3.15.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/components/drop-file/drop-file.spec.js +31 -0
- package/components/drop-file/src/DileDropFile.js +12 -0
- package/components/input/input-search.spec.js +42 -0
- package/components/input/src/DileInputSearch.js +32 -13
- package/components/order-switch/order-switch.spec.js +4 -4
- package/components/select/__screenshots__/select-search.spec.js/dile-select-search-Value-API-uses-the-label-as-accessible-name-of-the-selected-field-1.png +0 -0
- package/components/select/index.js +3 -1
- package/components/select/select-overlay.spec.js +16 -0
- package/components/select/select-search.js +6 -0
- package/components/select/select-search.spec.js +318 -0
- package/components/select/src/DileSelectAjaxOverlay.js +18 -449
- package/components/select/src/DileSelectOverlay.js +3 -0
- package/components/select/src/DileSelectSearch.js +129 -0
- package/components/select/src/DileSelectSearchableBase.js +544 -0
- package/package.json +2 -2
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
3
|
+
import '../../spinner/spinner-horizontal.js';
|
|
4
|
+
import '../../spinner/spinner-icon.js';
|
|
5
|
+
import '../../icon/icon.js';
|
|
6
|
+
import '../select-overlay.js';
|
|
7
|
+
import '../../input/input-search.js';
|
|
8
|
+
import { searchIcon, clearIcon } from '@dile/icons/index.js';
|
|
9
|
+
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
10
|
+
import { labelStyles } from '../../input/src/label-styles.js';
|
|
11
|
+
import { messageStyles } from '../../input/src/message-styles.js';
|
|
12
|
+
|
|
13
|
+
const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter', 'Escape', 'Tab'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Base class for the searchable members of the select family (dile-select-ajax-overlay,
|
|
17
|
+
* dile-select-search). It owns the whole UI: the search field (a combobox) composed over a
|
|
18
|
+
* nested dile-select-overlay with hidden trigger, the read-only "selected" state with its
|
|
19
|
+
* clear button, keyboard forwarding, live announcements and form integration.
|
|
20
|
+
*
|
|
21
|
+
* Subclasses only decide where the results come from, implementing:
|
|
22
|
+
* - loadData(): fill the results for the current `keyword`, calling setResults(data) when ready.
|
|
23
|
+
* - resolveSelectedText(): set `selectedText` for the current `value` (initial value or set()).
|
|
24
|
+
* - selectedTextChangedEventName: name of the event fired when the selected text changes.
|
|
25
|
+
*/
|
|
26
|
+
export class DileSelectSearchableBase extends DileEmmitChange(LitElement) {
|
|
27
|
+
|
|
28
|
+
static get styles() {
|
|
29
|
+
return [
|
|
30
|
+
labelStyles,
|
|
31
|
+
messageStyles,
|
|
32
|
+
css`
|
|
33
|
+
* {
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
}
|
|
36
|
+
:host {
|
|
37
|
+
display: block;
|
|
38
|
+
position: relative;
|
|
39
|
+
margin-bottom: 10px;
|
|
40
|
+
}
|
|
41
|
+
.search-wrapper {
|
|
42
|
+
position: relative;
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
width: var(--dile-input-width, 100%);
|
|
46
|
+
padding: var(--dile-input-padding, 5px);
|
|
47
|
+
border-radius: var(--dile-input-border-radius, 5px);
|
|
48
|
+
border: var(--dile-input-border-width, 1px) solid var(--dile-input-border-color, #888);
|
|
49
|
+
background-color: var(--dile-input-background-color, #fff);
|
|
50
|
+
}
|
|
51
|
+
.search-wrapper:focus-within {
|
|
52
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
53
|
+
}
|
|
54
|
+
.search-wrapper.errored {
|
|
55
|
+
border-color: var(--dile-input-error-border-color, #c00);
|
|
56
|
+
}
|
|
57
|
+
.search-wrapper.disabled {
|
|
58
|
+
opacity: 0.5;
|
|
59
|
+
}
|
|
60
|
+
input {
|
|
61
|
+
flex-grow: 1;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
border: none;
|
|
64
|
+
outline: none;
|
|
65
|
+
color: var(--dile-input-color, #303030);
|
|
66
|
+
font-size: var(--dile-input-font-size, 1em);
|
|
67
|
+
line-height: var(--dile-input-line-height, 1.5em);
|
|
68
|
+
background-color: transparent;
|
|
69
|
+
font-family: inherit;
|
|
70
|
+
}
|
|
71
|
+
input::placeholder {
|
|
72
|
+
color: var(--dile-input-placeholder-color, #ccc);
|
|
73
|
+
}
|
|
74
|
+
dile-icon {
|
|
75
|
+
margin-left: 1px;
|
|
76
|
+
display: flex;
|
|
77
|
+
}
|
|
78
|
+
.clear-button {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
margin: 0;
|
|
83
|
+
padding: 0;
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: 3px;
|
|
86
|
+
background: none;
|
|
87
|
+
color: inherit;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
}
|
|
90
|
+
.clear-button:focus-visible {
|
|
91
|
+
outline: 2px solid var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
92
|
+
outline-offset: 1px;
|
|
93
|
+
}
|
|
94
|
+
dile-spinner-icon {
|
|
95
|
+
margin-left: 1px;
|
|
96
|
+
--dile-icon-size: 18px;
|
|
97
|
+
}
|
|
98
|
+
.status {
|
|
99
|
+
margin-top: 0.5rem;
|
|
100
|
+
position: absolute;
|
|
101
|
+
z-index: var(--dile-select-overlay-z-index, 999);
|
|
102
|
+
width: 100%;
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
min-height: 2.5rem;
|
|
105
|
+
border-radius: var(--dile-select-overlay-border-radius, 5px);
|
|
106
|
+
padding: var(--dile-select-overlay-padding, 7px 10px);
|
|
107
|
+
background-color: var(--dile-select-overlay-background-color, #fff);
|
|
108
|
+
color: var(--dile-select-overlay-color, #303030);
|
|
109
|
+
box-shadow: var(--dile-select-overlay-box-shadow, 0 4px 10px rgba(0, 0, 0, 0.15));
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
}
|
|
113
|
+
.status p {
|
|
114
|
+
margin: 0;
|
|
115
|
+
}
|
|
116
|
+
dile-select-overlay {
|
|
117
|
+
margin-bottom: 0;
|
|
118
|
+
/* Match the search field's own sizing (dile-input-search-like padding/font-size)
|
|
119
|
+
instead of dile-select-overlay's native-select-like defaults, so the visible
|
|
120
|
+
trigger shown in static mode looks consistent with the search field. */
|
|
121
|
+
--dile-input-padding: 5px;
|
|
122
|
+
--dile-select-font-size: 1em;
|
|
123
|
+
}
|
|
124
|
+
.visually-hidden {
|
|
125
|
+
position: absolute;
|
|
126
|
+
width: 1px;
|
|
127
|
+
height: 1px;
|
|
128
|
+
padding: 0;
|
|
129
|
+
margin: -1px;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
clip: rect(0, 0, 0, 0);
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
border: 0;
|
|
134
|
+
}
|
|
135
|
+
`];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static get formAssociated() {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static get properties() {
|
|
143
|
+
return {
|
|
144
|
+
label: { type: String },
|
|
145
|
+
value: { type: String },
|
|
146
|
+
name: { type: String, reflect: true },
|
|
147
|
+
disabled: { type: Boolean },
|
|
148
|
+
errored: { type: Boolean },
|
|
149
|
+
data: { type: Array },
|
|
150
|
+
placeholder: { type: String },
|
|
151
|
+
emptyMessage: { type: String },
|
|
152
|
+
selectedText: { type: String },
|
|
153
|
+
displayProperty: { type: String },
|
|
154
|
+
idProperty: { type: String },
|
|
155
|
+
/** Property in each data object holding a dile-iconlib "family.name" icon string, applied
|
|
156
|
+
* per option. Leave unset if you don't need per-option icons. */
|
|
157
|
+
iconProperty: { type: String },
|
|
158
|
+
/** dile-iconlib "family.name" icon string applied to every option that has no icon of its
|
|
159
|
+
* own via iconProperty. */
|
|
160
|
+
icon: { type: String },
|
|
161
|
+
delay: { type: Number },
|
|
162
|
+
keyword: {
|
|
163
|
+
type: String,
|
|
164
|
+
state: true,
|
|
165
|
+
},
|
|
166
|
+
loading: {
|
|
167
|
+
type: Boolean,
|
|
168
|
+
state: true,
|
|
169
|
+
},
|
|
170
|
+
isSelected: {
|
|
171
|
+
type: Boolean,
|
|
172
|
+
state: true,
|
|
173
|
+
},
|
|
174
|
+
/** Accessible name of the button that clears the selected option */
|
|
175
|
+
clearSelectionLabel: { type: String },
|
|
176
|
+
/** Accessible name of the button that clears the text typed in the search field */
|
|
177
|
+
clearSearchLabel: { type: String },
|
|
178
|
+
/** Message Displayed */
|
|
179
|
+
message: { type: String },
|
|
180
|
+
/** Hide errors on input */
|
|
181
|
+
hideErrorOnInput: { type: Boolean },
|
|
182
|
+
/** Text announced in a live region as the user navigates results with the keyboard */
|
|
183
|
+
_liveAnnouncement: { state: true },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
updated(changedProperties) {
|
|
188
|
+
if(changedProperties.has("value")) {
|
|
189
|
+
this.emmitChange();
|
|
190
|
+
this.dispatchSelectedTextChanged();
|
|
191
|
+
this.internals.setFormValue(this.value);
|
|
192
|
+
}
|
|
193
|
+
// In search mode the nested dile-select-overlay's own trigger is hidden and collapsed to
|
|
194
|
+
// zero height, sitting right after this component's real, visible search field. Point its
|
|
195
|
+
// positioning at that real field instead, so a flip-to-top popup correctly leaves room for
|
|
196
|
+
// the field's actual height rather than covering it. Without a search field (e.g. static
|
|
197
|
+
// mode) the nested component keeps its own visible trigger, which is already the right
|
|
198
|
+
// thing to position against.
|
|
199
|
+
// Re-checked on every update (not just firstUpdated) because clearing a selection swaps
|
|
200
|
+
// back to a *new* dile-select-overlay instance (isSelected toggles the whole template
|
|
201
|
+
// branch), which needs the same wiring the original instance got.
|
|
202
|
+
if (this.hasSearchField && this.select && this._wiredTriggerFor !== this.select) {
|
|
203
|
+
const nestedSelect = this.select;
|
|
204
|
+
this._wiredTriggerFor = nestedSelect;
|
|
205
|
+
// The nested element's own firstUpdated() (which sets its default internal trigger
|
|
206
|
+
// reference) runs on its own update cycle, not synchronously with this one — wait for it
|
|
207
|
+
// to resolve first, otherwise it overwrites the external trigger we just set here.
|
|
208
|
+
nestedSelect.updateComplete.then(() => {
|
|
209
|
+
nestedSelect.setTriggerElement(this.shadowRoot.querySelector('.search-wrapper'));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
constructor() {
|
|
215
|
+
super();
|
|
216
|
+
this.placeholder = "Search to choose...";
|
|
217
|
+
this.emptyMessage = "No results found";
|
|
218
|
+
this.clearSelectionLabel = "Clear selection";
|
|
219
|
+
this.clearSearchLabel = "Clear search";
|
|
220
|
+
this.data = [];
|
|
221
|
+
this.keyword = '';
|
|
222
|
+
this.loading = false;
|
|
223
|
+
this.errored = false;
|
|
224
|
+
this.isSelected = false;
|
|
225
|
+
this.delay = 300;
|
|
226
|
+
this.idProperty = 'id';
|
|
227
|
+
this._liveAnnouncement = '';
|
|
228
|
+
this._debounceTimeout = null;
|
|
229
|
+
this.internals = this.attachInternals();
|
|
230
|
+
this.selectId = `${this.localName}-${Math.random().toString(36).substr(2, 9)}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
firstUpdated() {
|
|
234
|
+
if(this.value) {
|
|
235
|
+
this.isSelected = true;
|
|
236
|
+
this.resolveSelectedText();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
disconnectedCallback() {
|
|
241
|
+
super.disconnectedCallback();
|
|
242
|
+
if (this._debounceTimeout) {
|
|
243
|
+
clearTimeout(this._debounceTimeout);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Template method: fill the results for the current keyword, calling setResults(data). */
|
|
248
|
+
loadData() {}
|
|
249
|
+
|
|
250
|
+
/** Template method: set `selectedText` for the current `value`. */
|
|
251
|
+
resolveSelectedText() {}
|
|
252
|
+
|
|
253
|
+
/** Name of the event dispatched when the selected text changes. */
|
|
254
|
+
get selectedTextChangedEventName() {
|
|
255
|
+
return 'dile-select-selected-text-changed';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Whether the component renders its own search field. When false, the nested
|
|
259
|
+
* dile-select-overlay is shown with its own visible trigger instead. */
|
|
260
|
+
get hasSearchField() {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Text shown in the status box when a search returns no results. */
|
|
265
|
+
get statusText() {
|
|
266
|
+
return this.emptyMessage;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Whether the results popup should open once results are set. */
|
|
270
|
+
get shouldOpenResults() {
|
|
271
|
+
return this.keyword.length > 0 && this.data.length > 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Accessible name of the read-only field shown once an option is selected. */
|
|
275
|
+
get selectedAccessibleLabel() {
|
|
276
|
+
return this.label || undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get search() {
|
|
280
|
+
return this.shadowRoot.getElementById('search');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get select() {
|
|
284
|
+
return this.shadowRoot.getElementById('elselect');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get opened() {
|
|
288
|
+
return this.select ? this.select.opened : false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
render() {
|
|
292
|
+
return html`
|
|
293
|
+
<div>
|
|
294
|
+
${this.label
|
|
295
|
+
? html`<label for="${this.isSelected ? 'result' : 'search'}">${this.label}</label>`
|
|
296
|
+
: ""}
|
|
297
|
+
${this.isSelected
|
|
298
|
+
? this.selectedTemplate
|
|
299
|
+
: this.hasSearchField ? this.searchTemplate : this.dataTemplate
|
|
300
|
+
}
|
|
301
|
+
</div>
|
|
302
|
+
${this.messageTemplate}
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
get selectedTemplate() {
|
|
307
|
+
return html`
|
|
308
|
+
<dile-input-search
|
|
309
|
+
id="result"
|
|
310
|
+
?errored=${this.errored}
|
|
311
|
+
?disabled=${this.disabled}
|
|
312
|
+
value="${this.selectedText}"
|
|
313
|
+
readOnly
|
|
314
|
+
.accessibleLabel=${this.selectedAccessibleLabel}
|
|
315
|
+
.clearButtonLabel=${this.clearSelectionLabel}
|
|
316
|
+
@dile-input-search-cleared=${this.onClearSelected}
|
|
317
|
+
@dile-input-search=${this.hideErrorOnInteraction}
|
|
318
|
+
></dile-input-search>
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
get searchTemplate() {
|
|
323
|
+
return html`
|
|
324
|
+
<div
|
|
325
|
+
class="search-wrapper ${this.errored ? 'errored' : ''} ${this.disabled ? 'disabled' : ''}"
|
|
326
|
+
@click=${(e) => e.stopPropagation()}
|
|
327
|
+
>
|
|
328
|
+
<input
|
|
329
|
+
type="text"
|
|
330
|
+
id="search"
|
|
331
|
+
role="combobox"
|
|
332
|
+
aria-haspopup="listbox"
|
|
333
|
+
aria-expanded="${this.opened}"
|
|
334
|
+
aria-invalid="${this.errored}"
|
|
335
|
+
aria-describedby="${ifDefined(this.message ? `${this.selectId}-message` : undefined)}"
|
|
336
|
+
placeholder="${this.placeholder}"
|
|
337
|
+
.value="${this.keyword}"
|
|
338
|
+
?disabled=${this.disabled}
|
|
339
|
+
autocomplete="off"
|
|
340
|
+
@input=${this.onSearchInput}
|
|
341
|
+
@keydown=${this.onSearchKeydown}
|
|
342
|
+
@focus=${this.onSearchFocus}
|
|
343
|
+
@focusout=${this.onSearchFocusOut}
|
|
344
|
+
>
|
|
345
|
+
${this.loading
|
|
346
|
+
? html`<dile-spinner-icon active aria-hidden="true"></dile-spinner-icon>`
|
|
347
|
+
: this.keyword.length
|
|
348
|
+
? html`
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
class="clear-button"
|
|
352
|
+
aria-label="${this.clearSearchLabel}"
|
|
353
|
+
?disabled=${this.disabled}
|
|
354
|
+
@click=${this.onIconClick}
|
|
355
|
+
><dile-icon .icon="${clearIcon}" aria-hidden="true"></dile-icon></button>
|
|
356
|
+
`
|
|
357
|
+
: html`<dile-icon .icon="${searchIcon}" aria-hidden="true"></dile-icon>`
|
|
358
|
+
}
|
|
359
|
+
</div>
|
|
360
|
+
<div class="visually-hidden" role="status" aria-live="polite">${this._liveAnnouncement}</div>
|
|
361
|
+
${this.keyword.length > 0 && this.data.length === 0 ? this.statusTemplate : ''}
|
|
362
|
+
${this.dataTemplate}
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
get statusTemplate() {
|
|
367
|
+
return html`
|
|
368
|
+
<div class="status">
|
|
369
|
+
${this.loading
|
|
370
|
+
? html`<dile-spinner-horizontal active></dile-spinner-horizontal>`
|
|
371
|
+
: html`<p>${this.statusText}</p>`
|
|
372
|
+
}
|
|
373
|
+
</div>
|
|
374
|
+
`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get dataTemplate() {
|
|
378
|
+
return html`
|
|
379
|
+
<dile-select-overlay
|
|
380
|
+
id="elselect"
|
|
381
|
+
?hideTrigger=${this.hasSearchField}
|
|
382
|
+
name="generated-select-field"
|
|
383
|
+
placeholder="${this.placeholder}"
|
|
384
|
+
?disabled=${this.disabled}
|
|
385
|
+
@element-changed=${this.doSelected}
|
|
386
|
+
>
|
|
387
|
+
<select slot="select" data-icon="${ifDefined(this.icon)}">
|
|
388
|
+
${this.data.map(item => html`
|
|
389
|
+
<option value="${item[this.idProperty]}" data-icon="${ifDefined(this.iconProperty ? item[this.iconProperty] : undefined)}">${item[this.displayProperty]}</option>
|
|
390
|
+
`)}
|
|
391
|
+
</select>
|
|
392
|
+
</dile-select-overlay>
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
get messageTemplate() {
|
|
397
|
+
return html`
|
|
398
|
+
${this.message
|
|
399
|
+
? html`<div class="message ${this.errored ? 'errored-msg' : ''}" id="${this.selectId}-message"><span>${this.message}</span></div>`
|
|
400
|
+
: ''
|
|
401
|
+
}
|
|
402
|
+
`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
onSearchInput(e) {
|
|
406
|
+
this.keyword = e.target.value;
|
|
407
|
+
this.hideErrorOnInteraction();
|
|
408
|
+
if (this._debounceTimeout) {
|
|
409
|
+
clearTimeout(this._debounceTimeout);
|
|
410
|
+
}
|
|
411
|
+
if (!this.delay) {
|
|
412
|
+
this.loadData();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// Mark as loading right away, not just once the debounced request actually starts,
|
|
416
|
+
// otherwise the empty-results message flashes for the length of the debounce delay
|
|
417
|
+
// before there's been any real request to come back empty.
|
|
418
|
+
if (this.keyword.length > 0) {
|
|
419
|
+
this.loading = true;
|
|
420
|
+
}
|
|
421
|
+
this._debounceTimeout = setTimeout(() => {
|
|
422
|
+
this.loadData();
|
|
423
|
+
}, this.delay);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
onSearchKeydown(e) {
|
|
427
|
+
if (!NAVIGATION_KEYS.includes(e.key) || !this.select) return;
|
|
428
|
+
this.select.handleExternalKeydown(e);
|
|
429
|
+
this._liveAnnouncement = this.select.highlightedText;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
onSearchFocus() {
|
|
433
|
+
if (this.shouldOpenResults) {
|
|
434
|
+
this.select?.open();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
onSearchFocusOut(e) {
|
|
439
|
+
if (!this.shadowRoot.contains(e.relatedTarget)) {
|
|
440
|
+
this.select?.close();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
onIconClick() {
|
|
445
|
+
if (!this.keyword.length) return;
|
|
446
|
+
this.keyword = '';
|
|
447
|
+
this.data = [];
|
|
448
|
+
this.select?.close();
|
|
449
|
+
this.search.focus();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
hideErrorOnInteraction() {
|
|
453
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
454
|
+
this.clearError();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
setResults(data) {
|
|
459
|
+
this.data = data;
|
|
460
|
+
this.updateComplete.then(() => {
|
|
461
|
+
this.loading = false;
|
|
462
|
+
if (this.shouldOpenResults) {
|
|
463
|
+
this.select?.open();
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
doSelected(e) {
|
|
469
|
+
e.stopPropagation();
|
|
470
|
+
let value = e.detail.value;
|
|
471
|
+
// The nested dile-select-overlay also fires element-changed with an empty value while
|
|
472
|
+
// syncing itself to its placeholder option on mount (before any real selection) since,
|
|
473
|
+
// unlike the original dile-select, it's kept mounted from the start so its MutationObserver
|
|
474
|
+
// is ready to pick up results as soon as they load. Ignore that non-interaction here.
|
|
475
|
+
if(e.detail.name === 'generated-select-field' && value !== '') {
|
|
476
|
+
this.selectOptionValue(value);
|
|
477
|
+
this.hideErrorOnInteraction();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Applies the option picked by the user in the results popup. */
|
|
482
|
+
selectOptionValue(value) {
|
|
483
|
+
this.selectedText = this.select.getOptionByValue(value).innerText;
|
|
484
|
+
this.value = value;
|
|
485
|
+
this.isSelected = true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
clearError() {
|
|
489
|
+
this.errored = false;
|
|
490
|
+
this.message = '';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
onClearSelected() {
|
|
494
|
+
this.value = undefined;
|
|
495
|
+
this.isSelected = false;
|
|
496
|
+
if(this.hasSearchField) {
|
|
497
|
+
this.keyword = '';
|
|
498
|
+
this.selectedText = '';
|
|
499
|
+
this.data = [];
|
|
500
|
+
this.updateComplete.then(() => {
|
|
501
|
+
this.search.focus();
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
this.hideErrorOnInteraction();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
close() {
|
|
508
|
+
this.select?.close();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
clear() {
|
|
512
|
+
this.isSelected = false;
|
|
513
|
+
this.value = undefined;
|
|
514
|
+
this.selectedText = '';
|
|
515
|
+
// isSelected flipping back to false remounts a brand new nested dile-select-overlay (the
|
|
516
|
+
// read-only and search branches are mutually exclusive templates). Its firstUpdated() syncs
|
|
517
|
+
// its value to the native <select>'s current value, and this component's <select> has no
|
|
518
|
+
// placeholder option — so leftover data/keyword from the previous search would leave a real
|
|
519
|
+
// option in place, get auto-picked as the new instance's value, and re-fire element-changed
|
|
520
|
+
// for the very same item that was just cleared (an infinite loop under addOnSelect).
|
|
521
|
+
if (this.hasSearchField) {
|
|
522
|
+
this.keyword = '';
|
|
523
|
+
this.data = [];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
set(value) {
|
|
528
|
+
this.value = value;
|
|
529
|
+
if(value) {
|
|
530
|
+
this.isSelected = true;
|
|
531
|
+
this.resolveSelectedText();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
dispatchSelectedTextChanged() {
|
|
536
|
+
this.dispatchEvent(new CustomEvent(this.selectedTextChangedEventName,
|
|
537
|
+
{
|
|
538
|
+
bubbles: true,
|
|
539
|
+
composed: true,
|
|
540
|
+
detail: { selectedText: this.selectedText }
|
|
541
|
+
}
|
|
542
|
+
));
|
|
543
|
+
}
|
|
544
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "5ef91e1892e270fade848c64082184fa3fbddb53"
|
|
32
32
|
}
|